diff --git a/api/funkwhale_api/common/serializers.py b/api/funkwhale_api/common/serializers.py index b995afcaa0e28cf32f1746a198a46e05d0e5e466..161c581025da4c68d33de0277d956ed710f1b4ed 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 5f83ebf1a3143e201891448e2f43206c14015d7c..8098ef1a2f49ee8b6275351a56faae77eb7e8dd6 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 c639d3a3c2ad26301c203df9c2f393df56f8e926..db5b9272675a6ec8fc3461ebaf69da3ef1cd5088 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 3d4e15db9327855ff4df5f983fe8a70dba26d452..8285ade0699b45e49cc45e654bfa1baa467406ee 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 ae3c08a57c829dbc8330c37568a3043fa5f8484e..89d2afe4593f5fe0c34118af9976e43307feaf05 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 8b638ce7daff025cd7d68eaba2d93dcb7ec1f562..4f5e3dfc66b7b83247d9dc628176739c29498caf 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 dbbd38a0dc442bc04358eef8e14ff4e2a89310a9..e07bf8e826bfec8ebe77de5be57b1b7ce9f2d553 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 2f0c6bc2568e4a2a7c9e9755a00eda50ecd9dd92..9742b098d2026bd5c0da09b810bc29bea9f91a50 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 d54fca5ddafe3b570bf0d671846587b8132c4f77..baf816fc860ba7d2dbc3b1f6dbdfdc8d2187b541 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