From 9767c8f415cb4a2c340be99d15edd521041721d9 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Thu, 21 Jun 2018 23:31:12 +0200
Subject: [PATCH] See #190: API and serializers to manage import requests

---
 api/funkwhale_api/common/serializers.py |  8 ++-
 api/funkwhale_api/manage/filters.py     | 11 ++++
 api/funkwhale_api/manage/serializers.py | 74 ++++++++++++++++++++++++-
 api/funkwhale_api/manage/urls.py        |  7 +++
 api/funkwhale_api/manage/views.py       | 35 ++++++++++--
 api/funkwhale_api/music/models.py       |  2 +-
 api/tests/common/test_serializers.py    |  6 +-
 api/tests/manage/test_serializers.py    | 41 ++++++++++++++
 api/tests/manage/test_views.py          | 13 +++++
 9 files changed, 186 insertions(+), 11 deletions(-)

diff --git a/api/funkwhale_api/common/serializers.py b/api/funkwhale_api/common/serializers.py
index b995afca..161c5810 100644
--- a/api/funkwhale_api/common/serializers.py
+++ b/api/funkwhale_api/common/serializers.py
@@ -2,10 +2,10 @@ from rest_framework import serializers
 
 
 class Action(object):
-    def __init__(self, name, allow_all=False, filters=None):
+    def __init__(self, name, allow_all=False, qs_filter=None):
         self.name = name
         self.allow_all = allow_all
-        self.filters = filters or {}
+        self.qs_filter = qs_filter
 
     def __repr__(self):
         return "<Action {}>".format(self.name)
@@ -65,7 +65,6 @@ class ActionSerializer(serializers.Serializer):
                 "You cannot apply this action on all objects"
             )
         final_filters = data.get("filters", {}) or {}
-        final_filters.update(data["action"].filters)
         if self.filterset_class and final_filters:
             qs_filterset = self.filterset_class(final_filters, queryset=data["objects"])
             try:
@@ -74,6 +73,9 @@ class ActionSerializer(serializers.Serializer):
                 raise serializers.ValidationError("Invalid filters")
             data["objects"] = qs_filterset.qs
 
+        if data["action"].qs_filter:
+            data["objects"] = data["action"].qs_filter(data["objects"])
+
         data["count"] = data["objects"].count()
         if data["count"] < 1:
             raise serializers.ValidationError("No object matching your request")
diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py
index 5f83ebf1..8098ef1a 100644
--- a/api/funkwhale_api/manage/filters.py
+++ b/api/funkwhale_api/manage/filters.py
@@ -2,6 +2,7 @@ from django_filters import rest_framework as filters
 
 from funkwhale_api.common import fields
 from funkwhale_api.music import models as music_models
+from funkwhale_api.requests import models as requests_models
 from funkwhale_api.users import models as users_models
 
 
@@ -50,3 +51,13 @@ class ManageInvitationFilterSet(filters.FilterSet):
         if value is None:
             return queryset
         return queryset.open(value)
+
+
+class ManageImportRequestFilterSet(filters.FilterSet):
+    q = fields.SearchFilter(
+        search_fields=["user__username", "albums", "artist_name", "comment"]
+    )
+
+    class Meta:
+        model = requests_models.ImportRequest
+        fields = ["q", "status"]
diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py
index c639d3a3..db5b9272 100644
--- a/api/funkwhale_api/manage/serializers.py
+++ b/api/funkwhale_api/manage/serializers.py
@@ -1,8 +1,10 @@
 from django.db import transaction
+from django.utils import timezone
 from rest_framework import serializers
 
 from funkwhale_api.common import serializers as common_serializers
 from funkwhale_api.music import models as music_models
+from funkwhale_api.requests import models as requests_models
 from funkwhale_api.users import models as users_models
 
 from . import filters
@@ -154,9 +156,79 @@ class ManageInvitationSerializer(serializers.ModelSerializer):
 
 
 class ManageInvitationActionSerializer(common_serializers.ActionSerializer):
-    actions = [common_serializers.Action("delete", allow_all=False)]
+    actions = [
+        common_serializers.Action(
+            "delete", allow_all=False, qs_filter=lambda qs: qs.open()
+        )
+    ]
     filterset_class = filters.ManageInvitationFilterSet
 
     @transaction.atomic
     def handle_delete(self, objects):
         return objects.delete()
+
+
+class ManageImportRequestSerializer(serializers.ModelSerializer):
+    user = ManageUserSimpleSerializer(required=False)
+
+    class Meta:
+        model = requests_models.ImportRequest
+        fields = [
+            "id",
+            "status",
+            "creation_date",
+            "imported_date",
+            "user",
+            "albums",
+            "artist_name",
+            "comment",
+        ]
+        read_only_fields = [
+            "id",
+            "status",
+            "creation_date",
+            "imported_date",
+            "user",
+            "albums",
+            "artist_name",
+            "comment",
+        ]
+
+    def validate_code(self, value):
+        if not value:
+            return value
+        if users_models.Invitation.objects.filter(code__iexact=value).exists():
+            raise serializers.ValidationError(
+                "An invitation with this code already exists"
+            )
+        return value
+
+
+class ManageImportRequestActionSerializer(common_serializers.ActionSerializer):
+    actions = [
+        common_serializers.Action(
+            "mark_closed",
+            allow_all=True,
+            qs_filter=lambda qs: qs.filter(status__in=["pending", "accepted"]),
+        ),
+        common_serializers.Action(
+            "mark_imported",
+            allow_all=True,
+            qs_filter=lambda qs: qs.filter(status__in=["pending", "accepted"]),
+        ),
+        common_serializers.Action("delete", allow_all=False),
+    ]
+    filterset_class = filters.ManageImportRequestFilterSet
+
+    @transaction.atomic
+    def handle_delete(self, objects):
+        return objects.delete()
+
+    @transaction.atomic
+    def handle_mark_closed(self, objects):
+        return objects.update(status="closed")
+
+    @transaction.atomic
+    def handle_mark_imported(self, objects):
+        now = timezone.now()
+        return objects.update(status="imported", imported_date=now)
diff --git a/api/funkwhale_api/manage/urls.py b/api/funkwhale_api/manage/urls.py
index 3d4e15db..8285ade0 100644
--- a/api/funkwhale_api/manage/urls.py
+++ b/api/funkwhale_api/manage/urls.py
@@ -5,6 +5,10 @@ from . import views
 
 library_router = routers.SimpleRouter()
 library_router.register(r"track-files", views.ManageTrackFileViewSet, "track-files")
+requests_router = routers.SimpleRouter()
+requests_router.register(
+    r"import-requests", views.ManageImportRequestViewSet, "import-requests"
+)
 users_router = routers.SimpleRouter()
 users_router.register(r"users", views.ManageUserViewSet, "users")
 users_router.register(r"invitations", views.ManageInvitationViewSet, "invitations")
