Verified Commit e87e2654 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Permissions and db state fixes with new index field

parent 257e67b5
......@@ -67,13 +67,18 @@ class Playlist(models.Model):
plt.save(update_fields=['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):
track = models.ForeignKey(
'music.Track',
related_name='playlist_tracks',
on_delete=models.CASCADE)
index = models.PositiveIntegerField(null=True)
index = models.PositiveIntegerField(null=True, blank=True)
playlist = models.ForeignKey(
Playlist, related_name='playlist_tracks', on_delete=models.CASCADE)
creation_date = models.DateTimeField(default=timezone.now)
......@@ -81,3 +86,12 @@ class PlaylistTrack(models.Model):
class Meta:
ordering = ('-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.db import transaction
from rest_framework import serializers
from taggit.models import Tag
......@@ -12,16 +13,23 @@ class PlaylistTrackSerializer(serializers.ModelSerializer):
class Meta:
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:
model = models.PlaylistTrack
fields = ('id', 'track', 'playlist', 'index')
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()
if existing >= settings.PLAYLISTS_MAX_TRACKS:
raise serializers.ValidationError(
......@@ -29,6 +37,29 @@ class PlaylistTrackCreateSerializer(serializers.ModelSerializer):
settings.PLAYLISTS_MAX_TRACKS))
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):
......
......@@ -27,6 +27,7 @@ class PlaylistViewSet(
permissions.OwnerPermission,
IsAuthenticatedOrReadOnly,
]
owner_checks = ['write']
@detail_route(methods=['get'])
def tracks(self, request, *args, **kwargs):
......@@ -66,25 +67,19 @@ class PlaylistTrackViewSet(
permissions.OwnerPermission,
IsAuthenticatedOrReadOnly,
]
owner_field = 'playlist.user'
owner_checks = ['write']
def create(self, request, *args, **kwargs):
serializer = serializers.PlaylistTrackCreateSerializer(
data=request.data)
serializer.is_valid(raise_exception=True)
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_serializer_class(self):
if self.request.method in ['PUT', 'PATCH', 'DELETE', 'POST']:
return serializers.PlaylistTrackWriteSerializer
return self.serializer_class
def get_queryset(self):
return self.queryset.filter(
fields.privacy_level_query(
self.request.user,
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):
new = factories['playlists.PlaylistTrack'](playlist=plt.playlist)
with pytest.raises(forms.ValidationError):
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
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
playlist = factories['playlists.Playlist']()
plts = factories['playlists.PlaylistTrack'].create_batch(
size=2, playlist=playlist)
track = factories['music.Track']()
serializer = serializers.PlaylistTrackCreateSerializer(data={
serializer = serializers.PlaylistTrackWriteSerializer(data={
'playlist': playlist.pk,
'track': track.pk,
})
assert serializer.is_valid() is False
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(
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'])
def test_playlist_privacy_respected_in_list_anon(level, factories, api_client):
factories['playlists.Playlist'](privacy_level=level)
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment