From 9f3076f4dbea9cac016e28788f5248a499949f67 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Tue, 9 Jul 2019 16:03:41 +0200
Subject: [PATCH] See #432: API endpoint to query tags

---
 api/config/api_urls.py                        |  3 +-
 api/funkwhale_api/music/views.py              |  8 ---
 api/funkwhale_api/tags/filters.py             | 21 ++++++++
 api/funkwhale_api/tags/serializers.py         |  9 ++++
 api/funkwhale_api/tags/views.py               | 22 ++++++++
 api/tests/tags/test_filters.py                | 16 ++++++
 api/tests/tags/test_serializers.py            | 14 ++++++
 api/tests/tags/test_views.py                  | 50 +++++++++++++++++++
 api/tests/users/oauth/test_api_permissions.py |  1 +
 9 files changed, 135 insertions(+), 9 deletions(-)
 create mode 100644 api/funkwhale_api/tags/filters.py
 create mode 100644 api/funkwhale_api/tags/serializers.py
 create mode 100644 api/funkwhale_api/tags/views.py
 create mode 100644 api/tests/tags/test_filters.py
 create mode 100644 api/tests/tags/test_serializers.py
 create mode 100644 api/tests/tags/test_views.py

diff --git a/api/config/api_urls.py b/api/config/api_urls.py
index 2631309eb..da1981585 100644
--- a/api/config/api_urls.py
+++ b/api/config/api_urls.py
@@ -10,11 +10,12 @@ from funkwhale_api.common import routers as common_routers
 from funkwhale_api.music import views
 from funkwhale_api.playlists import views as playlists_views
 from funkwhale_api.subsonic.views import SubsonicViewSet
+from funkwhale_api.tags import views as tags_views
 
 router = common_routers.OptionalSlashRouter()
 router.register(r"settings", GlobalPreferencesViewSet, basename="settings")
 router.register(r"activity", activity_views.ActivityViewSet, "activity")
