diff --git a/api/funkwhale_api/federation/filters.py b/api/funkwhale_api/federation/filters.py index 2803186ba..c911f1a89 100644 --- a/api/funkwhale_api/federation/filters.py +++ b/api/funkwhale_api/federation/filters.py @@ -43,6 +43,7 @@ class LibraryTrackFilter(django_filters.FilterSet): class FollowFilter(django_filters.FilterSet): + pending = django_filters.CharFilter(method='filter_pending') ordering = django_filters.OrderingFilter( # tuple-mapping retains order fields=( @@ -50,9 +51,16 @@ class FollowFilter(django_filters.FilterSet): ('modification_date', 'modification_date'), ), ) + q = fields.SearchFilter(search_fields=[ + 'actor__domain', + 'actor__preferred_username', + ]) class Meta: model = models.Follow - fields = { - 'approved': ['exact'], - } + fields = ['approved', 'pending', 'q'] + + def filter_pending(self, queryset, field_name, value): + if value.lower() in ['true', '1', 'yes']: + queryset = queryset.filter(approved__isnull=True) + return queryset diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py index e6ad0c0be..4964106d8 100644 --- a/api/funkwhale_api/federation/serializers.py +++ b/api/funkwhale_api/federation/serializers.py @@ -190,6 +190,35 @@ class APILibraryScanSerializer(serializers.Serializer): until = serializers.DateTimeField(required=False) +class APILibraryFollowUpdateSerializer(serializers.Serializer): + follow = serializers.IntegerField() + approved = serializers.BooleanField() + + def validate_follow(self, value): + from . import actors + library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + qs = models.Follow.objects.filter( + pk=value, + target=library_actor, + ) + try: + return qs.get() + except models.Follow.DoesNotExist: + raise serializers.ValidationError('Invalid follow') + + def save(self): + new_status = self.validated_data['approved'] + follow = self.validated_data['follow'] + if new_status == follow.approved: + return follow + + follow.approved = new_status + follow.save(update_fields=['approved', 'modification_date']) + if new_status: + activity.accept_follow(follow) + return follow + + class APILibraryCreateSerializer(serializers.ModelSerializer): actor = serializers.URLField() federation_enabled = serializers.BooleanField() @@ -233,8 +262,13 @@ class APILibraryCreateSerializer(serializers.ModelSerializer): library_data = library.get_library_data( acs.validated_data['library_url']) if 'errors' in library_data: - raise serializers.ValidationError(str(library_data['errors'])) + # we pass silently because it may means we require permission + # before scanning + pass validated_data['library'] = library_data + validated_data['library'].setdefault( + 'id', acs.validated_data['library_url'] + ) validated_data['actor'] = actor return validated_data @@ -244,7 +278,7 @@ class APILibraryCreateSerializer(serializers.ModelSerializer): defaults={ 'actor': validated_data['actor'], 'follow': validated_data['follow'], - 'tracks_count': validated_data['library']['totalItems'], + 'tracks_count': validated_data['library'].get('totalItems'), 'federation_enabled': validated_data['federation_enabled'], 'autoimport': validated_data['autoimport'], 'download_files': validated_data['download_files'], diff --git a/api/funkwhale_api/federation/views.py b/api/funkwhale_api/federation/views.py index a3f02a372..381f87eff 100644 --- a/api/funkwhale_api/federation/views.py +++ b/api/funkwhale_api/federation/views.py @@ -221,31 +221,42 @@ class LibraryViewSet( queryset = models.Follow.objects.filter( actor=library_actor ).select_related( - 'target', + 'actor', 'target', ).order_by('-creation_date') filterset = filters.FollowFilter(request.GET, queryset=queryset) - serializer = serializers.APIFollowSerializer(filterset.qs, many=True) + final_qs = filterset.qs + serializer = serializers.APIFollowSerializer(final_qs, many=True) data = { 'results': serializer.data, - 'count': len(filterset.qs), + 'count': len(final_qs), } return response.Response(data) - @list_route(methods=['get']) + @list_route(methods=['get', 'patch']) def followers(self, request, *args, **kwargs): + if request.method.lower() == 'patch': + serializer = serializers.APILibraryFollowUpdateSerializer( + data=request.data) + serializer.is_valid(raise_exception=True) + follow = serializer.save() + return response.Response( + serializers.APIFollowSerializer(follow).data + ) + library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() queryset = models.Follow.objects.filter( target=library_actor ).select_related( - 'target', + 'actor', 'target', ).order_by('-creation_date') filterset = filters.FollowFilter(request.GET, queryset=queryset) - serializer = serializers.APIFollowSerializer(filterset.qs, many=True) + final_qs = filterset.qs + serializer = serializers.APIFollowSerializer(final_qs, many=True) data = { 'results': serializer.data, - 'count': len(filterset.qs), + 'count': len(final_qs), } return response.Response(data) diff --git a/api/tests/federation/test_views.py b/api/tests/federation/test_views.py index 3e5bdf1a5..8c5235b8b 100644 --- a/api/tests/federation/test_views.py +++ b/api/tests/federation/test_views.py @@ -346,3 +346,37 @@ def test_list_library_tracks(factories, superuser_api_client): 'previous': None, 'next': None, } + + +def test_can_update_follow_status(factories, superuser_api_client, mocker): + patched_accept = mocker.patch( + 'funkwhale_api.federation.activity.accept_follow' + ) + library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + follow = factories['federation.Follow'](target=library_actor) + + payload = { + 'follow': follow.pk, + 'approved': True + } + url = reverse('api:v1:federation:libraries-followers') + response = superuser_api_client.patch(url, payload) + follow.refresh_from_db() + + assert response.status_code == 200 + assert follow.approved is True + patched_accept.assert_called_once_with(follow) + + +def test_can_filter_pending_follows(factories, superuser_api_client): + library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + follow = factories['federation.Follow']( + target=library_actor, + approved=True) + + params = {'pending': True} + url = reverse('api:v1:federation:libraries-followers') + response = superuser_api_client.get(url, params) + + assert response.status_code == 200 + assert len(response.data['results']) == 0 diff --git a/front/src/components/common/DangerousButton.vue b/front/src/components/common/DangerousButton.vue index 910209b9d..690291d5b 100644 --- a/front/src/components/common/DangerousButton.vue +++ b/front/src/components/common/DangerousButton.vue @@ -26,7 +26,7 @@ import Modal from '@/components/semantic/Modal' export default { props: { - action: {type: Function, required: true}, + action: {type: Function, required: false}, disabled: {type: Boolean, default: false}, color: {type: String, default: 'red'} }, @@ -41,7 +41,10 @@ export default { methods: { confirm () { this.showModal = false - this.action() + this.$emit('confirm') + if (this.action) { + this.action() + } } } } diff --git a/front/src/components/federation/LibraryCard.vue b/front/src/components/federation/LibraryCard.vue index 267d41bd0..a5579c125 100644 --- a/front/src/components/federation/LibraryCard.vue +++ b/front/src/components/federation/LibraryCard.vue @@ -15,7 +15,7 @@ Open - + {{ totalItems }} tracks @@ -25,10 +25,6 @@ Follow request pending approval -
+
+
+
+
+ +
+
+
+ + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ActorCreation dateStatusActions
+ {{ follow.actor.preferred_username }}@{{ follow.actor.domain }} + + + + + + + + + Deny +

Deny access?

+

By confirming, {{ follow.actor.preferred_username }}@{{ follow.actor.domain }} will be denied access to your library.

+

Deny

+
+ + Approve +

Approve access?

+

By confirming, {{ follow.actor.preferred_username }}@{{ follow.actor.domain }} will be granted access to your library.

+

Approve

+
+
+ + + Showing results {{ ((page-1) * paginateBy) + 1 }}-{{ ((page-1) * paginateBy) + result.results.length }} on {{ result.count }}
+
+ + + diff --git a/front/src/components/federation/LibraryTrackTable.vue b/front/src/components/federation/LibraryTrackTable.vue index e5255252e..6404f3990 100644 --- a/front/src/components/federation/LibraryTrackTable.vue +++ b/front/src/components/federation/LibraryTrackTable.vue @@ -64,13 +64,19 @@ > - Showing results {{ ((page-1) * paginateBy) + 1 }}-{{ ((page-1) * paginateBy) + result.results.length }} on {{ result.count }} + + Showing results {{ ((page-1) * paginateBy) + 1 }}-{{ ((page-1) * paginateBy) + result.results.length }} on {{ result.count }} + + Import #{{ importBatch.id }} launched + @@ -104,7 +110,8 @@ export default { paginateBy: 25, search: '', checked: {}, - isImporting: false + isImporting: false, + importBatch: null } }, created () { @@ -135,6 +142,7 @@ export default { library_tracks: this.checked } axios.post('/submit/federation/', payload).then((response) => { + self.importBatch = response.data self.isImporting = false self.fetchData() }, error => { diff --git a/front/src/router/index.js b/front/src/router/index.js index 0ef3dcf24..a2bf78195 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -30,6 +30,7 @@ import FederationScan from '@/views/federation/Scan' import FederationLibraryDetail from '@/views/federation/LibraryDetail' import FederationLibraryList from '@/views/federation/LibraryList' import FederationTrackList from '@/views/federation/LibraryTrackList' +import FederationFollowersList from '@/views/federation/LibraryFollowersList' Vue.use(Router) @@ -118,6 +119,17 @@ export default new Router({ defaultPage: route.query.page }) }, + { + path: 'followers', + name: 'federation.followers.list', + component: FederationFollowersList, + props: (route) => ({ + defaultOrdering: route.query.ordering, + defaultQuery: route.query.query, + defaultPaginateBy: route.query.paginateBy, + defaultPage: route.query.page + }) + }, { path: 'libraries/:id', name: 'federation.libraries.detail', component: FederationLibraryDetail, props: true } ] }, diff --git a/front/src/views/federation/Base.vue b/front/src/views/federation/Base.vue index b90ba723a..7958bb36b 100644 --- a/front/src/views/federation/Base.vue +++ b/front/src/views/federation/Base.vue @@ -7,10 +7,39 @@ Tracks +
+