@@ -12,4 +16,7 @@ users_router.register(r"invitations", views.ManageInvitationViewSet, "invitation
 urlpatterns = [
     url(r"^library/", include((library_router.urls, "instance"), namespace="library")),
     url(r"^users/", include((users_router.urls, "instance"), namespace="users")),
+    url(
+        r"^requests/", include((requests_router.urls, "instance"), namespace="requests")
+    ),
 ]
diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py
index ae3c08a5..89d2afe4 100644
--- a/api/funkwhale_api/manage/views.py
+++ b/api/funkwhale_api/manage/views.py
@@ -3,6 +3,7 @@ from rest_framework.decorators import list_route
 
 from funkwhale_api.common import preferences
 from funkwhale_api.music import models as music_models
+from funkwhale_api.requests import models as requests_models
 from funkwhale_api.users import models as users_models
 from funkwhale_api.users.permissions import HasUserPermission
 
@@ -10,10 +11,7 @@ from . import filters, serializers
 
 
 class ManageTrackFileViewSet(
-    mixins.ListModelMixin,
-    mixins.RetrieveModelMixin,
-    mixins.DestroyModelMixin,
-    viewsets.GenericViewSet,
+    mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
 ):
     queryset = (
         music_models.TrackFile.objects.all()
@@ -69,7 +67,6 @@ class ManageInvitationViewSet(
     mixins.ListModelMixin,
     mixins.RetrieveModelMixin,
     mixins.UpdateModelMixin,
-    mixins.DestroyModelMixin,
     viewsets.GenericViewSet,
 ):
     queryset = (
@@ -96,3 +93,31 @@ class ManageInvitationViewSet(
         serializer.is_valid(raise_exception=True)
         result = serializer.save()
         return response.Response(result, status=200)
+
+
+class ManageImportRequestViewSet(
+    mixins.ListModelMixin,
+    mixins.RetrieveModelMixin,
+    mixins.UpdateModelMixin,
+    viewsets.GenericViewSet,
+):
+    queryset = (
+        requests_models.ImportRequest.objects.all()
+        .order_by("-id")
+        .select_related("user")
+    )
+    serializer_class = serializers.ManageImportRequestSerializer
+    filter_class = filters.ManageImportRequestFilterSet
+    permission_classes = (HasUserPermission,)
+    required_permissions = ["library"]
+    ordering_fields = ["creation_date", "imported_date"]
+
+    @list_route(methods=["post"])
+    def action(self, request, *args, **kwargs):
+        queryset = self.get_queryset()
+        serializer = serializers.ManageImportRequestActionSerializer(
+            request.data, queryset=queryset
+        )
+        serializer.is_valid(raise_exception=True)
+        result = serializer.save()
+        return response.Response(result, status=200)
diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py
index 8b638ce7..4f5e3dfc 100644
--- a/api/funkwhale_api/music/models.py
+++ b/api/funkwhale_api/music/models.py
@@ -539,7 +539,7 @@ class ImportBatch(models.Model):
         related_name="import_batches",
         null=True,
         blank=True,
-        on_delete=models.CASCADE,
+        on_delete=models.SET_NULL,
     )
 
     class Meta:
diff --git a/api/tests/common/test_serializers.py b/api/tests/common/test_serializers.py
index dbbd38a0..e07bf8e8 100644
--- a/api/tests/common/test_serializers.py
+++ b/api/tests/common/test_serializers.py
@@ -32,7 +32,11 @@ class TestDangerousSerializer(serializers.ActionSerializer):
 
 
 class TestDeleteOnlyInactiveSerializer(serializers.ActionSerializer):
-    actions = [serializers.Action("test", allow_all=True, filters={"is_active": False})]
+    actions = [
+        serializers.Action(
+            "test", allow_all=True, qs_filter=lambda qs: qs.filter(is_active=False)
+        )
+    ]
     filterset_class = TestActionFilterSet
 
     def handle_test(self, objects):
diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py
index 2f0c6bc2..9742b098 100644
--- a/api/tests/manage/test_serializers.py
+++ b/api/tests/manage/test_serializers.py
@@ -31,3 +31,44 @@ def test_user_update_permission(factories):
     assert user.permission_upload is True
     assert user.permission_library is False
     assert user.permission_settings is True
+
+
+def test_manage_import_request_mark_closed(factories):
+    affected = factories["requests.ImportRequest"].create_batch(
+        size=5, status="pending"
+    )
+    # we do not update imported requests
+    factories["requests.ImportRequest"].create_batch(size=5, status="imported")
+    s = serializers.ManageImportRequestActionSerializer(
+        queryset=affected[0].__class__.objects.all(),
+        data={"objects": "all", "action": "mark_closed"},
+    )
+
+    assert s.is_valid(raise_exception=True) is True
+    s.save()
+
+    assert affected[0].__class__.objects.filter(status="imported").count() == 5
+    for ir in affected:
+        ir.refresh_from_db()
+        assert ir.status == "closed"
+
+
+def test_manage_import_request_mark_imported(factories, now):
+    affected = factories["requests.ImportRequest"].create_batch(
+        size=5, status="pending"
+    )
+    # we do not update closed requests
+    factories["requests.ImportRequest"].create_batch(size=5, status="closed")
+    s = serializers.ManageImportRequestActionSerializer(
+        queryset=affected[0].__class__.objects.all(),
+        data={"objects": "all", "action": "mark_imported"},
+    )
+
+    assert s.is_valid(raise_exception=True) is True
+    s.save()
+
+    assert affected[0].__class__.objects.filter(status="closed").count() == 5
+    for ir in affected:
+        ir.refresh_from_db()
+        assert ir.status == "imported"
+        assert ir.imported_date == now
diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py
index d54fca5d..baf816fc 100644
--- a/api/tests/manage/test_views.py
+++ b/api/tests/manage/test_views.py
@@ -10,6 +10,7 @@ from funkwhale_api.manage import serializers, views
         (views.ManageTrackFileViewSet, ["library"], "and"),
         (views.ManageUserViewSet, ["settings"], "and"),
         (views.ManageInvitationViewSet, ["settings"], "and"),
+        (views.ManageImportRequestViewSet, ["library"], "and"),
     ],
 )
 def test_permissions(assert_user_permission, view, permissions, operator):
@@ -63,3 +64,15 @@ def test_invitation_view_create(factories, superuser_api_client, mocker):
 
     assert response.status_code == 201
     assert superuser_api_client.user.invitations.latest("id") is not None
+
+
+def test_music_requests_view(factories, superuser_api_client, mocker):
+    invitations = factories["requests.ImportRequest"].create_batch(size=5)
+    qs = invitations[0].__class__.objects.order_by("-id")
+    url = reverse("api:v1:manage:requests:import-requests-list")
+
+    response = superuser_api_client.get(url, {"sort": "-id"})
+    expected = serializers.ManageImportRequestSerializer(qs, many=True).data
+
+    assert response.data["count"] == len(invitations)
+    assert response.data["results"] == expected
-- 
GitLab