-router.register(r"tags", views.TagViewSet, "tags")
+router.register(r"tags", tags_views.TagViewSet, "tags")
 router.register(r"tracks", views.TrackViewSet, "tracks")
 router.register(r"uploads", views.UploadViewSet, "uploads")
 router.register(r"libraries", views.LibraryViewSet, "libraries")
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index 9aa792480..5b34c3ce2 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -447,14 +447,6 @@ class UploadViewSet(
         instance.delete()
 
 
-class TagViewSet(viewsets.ReadOnlyModelViewSet):
-    queryset = Tag.objects.all().order_by("name")
-    serializer_class = serializers.TagSerializer
-    permission_classes = [oauth_permissions.ScopePermission]
-    required_scope = "libraries"
-    anonymous_policy = "setting"
-
-
 class Search(views.APIView):
     max_results = 3
     permission_classes = [oauth_permissions.ScopePermission]
diff --git a/api/funkwhale_api/tags/filters.py b/api/funkwhale_api/tags/filters.py
new file mode 100644
index 000000000..4be4afeef
--- /dev/null
+++ b/api/funkwhale_api/tags/filters.py
@@ -0,0 +1,21 @@
+import django_filters
+from django_filters import rest_framework as filters
+
+from funkwhale_api.common import fields
+
+from . import models
+
+
+class TagFilter(filters.FilterSet):
+    q = fields.SearchFilter(search_fields=["name"])
+    ordering = django_filters.OrderingFilter(
+        fields=(
+            ("name", "name"),
+            ("creation_date", "creation_date"),
+            ("__size", "length"),
+        )
+    )
+
+    class Meta:
+        model = models.Tag
+        fields = ["q"]
diff --git a/api/funkwhale_api/tags/serializers.py b/api/funkwhale_api/tags/serializers.py
new file mode 100644
index 000000000..f656f856c
--- /dev/null
+++ b/api/funkwhale_api/tags/serializers.py
@@ -0,0 +1,9 @@
+from rest_framework import serializers
+
+from . import models
+
+
+class TagSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = models.Tag
+        fields = ["name", "creation_date"]
diff --git a/api/funkwhale_api/tags/views.py b/api/funkwhale_api/tags/views.py
new file mode 100644
index 000000000..d7b1d8aa5
--- /dev/null
+++ b/api/funkwhale_api/tags/views.py
@@ -0,0 +1,22 @@
+from django.db.models import functions
+from rest_framework import viewsets
+
+from funkwhale_api.users.oauth import permissions as oauth_permissions
+
+from . import filters
+from . import models
+from . import serializers
+
+
+class TagViewSet(viewsets.ReadOnlyModelViewSet):
+    lookup_field = "name"
+    queryset = (
+        models.Tag.objects.all()
+        .annotate(__size=functions.Length("name"))
+        .order_by("name")
+    )
+    serializer_class = serializers.TagSerializer
+    permission_classes = [oauth_permissions.ScopePermission]
+    required_scope = "libraries"
+    anonymous_policy = "setting"
+    filterset_class = filters.TagFilter
diff --git a/api/tests/tags/test_filters.py b/api/tests/tags/test_filters.py
new file mode 100644
index 000000000..3b812f0fe
--- /dev/null
+++ b/api/tests/tags/test_filters.py
@@ -0,0 +1,16 @@
+from funkwhale_api.tags import filters
+from funkwhale_api.tags import models
+
+
+def test_filter_search_tag(factories, queryset_equal_list):
+    matches = [
+        factories["tags.Tag"](name="Tag1"),
+        factories["tags.Tag"](name="TestTag1"),
+        factories["tags.Tag"](name="TestTag12"),
+    ]
+    factories["tags.Tag"](name="TestTag")
+    factories["tags.Tag"](name="TestTag2")
+    qs = models.Tag.objects.all().order_by("name")
+    filterset = filters.TagFilter({"q": "tag1"}, queryset=qs)
+
+    assert filterset.qs == matches
diff --git a/api/tests/tags/test_serializers.py b/api/tests/tags/test_serializers.py
new file mode 100644
index 000000000..8909a9edc
--- /dev/null
+++ b/api/tests/tags/test_serializers.py
@@ -0,0 +1,14 @@
+from funkwhale_api.tags import serializers
+
+
+def test_tag_serializer(factories):
+    tag = factories["tags.Tag"]()
+
+    serializer = serializers.TagSerializer(tag)
+
+    expected = {
+        "name": tag.name,
+        "creation_date": tag.creation_date.isoformat().split("+")[0] + "Z",
+    }
+
+    assert serializer.data == expected
diff --git a/api/tests/tags/test_views.py b/api/tests/tags/test_views.py
new file mode 100644
index 000000000..fd3246adb
--- /dev/null
+++ b/api/tests/tags/test_views.py
@@ -0,0 +1,50 @@
+from django.urls import reverse
+
+from funkwhale_api.tags import serializers
+
+
+def test_tags_list(factories, logged_in_api_client):
+    url = reverse("api:v1:tags-list")
+    tag = factories["tags.Tag"]()
+
+    expected = {
+        "count": 1,
+        "next": None,
+        "previous": None,
+        "results": [serializers.TagSerializer(tag).data],
+    }
+
+    response = logged_in_api_client.get(url)
+
+    assert response.data == expected
+
+
+def test_tags_list_ordering_length(factories, logged_in_api_client):
+    url = reverse("api:v1:tags-list")
+    tags = [
+        factories["tags.Tag"](name="iamareallylongtag"),
+        factories["tags.Tag"](name="reallylongtag"),
+        factories["tags.Tag"](name="short"),
+        factories["tags.Tag"](name="bar"),
+    ]
+    expected = {
+        "count": 4,
+        "next": None,
+        "previous": None,
+        "results": [serializers.TagSerializer(tag).data for tag in tags],
+    }
+
+    response = logged_in_api_client.get(url, {"ordering": "-length"})
+
+    assert response.data == expected
+
+
+def test_tags_detail(factories, logged_in_api_client):
+    tag = factories["tags.Tag"]()
+    url = reverse("api:v1:tags-detail", kwargs={"name": tag.name})
+
+    expected = serializers.TagSerializer(tag).data
+
+    response = logged_in_api_client.get(url)
+
+    assert response.data == expected
diff --git a/api/tests/users/oauth/test_api_permissions.py b/api/tests/users/oauth/test_api_permissions.py
index e73d3a3f9..b030803ee 100644
--- a/api/tests/users/oauth/test_api_permissions.py
+++ b/api/tests/users/oauth/test_api_permissions.py
@@ -35,6 +35,7 @@ from funkwhale_api.users.oauth import scopes
             "get",
         ),
         ("api:v1:federation:library-follows-list", {}, "read:follows", "get"),
+        ("api:v1:tags-list", {}, "read:libraries", "get"),
         # admin / privileged stuff
         ("api:v1:instance:admin-settings-list", {}, "read:instance:settings", "get"),
         (
-- 
GitLab