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

Merge branch '3-playlists' into 'develop'

Resolve "Playlists integration"

Closes #3, #93, and #94

See merge request funkwhale/funkwhale!98
parents 02741182 529114c8
No related branches found
No related tags found
No related merge requests found
Showing
with 670 additions and 63 deletions
......@@ -57,7 +57,6 @@ THIRD_PARTY_APPS = (
'taggit',
'rest_auth',
'rest_auth.registration',
'mptt',
'dynamic_preferences',
'django_filters',
'cacheops',
......@@ -383,3 +382,6 @@ CACHEOPS = {
# Custom Admin URL, use {% url 'admin:index' %}
ADMIN_URL = env('DJANGO_ADMIN_URL', default='^api/admin/')
CSRF_USE_SESSIONS = True
# Playlist settings
PLAYLISTS_MAX_TRACKS = env.int('PLAYLISTS_MAX_TRACKS', default=250)
from django.db import models
PRIVACY_LEVEL_CHOICES = [
('me', 'Only me'),
('followers', 'Me and my followers'),
('instance', 'Everyone on my instance, and my followers'),
('everyone', 'Everyone, including people on other instances'),
]
def get_privacy_field():
return models.CharField(
max_length=30, choices=PRIVACY_LEVEL_CHOICES, default='instance')
def privacy_level_query(user, lookup_field='privacy_level'):
if user.is_anonymous:
return models.Q(**{
lookup_field: 'everyone',
})
return models.Q(**{
'{}__in'.format(lookup_field): [
'me', 'followers', 'instance', 'everyone'
]
})
import operator
from django.conf import settings
from django.http import Http404
from rest_framework.permissions import BasePermission, DjangoModelPermissions
......@@ -20,3 +23,39 @@ class HasModelPermission(DjangoModelPermissions):
"""
def get_required_permissions(self, method, model_cls):
return super().get_required_permissions(method, self.model)
class OwnerPermission(BasePermission):
"""
Ensure the request user is the owner of the object.
Usage:
class MyView(APIView):
model = MyModel
permission_classes = [OwnerPermission]
owner_field = 'owner'
owner_checks = ['read', 'write']
"""
perms_map = {
'GET': 'read',
'OPTIONS': 'read',
'HEAD': 'read',
'POST': 'write',
'PUT': 'write',
'PATCH': 'write',
'DELETE': 'write',
}
def has_object_permission(self, request, view, obj):
method_check = self.perms_map[request.method]
owner_checks = getattr(view, 'owner_checks', ['read', 'write'])
if method_check not in owner_checks:
# check not enabled
return True
owner_field = getattr(view, 'owner_field', 'user')
owner = operator.attrgetter(owner_field)(obj)
if owner != request.user:
raise Http404
return True
......@@ -5,13 +5,13 @@ from . import models
@admin.register(models.Playlist)
class PlaylistAdmin(admin.ModelAdmin):
list_display = ['name', 'user', 'is_public', 'creation_date']
list_display = ['name', 'user', 'privacy_level', 'creation_date']
search_fields = ['name', ]
list_select_related = True
@admin.register(models.PlaylistTrack)
class PlaylistTrackAdmin(admin.ModelAdmin):
list_display = ['playlist', 'track', 'position', ]
list_display = ['playlist', 'track', 'index']
search_fields = ['track__name', 'playlist__name']
list_select_related = True
import factory
from funkwhale_api.factories import registry
from funkwhale_api.music.factories import TrackFactory
from funkwhale_api.users.factories import UserFactory
......@@ -11,3 +12,12 @@ class PlaylistFactory(factory.django.DjangoModelFactory):
class Meta:
model = 'playlists.Playlist'
@registry.register
class PlaylistTrackFactory(factory.django.DjangoModelFactory):
playlist = factory.SubFactory(PlaylistFactory)
track = factory.SubFactory(TrackFactory)
class Meta:
model = 'playlists.PlaylistTrack'
from django_filters import rest_framework as filters
from funkwhale_api.music import utils
from . import models
class PlaylistFilter(filters.FilterSet):
q = filters.CharFilter(name='_', method='filter_q')
class Meta:
model = models.Playlist
fields = {
'user': ['exact'],
'name': ['exact', 'icontains'],
'q': 'exact',
}
def filter_q(self, queryset, name, value):
query = utils.get_query(value, ['name', 'user__username'])
return queryset.filter(query)
......@@ -4,7 +4,6 @@ from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import django.utils.timezone
import mptt.fields
class Migration(migrations.Migration):
......@@ -34,7 +33,7 @@ class Migration(migrations.Migration):
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
('position', models.PositiveIntegerField(db_index=True, editable=False)),
('playlist', models.ForeignKey(to='playlists.Playlist', related_name='playlist_tracks', on_delete=models.CASCADE)),
('previous', mptt.fields.TreeOneToOneField(null=True, to='playlists.PlaylistTrack', related_name='next', blank=True, on_delete=models.CASCADE)),
('previous', models.OneToOneField(null=True, to='playlists.PlaylistTrack', related_name='next', blank=True, on_delete=models.CASCADE)),
('track', models.ForeignKey(to='music.Track', related_name='playlist_tracks', on_delete=models.CASCADE)),
],
options={
......
# Generated by Django 2.0.3 on 2018-03-16 22:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlists', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='playlist',
name='is_public',
),
migrations.AddField(
model_name='playlist',
name='privacy_level',
field=models.CharField(choices=[('me', 'Only me'), ('followers', 'Me and my followers'), ('instance', 'Everyone on my instance, and my followers'), ('everyone', 'Everyone, including people on other instances')], default='instance', max_length=30),
),
]
# Generated by Django 2.0.3 on 2018-03-19 12:14
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('playlists', '0002_auto_20180316_2217'),
]
operations = [
migrations.AlterModelOptions(
name='playlisttrack',
options={'ordering': ('-playlist', 'index')},
),
migrations.AddField(
model_name='playlisttrack',
name='creation_date',
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name='playlisttrack',
name='index',
field=models.PositiveIntegerField(null=True),
),
migrations.RemoveField(
model_name='playlisttrack',
name='lft',
),
migrations.RemoveField(
model_name='playlisttrack',
name='position',
),
migrations.RemoveField(
model_name='playlisttrack',
name='previous',
),
migrations.RemoveField(
model_name='playlisttrack',
name='rght',
),
migrations.RemoveField(
model_name='playlisttrack',
name='tree_id',
),
migrations.AlterUniqueTogether(
name='playlisttrack',
unique_together={('playlist', 'index')},
),
]
# Generated by Django 2.0.3 on 2018-03-20 17:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlists', '0003_auto_20180319_1214'),
]
operations = [
migrations.AddField(
model_name='playlist',
name='modification_date',
field=models.DateTimeField(auto_now=True),
),
migrations.AlterField(
model_name='playlisttrack',
name='index',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AlterUniqueTogether(
name='playlisttrack',
unique_together=set(),
),
]
from django.conf import settings
from django.db import models
from django.db import transaction
from django.utils import timezone
from mptt.models import MPTTModel, TreeOneToOneField
from rest_framework import exceptions
from funkwhale_api.common import fields
class Playlist(models.Model):
name = models.CharField(max_length=50)
is_public = models.BooleanField(default=False)
user = models.ForeignKey(
'users.User', related_name="playlists", on_delete=models.CASCADE)
creation_date = models.DateTimeField(default=timezone.now)
modification_date = models.DateTimeField(
auto_now=True)
privacy_level = fields.get_privacy_field()
def __str__(self):
return self.name
def add_track(self, track, previous=None):
plt = PlaylistTrack(previous=previous, track=track, playlist=self)
plt.save()
@transaction.atomic
def insert(self, plt, index=None):
"""
Given a PlaylistTrack, insert it at the correct index in the playlist,
and update other tracks index if necessary.
"""
old_index = plt.index
move = old_index is not None
if index is not None and index == old_index:
# moving at same position, just skip
return index
existing = self.playlist_tracks.select_for_update()
if move:
existing = existing.exclude(pk=plt.pk)
total = existing.filter(index__isnull=False).count()
if index is None:
# we simply increment the last track index by 1
index = total
if index > total:
raise exceptions.ValidationError('Index is not continuous')
if index < 0:
raise exceptions.ValidationError('Index must be zero or positive')
if move:
# we remove the index temporarily, to avoid integrity errors
plt.index = None
plt.save(update_fields=['index'])
if index > old_index:
# new index is higher than current, we decrement previous tracks
to_update = existing.filter(
index__gt=old_index, index__lte=index)
to_update.update(index=models.F('index') - 1)
if index < old_index:
# new index is lower than current, we increment next tracks
to_update = existing.filter(index__lt=old_index, index__gte=index)
to_update.update(index=models.F('index') + 1)
else:
to_update = existing.filter(index__gte=index)
to_update.update(index=models.F('index') + 1)
return plt
plt.index = index
plt.save(update_fields=['index'])
self.save(update_fields=['modification_date'])
return index
@transaction.atomic
def remove(self, index):
existing = self.playlist_tracks.select_for_update()
self.save(update_fields=['modification_date'])
to_update = existing.filter(index__gt=index)
return to_update.update(index=models.F('index') - 1)
class PlaylistTrack(MPTTModel):
@transaction.atomic
def insert_many(self, tracks):
existing = self.playlist_tracks.select_for_update()
now = timezone.now()
total = existing.filter(index__isnull=False).count()
if existing.count() + len(tracks) > settings.PLAYLISTS_MAX_TRACKS:
raise exceptions.ValidationError(
'Playlist would reach the maximum of {} tracks'.format(
settings.PLAYLISTS_MAX_TRACKS))
self.save(update_fields=['modification_date'])
start = total
plts = [
PlaylistTrack(
creation_date=now, playlist=self, track=track, index=start+i)
for i, track in enumerate(tracks)
]
return PlaylistTrack.objects.bulk_create(plts)
class PlaylistTrackQuerySet(models.QuerySet):
def for_nested_serialization(self):
return (self.select_related()
.select_related('track__album__artist')
.prefetch_related(
'track__tags',
'track__files',
'track__artist__albums__tracks__tags'))
class PlaylistTrack(models.Model):
track = models.ForeignKey(
'music.Track',
related_name='playlist_tracks',
on_delete=models.CASCADE)
previous = TreeOneToOneField(
'self',
blank=True,
null=True,
related_name='next',
on_delete=models.CASCADE)
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)
class MPTTMeta:
level_attr = 'position'
parent_attr = 'previous'
objects = PlaylistTrackQuerySet.as_manager()
class Meta:
ordering = ('-playlist', 'position')
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
from funkwhale_api.music.models import Track
from funkwhale_api.music.serializers import TrackSerializerNested
from funkwhale_api.users.serializers import UserBasicSerializer
from . import models
......@@ -11,20 +14,81 @@ 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', 'position')
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(
'Playlist has reached the maximum of {} tracks'.format(
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):
playlist_tracks = PlaylistTrackSerializer(many=True, read_only=True)
tracks_count = serializers.SerializerMethodField(read_only=True)
user = UserBasicSerializer(read_only=True)
class Meta:
model = models.Playlist
fields = ('id', 'name', 'is_public', 'creation_date', 'playlist_tracks')
read_only_fields = ['id', 'playlist_tracks', 'creation_date']
fields = (
'id',
'name',
'tracks_count',
'user',
'modification_date',
'creation_date',
'privacy_level',)
read_only_fields = [
'id',
'modification_date',
'creation_date',]
def get_tracks_count(self, obj):
try:
return obj.tracks_count
except AttributeError:
# no annotation?
return obj.playlist_tracks.count()
class PlaylistAddManySerializer(serializers.Serializer):
tracks = serializers.PrimaryKeyRelatedField(
many=True, queryset=Track.objects.for_nested_serialization())
from django.db.models import Count
from django.db import transaction
from rest_framework import exceptions
from rest_framework import generics, mixins, viewsets
from rest_framework import status
from rest_framework.decorators import detail_route
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from funkwhale_api.common import permissions
from funkwhale_api.common import fields
from funkwhale_api.music.models import Track
from funkwhale_api.common.permissions import ConditionalAuthentication
from . import filters
from . import models
from . import serializers
class PlaylistViewSet(
mixins.RetrieveModelMixin,
mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet):
serializer_class = serializers.PlaylistSerializer
queryset = (models.Playlist.objects.all())
permission_classes = [ConditionalAuthentication]
queryset = (
models.Playlist.objects.all().select_related('user')
.annotate(tracks_count=Count('playlist_tracks'))
)
permission_classes = [
permissions.ConditionalAuthentication,
permissions.OwnerPermission,
IsAuthenticatedOrReadOnly,
]
owner_checks = ['write']
filter_class = filters.PlaylistFilter
ordering_fields = ('id', 'name', 'creation_date', 'modification_date')
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
@detail_route(methods=['get'])
def tracks(self, request, *args, **kwargs):
playlist = self.get_object()
plts = playlist.playlist_tracks.all().for_nested_serialization()
serializer = serializers.PlaylistTrackSerializer(plts, many=True)
data = {
'count': len(plts),
'results': serializer.data
}
return Response(data, status=200)
@detail_route(methods=['post'])
@transaction.atomic
def add(self, request, *args, **kwargs):
playlist = self.get_object()
serializer = serializers.PlaylistAddManySerializer(data=request.data)
serializer.is_valid(raise_exception=True)
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)
try:
plts = playlist.insert_many(serializer.validated_data['tracks'])
except exceptions.ValidationError as e:
payload = {'playlist': e.detail}
return Response(payload, status=400)
ids = [p.id for p in plts]
plts = models.PlaylistTrack.objects.filter(
pk__in=ids).order_by('index').for_nested_serialization()
serializer = serializers.PlaylistTrackSerializer(plts, many=True)
data = {
'count': len(plts),
'results': serializer.data
}
return Response(data, status=201)
@detail_route(methods=['delete'])
@transaction.atomic
def clear(self, request, *args, **kwargs):
playlist = self.get_object()
playlist.playlist_tracks.all().delete()
playlist.save(update_fields=['modification_date'])
return Response(status=204)
def get_queryset(self):
return self.queryset.filter(user=self.request.user)
return self.queryset.filter(
fields.privacy_level_query(self.request.user))
def perform_create(self, serializer):
return serializer.save(user=self.request.user)
return serializer.save(
user=self.request.user,
privacy_level=serializer.validated_data.get(
'privacy_level', self.request.user.privacy_level)
)
class PlaylistTrackViewSet(
mixins.RetrieveModelMixin,
mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet):
serializer_class = serializers.PlaylistTrackSerializer
queryset = (models.PlaylistTrack.objects.all())
permission_classes = [ConditionalAuthentication]
queryset = (models.PlaylistTrack.objects.all().for_nested_serialization())
permission_classes = [
permissions.ConditionalAuthentication,
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)
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(playlist__user=self.request.user)
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)
......@@ -10,15 +10,9 @@ from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from funkwhale_api.common import fields
PRIVACY_LEVEL_CHOICES = [
('me', 'Only me'),
('followers', 'Me and my followers'),
('instance', 'Everyone on my instance, and my followers'),
('everyone', 'Everyone, including people on other instances'),
]
@python_2_unicode_compatible
class User(AbstractUser):
......@@ -39,8 +33,8 @@ class User(AbstractUser):
},
}
privacy_level = models.CharField(
max_length=30, choices=PRIVACY_LEVEL_CHOICES, default='instance')
privacy_level = fields.get_privacy_field()
def __str__(self):
return self.username
......
......@@ -33,7 +33,6 @@ musicbrainzngs==0.6
youtube_dl>=2017.12.14
djangorestframework>=3.7,<3.8
djangorestframework-jwt>=1.11,<1.12
django-mptt>=0.9,<0.10
google-api-python-client>=1.6,<1.7
arrow>=0.12,<0.13
persisting-theory>=0.2,<0.3
......
......@@ -6,7 +6,7 @@ exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules
max-line-length = 120
exclude=.tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules
[pytest]
[tool:pytest]
DJANGO_SETTINGS_MODULE=config.settings.test
python_files = tests.py test_*.py *_tests.py
testpaths = tests
import pytest
from django.contrib.auth.models import AnonymousUser
from django.db.models import Q
from funkwhale_api.common import fields
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'])),
])
def test_privacy_level_query(user,expected):
query = fields.privacy_level_query(user)
assert query == expected
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
def test_owner_permission_owner_field_ok(nodb_factories, api_request):
playlist = nodb_factories['playlists.Playlist']()
view = APIView.as_view()
permission = permissions.OwnerPermission()
request = api_request.get('/')
setattr(request, 'user', playlist.user)
check = permission.has_object_permission(request, view, playlist)
assert check is True
def test_owner_permission_owner_field_not_ok(nodb_factories, api_request):
playlist = nodb_factories['playlists.Playlist']()
view = APIView.as_view()
permission = permissions.OwnerPermission()
request = api_request.get('/')
setattr(request, 'user', AnonymousUser())
with pytest.raises(Http404):
permission.has_object_permission(request, view, playlist)
def test_owner_permission_read_only(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())
check = permission.has_object_permission(request, view, playlist)
assert check is True
import factory
import tempfile
import shutil
import pytest
from django.core.cache import cache as django_cache
from dynamic_preferences.registries import global_preferences_registry
from rest_framework.test import APIClient
from rest_framework.test import APIRequestFactory
......@@ -27,6 +30,16 @@ def cache():
@pytest.fixture
def factories(db):
from funkwhale_api import factories
for v in factories.registry.values():
v._meta.strategy = factory.CREATE_STRATEGY
yield factories.registry
@pytest.fixture
def nodb_factories():
from funkwhale_api import factories
for v in factories.registry.values():
v._meta.strategy = factory.BUILD_STRATEGY
yield factories.registry
......
import pytest
from rest_framework import exceptions
def test_can_insert_plt(factories):
plt = factories['playlists.PlaylistTrack']()
modification_date = plt.playlist.modification_date
assert plt.index is None
plt.playlist.insert(plt)
plt.refresh_from_db()
assert plt.index == 0
assert plt.playlist.modification_date > modification_date
def test_insert_use_last_idx_by_default(factories):
playlist = factories['playlists.Playlist']()
plts = factories['playlists.PlaylistTrack'].create_batch(
size=3, playlist=playlist)
for i, plt in enumerate(plts):
index = playlist.insert(plt)
plt.refresh_from_db()
assert index == i
assert plt.index == i
def test_can_insert_at_index(factories):
playlist = factories['playlists.Playlist']()
first = factories['playlists.PlaylistTrack'](playlist=playlist)
playlist.insert(first)
new_first = factories['playlists.PlaylistTrack'](playlist=playlist)
index = playlist.insert(new_first, index=0)
first.refresh_from_db()
new_first.refresh_from_db()
assert index == 0
assert first.index == 1
assert new_first.index == 0
def test_can_insert_and_move(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)
playlist.insert(second, index=0)
first.refresh_from_db()
second.refresh_from_db()
third.refresh_from_db()
assert third.index == 2
assert second.index == 0
assert first.index == 1
def test_can_insert_and_move_last_to_0(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)
playlist.insert(third, index=0)
first.refresh_from_db()
second.refresh_from_db()
third.refresh_from_db()
assert third.index == 0
assert first.index == 1
assert second.index == 2
def test_cannot_insert_at_wrong_index(factories):
plt = factories['playlists.PlaylistTrack']()
new = factories['playlists.PlaylistTrack'](playlist=plt.playlist)
with pytest.raises(exceptions.ValidationError):
plt.playlist.insert(new, 2)
def test_cannot_insert_at_negative_index(factories):
plt = factories['playlists.PlaylistTrack']()
new = factories['playlists.PlaylistTrack'](playlist=plt.playlist)
with pytest.raises(exceptions.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
def test_can_insert_many(factories):
playlist = factories['playlists.Playlist']()
existing = factories['playlists.PlaylistTrack'](playlist=playlist, index=0)
tracks = factories['music.Track'].create_batch(size=3)
plts = playlist.insert_many(tracks)
for i, plt in enumerate(plts):
assert plt.index == i + 1
assert plt.track == tracks[i]
assert plt.playlist == playlist
def test_insert_many_honor_max_tracks(factories, settings):
settings.PLAYLISTS_MAX_TRACKS = 4
playlist = factories['playlists.Playlist']()
plts = factories['playlists.PlaylistTrack'].create_batch(
size=2, playlist=playlist)
track = factories['music.Track']()
with pytest.raises(exceptions.ValidationError):
playlist.insert_many([track, track, track])
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