diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 7a450e4f5..48e5982da 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -62,9 +62,32 @@ class ActorQuerySet(models.QuerySet): return qs +class DomainQuerySet(models.QuerySet): + def external(self): + return self.exclude(pk=settings.FEDERATION_HOSTNAME) + + def with_last_activity_date(self): + activities = Activity.objects.filter( + actor__domain=models.OuterRef("pk") + ).order_by("-creation_date") + + return self.annotate( + last_activity_date=models.Subquery(activities.values("creation_date")[:1]) + ) + + def with_actors_count(self): + return self.annotate(actors_count=models.Count("actors", distinct=True)) + + def with_outbox_activities_count(self): + return self.annotate( + outbox_activities_count=models.Count("actors__outbox_activities") + ) + + class Domain(models.Model): name = models.CharField(primary_key=True, max_length=255) creation_date = models.DateTimeField(default=timezone.now) + objects = DomainQuerySet.as_manager() def __str__(self): return self.name diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py index 4347b4cc4..d9b9bfc1d 100644 --- a/api/funkwhale_api/manage/filters.py +++ b/api/funkwhale_api/manage/filters.py @@ -1,6 +1,7 @@ from django_filters import rest_framework as filters from funkwhale_api.common import fields +from funkwhale_api.federation import models as federation_models from funkwhale_api.music import models as music_models from funkwhale_api.users import models as users_models @@ -20,6 +21,14 @@ class ManageUploadFilterSet(filters.FilterSet): fields = ["q", "track__album", "track__artist", "track"] +class ManageDomainFilterSet(filters.FilterSet): + q = fields.SearchFilter(search_fields=["name"]) + + class Meta: + model = federation_models.Domain + fields = ["name"] + + class ManageUserFilterSet(filters.FilterSet): q = fields.SearchFilter(search_fields=["username", "email", "name"]) diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py index 9b5e24f66..8686a99b9 100644 --- a/api/funkwhale_api/manage/serializers.py +++ b/api/funkwhale_api/manage/serializers.py @@ -3,6 +3,7 @@ from django.db import transaction from rest_framework import serializers from funkwhale_api.common import serializers as common_serializers +from funkwhale_api.federation import models as federation_models from funkwhale_api.music import models as music_models from funkwhale_api.users import models as users_models @@ -168,3 +169,28 @@ class ManageInvitationActionSerializer(common_serializers.ActionSerializer): @transaction.atomic def handle_delete(self, objects): return objects.delete() + + +class ManageDomainSerializer(serializers.ModelSerializer): + actors_count = serializers.SerializerMethodField() + last_activity_date = serializers.SerializerMethodField() + outbox_activities_count = serializers.SerializerMethodField() + + class Meta: + model = federation_models.Domain + fields = [ + "name", + "creation_date", + "actors_count", + "last_activity_date", + "outbox_activities_count", + ] + + def get_actors_count(self, o): + return getattr(o, "actors_count", 0) + + def get_last_activity_date(self, o): + return getattr(o, "last_activity_date", None) + + def get_outbox_activities_count(self, o): + return getattr(o, "outbox_activities_count", 0) diff --git a/api/funkwhale_api/manage/urls.py b/api/funkwhale_api/manage/urls.py index 9f5503978..26832f946 100644 --- a/api/funkwhale_api/manage/urls.py +++ b/api/funkwhale_api/manage/urls.py @@ -3,6 +3,8 @@ from rest_framework import routers from . import views +federation_router = routers.SimpleRouter() +federation_router.register(r"domains", views.ManageDomainViewSet, "domains") library_router = routers.SimpleRouter() library_router.register(r"uploads", views.ManageUploadViewSet, "uploads") users_router = routers.SimpleRouter() @@ -10,6 +12,10 @@ users_router.register(r"users", views.ManageUserViewSet, "users") users_router.register(r"invitations", views.ManageInvitationViewSet, "invitations") urlpatterns = [ + url( + r"^federation/", + include((federation_router.urls, "federation"), namespace="federation"), + ), url(r"^library/", include((library_router.urls, "instance"), namespace="library")), url(r"^users/", include((users_router.urls, "instance"), namespace="users")), ] diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py index bfd5b2ef2..30f7179e8 100644 --- a/api/funkwhale_api/manage/views.py +++ b/api/funkwhale_api/manage/views.py @@ -2,6 +2,7 @@ from rest_framework import mixins, response, viewsets from rest_framework.decorators import list_route from funkwhale_api.common import preferences +from funkwhale_api.federation import models as federation_models from funkwhale_api.music import models as music_models from funkwhale_api.users import models as users_models from funkwhale_api.users.permissions import HasUserPermission @@ -92,3 +93,26 @@ class ManageInvitationViewSet( serializer.is_valid(raise_exception=True) result = serializer.save() return response.Response(result, status=200) + + +class ManageDomainViewSet( + mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet +): + queryset = ( + federation_models.Domain.objects.external() + .with_last_activity_date() + .with_actors_count() + .with_outbox_activities_count() + .order_by("name") + ) + serializer_class = serializers.ManageDomainSerializer + filter_class = filters.ManageDomainFilterSet + permission_classes = (HasUserPermission,) + required_permissions = ["moderation"] + ordering_fields = [ + "name", + "creation_date", + "last_activity_date", + "actors_count", + "outbox_activities_count", + ] diff --git a/api/tests/federation/test_models.py b/api/tests/federation/test_models.py index 18966430e..c3032817c 100644 --- a/api/tests/federation/test_models.py +++ b/api/tests/federation/test_models.py @@ -1,6 +1,8 @@ import pytest from django import db +from funkwhale_api.federation import models + def test_cannot_duplicate_actor(factories): actor = factories["federation.Actor"]() @@ -67,3 +69,11 @@ def test_actor_get_quota(factories): def test_domain_name_saved_properly(value, expected, factories): domain = factories["federation.Domain"](name=value) assert domain.name == expected + + +def test_external_domains(factories, settings): + d1 = factories["federation.Domain"]() + d2 = factories["federation.Domain"]() + settings.FEDERATION_HOSTNAME = d1.pk + + assert list(models.Domain.objects.external()) == [d2] diff --git a/api/tests/manage/test_filters.py b/api/tests/manage/test_filters.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py index 2b46ee839..be02e6727 100644 --- a/api/tests/manage/test_serializers.py +++ b/api/tests/manage/test_serializers.py @@ -21,7 +21,7 @@ def test_user_update_permission(factories): user, data={ "is_active": False, - "permissions": {"federation": False, "upload": True}, + "permissions": {"moderation": True, "settings": False}, "upload_quota": 12, }, ) @@ -33,4 +33,21 @@ def test_user_update_permission(factories): assert user.upload_quota == 12 assert user.permission_moderation is True assert user.permission_library is False - assert user.permission_settings is True + assert user.permission_settings is False + + +def test_manage_domain_serializer(factories, now): + domain = factories["federation.Domain"]() + setattr(domain, "actors_count", 42) + setattr(domain, "outbox_activities_count", 23) + setattr(domain, "last_activity_date", now) + expected = { + "name": domain.name, + "creation_date": domain.creation_date.isoformat().split("+")[0] + "Z", + "last_activity_date": now, + "actors_count": 42, + "outbox_activities_count": 23, + } + s = serializers.ManageDomainSerializer(domain) + + assert s.data == expected diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py index a9920ce07..3d153073a 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.ManageUploadViewSet, ["library"], "and"), (views.ManageUserViewSet, ["settings"], "and"), (views.ManageInvitationViewSet, ["settings"], "and"), + (views.ManageDomainViewSet, ["moderation"], "and"), ], ) def test_permissions(assert_user_permission, view, permissions, operator): @@ -64,3 +65,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_domain_list(factories, superuser_api_client, settings): + factories["federation.Domain"](pk=settings.FEDERATION_HOSTNAME) + d = factories["federation.Domain"]() + url = reverse("api:v1:manage:federation:domains-list") + response = superuser_api_client.get(url) + + assert response.status_code == 200 + + assert response.data["count"] == 1 + assert response.data["results"][0]["name"] == d.pk diff --git a/api/tests/radios/test_filters.py b/api/tests/radios/test_filters.py deleted file mode 100644 index 89bb726af..000000000 --- a/api/tests/radios/test_filters.py +++ /dev/null @@ -1,153 +0,0 @@ -import pytest -from django.core.exceptions import ValidationError - -from funkwhale_api.music.models import Track -from funkwhale_api.radios import filters - - -@filters.registry.register -class NoopFilter(filters.RadioFilter): - code = "noop" - - def get_query(self, candidates, **kwargs): - return - - -def test_most_simple_radio_does_not_filter_anything(factories): - factories["music.Track"].create_batch(3) - radio = factories["radios.Radio"](config=[{"type": "noop"}]) - - assert radio.version == 0 - assert radio.get_candidates().count() == 3 - - -def test_filter_can_use_custom_queryset(factories): - tracks = factories["music.Track"].create_batch(3) - candidates = Track.objects.filter(pk=tracks[0].pk) - - qs = filters.run([{"type": "noop"}], candidates=candidates) - assert qs.count() == 1 - assert qs.first() == tracks[0] - - -def test_filter_on_tag(factories): - tracks = factories["music.Track"].create_batch(3, tags=["metal"]) - factories["music.Track"].create_batch(3, tags=["pop"]) - expected = tracks - f = [{"type": "tag", "names": ["metal"]}] - - candidates = filters.run(f) - assert list(candidates.order_by("pk")) == expected - - -def test_filter_on_artist(factories): - artist1 = factories["music.Artist"]() - artist2 = factories["music.Artist"]() - factories["music.Track"].create_batch(3, artist=artist1) - factories["music.Track"].create_batch(3, artist=artist2) - expected = list(artist1.tracks.order_by("pk")) - f = [{"type": "artist", "ids": [artist1.pk]}] - - candidates = filters.run(f) - assert list(candidates.order_by("pk")) == expected - - -def test_can_combine_with_or(factories): - artist1 = factories["music.Artist"]() - artist2 = factories["music.Artist"]() - artist3 = factories["music.Artist"]() - factories["music.Track"].create_batch(3, artist=artist1) - factories["music.Track"].create_batch(3, artist=artist2) - factories["music.Track"].create_batch(3, artist=artist3) - expected = Track.objects.exclude(artist=artist3).order_by("pk") - f = [ - {"type": "artist", "ids": [artist1.pk]}, - {"type": "artist", "ids": [artist2.pk], "operator": "or"}, - ] - - candidates = filters.run(f) - assert list(candidates.order_by("pk")) == list(expected) - - -def test_can_combine_with_and(factories): - artist1 = factories["music.Artist"]() - artist2 = factories["music.Artist"]() - metal_tracks = factories["music.Track"].create_batch( - 2, artist=artist1, tags=["metal"] - ) - factories["music.Track"].create_batch(2, artist=artist1, tags=["pop"]) - factories["music.Track"].create_batch(3, artist=artist2) - expected = metal_tracks - f = [ - {"type": "artist", "ids": [artist1.pk]}, - {"type": "tag", "names": ["metal"], "operator": "and"}, - ] - - candidates = filters.run(f) - assert list(candidates.order_by("pk")) == list(expected) - - -def test_can_negate(factories): - artist1 = factories["music.Artist"]() - artist2 = factories["music.Artist"]() - factories["music.Track"].create_batch(3, artist=artist1) - factories["music.Track"].create_batch(3, artist=artist2) - expected = artist2.tracks.order_by("pk") - f = [{"type": "artist", "ids": [artist1.pk], "not": True}] - - candidates = filters.run(f) - assert list(candidates.order_by("pk")) == list(expected) - - -def test_can_group(factories): - artist1 = factories["music.Artist"]() - artist2 = factories["music.Artist"]() - factories["music.Track"].create_batch(2, artist=artist1) - t1 = factories["music.Track"].create_batch(2, artist=artist1, tags=["metal"]) - factories["music.Track"].create_batch(2, artist=artist2) - t2 = factories["music.Track"].create_batch(2, artist=artist2, tags=["metal"]) - factories["music.Track"].create_batch(2, tags=["metal"]) - expected = t1 + t2 - f = [ - {"type": "tag", "names": ["metal"]}, - { - "type": "group", - "operator": "and", - "filters": [ - {"type": "artist", "ids": [artist1.pk], "operator": "or"}, - {"type": "artist", "ids": [artist2.pk], "operator": "or"}, - ], - }, - ] - - candidates = filters.run(f) - assert list(candidates.order_by("pk")) == list(expected) - - -def test_artist_filter_clean_config(factories): - artist1 = factories["music.Artist"]() - artist2 = factories["music.Artist"]() - - config = filters.clean_config({"type": "artist", "ids": [artist2.pk, artist1.pk]}) - - expected = { - "type": "artist", - "ids": [artist1.pk, artist2.pk], - "names": [artist1.name, artist2.name], - } - assert filters.clean_config(config) == expected - - -def test_can_check_artist_filter(factories): - artist = factories["music.Artist"]() - - assert filters.validate({"type": "artist", "ids": [artist.pk]}) - with pytest.raises(ValidationError): - filters.validate({"type": "artist", "ids": [artist.pk + 1]}) - - -def test_can_check_operator(): - assert filters.validate({"type": "group", "operator": "or", "filters": []}) - assert filters.validate({"type": "group", "operator": "and", "filters": []}) - with pytest.raises(ValidationError): - assert filters.validate({"type": "group", "operator": "nope", "filters": []}) diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index f072ce808..3a5bf2db8 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -76,19 +76,27 @@ class="item" :to="{name: 'content.index'}">Add content -
+
Administration
diff --git a/front/src/components/common/ActionTable.vue b/front/src/components/common/ActionTable.vue index e8dec339a..5b138a3c6 100644 --- a/front/src/components/common/ActionTable.vue +++ b/front/src/components/common/ActionTable.vue @@ -1,7 +1,7 @@