From 13f36beec3f6b210a9bdc0f09042a13d081160d9 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Wed, 24 Jul 2019 10:24:30 +0200
Subject: [PATCH] See #432: added admin API endpoints to retrieve and delete
 tags

---
 api/funkwhale_api/manage/filters.py     |  9 ++++++
 api/funkwhale_api/manage/serializers.py | 28 ++++++++++++++++
 api/funkwhale_api/manage/urls.py        |  1 +
 api/funkwhale_api/manage/views.py       | 43 ++++++++++++++++++++++++-
 api/tests/manage/test_serializers.py    | 19 +++++++++++
 api/tests/manage/test_views.py          | 28 ++++++++++++++++
 6 files changed, 127 insertions(+), 1 deletion(-)

diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py
index de12ab1a..c8b8e60a 100644
--- a/api/funkwhale_api/manage/filters.py
+++ b/api/funkwhale_api/manage/filters.py
@@ -13,6 +13,7 @@ from funkwhale_api.federation import utils as federation_utils
 from funkwhale_api.moderation import models as moderation_models
 from funkwhale_api.music import models as music_models
 from funkwhale_api.users import models as users_models
+from funkwhale_api.tags import models as tags_models
 
 
 class ActorField(forms.CharField):
@@ -340,3 +341,11 @@ class ManageInstancePolicyFilterSet(filters.FilterSet):
             "silence_notifications",
             "reject_media",
         ]
+
+
+class ManageTagFilterSet(filters.FilterSet):
+    q = fields.SearchFilter(search_fields=["name"])
+
+    class Meta:
+        model = tags_models.Tag
+        fields = ["q"]
diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py
index 67e0178e..eab3b87f 100644
--- a/api/funkwhale_api/manage/serializers.py
+++ b/api/funkwhale_api/manage/serializers.py
@@ -10,6 +10,7 @@ from funkwhale_api.federation import tasks as federation_tasks
 from funkwhale_api.moderation import models as moderation_models
 from funkwhale_api.music import models as music_models
 from funkwhale_api.music import serializers as music_serializers
+from funkwhale_api.tags import models as tags_models
 from funkwhale_api.users import models as users_models
 
 from . import filters
@@ -564,3 +565,30 @@ class ManageUploadSerializer(serializers.ModelSerializer):
             "track",
             "library",
         )
