diff --git a/api/config/api_urls.py b/api/config/api_urls.py index ff6db0d069395c316d207a640e0187ccf92b12df..cab6805b67e394838ec942ecf8b162edb08a88cf 100644 --- a/api/config/api_urls.py +++ b/api/config/api_urls.py @@ -1,5 +1,6 @@ from rest_framework import routers from django.conf.urls import include, url +from funkwhale_api.activity import views as activity_views from funkwhale_api.instance import views as instance_views from funkwhale_api.music import views from funkwhale_api.playlists import views as playlists_views @@ -10,6 +11,7 @@ from dynamic_preferences.users.viewsets import UserPreferencesViewSet router = routers.SimpleRouter() router.register(r'settings', GlobalPreferencesViewSet, base_name='settings') +router.register(r'activity', activity_views.ActivityViewSet, 'activity') router.register(r'tags', views.TagViewSet, 'tags') router.register(r'tracks', views.TrackViewSet, 'tracks') router.register(r'trackfiles', views.TrackFileViewSet, 'trackfiles') diff --git a/api/funkwhale_api/activity/serializers.py b/api/funkwhale_api/activity/serializers.py index 325d1e820db5699abca69b57b3421b0e0ca1d68b..fd9b185cf9a6d3891f0356208f62b2bb54e8686f 100644 --- a/api/funkwhale_api/activity/serializers.py +++ b/api/funkwhale_api/activity/serializers.py @@ -1,5 +1,7 @@ from rest_framework import serializers +from funkwhale_api.activity import record + class ModelSerializer(serializers.ModelSerializer): id = serializers.CharField(source='get_activity_url') @@ -8,3 +10,15 @@ class ModelSerializer(serializers.ModelSerializer): def get_url(self, obj): return self.get_id(obj) + + +class AutoSerializer(serializers.Serializer): + """ + A serializer that will automatically use registered activity serializers + to serialize an henerogeneous list of objects (favorites, listenings, etc.) + """ + def to_representation(self, instance): + serializer = record.registry[instance._meta.label]['serializer']( + instance + ) + return serializer.data diff --git a/api/funkwhale_api/activity/utils.py b/api/funkwhale_api/activity/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..46336930ef693e29d9d5696ee3ccf12446c2bdad --- /dev/null +++ b/api/funkwhale_api/activity/utils.py @@ -0,0 +1,64 @@ +from django.db import models + +from funkwhale_api.common import fields +from funkwhale_api.favorites.models import TrackFavorite +from funkwhale_api.history.models import Listening + + +def combined_recent(limit, **kwargs): + datetime_field = kwargs.pop('datetime_field', 'creation_date') + source_querysets = { + qs.model._meta.label: qs for qs in kwargs.pop('querysets') + } + querysets = { + k: qs.annotate( + __type=models.Value( + qs.model._meta.label, output_field=models.CharField() + ) + ).values('pk', datetime_field, '__type') + for k, qs in source_querysets.items() + } + _qs_list = list(querysets.values()) + union_qs = _qs_list[0].union(*_qs_list[1:]) + records = [] + for row in union_qs.order_by('-{}'.format(datetime_field))[:limit]: + records.append({ + 'type': row['__type'], + 'when': row[datetime_field], + 'pk': row['pk'] + }) + # Now we bulk-load each object type in turn + to_load = {} + for record in records: + to_load.setdefault(record['type'], []).append(record['pk']) + fetched = {} + + for key, pks in to_load.items(): + for item in source_querysets[key].filter(pk__in=pks): + fetched[(key, item.pk)] = item + + # Annotate 'records' with loaded objects + for record in records: + record['object'] = fetched[(record['type'], record['pk'])] + return records + + +def get_activity(user, limit=20): + query = fields.privacy_level_query( + user, lookup_field='user__privacy_level') + querysets = [ + Listening.objects.filter(query).select_related( + 'track', + 'user', + 'track__artist', + 'track__album__artist', + ), + TrackFavorite.objects.filter(query).select_related( + 'track', + 'user', + 'track__artist', + 'track__album__artist', + ), + ] + records = combined_recent(limit=limit, querysets=querysets) + return [r['object'] for r in records] diff --git a/api/funkwhale_api/activity/views.py b/api/funkwhale_api/activity/views.py new file mode 100644 index 0000000000000000000000000000000000000000..e66de1ccfdc94f51cd823fa5c6b104488a4aad7f --- /dev/null +++ b/api/funkwhale_api/activity/views.py @@ -0,0 +1,20 @@ +from rest_framework import viewsets +from rest_framework.response import Response + +from funkwhale_api.common.permissions import ConditionalAuthentication +from funkwhale_api.favorites.models import TrackFavorite + +from . import serializers +from . import utils + + +class ActivityViewSet(viewsets.GenericViewSet): + + serializer_class = serializers.AutoSerializer + permission_classes = [ConditionalAuthentication] + queryset = TrackFavorite.objects.none() + + def list(self, request, *args, **kwargs): + activity = utils.get_activity(user=request.user) + serializer = self.serializer_class(activity, many=True) + return Response({'results': serializer.data}, status=200) diff --git a/api/tests/activity/test_serializers.py b/api/tests/activity/test_serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..792fa74b9cbb3ed778c5e84bd746fb210e738acf --- /dev/null +++ b/api/tests/activity/test_serializers.py @@ -0,0 +1,17 @@ +from funkwhale_api.activity import serializers +from funkwhale_api.favorites.serializers import TrackFavoriteActivitySerializer +from funkwhale_api.history.serializers import \ + ListeningActivitySerializer + + +def test_autoserializer(factories): + favorite = factories['favorites.TrackFavorite']() + listening = factories['history.Listening']() + objects = [favorite, listening] + serializer = serializers.AutoSerializer(objects, many=True) + expected = [ + TrackFavoriteActivitySerializer(favorite).data, + ListeningActivitySerializer(listening).data, + ] + + assert serializer.data == expected diff --git a/api/tests/activity/test_utils.py b/api/tests/activity/test_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..43bb45df84931ccd3ba2a56e555b991627c3a62c --- /dev/null +++ b/api/tests/activity/test_utils.py @@ -0,0 +1,21 @@ +from funkwhale_api.activity import utils + + +def test_get_activity(factories): + user = factories['users.User']() + listening = factories['history.Listening']() + favorite = factories['favorites.TrackFavorite']() + + objects = list(utils.get_activity(user)) + assert objects == [favorite, listening] + + +def test_get_activity_honors_privacy_level(factories, anonymous_user): + listening = factories['history.Listening'](user__privacy_level='me') + favorite1 = factories['favorites.TrackFavorite']( + user__privacy_level='everyone') + favorite2 = factories['favorites.TrackFavorite']( + user__privacy_level='instance') + + objects = list(utils.get_activity(anonymous_user)) + assert objects == [favorite1] diff --git a/api/tests/activity/test_views.py b/api/tests/activity/test_views.py new file mode 100644 index 0000000000000000000000000000000000000000..bdc3c6339ffe91981621c8f8272788347a01cc8e --- /dev/null +++ b/api/tests/activity/test_views.py @@ -0,0 +1,18 @@ +from django.urls import reverse + +from funkwhale_api.activity import serializers +from funkwhale_api.activity import utils + + +def test_activity_view(factories, api_client, settings, anonymous_user): + settings.API_AUTHENTICATION_REQUIRED = False + favorite = factories['favorites.TrackFavorite']( + user__privacy_level='everyone') + listening = factories['history.Listening']() + url = reverse('api:v1:activity-list') + objects = utils.get_activity(anonymous_user) + serializer = serializers.AutoSerializer(objects, many=True) + response = api_client.get(url) + + assert response.status_code == 200 + assert response.data['results'] == serializer.data