Commit 102c90d4 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

See #170: admin UI for channels, reporting channels

parent ae52969e
...@@ -69,6 +69,15 @@ class Channel(models.Model): ...@@ -69,6 +69,15 @@ class Channel(models.Model):
objects = ChannelQuerySet.as_manager() objects = ChannelQuerySet.as_manager()
@property
def fid(self):
if not self.is_external_rss:
return self.actor.fid
@property
def is_external_rss(self):
return self.actor.preferred_username.startswith("rssfeed-")
def get_absolute_url(self): def get_absolute_url(self):
suffix = self.uuid suffix = self.uuid
if self.actor.is_local: if self.actor.is_local:
...@@ -78,9 +87,7 @@ class Channel(models.Model): ...@@ -78,9 +87,7 @@ class Channel(models.Model):
return federation_utils.full_url("/channels/{}".format(suffix)) return federation_utils.full_url("/channels/{}".format(suffix))
def get_rss_url(self): def get_rss_url(self):
if not self.artist.is_local or self.actor.preferred_username.startswith( if not self.artist.is_local or self.is_external_rss:
"rssfeed-"
):
return self.rss_url return self.rss_url
return federation_utils.full_url( return federation_utils.full_url(
...@@ -90,10 +97,6 @@ class Channel(models.Model): ...@@ -90,10 +97,6 @@ class Channel(models.Model):
) )
) )
@property
def fid(self):
return self.actor.fid
def generate_actor(username, **kwargs): def generate_actor(username, **kwargs):
actor_data = user_models.get_actor_data(username, **kwargs) actor_data = user_models.get_actor_data(username, **kwargs)
......
...@@ -145,6 +145,7 @@ class Domain(models.Model): ...@@ -145,6 +145,7 @@ class Domain(models.Model):
actors=models.Count("actors", distinct=True), actors=models.Count("actors", distinct=True),
outbox_activities=models.Count("actors__outbox_activities", distinct=True), outbox_activities=models.Count("actors__outbox_activities", distinct=True),
libraries=models.Count("actors__libraries", distinct=True), libraries=models.Count("actors__libraries", distinct=True),
channels=models.Count("actors__owned_channels", distinct=True),
received_library_follows=models.Count( received_library_follows=models.Count(
"actors__libraries__received_follows", distinct=True "actors__libraries__received_follows", distinct=True
), ),
...@@ -283,6 +284,7 @@ class Actor(models.Model): ...@@ -283,6 +284,7 @@ class Actor(models.Model):
data = Actor.objects.filter(pk=self.pk).aggregate( data = Actor.objects.filter(pk=self.pk).aggregate(
outbox_activities=models.Count("outbox_activities", distinct=True), outbox_activities=models.Count("outbox_activities", distinct=True),
libraries=models.Count("libraries", distinct=True), libraries=models.Count("libraries", distinct=True),
channels=models.Count("owned_channels", distinct=True),
received_library_follows=models.Count( received_library_follows=models.Count(
"libraries__received_follows", distinct=True "libraries__received_follows", distinct=True
), ),
......
...@@ -482,6 +482,8 @@ def inbox_flag(payload, context): ...@@ -482,6 +482,8 @@ def inbox_flag(payload, context):
@outbox.register({"type": "Flag"}) @outbox.register({"type": "Flag"})
def outbox_flag(context): def outbox_flag(context):
report = context["report"] report = context["report"]
if not report.target or not report.target.fid:
return
actor = actors.get_service_actor() actor = actors.get_service_actor()
serializer = serializers.FlagSerializer(report) serializer = serializers.FlagSerializer(report)
yield { yield {
......
...@@ -266,5 +266,11 @@ def get_object_by_fid(fid, local=None): ...@@ -266,5 +266,11 @@ def get_object_by_fid(fid, local=None):
if not result: if not result:
raise ObjectDoesNotExist() raise ObjectDoesNotExist()
model = apps.get_model(*result["__type"].split("."))
return apps.get_model(*result["__type"].split(".")).objects.get(fid=fid) instance = model.objects.get(fid=fid)
if model._meta.label == "federation.Actor":
channel = instance.get_channel()
if channel:
return channel
return instance
...@@ -8,6 +8,7 @@ from funkwhale_api.common import fields ...@@ -8,6 +8,7 @@ from funkwhale_api.common import fields
from funkwhale_api.common import filters as common_filters from funkwhale_api.common import filters as common_filters
from funkwhale_api.common import search from funkwhale_api.common import search
from funkwhale_api.audio import models as audio_models
from funkwhale_api.federation import models as federation_models from funkwhale_api.federation import models as federation_models
from funkwhale_api.federation import utils as federation_utils from funkwhale_api.federation import utils as federation_utils
from funkwhale_api.moderation import models as moderation_models from funkwhale_api.moderation import models as moderation_models
...@@ -34,6 +35,34 @@ def get_actor_filter(actor_field): ...@@ -34,6 +35,34 @@ def get_actor_filter(actor_field):
return {"field": ActorField(), "handler": handler} return {"field": ActorField(), "handler": handler}
class ManageChannelFilterSet(filters.FilterSet):
q = fields.SmartSearchFilter(
config=search.SearchConfig(
search_fields={
"name": {"to": "artist__name"},
"username": {"to": "artist__name"},
"fid": {"to": "artist__fid"},
"rss": {"to": "rss_url"},
},
filter_fields={
"uuid": {"to": "uuid"},
"category": {"to": "artist__content_category"},
"domain": {
"handler": lambda v: federation_utils.get_domain_query_from_url(
v, url_field="attributed_to__fid"
)
},
"tag": {"to": "artist__tagged_items__tag__name", "distinct": True},
"account": get_actor_filter("attributed_to"),
},
)
)
class Meta:
model = audio_models.Channel
fields = ["q"]
class ManageArtistFilterSet(filters.FilterSet): class ManageArtistFilterSet(filters.FilterSet):
q = fields.SmartSearchFilter( q = fields.SmartSearchFilter(
config=search.SearchConfig( config=search.SearchConfig(
...@@ -52,6 +81,7 @@ class ManageArtistFilterSet(filters.FilterSet): ...@@ -52,6 +81,7 @@ class ManageArtistFilterSet(filters.FilterSet):
"field": forms.IntegerField(), "field": forms.IntegerField(),
"distinct": True, "distinct": True,
}, },
"category": {"to": "content_category"},
"tag": {"to": "tagged_items__tag__name", "distinct": True}, "tag": {"to": "tagged_items__tag__name", "distinct": True},
}, },
) )
...@@ -59,7 +89,7 @@ class ManageArtistFilterSet(filters.FilterSet): ...@@ -59,7 +89,7 @@ class ManageArtistFilterSet(filters.FilterSet):
class Meta: class Meta:
model = music_models.Artist model = music_models.Artist
fields = ["q", "name", "mbid", "fid"] fields = ["q", "name", "mbid", "fid", "content_category"]
class ManageAlbumFilterSet(filters.FilterSet): class ManageAlbumFilterSet(filters.FilterSet):
......
...@@ -3,6 +3,7 @@ from django.db import transaction ...@@ -3,6 +3,7 @@ from django.db import transaction
from rest_framework import serializers from rest_framework import serializers
from funkwhale_api.audio import models as audio_models
from funkwhale_api.common import fields as common_fields from funkwhale_api.common import fields as common_fields
from funkwhale_api.common import serializers as common_serializers from funkwhale_api.common import serializers as common_serializers
from funkwhale_api.common import utils as common_utils from funkwhale_api.common import utils as common_utils
...@@ -386,26 +387,39 @@ class ManageNestedAlbumSerializer(ManageBaseAlbumSerializer): ...@@ -386,26 +387,39 @@ class ManageNestedAlbumSerializer(ManageBaseAlbumSerializer):
class ManageArtistSerializer( class ManageArtistSerializer(
music_serializers.OptionalDescriptionMixin, ManageBaseArtistSerializer music_serializers.OptionalDescriptionMixin, ManageBaseArtistSerializer
): ):
albums = ManageNestedAlbumSerializer(many=True)
tracks = ManageNestedTrackSerializer(many=True)
attributed_to = ManageBaseActorSerializer() attributed_to = ManageBaseActorSerializer()
tags = serializers.SerializerMethodField() tags = serializers.SerializerMethodField()
tracks_count = serializers.SerializerMethodField()
albums_count = serializers.SerializerMethodField()
channel = serializers.SerializerMethodField()
cover = music_serializers.cover_field cover = music_serializers.cover_field
class Meta: class Meta:
model = music_models.Artist model = music_models.Artist
fields = ManageBaseArtistSerializer.Meta.fields + [ fields = ManageBaseArtistSerializer.Meta.fields + [
"albums", "tracks_count",
"tracks", "albums_count",
"attributed_to", "attributed_to",
"tags", "tags",
"cover", "cover",
"channel",
"content_category",
] ]
def get_tracks_count(self, obj):
return getattr(obj, "_tracks_count", None)
def get_albums_count(self, obj):
return getattr(obj, "_albums_count", None)
def get_tags(self, obj): def get_tags(self, obj):
tagged_items = getattr(obj, "_prefetched_tagged_items", []) tagged_items = getattr(obj, "_prefetched_tagged_items", [])
return [ti.tag.name for ti in tagged_items] return [ti.tag.name for ti in tagged_items]
def get_channel(self, obj):
if "channel" in obj._state.fields_cache and obj.get_channel():
return str(obj.channel.uuid)
class ManageNestedArtistSerializer(ManageBaseArtistSerializer): class ManageNestedArtistSerializer(ManageBaseArtistSerializer):
pass pass
...@@ -743,3 +757,23 @@ class ManageUserRequestSerializer(serializers.ModelSerializer): ...@@ -743,3 +757,23 @@ class ManageUserRequestSerializer(serializers.ModelSerializer):
def get_notes(self, o): def get_notes(self, o):
notes = getattr(o, "_prefetched_notes", []) notes = getattr(o, "_prefetched_notes", [])
return ManageBaseNoteSerializer(notes, many=True).data return ManageBaseNoteSerializer(notes, many=True).data
class ManageChannelSerializer(serializers.ModelSerializer):
attributed_to = ManageBaseActorSerializer()
actor = ManageBaseActorSerializer()
artist = ManageArtistSerializer()
class Meta:
model = audio_models.Channel
fields = [
"id",
"uuid",
"creation_date",
"artist",
"attributed_to",
"actor",
"rss_url",
"metadata",
]
read_only_fields = fields
...@@ -27,6 +27,7 @@ users_router.register(r"invitations", views.ManageInvitationViewSet, "invitation ...@@ -27,6 +27,7 @@ users_router.register(r"invitations", views.ManageInvitationViewSet, "invitation
other_router = routers.OptionalSlashRouter() other_router = routers.OptionalSlashRouter()
other_router.register(r"accounts", views.ManageActorViewSet, "accounts") other_router.register(r"accounts", views.ManageActorViewSet, "accounts")
other_router.register(r"channels", views.ManageChannelViewSet, "channels")
other_router.register(r"tags", views.ManageTagViewSet, "tags") other_router.register(r"tags", views.ManageTagViewSet, "tags")
urlpatterns = [ urlpatterns = [
......
...@@ -6,12 +6,15 @@ from django.db.models import Count, Prefetch, Q, Sum, OuterRef, Subquery ...@@ -6,12 +6,15 @@ from django.db.models import Count, Prefetch, Q, Sum, OuterRef, Subquery
from django.db.models.functions import Coalesce, Length from django.db.models.functions import Coalesce, Length
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from funkwhale_api.audio import models as audio_models
from funkwhale_api.common.mixins import MultipleLookupDetailMixin
from funkwhale_api.common import models as common_models from funkwhale_api.common import models as common_models
from funkwhale_api.common import preferences, decorators from funkwhale_api.common import preferences, decorators
from funkwhale_api.common import utils as common_utils from funkwhale_api.common import utils as common_utils
from funkwhale_api.favorites import models as favorites_models from funkwhale_api.favorites import models as favorites_models
from funkwhale_api.federation import models as federation_models from funkwhale_api.federation import models as federation_models
from funkwhale_api.federation import tasks as federation_tasks from funkwhale_api.federation import tasks as federation_tasks
from funkwhale_api.federation import utils as federation_utils
from funkwhale_api.history import models as history_models from funkwhale_api.history import models as history_models
from funkwhale_api.music import models as music_models from funkwhale_api.music import models as music_models
from funkwhale_api.music import views as music_views from funkwhale_api.music import views as music_views
...@@ -25,37 +28,39 @@ from funkwhale_api.users import models as users_models ...@@ -25,37 +28,39 @@ from funkwhale_api.users import models as users_models
from . import filters, serializers from . import filters, serializers
def get_stats(tracks, target): def get_stats(tracks, target, ignore_fields=[]):
data = {}
tracks = list(tracks.values_list("pk", flat=True)) tracks = list(tracks.values_list("pk", flat=True))
uploads = music_models.Upload.objects.filter(track__in=tracks) uploads = music_models.Upload.objects.filter(track__in=tracks)
data["listenings"] = history_models.Listening.objects.filter( fields = {
track__in=tracks "listenings": history_models.Listening.objects.filter(track__in=tracks),
).count() "mutations": common_models.Mutation.objects.get_for_target(target),
data["mutations"] = common_models.Mutation.objects.get_for_target(target).count() "playlists": (
data["playlists"] = ( playlists_models.PlaylistTrack.objects.filter(track__in=tracks)
playlists_models.PlaylistTrack.objects.filter(track__in=tracks) .values_list("playlist", flat=True)
.values_list("playlist", flat=True) .distinct()
.distinct() ),
.count() "track_favorites": (
) favorites_models.TrackFavorite.objects.filter(track__in=tracks)
data["track_favorites"] = favorites_models.TrackFavorite.objects.filter( ),
track__in=tracks "libraries": (
).count() uploads.filter(library__channel=None)
data["libraries"] = ( .values_list("library", flat=True)
uploads.filter(library__channel=None) .distinct()
.values_list("library", flat=True) ),
.distinct() "channels": (
.count() uploads.exclude(library__channel=None)
) .values_list("library", flat=True)
data["channels"] = ( .distinct()
uploads.exclude(library__channel=None) ),
.values_list("library", flat=True) "uploads": uploads,
.distinct() "reports": moderation_models.Report.objects.get_for_target(target),
.count() }
) data = {}
data["uploads"] = uploads.count() for key, qs in fields.items():
data["reports"] = moderation_models.Report.objects.get_for_target(target).count() if key in ignore_fields:
continue
data[key] = qs.count()
data.update(get_media_stats(uploads)) data.update(get_media_stats(uploads))
return data return data
...@@ -78,17 +83,10 @@ class ManageArtistViewSet( ...@@ -78,17 +83,10 @@ class ManageArtistViewSet(
queryset = ( queryset = (
music_models.Artist.objects.all() music_models.Artist.objects.all()
.order_by("-id") .order_by("-id")
.select_related("attributed_to", "attachment_cover",) .select_related("attributed_to", "attachment_cover", "channel")
.prefetch_related( .annotate(_tracks_count=Count("tracks"))
"tracks", .annotate(_albums_count=Count("albums"))
Prefetch( .prefetch_related(music_views.TAG_PREFETCH)
"albums",
queryset=music_models.Album.objects.select_related(
"attachment_cover"
).annotate(tracks_count=Count("tracks")),
),
music_views.TAG_PREFETCH,
)
) )
serializer_class = serializers.ManageArtistSerializer serializer_class = serializers.ManageArtistSerializer
filterset_class = filters.ManageArtistFilterSet filterset_class = filters.ManageArtistFilterSet
...@@ -661,3 +659,64 @@ class ManageUserRequestViewSet( ...@@ -661,3 +659,64 @@ class ManageUserRequestViewSet(
) )
else: else:
serializer.save() serializer.save()
class ManageChannelViewSet(
MultipleLookupDetailMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
url_lookups = [
{
"lookup_field": "uuid",
"validator": serializers.serializers.UUIDField().to_internal_value,
},
{
"lookup_field": "username",
"validator": federation_utils.get_actor_data_from_username,
"get_query": lambda v: Q(
actor__domain=v["domain"],
actor__preferred_username__iexact=v["username"],
),
},
]
queryset = (
audio_models.Channel.objects.all()
.order_by("-id")
.select_related("attributed_to", "actor",)
.prefetch_related(
Prefetch(
"artist",
queryset=(
music_models.Artist.objects.all()
.order_by("-id")
.select_related("attributed_to", "attachment_cover", "channel")
.annotate(_tracks_count=Count("tracks"))
.annotate(_albums_count=Count("albums"))
.prefetch_related(music_views.TAG_PREFETCH)
),
)
)
)
serializer_class = serializers.ManageChannelSerializer
filterset_class = filters.ManageChannelFilterSet
required_scope = "instance:libraries"
ordering_fields = ["creation_date", "name"]
@rest_decorators.action(methods=["get"], detail=True)
def stats(self, request, *args, **kwargs):
channel = self.get_object()
tracks = music_models.Track.objects.filter(
Q(artist=channel.artist) | Q(album__artist=channel.artist)
)
data = get_stats(tracks, channel, ignore_fields=["libraries", "channels"])
data["follows"] = channel.actor.received_follows.count()
return response.Response(data, status=200)
def get_serializer_context(self):
context = super().get_serializer_context()
context["description"] = self.action in ["retrieve", "create", "update"]
return context
...@@ -6,6 +6,7 @@ from django.core.serializers.json import DjangoJSONEncoder ...@@ -6,6 +6,7 @@ from django.core.serializers.json import DjangoJSONEncoder
import persisting_theory import persisting_theory
from rest_framework import serializers from rest_framework import serializers
from funkwhale_api.audio import models as audio_models
from funkwhale_api.common import fields as common_fields from funkwhale_api.common import fields as common_fields
from funkwhale_api.common import preferences from funkwhale_api.common import preferences
from funkwhale_api.federation import models as federation_models from funkwhale_api.federation import models as federation_models
...@@ -61,20 +62,36 @@ class UserFilterSerializer(serializers.ModelSerializer): ...@@ -61,20 +62,36 @@ class UserFilterSerializer(serializers.ModelSerializer):
state_serializers = persisting_theory.Registry() state_serializers = persisting_theory.Registry()
class DescriptionStateMixin(object):
def get_description(self, o):
if o.description:
return o.description.text
TAGS_FIELD = serializers.ListField(source="get_tags") TAGS_FIELD = serializers.ListField(source="get_tags")
@state_serializers.register(name="music.Artist") @state_serializers.register(name="music.Artist")
class ArtistStateSerializer(serializers.ModelSerializer): class ArtistStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
tags = TAGS_FIELD tags = TAGS_FIELD
class Meta: class Meta:
model = music_models.Artist model = music_models.Artist
fields = ["id", "name", "mbid", "fid", "creation_date", "uuid", "tags"] fields = [
"id",
"name",
"mbid",
"fid",
"creation_date",
"uuid",
"tags",
"content_category",
"description",
]
@state_serializers.register(name="music.Album") @state_serializers.register(name="music.Album")
class AlbumStateSerializer(serializers.ModelSerializer): class AlbumStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
tags = TAGS_FIELD tags = TAGS_FIELD
artist = ArtistStateSerializer() artist = ArtistStateSerializer()
...@@ -90,11 +107,12 @@ class AlbumStateSerializer(serializers.ModelSerializer): ...@@ -90,11 +107,12 @@ class AlbumStateSerializer(serializers.ModelSerializer):
"artist", "artist",
"release_date", "release_date",
"tags", "tags",
"description",
] ]
@state_serializers.register(name="music.Track") @state_serializers.register(name="music.Track")
class TrackStateSerializer(serializers.ModelSerializer): class TrackStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
tags = TAGS_FIELD tags = TAGS_FIELD
artist = ArtistStateSerializer() artist = ArtistStateSerializer()
album = AlbumStateSerializer() album = AlbumStateSerializer()
...@@ -115,6 +133,7 @@ class TrackStateSerializer(serializers.ModelSerializer): ...@@ -115,6 +133,7 @@ class TrackStateSerializer(serializers.ModelSerializer):
"license", "license",
"copyright", "copyright",
"tags", "tags",
"description",
] ]
...@@ -156,6 +175,36 @@ class ActorStateSerializer(serializers.ModelSerializer): ...@@ -156,6 +175,36 @@ class ActorStateSerializer(serializers.ModelSerializer):
] ]
@state_serializers.register(name="audio.Channel")
class ChannelStateSerializer(serializers.ModelSerializer):
rss_url = serializers.CharField(source="get_rss_url")