+
+
+class ManageTagSerializer(ManageBaseAlbumSerializer):
+
+    tracks_count = serializers.SerializerMethodField()
+    albums_count = serializers.SerializerMethodField()
+    artists_count = serializers.SerializerMethodField()
+
+    class Meta:
+        model = tags_models.Tag
+        fields = [
+            "id",
+            "name",
+            "creation_date",
+            "tracks_count",
+            "albums_count",
+            "artists_count",
+        ]
+
+    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_artists_count(self, obj):
+        return getattr(obj, "_artists_count", None)
diff --git a/api/funkwhale_api/manage/urls.py b/api/funkwhale_api/manage/urls.py
index 2af18f5b..b830f002 100644
--- a/api/funkwhale_api/manage/urls.py
+++ b/api/funkwhale_api/manage/urls.py
@@ -24,6 +24,7 @@ users_router.register(r"invitations", views.ManageInvitationViewSet, "invitation
 
 other_router = routers.OptionalSlashRouter()
 other_router.register(r"accounts", views.ManageActorViewSet, "accounts")
+other_router.register(r"tags", views.ManageTagViewSet, "tags")
 
 urlpatterns = [
     url(
diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py
index c788dd96..09737a7a 100644
--- a/api/funkwhale_api/manage/views.py
+++ b/api/funkwhale_api/manage/views.py
@@ -2,7 +2,7 @@ from rest_framework import mixins, response, viewsets
 from rest_framework import decorators as rest_decorators
 
 from django.db.models import Count, Prefetch, Q, Sum, OuterRef, Subquery
-from django.db.models.functions import Coalesce
+from django.db.models.functions import Coalesce, Length
 from django.shortcuts import get_object_or_404
 
 from funkwhale_api.common import models as common_models
@@ -14,6 +14,7 @@ from funkwhale_api.history import models as history_models
 from funkwhale_api.music import models as music_models
 from funkwhale_api.moderation import models as moderation_models
 from funkwhale_api.playlists import models as playlists_models
+from funkwhale_api.tags import models as tags_models
 from funkwhale_api.users import models as users_models
 
 
@@ -452,3 +453,43 @@ class ManageInstancePolicyViewSet(
 
     def perform_create(self, serializer):
         serializer.save(actor=self.request.user.actor)
+
+
+class ManageTagViewSet(
+    mixins.ListModelMixin,
+    mixins.RetrieveModelMixin,
+    mixins.DestroyModelMixin,
+    mixins.CreateModelMixin,
+    viewsets.GenericViewSet,
+):
+    lookup_field = "name"
+    queryset = (
+        tags_models.Tag.objects.all()
+        .order_by("-creation_date")
+        .annotate(items_count=Count("tagged_items"))
+        .annotate(length=Length("name"))
+    )
+    serializer_class = serializers.ManageTagSerializer
+    filterset_class = filters.ManageTagFilterSet
+    required_scope = "instance:libraries"
+    ordering_fields = ["id", "creation_date", "name", "items_count", "length"]
+
+    def get_queryset(self):
+        queryset = super().get_queryset()
+        from django.contrib.contenttypes.models import ContentType
+
+        album_ct = ContentType.objects.get_for_model(music_models.Album)
+        track_ct = ContentType.objects.get_for_model(music_models.Track)
+        artist_ct = ContentType.objects.get_for_model(music_models.Artist)
+        queryset = queryset.annotate(
+            _albums_count=Count(
+                "tagged_items", filter=Q(tagged_items__content_type=album_ct)
+            ),
+            _tracks_count=Count(
+                "tagged_items", filter=Q(tagged_items__content_type=track_ct)
+            ),
+            _artists_count=Count(
+                "tagged_items", filter=Q(tagged_items__content_type=artist_ct)
+            ),
+        )
+        return queryset
diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py
index e621bf14..c1c532fa 100644
--- a/api/tests/manage/test_serializers.py
+++ b/api/tests/manage/test_serializers.py
@@ -496,3 +496,22 @@ def test_action_serializer_delete(factory, serializer_class, factories):
     s.handle_delete(objects[0].__class__.objects.all())
 
     assert objects[0].__class__.objects.count() == 0
+
+
+def test_manage_tag_serializer(factories):
+    tag = factories["tags.Tag"]()
+
+    setattr(tag, "_tracks_count", 42)
+    setattr(tag, "_albums_count", 54)
+    setattr(tag, "_artists_count", 66)
+    expected = {
+        "id": tag.id,
+        "name": tag.name,
+        "creation_date": tag.creation_date.isoformat().split("+")[0] + "Z",
+        "tracks_count": 42,
+        "albums_count": 54,
+        "artists_count": 66,
+    }
+    s = serializers.ManageTagSerializer(tag)
+
+    assert s.data == expected
diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py
index 72394052..710722b9 100644
--- a/api/tests/manage/test_views.py
+++ b/api/tests/manage/test_views.py
@@ -377,3 +377,31 @@ def test_upload_delete(factories, superuser_api_client):
     response = superuser_api_client.delete(url)
 
     assert response.status_code == 204
+
+
+def test_tag_detail(factories, superuser_api_client):
+    tag = factories["tags.Tag"]()
+    url = reverse("api:v1:manage:tags-detail", kwargs={"name": tag.name})
+    response = superuser_api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data["name"] == tag.name
+
+
+def test_tag_list(factories, superuser_api_client, settings):
+    tag = factories["tags.Tag"]()
+    url = reverse("api:v1:manage:tags-list")
+    response = superuser_api_client.get(url)
+
+    assert response.status_code == 200
+
+    assert response.data["count"] == 1
+    assert response.data["results"][0]["name"] == tag.name
+
+
+def test_tag_delete(factories, superuser_api_client):
+    tag = factories["tags.Tag"]()
+    url = reverse("api:v1:manage:tags-detail", kwargs={"name": tag.name})
+    response = superuser_api_client.delete(url)
+
+    assert response.status_code == 204
-- 
GitLab