From d50cce36e2537af3732d6908805e02dac193f63f Mon Sep 17 00:00:00 2001 From: Agate Date: Mon, 22 Jun 2020 14:39:50 +0200 Subject: [PATCH] Added a new ?related=obj_id filter for artists, albums and tracks, based on tags --- api/funkwhale_api/music/filters.py | 29 ++++++++++++++++ api/funkwhale_api/tags/filters.py | 18 ++++++++++ api/tests/music/test_filters.py | 38 +++++++++++++++++++++ changes/changelog.d/1145.enhancement | 1 + changes/changelog.d/api-related.enhancement | 1 + 5 files changed, 87 insertions(+) create mode 100644 changes/changelog.d/1145.enhancement create mode 100644 changes/changelog.d/api-related.enhancement diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py index 7f09924d1..feebaa542 100644 --- a/api/funkwhale_api/music/filters.py +++ b/api/funkwhale_api/music/filters.py @@ -9,6 +9,7 @@ from funkwhale_api.common import fields from funkwhale_api.common import filters as common_filters from funkwhale_api.common import search from funkwhale_api.moderation import filters as moderation_filters +from funkwhale_api.tags import filters as tags_filters from . import models from . import utils @@ -24,6 +25,28 @@ def filter_tags(queryset, name, value): TAG_FILTER = common_filters.MultipleQueryFilter(method=filter_tags) +class RelatedFilterSet(filters.FilterSet): + related_type = int + related_field = "pk" + related = filters.CharFilter(field_name="_", method="filter_related") + + def filter_related(self, queryset, name, value): + if not value: + return queryset.none() + try: + pk = self.related_type(value) + except (TypeError, ValueError): + return queryset.none() + + try: + obj = queryset.model.objects.get(**{self.related_field: pk}) + except queryset.model.DoesNotExist: + return queryset.none() + + queryset = queryset.exclude(pk=obj.pk) + return tags_filters.get_by_similar_tags(queryset, obj.get_tags()) + + class ChannelFilterSet(filters.FilterSet): channel = filters.CharFilter(field_name="_", method="filter_channel") @@ -70,6 +93,7 @@ class LibraryFilterSet(filters.FilterSet): class ArtistFilter( + RelatedFilterSet, LibraryFilterSet, audio_filters.IncludeChannelsFilterSet, moderation_filters.HiddenContentFilterSet, @@ -88,6 +112,7 @@ class ArtistFilter( ("creation_date", "creation_date"), ("modification_date", "modification_date"), ("?", "random"), + ("tag_matches", "related"), ) ) @@ -109,6 +134,7 @@ class ArtistFilter( class TrackFilter( + RelatedFilterSet, ChannelFilterSet, LibraryFilterSet, audio_filters.IncludeChannelsFilterSet, @@ -140,6 +166,7 @@ class TrackFilter( ("artist__name", "artist__name"), ("artist__modification_date", "artist__modification_date"), ("?", "random"), + ("tag_matches", "related"), ) ) @@ -217,6 +244,7 @@ class UploadFilter(audio_filters.IncludeChannelsFilterSet): class AlbumFilter( + RelatedFilterSet, ChannelFilterSet, LibraryFilterSet, audio_filters.IncludeChannelsFilterSet, @@ -239,6 +267,7 @@ class AlbumFilter( ("title", "title"), ("artist__modification_date", "artist__modification_date"), ("?", "random"), + ("tag_matches", "related"), ) ) diff --git a/api/funkwhale_api/tags/filters.py b/api/funkwhale_api/tags/filters.py index e0ac9675a..c41ace91b 100644 --- a/api/funkwhale_api/tags/filters.py +++ b/api/funkwhale_api/tags/filters.py @@ -1,3 +1,5 @@ +from django.db import models as dj_models + import django_filters from django_filters import rest_framework as filters @@ -19,3 +21,19 @@ class TagFilter(filters.FilterSet): class Meta: model = models.Tag fields = {"q": ["exact"], "name": ["exact", "startswith"]} + + +def get_by_similar_tags(qs, tags): + """ + Return a queryset of obects with at least one matching tag. + Annotate the queryset so you can order later by number of matches. + """ + qs = qs.filter(tagged_items__tag__name__in=tags).annotate( + tag_matches=dj_models.Count( + dj_models.Case( + dj_models.When(tagged_items__tag__name__in=tags, then=1), + output_field=dj_models.IntegerField(), + ) + ) + ) + return qs.distinct() diff --git a/api/tests/music/test_filters.py b/api/tests/music/test_filters.py index 87d8c4816..f078932a8 100644 --- a/api/tests/music/test_filters.py +++ b/api/tests/music/test_filters.py @@ -203,3 +203,41 @@ def test_track_filter_artist_includes_album_artist( ) assert filterset.qs == [track2, track1] + + +@pytest.mark.parametrize( + "factory_name, filterset_class", + [ + ("music.Track", filters.TrackFilter), + ("music.Artist", filters.ArtistFilter), + ("music.Album", filters.AlbumFilter), + ], +) +def test_filter_tag_related( + factory_name, + filterset_class, + factories, + anonymous_user, + queryset_equal_list, + mocker, +): + factories["tags.Tag"](name="foo") + factories["tags.Tag"](name="bar") + factories["tags.Tag"](name="baz") + factories["tags.Tag"]() + factories["tags.Tag"]() + + matches = [ + factories[factory_name](set_tags=["foo", "bar", "baz", "noop"]), + factories[factory_name](set_tags=["foo", "baz", "noop"]), + factories[factory_name](set_tags=["baz", "noop"]), + ] + factories[factory_name](set_tags=["noop"]), + obj = factories[factory_name](set_tags=["foo", "bar", "baz"]) + + filterset = filterset_class( + {"related": obj.pk, "ordering": "-related"}, + request=mocker.Mock(user=anonymous_user, actor=None), + queryset=obj.__class__.objects.all(), + ) + assert filterset.qs == matches diff --git a/changes/changelog.d/1145.enhancement b/changes/changelog.d/1145.enhancement new file mode 100644 index 000000000..eefc8d46d --- /dev/null +++ b/changes/changelog.d/1145.enhancement @@ -0,0 +1 @@ +Support ordering=random for artists, albums, tracks and channels endpoints (#1145) diff --git a/changes/changelog.d/api-related.enhancement b/changes/changelog.d/api-related.enhancement new file mode 100644 index 000000000..82be1c0f0 --- /dev/null +++ b/changes/changelog.d/api-related.enhancement @@ -0,0 +1 @@ +Added a new ?related=obj_id filter for artists, albums and tracks, based on tags