From ca056f717e97a6aeff2fa57dd214c2894bbd77f0 Mon Sep 17 00:00:00 2001 From: Agate Date: Tue, 21 Jul 2020 14:32:59 +0200 Subject: [PATCH] Fix #1116: Can now filter subscribed content through API --- api/funkwhale_api/common/filters.py | 71 +++++++++++++++++++--------- api/funkwhale_api/music/filters.py | 4 +- api/tests/common/test_filters.py | 21 ++++++-- changes/changelog.d/1116.enhancement | 1 + docs/api/parameters.yml | 5 ++ 5 files changed, 75 insertions(+), 27 deletions(-) create mode 100644 changes/changelog.d/1116.enhancement diff --git a/api/funkwhale_api/common/filters.py b/api/funkwhale_api/common/filters.py index dec4a89ab..2548bcfb9 100644 --- a/api/funkwhale_api/common/filters.py +++ b/api/funkwhale_api/common/filters.py @@ -7,6 +7,7 @@ from django_filters import rest_framework as filters from . import fields from . import models from . import search +from . import utils class NoneObject(object): @@ -170,13 +171,17 @@ class MutationFilter(filters.FilterSet): fields = ["is_approved", "is_applied", "type"] +class EmptyQuerySet(ValueError): + pass + + class ActorScopeFilter(filters.CharFilter): def __init__(self, *args, **kwargs): self.actor_field = kwargs.pop("actor_field") + self.library_field = kwargs.pop("library_field", None) super().__init__(*args, **kwargs) def filter(self, queryset, value): - from funkwhale_api.federation import models as federation_models if not value: return queryset @@ -186,35 +191,57 @@ class ActorScopeFilter(filters.CharFilter): return queryset.none() user = getattr(request, "user", None) - qs = queryset - if value.lower() == "me": - qs = self.filter_me(user=user, queryset=queryset) - elif value.lower() == "all": - return queryset - elif value.lower().startswith("actor:"): - full_username = value.split("actor:", 1)[1] + actor = getattr(user, "actor", None) + scopes = [v.strip().lower() for v in value.split(",")] + query = None + for scope in scopes: + try: + right_query = self.get_query(scope, user, actor) + except ValueError: + return queryset.none() + query = utils.join_queries_or(query, right_query) + + return queryset.filter(query).distinct() + + def get_query(self, scope, user, actor): + from funkwhale_api.federation import models as federation_models + + if scope == "me": + return self.filter_me(actor) + elif scope == "all": + return Q(pk__gte=0) + + elif scope == "subscribed": + if not actor or self.library_field is None: + raise EmptyQuerySet() + followed_libraries = federation_models.LibraryFollow.objects.filter( + approved=True, actor=user.actor + ).values_list("target_id", flat=True) + if not self.library_field: + predicate = "pk__in" + else: + predicate = "{}__in".format(self.library_field) + return Q(**{predicate: followed_libraries}) + + elif scope.startswith("actor:"): + full_username = scope.split("actor:", 1)[1] username, domain = full_username.split("@") try: actor = federation_models.Actor.objects.get( preferred_username=username, domain_id=domain, ) except federation_models.Actor.DoesNotExist: - return queryset.none() + raise EmptyQuerySet() - return queryset.filter(**{self.actor_field: actor}) - elif value.lower().startswith("domain:"): - domain = value.split("domain:", 1)[1] - return queryset.filter(**{"{}__domain_id".format(self.actor_field): domain}) + return Q(**{self.actor_field: actor}) + elif scope.startswith("domain:"): + domain = scope.split("domain:", 1)[1] + return Q(**{"{}__domain_id".format(self.actor_field): domain}) else: - return queryset.none() + raise EmptyQuerySet() - if self.distinct: - qs = qs.distinct() - return qs - - def filter_me(self, user, queryset): - actor = getattr(user, "actor", None) + def filter_me(self, actor): if not actor: - return queryset.none() + raise EmptyQuerySet() - return queryset.filter(**{self.actor_field: actor}) + return Q(**{self.actor_field: actor}) diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py index feebaa542..51ef1f0d7 100644 --- a/api/funkwhale_api/music/filters.py +++ b/api/funkwhale_api/music/filters.py @@ -148,7 +148,9 @@ class TrackFilter( tag = TAG_FILTER id = common_filters.MultipleQueryFilter(coerce=int) scope = common_filters.ActorScopeFilter( - actor_field="uploads__library__actor", distinct=True + actor_field="uploads__library__actor", + library_field="uploads__library", + distinct=True, ) artist = filters.ModelChoiceFilter( field_name="_", method="filter_artist", queryset=models.Artist.objects.all() diff --git a/api/tests/common/test_filters.py b/api/tests/common/test_filters.py index 6e8a7c354..bc155ac8c 100644 --- a/api/tests/common/test_filters.py +++ b/api/tests/common/test_filters.py @@ -44,15 +44,20 @@ def test_mutation_filter_is_approved(value, expected, factories): ("me", 0, [0]), ("me", 1, [1]), ("me", 2, []), - ("all", 0, [0, 1, 2]), - ("all", 1, [0, 1, 2]), - ("all", 2, [0, 1, 2]), + ("all", 0, [0, 1, 2, 3]), + ("all", 1, [0, 1, 2, 3]), + ("all", 2, [0, 1, 2, 3]), ("noop", 0, []), ("noop", 1, []), ("noop", 2, []), ("actor:actor1@domain.test", 0, [0]), ("actor:actor2@domain.test", 0, [1]), ("domain:domain.test", 0, [0, 1]), + ("subscribed", 0, [3]), + ("subscribed", 1, []), + ("subscribed", 2, []), + ("me,subscribed", 0, [0, 3]), + ("me,subscribed", 1, [1]), ], ) def test_actor_scope_filter( @@ -72,15 +77,23 @@ def test_actor_scope_filter( preferred_username="actor2", domain=domain ) users = [actor1.user, actor2.user, anonymous_user] + followed_library = factories["music.Library"]() tracks = [ factories["music.Upload"](library__actor=actor1, playable=True).track, factories["music.Upload"](library__actor=actor2, playable=True).track, factories["music.Upload"](playable=True).track, + factories["music.Upload"](playable=True, library=followed_library).track, ] + factories["federation.LibraryFollow"]( + actor=actor1, target=followed_library, approved=True + ) + class FS(filters.filters.FilterSet): scope = filters.ActorScopeFilter( - actor_field="uploads__library__actor", distinct=True + actor_field="uploads__library__actor", + library_field="uploads__library", + distinct=True, ) class Meta: diff --git a/changes/changelog.d/1116.enhancement b/changes/changelog.d/1116.enhancement new file mode 100644 index 000000000..d8c4cfbad --- /dev/null +++ b/changes/changelog.d/1116.enhancement @@ -0,0 +1 @@ +Can now filter subscribed content through API (#1116) \ No newline at end of file diff --git a/docs/api/parameters.yml b/docs/api/parameters.yml index e8fdd076b..18313b358 100644 --- a/docs/api/parameters.yml +++ b/docs/api/parameters.yml @@ -76,14 +76,19 @@ Scope: Limit the results to a given user or pod: - Use `all` (or do not specify the property to disable scope filtering) - Use `me` to retrieve content relative to the current user + - Use `subscribed` to retrieve content in libraries you follow - Use `actor:alice@example.com` to retrieve content relative to the account `alice@example.com - Use `domain:example.com` to retrieve content relative to the domain `example.com + + You can specify multiple coma separated scopes, e.g `scope=me,subscribed` to retrieve content matching either scopes. + schema: required: false type: "string" enum: - "me" - "all" + - "subscribed" - "actor:alice@example.com" - "domain:example.com"