From a6da10be41e17fb6c3ab26d2ea4513b886fc94cc Mon Sep 17 00:00:00 2001 From: Eliot Berriot <contact@eliotberriot.com> Date: Thu, 1 Mar 2018 23:41:51 +0100 Subject: [PATCH] API refinements for activity stream --- api/funkwhale_api/activity/serializers.py | 1 + api/funkwhale_api/favorites/serializers.py | 10 +-- api/funkwhale_api/history/activities.py | 19 ++++++ api/funkwhale_api/history/models.py | 5 ++ api/funkwhale_api/history/serializers.py | 28 ++++++++ api/funkwhale_api/history/views.py | 9 ++- api/funkwhale_api/music/serializers.py | 23 +++++++ api/funkwhale_api/users/serializers.py | 2 + api/tests/favorites/test_activity.py | 10 ++- api/tests/history/test_activity.py | 75 ++++++++++++++++++++++ api/tests/{ => history}/test_history.py | 17 ++++- api/tests/users/test_activity.py | 1 + 12 files changed, 188 insertions(+), 12 deletions(-) create mode 100644 api/funkwhale_api/history/activities.py create mode 100644 api/tests/history/test_activity.py rename api/tests/{ => history}/test_history.py (70%) diff --git a/api/funkwhale_api/activity/serializers.py b/api/funkwhale_api/activity/serializers.py index 4b40bb0d..325d1e82 100644 --- a/api/funkwhale_api/activity/serializers.py +++ b/api/funkwhale_api/activity/serializers.py @@ -3,6 +3,7 @@ from rest_framework import serializers class ModelSerializer(serializers.ModelSerializer): id = serializers.CharField(source='get_activity_url') + local_id = serializers.IntegerField(source='id') # url = serializers.SerializerMethodField() def get_url(self, obj): diff --git a/api/funkwhale_api/favorites/serializers.py b/api/funkwhale_api/favorites/serializers.py index 01ad2e47..276b0f6b 100644 --- a/api/funkwhale_api/favorites/serializers.py +++ b/api/funkwhale_api/favorites/serializers.py @@ -4,17 +4,15 @@ from rest_framework import serializers from funkwhale_api.activity import serializers as activity_serializers from funkwhale_api.music.serializers import TrackSerializerNested +from funkwhale_api.music.serializers import TrackActivitySerializer from funkwhale_api.users.serializers import UserActivitySerializer from . import models - - - class TrackFavoriteActivitySerializer(activity_serializers.ModelSerializer): type = serializers.SerializerMethodField() - object = serializers.CharField(source='track.get_activity_url') + object = TrackActivitySerializer(source='track') actor = UserActivitySerializer(source='user') published = serializers.DateTimeField(source='creation_date') @@ -22,6 +20,7 @@ class TrackFavoriteActivitySerializer(activity_serializers.ModelSerializer): model = models.TrackFavorite fields = [ 'id', + 'local_id', 'object', 'type', 'actor', @@ -34,9 +33,6 @@ class TrackFavoriteActivitySerializer(activity_serializers.ModelSerializer): def get_type(self, obj): return 'Like' - def get_object(self, obj): - return obj.track.get_activity_url() - class UserTrackFavoriteSerializer(serializers.ModelSerializer): # track = TrackSerializerNested(read_only=True) diff --git a/api/funkwhale_api/history/activities.py b/api/funkwhale_api/history/activities.py new file mode 100644 index 00000000..e478f9b7 --- /dev/null +++ b/api/funkwhale_api/history/activities.py @@ -0,0 +1,19 @@ +from funkwhale_api.common import channels +from funkwhale_api.activity import record + +from . import serializers + +record.registry.register_serializer( + serializers.ListeningActivitySerializer) + + +@record.registry.register_consumer('history.Listening') +def broadcast_listening_to_instance_activity(data, obj): + if obj.user.privacy_level not in ['instance', 'everyone']: + return + + channels.group_send('instance_activity', { + 'type': 'event.send', + 'text': '', + 'data': data + }) diff --git a/api/funkwhale_api/history/models.py b/api/funkwhale_api/history/models.py index f7f62de6..56310ddc 100644 --- a/api/funkwhale_api/history/models.py +++ b/api/funkwhale_api/history/models.py @@ -25,3 +25,8 @@ class Listening(models.Model): raise ValidationError('Cannot have both session_key and user empty for listening') super().save(**kwargs) + + + def get_activity_url(self): + return '{}/listenings/tracks/{}'.format( + self.user.get_activity_url(), self.pk) diff --git a/api/funkwhale_api/history/serializers.py b/api/funkwhale_api/history/serializers.py index 64bdf41c..7a2280ce 100644 --- a/api/funkwhale_api/history/serializers.py +++ b/api/funkwhale_api/history/serializers.py @@ -1,9 +1,37 @@ from rest_framework import serializers +from funkwhale_api.activity import serializers as activity_serializers from funkwhale_api.music.serializers import TrackSerializerNested +from funkwhale_api.music.serializers import TrackActivitySerializer +from funkwhale_api.users.serializers import UserActivitySerializer + from . import models +class ListeningActivitySerializer(activity_serializers.ModelSerializer): + type = serializers.SerializerMethodField() + object = TrackActivitySerializer(source='track') + actor = UserActivitySerializer(source='user') + published = serializers.DateTimeField(source='end_date') + + class Meta: + model = models.Listening + fields = [ + 'id', + 'local_id', + 'object', + 'type', + 'actor', + 'published' + ] + + def get_actor(self, obj): + return UserActivitySerializer(obj.user).data + + def get_type(self, obj): + return 'Listen' + + class ListeningSerializer(serializers.ModelSerializer): class Meta: diff --git a/api/funkwhale_api/history/views.py b/api/funkwhale_api/history/views.py index 59dcbd26..d5cbe316 100644 --- a/api/funkwhale_api/history/views.py +++ b/api/funkwhale_api/history/views.py @@ -3,8 +3,9 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.decorators import detail_route -from funkwhale_api.music.serializers import TrackSerializerNested +from funkwhale_api.activity import record from funkwhale_api.common.permissions import ConditionalAuthentication +from funkwhale_api.music.serializers import TrackSerializerNested from . import models from . import serializers @@ -17,6 +18,12 @@ class ListeningViewSet(mixins.CreateModelMixin, queryset = models.Listening.objects.all() permission_classes = [ConditionalAuthentication] + def perform_create(self, serializer): + r = super().perform_create(serializer) + if self.request.user.is_authenticated: + record.send(serializer.instance) + return r + def get_queryset(self): queryset = super().get_queryset() if self.request.user.is_authenticated: diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index db6298a9..48419bbe 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -1,6 +1,8 @@ from rest_framework import serializers from taggit.models import Tag +from funkwhale_api.activity import serializers as activity_serializers + from . import models @@ -127,3 +129,24 @@ class ImportBatchSerializer(serializers.ModelSerializer): model = models.ImportBatch fields = ('id', 'jobs', 'status', 'creation_date', 'import_request') read_only_fields = ('creation_date',) + + +class TrackActivitySerializer(activity_serializers.ModelSerializer): + type = serializers.SerializerMethodField() + name = serializers.CharField(source='title') + artist = serializers.CharField(source='artist.name') + album = serializers.CharField(source='album.title') + + class Meta: + model = models.Track + fields = [ + 'id', + 'local_id', + 'name', + 'type', + 'artist', + 'album', + ] + + def get_type(self, obj): + return 'Audio' diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py index 2e873d94..e8adf9ed 100644 --- a/api/funkwhale_api/users/serializers.py +++ b/api/funkwhale_api/users/serializers.py @@ -8,11 +8,13 @@ from . import models class UserActivitySerializer(activity_serializers.ModelSerializer): type = serializers.SerializerMethodField() name = serializers.CharField(source='username') + local_id = serializers.CharField(source='username') class Meta: model = models.User fields = [ 'id', + 'local_id', 'name', 'type' ] diff --git a/api/tests/favorites/test_activity.py b/api/tests/favorites/test_activity.py index 74695ed8..63174f9e 100644 --- a/api/tests/favorites/test_activity.py +++ b/api/tests/favorites/test_activity.py @@ -1,4 +1,5 @@ from funkwhale_api.users.serializers import UserActivitySerializer +from funkwhale_api.music.serializers import TrackActivitySerializer from funkwhale_api.favorites import serializers from funkwhale_api.favorites import activities @@ -18,9 +19,10 @@ def test_activity_favorite_serializer(factories): field = serializers.serializers.DateTimeField() expected = { "type": "Like", + "local_id": favorite.pk, "id": favorite.get_activity_url(), "actor": actor, - "object": favorite.track.get_activity_url(), + "object": TrackActivitySerializer(favorite.track).data, "published": field.to_representation(favorite.creation_date), } @@ -48,7 +50,8 @@ def test_broadcast_track_favorite_to_instance_activity( data = serializers.TrackFavoriteActivitySerializer(favorite).data consumer = activities.broadcast_track_favorite_to_instance_activity message = { - "type": 'event', + "type": 'event.send', + "text": '', "data": data } consumer(data=data, obj=favorite) @@ -64,7 +67,8 @@ def test_broadcast_track_favorite_to_instance_activity_private( data = serializers.TrackFavoriteActivitySerializer(favorite).data consumer = activities.broadcast_track_favorite_to_instance_activity message = { - "type": 'event', + "type": 'event.send', + "text": '', "data": data } consumer(data=data, obj=favorite) diff --git a/api/tests/history/test_activity.py b/api/tests/history/test_activity.py new file mode 100644 index 00000000..b5ab07b8 --- /dev/null +++ b/api/tests/history/test_activity.py @@ -0,0 +1,75 @@ +from funkwhale_api.users.serializers import UserActivitySerializer +from funkwhale_api.music.serializers import TrackActivitySerializer +from funkwhale_api.history import serializers +from funkwhale_api.history import activities + + +def test_get_listening_activity_url(settings, factories): + listening = factories['history.Listening']() + user_url = listening.user.get_activity_url() + expected = '{}/listenings/tracks/{}'.format( + user_url, listening.pk) + assert listening.get_activity_url() == expected + + +def test_activity_listening_serializer(factories): + listening = factories['history.Listening']() + + actor = UserActivitySerializer(listening.user).data + field = serializers.serializers.DateTimeField() + expected = { + "type": "Listen", + "local_id": listening.pk, + "id": listening.get_activity_url(), + "actor": actor, + "object": TrackActivitySerializer(listening.track).data, + "published": field.to_representation(listening.end_date), + } + + data = serializers.ListeningActivitySerializer(listening).data + + assert data == expected + + +def test_track_listening_serializer_is_connected(activity_registry): + conf = activity_registry['history.Listening'] + assert conf['serializer'] == serializers.ListeningActivitySerializer + + +def test_track_listening_serializer_instance_activity_consumer( + activity_registry): + conf = activity_registry['history.Listening'] + consumer = activities.broadcast_listening_to_instance_activity + assert consumer in conf['consumers'] + + +def test_broadcast_listening_to_instance_activity( + factories, mocker): + p = mocker.patch('funkwhale_api.common.channels.group_send') + listening = factories['history.Listening']() + data = serializers.ListeningActivitySerializer(listening).data + consumer = activities.broadcast_listening_to_instance_activity + message = { + "type": 'event.send', + "text": '', + "data": data + } + consumer(data=data, obj=listening) + p.assert_called_once_with('instance_activity', message) + + +def test_broadcast_listening_to_instance_activity_private( + factories, mocker): + p = mocker.patch('funkwhale_api.common.channels.group_send') + listening = factories['history.Listening']( + user__privacy_level='me' + ) + data = serializers.ListeningActivitySerializer(listening).data + consumer = activities.broadcast_listening_to_instance_activity + message = { + "type": 'event.send', + "text": '', + "data": data + } + consumer(data=data, obj=listening) + p.assert_not_called() diff --git a/api/tests/test_history.py b/api/tests/history/test_history.py similarity index 70% rename from api/tests/test_history.py rename to api/tests/history/test_history.py index 113e5ff6..ec8689e9 100644 --- a/api/tests/test_history.py +++ b/api/tests/history/test_history.py @@ -28,7 +28,8 @@ def test_anonymous_user_can_create_listening_via_api(client, factories, settings assert listening.session_key == client.session.session_key -def test_logged_in_user_can_create_listening_via_api(logged_in_client, factories): +def test_logged_in_user_can_create_listening_via_api( + logged_in_client, factories, activity_muted): track = factories['music.Track']() url = reverse('api:v1:history:listenings-list') @@ -40,3 +41,17 @@ def test_logged_in_user_can_create_listening_via_api(logged_in_client, factories assert listening.track == track assert listening.user == logged_in_client.user + + +def test_adding_listening_calls_activity_record( + factories, logged_in_client, activity_muted): + track = factories['music.Track']() + + url = reverse('api:v1:history:listenings-list') + response = logged_in_client.post(url, { + 'track': track.pk, + }) + + listening = models.Listening.objects.latest('id') + + activity_muted.assert_called_once_with(listening) diff --git a/api/tests/users/test_activity.py b/api/tests/users/test_activity.py index 3cee4fb4..26d0b11f 100644 --- a/api/tests/users/test_activity.py +++ b/api/tests/users/test_activity.py @@ -13,6 +13,7 @@ def test_activity_user_serializer(factories): expected = { "type": "Person", "id": user.get_activity_url(), + "local_id": user.username, "name": user.username, } -- GitLab