Skip to content
Snippets Groups Projects
Commit cc95e2f1 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Merge branch '141-timeline-first-load' into 'develop'

Resolve "Populate timeline on first load"

Closes #141

See merge request funkwhale/funkwhale!109
parents f36a9e2a a87a2837
No related branches found
No related tags found
No related merge requests found
Showing
with 207 additions and 13 deletions
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')
......
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
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]
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)
......@@ -22,6 +22,6 @@ def privacy_level_query(user, lookup_field='privacy_level'):
return models.Q(**{
'{}__in'.format(lookup_field): [
'me', 'followers', 'instance', 'everyone'
'followers', 'instance', 'everyone'
]
})
......@@ -4,7 +4,7 @@ from . import models
@admin.register(models.Listening)
class ListeningAdmin(admin.ModelAdmin):
list_display = ['track', 'end_date', 'user', 'session_key']
list_display = ['track', 'creation_date', 'user', 'session_key']
search_fields = ['track__name', 'user__username']
list_select_related = [
'user',
......
# Generated by Django 2.0.3 on 2018-03-25 14:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('history', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='listening',
options={'ordering': ('-creation_date',)},
),
migrations.RenameField(
model_name='listening',
old_name='end_date',
new_name='creation_date',
),
]
......@@ -6,7 +6,8 @@ from funkwhale_api.music.models import Track
class Listening(models.Model):
end_date = models.DateTimeField(default=timezone.now, null=True, blank=True)
creation_date = models.DateTimeField(
default=timezone.now, null=True, blank=True)
track = models.ForeignKey(
Track, related_name="listenings", on_delete=models.CASCADE)
user = models.ForeignKey(
......@@ -18,7 +19,7 @@ class Listening(models.Model):
session_key = models.CharField(max_length=100, null=True, blank=True)
class Meta:
ordering = ('-end_date',)
ordering = ('-creation_date',)
def save(self, **kwargs):
if not self.user and not self.session_key:
......
......@@ -12,7 +12,7 @@ class ListeningActivitySerializer(activity_serializers.ModelSerializer):
type = serializers.SerializerMethodField()
object = TrackActivitySerializer(source='track')
actor = UserActivitySerializer(source='user')
published = serializers.DateTimeField(source='end_date')
published = serializers.DateTimeField(source='creation_date')
class Meta:
model = models.Listening
......@@ -36,7 +36,7 @@ class ListeningSerializer(serializers.ModelSerializer):
class Meta:
model = models.Listening
fields = ('id', 'user', 'session_key', 'track', 'end_date')
fields = ('id', 'user', 'session_key', 'track', 'creation_date')
def create(self, validated_data):
......
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
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]
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
......@@ -10,7 +10,7 @@ from funkwhale_api.users.factories import UserFactory
@pytest.mark.parametrize('user,expected', [
(AnonymousUser(), Q(privacy_level='everyone')),
(UserFactory.build(pk=1),
Q(privacy_level__in=['me', 'followers', 'instance', 'everyone'])),
Q(privacy_level__in=['followers', 'instance', 'everyone'])),
])
def test_privacy_level_query(user,expected):
query = fields.privacy_level_query(user)
......
......@@ -2,7 +2,6 @@ import pytest
from rest_framework.views import APIView
from django.contrib.auth.models import AnonymousUser
from django.http import Http404
from funkwhale_api.common import permissions
......@@ -19,24 +18,26 @@ def test_owner_permission_owner_field_ok(nodb_factories, api_request):
assert check is True
def test_owner_permission_owner_field_not_ok(nodb_factories, api_request):
def test_owner_permission_owner_field_not_ok(
anonymous_user, nodb_factories, api_request):
playlist = nodb_factories['playlists.Playlist']()
view = APIView.as_view()
permission = permissions.OwnerPermission()
request = api_request.get('/')
setattr(request, 'user', AnonymousUser())
setattr(request, 'user', anonymous_user)
with pytest.raises(Http404):
permission.has_object_permission(request, view, playlist)
def test_owner_permission_read_only(nodb_factories, api_request):
def test_owner_permission_read_only(
anonymous_user, nodb_factories, api_request):
playlist = nodb_factories['playlists.Playlist']()
view = APIView.as_view()
setattr(view, 'owner_checks', ['write'])
permission = permissions.OwnerPermission()
request = api_request.get('/')
setattr(request, 'user', AnonymousUser())
setattr(request, 'user', anonymous_user)
check = permission.has_object_permission(request, view, playlist)
assert check is True
......@@ -3,6 +3,7 @@ import tempfile
import shutil
import pytest
from django.contrib.auth.models import AnonymousUser
from django.core.cache import cache as django_cache
from dynamic_preferences.registries import global_preferences_registry
......@@ -66,6 +67,11 @@ def logged_in_client(db, factories, client):
delattr(client, 'user')
@pytest.fixture
def anonymous_user():
return AnonymousUser()
@pytest.fixture
def api_client(client):
return APIClient()
......@@ -126,3 +132,11 @@ def activity_registry():
@pytest.fixture
def activity_muted(activity_registry, mocker):
yield mocker.patch.object(record, 'send')
@pytest.fixture(autouse=True)
def media_root(settings):
tmp_dir = tempfile.mkdtemp()
settings.MEDIA_ROOT = tmp_dir
yield settings.MEDIA_ROOT
shutil.rmtree(tmp_dir)
......@@ -23,7 +23,7 @@ def test_activity_listening_serializer(factories):
"id": listening.get_activity_url(),
"actor": actor,
"object": TrackActivitySerializer(listening.track).data,
"published": field.to_representation(listening.end_date),
"published": field.to_representation(listening.creation_date),
}
data = serializers.ListeningActivitySerializer(listening).data
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment