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

Permissions and db state fixes with new index field

parent 257e67b5
No related branches found
No related tags found
No related merge requests found
...@@ -67,13 +67,18 @@ class Playlist(models.Model): ...@@ -67,13 +67,18 @@ class Playlist(models.Model):
plt.save(update_fields=['index']) plt.save(update_fields=['index'])
return index return index
def remove(self, index):
existing = self.playlist_tracks.select_for_update()
to_update = existing.filter(index__gt=index)
return to_update.update(index=models.F('index') - 1)
class PlaylistTrack(models.Model): class PlaylistTrack(models.Model):
track = models.ForeignKey( track = models.ForeignKey(
'music.Track', 'music.Track',
related_name='playlist_tracks', related_name='playlist_tracks',
on_delete=models.CASCADE) on_delete=models.CASCADE)
index = models.PositiveIntegerField(null=True) index = models.PositiveIntegerField(null=True, blank=True)
playlist = models.ForeignKey( playlist = models.ForeignKey(
Playlist, related_name='playlist_tracks', on_delete=models.CASCADE) Playlist, related_name='playlist_tracks', on_delete=models.CASCADE)
creation_date = models.DateTimeField(default=timezone.now) creation_date = models.DateTimeField(default=timezone.now)
...@@ -81,3 +86,12 @@ class PlaylistTrack(models.Model): ...@@ -81,3 +86,12 @@ class PlaylistTrack(models.Model):
class Meta: class Meta:
ordering = ('-playlist', 'index') ordering = ('-playlist', 'index')
unique_together = ('playlist', 'index') unique_together = ('playlist', 'index')
def delete(self, *args, **kwargs):
playlist = self.playlist
index = self.index
update_indexes = kwargs.pop('update_indexes', False)
r = super().delete(*args, **kwargs)
if index is not None and update_indexes:
playlist.remove(index)
return r
from django.conf import settings from django.conf import settings
from django.db import transaction
from rest_framework import serializers from rest_framework import serializers
from taggit.models import Tag from taggit.models import Tag
...@@ -12,16 +13,23 @@ class PlaylistTrackSerializer(serializers.ModelSerializer): ...@@ -12,16 +13,23 @@ class PlaylistTrackSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.PlaylistTrack model = models.PlaylistTrack
fields = ('id', 'track', 'playlist', 'position') fields = ('id', 'track', 'playlist', 'index', 'creation_date')
class PlaylistTrackCreateSerializer(serializers.ModelSerializer): class PlaylistTrackWriteSerializer(serializers.ModelSerializer):
index = serializers.IntegerField(
required=False, min_value=0, allow_null=True)
class Meta: class Meta:
model = models.PlaylistTrack model = models.PlaylistTrack
fields = ('id', 'track', 'playlist', 'index') fields = ('id', 'track', 'playlist', 'index')
def validate_playlist(self, value): def validate_playlist(self, value):
if self.context.get('request'):
# validate proper ownership on the playlist
if self.context['request'].user != value.user:
raise serializers.ValidationError(
'You do not have the permission to edit this playlist')
existing = value.playlist_tracks.count() existing = value.playlist_tracks.count()
if existing >= settings.PLAYLISTS_MAX_TRACKS: if existing >= settings.PLAYLISTS_MAX_TRACKS:
raise serializers.ValidationError( raise serializers.ValidationError(
...@@ -29,6 +37,29 @@ class PlaylistTrackCreateSerializer(serializers.ModelSerializer): ...@@ -29,6 +37,29 @@ class PlaylistTrackCreateSerializer(serializers.ModelSerializer):
settings.PLAYLISTS_MAX_TRACKS)) settings.PLAYLISTS_MAX_TRACKS))
return value return value
@transaction.atomic
def create(self, validated_data):
index = validated_data.pop('index', None)
instance = super().create(validated_data)
instance.playlist.insert(instance, index)
return instance
@transaction.atomic
def update(self, instance, validated_data):
update_index = 'index' in validated_data
index = validated_data.pop('index', None)
super().update(instance, validated_data)
if update_index:
instance.playlist.insert(instance, index)
return instance
def get_unique_together_validators(self):
"""
We explicitely disable unique together validation here
because it collides with our internal logic
"""
return []
class PlaylistSerializer(serializers.ModelSerializer): class PlaylistSerializer(serializers.ModelSerializer):
......
...@@ -27,6 +27,7 @@ class PlaylistViewSet( ...@@ -27,6 +27,7 @@ class PlaylistViewSet(
permissions.OwnerPermission, permissions.OwnerPermission,
IsAuthenticatedOrReadOnly, IsAuthenticatedOrReadOnly,
] ]
owner_checks = ['write']
@detail_route(methods=['get']) @detail_route(methods=['get'])
def tracks(self, request, *args, **kwargs): def tracks(self, request, *args, **kwargs):
...@@ -66,25 +67,19 @@ class PlaylistTrackViewSet( ...@@ -66,25 +67,19 @@ class PlaylistTrackViewSet(
permissions.OwnerPermission, permissions.OwnerPermission,
IsAuthenticatedOrReadOnly, IsAuthenticatedOrReadOnly,
] ]
owner_field = 'playlist.user'
owner_checks = ['write']
def create(self, request, *args, **kwargs): def get_serializer_class(self):
serializer = serializers.PlaylistTrackCreateSerializer( if self.request.method in ['PUT', 'PATCH', 'DELETE', 'POST']:
data=request.data) return serializers.PlaylistTrackWriteSerializer
serializer.is_valid(raise_exception=True) return self.serializer_class
if serializer.validated_data['playlist'].user != request.user:
return Response(
{'playlist': [
'This playlist does not exists or you do not have the'
'permission to edit it']
},
status=400)
instance = self.perform_create(serializer)
serializer = self.get_serializer(instance=instance)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def get_queryset(self): def get_queryset(self):
return self.queryset.filter( return self.queryset.filter(
fields.privacy_level_query( fields.privacy_level_query(
self.request.user, self.request.user,
lookup_field='playlist__privacy_level')) lookup_field='playlist__privacy_level'))
def perform_destroy(self, instance):
instance.delete(update_indexes=True)
...@@ -72,3 +72,18 @@ def test_cannot_insert_at_negative_index(factories): ...@@ -72,3 +72,18 @@ def test_cannot_insert_at_negative_index(factories):
new = factories['playlists.PlaylistTrack'](playlist=plt.playlist) new = factories['playlists.PlaylistTrack'](playlist=plt.playlist)
with pytest.raises(forms.ValidationError): with pytest.raises(forms.ValidationError):
plt.playlist.insert(new, -1) plt.playlist.insert(new, -1)
def test_remove_update_indexes(factories):
playlist = factories['playlists.Playlist']()
first = factories['playlists.PlaylistTrack'](playlist=playlist, index=0)
second = factories['playlists.PlaylistTrack'](playlist=playlist, index=1)
third = factories['playlists.PlaylistTrack'](playlist=playlist, index=2)
second.delete(update_indexes=True)
first.refresh_from_db()
third.refresh_from_db()
assert first.index == 0
assert third.index == 1
from funkwhale_api.playlists import models
from funkwhale_api.playlists import serializers from funkwhale_api.playlists import serializers
def test_cannot_max_500_tracks_per_playlist(mocker, factories, settings): def test_cannot_max_500_tracks_per_playlist(factories, settings):
settings.PLAYLISTS_MAX_TRACKS = 2 settings.PLAYLISTS_MAX_TRACKS = 2
playlist = factories['playlists.Playlist']() playlist = factories['playlists.Playlist']()
plts = factories['playlists.PlaylistTrack'].create_batch( plts = factories['playlists.PlaylistTrack'].create_batch(
size=2, playlist=playlist) size=2, playlist=playlist)
track = factories['music.Track']() track = factories['music.Track']()
serializer = serializers.PlaylistTrackCreateSerializer(data={ serializer = serializers.PlaylistTrackWriteSerializer(data={
'playlist': playlist.pk, 'playlist': playlist.pk,
'track': track.pk, 'track': track.pk,
}) })
assert serializer.is_valid() is False assert serializer.is_valid() is False
assert 'playlist' in serializer.errors assert 'playlist' in serializer.errors
def test_create_insert_is_called_when_index_is_None(factories, mocker):
insert = mocker.spy(models.Playlist, 'insert')
playlist = factories['playlists.Playlist']()
track = factories['music.Track']()
serializer = serializers.PlaylistTrackWriteSerializer(data={
'playlist': playlist.pk,
'track': track.pk,
'index': None,
})
assert serializer.is_valid() is True
plt = serializer.save()
insert.assert_called_once_with(playlist, plt, None)
assert plt.index == 0
def test_create_insert_is_called_when_index_is_provided(factories, mocker):
playlist = factories['playlists.Playlist']()
first = factories['playlists.PlaylistTrack'](playlist=playlist, index=0)
insert = mocker.spy(models.Playlist, 'insert')
factories['playlists.Playlist']()
track = factories['music.Track']()
serializer = serializers.PlaylistTrackWriteSerializer(data={
'playlist': playlist.pk,
'track': track.pk,
'index': 0,
})
assert serializer.is_valid() is True
plt = serializer.save()
first.refresh_from_db()
insert.assert_called_once_with(playlist, plt, 0)
assert plt.index == 0
assert first.index == 1
def test_update_insert_is_called_when_index_is_provided(factories, mocker):
playlist = factories['playlists.Playlist']()
first = factories['playlists.PlaylistTrack'](playlist=playlist, index=0)
second = factories['playlists.PlaylistTrack'](playlist=playlist, index=1)
insert = mocker.spy(models.Playlist, 'insert')
factories['playlists.Playlist']()
track = factories['music.Track']()
serializer = serializers.PlaylistTrackWriteSerializer(second, data={
'playlist': playlist.pk,
'track': second.track.pk,
'index': 0,
})
assert serializer.is_valid() is True
plt = serializer.save()
first.refresh_from_db()
insert.assert_called_once_with(playlist, plt, 0)
assert plt.index == 0
assert first.index == 1
...@@ -80,6 +80,21 @@ def test_only_can_add_track_on_own_playlist_via_api( ...@@ -80,6 +80,21 @@ def test_only_can_add_track_on_own_playlist_via_api(
assert playlist.playlist_tracks.count() == 0 assert playlist.playlist_tracks.count() == 0
def test_deleting_plt_updates_indexes(
mocker, factories, logged_in_api_client):
remove = mocker.spy(models.Playlist, 'remove')
track = factories['music.Track']()
plt = factories['playlists.PlaylistTrack'](
index=0,
playlist__user=logged_in_api_client.user)
url = reverse('api:v1:playlist-tracks-detail', kwargs={'pk': plt.pk})
response = logged_in_api_client.delete(url)
assert response.status_code == 204
remove.assert_called_once_with(plt.playlist, 0)
@pytest.mark.parametrize('level', ['instance', 'me', 'followers']) @pytest.mark.parametrize('level', ['instance', 'me', 'followers'])
def test_playlist_privacy_respected_in_list_anon(level, factories, api_client): def test_playlist_privacy_respected_in_list_anon(level, factories, api_client):
factories['playlists.Playlist'](privacy_level=level) factories['playlists.Playlist'](privacy_level=level)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment