From 7f5cf2434016e4625e3a87345234278416740be6 Mon Sep 17 00:00:00 2001 From: Eliot Berriot <contact@eliotberriot.com> Date: Mon, 19 Dec 2016 22:54:30 +0100 Subject: [PATCH] WIP --- config/api_urls.py | 3 + config/settings/common.py | 3 +- .../migrations/0011_auto_20161122_1905.py | 20 +++++++ funkwhale_api/playlists/admin.py | 17 ++++++ .../playlists/migrations/0001_initial.py | 44 ++++++++++++++ .../playlists/migrations/__init__.py | 0 funkwhale_api/playlists/models.py | 34 +++++------ funkwhale_api/playlists/serializers.py | 22 +++++++ .../playlists/tests/test_playlists.py | 47 +++++++++++++-- funkwhale_api/playlists/views.py | 57 +++++++++++++++++++ requirements/base.txt | 1 + 11 files changed, 226 insertions(+), 22 deletions(-) create mode 100644 funkwhale_api/music/migrations/0011_auto_20161122_1905.py create mode 100644 funkwhale_api/playlists/admin.py create mode 100644 funkwhale_api/playlists/migrations/0001_initial.py create mode 100644 funkwhale_api/playlists/migrations/__init__.py create mode 100644 funkwhale_api/playlists/serializers.py create mode 100644 funkwhale_api/playlists/views.py diff --git a/config/api_urls.py b/config/api_urls.py index 2af30e3..5519cc6 100644 --- a/config/api_urls.py +++ b/config/api_urls.py @@ -1,6 +1,7 @@ from rest_framework import routers from django.conf.urls import include, url from funkwhale_api.music import views +from funkwhale_api.playlists import views as playlists_views router = routers.SimpleRouter() router.register(r'tags', views.TagViewSet, 'tags') @@ -9,6 +10,8 @@ router.register(r'artists', views.ArtistViewSet, 'artists') router.register(r'albums', views.AlbumViewSet, 'albums') router.register(r'import-batches', views.ImportBatchViewSet, 'import-batches') router.register(r'submit', views.SubmitViewSet, 'submit') +router.register(r'playlists', playlists_views.PlaylistViewSet, 'playlists') +router.register(r'playlist-tracks', playlists_views.PlaylistTrackViewSet, 'playlist-tracks') urlpatterns = router.urls urlpatterns += [ diff --git a/config/settings/common.py b/config/settings/common.py index 9ac3b47..6efad40 100644 --- a/config/settings/common.py +++ b/config/settings/common.py @@ -52,6 +52,7 @@ THIRD_PARTY_APPS = ( 'cachalot', 'rest_auth', 'rest_auth.registration', + 'mptt', ) # Apps specific for this project go here. @@ -62,7 +63,7 @@ LOCAL_APPS = ( 'funkwhale_api.favorites', 'funkwhale_api.radios', 'funkwhale_api.history', - # 'funkwhale_api.playlists', + 'funkwhale_api.playlists', ) # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps diff --git a/funkwhale_api/music/migrations/0011_auto_20161122_1905.py b/funkwhale_api/music/migrations/0011_auto_20161122_1905.py new file mode 100644 index 0000000..6b8f7b6 --- /dev/null +++ b/funkwhale_api/music/migrations/0011_auto_20161122_1905.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import versatileimagefield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('music', '0010_auto_20160920_1742'), + ] + + operations = [ + migrations.AlterField( + model_name='album', + name='cover', + field=versatileimagefield.fields.VersatileImageField(null=True, blank=True, upload_to='albums/covers/%Y/%m/%d'), + ), + ] diff --git a/funkwhale_api/playlists/admin.py b/funkwhale_api/playlists/admin.py new file mode 100644 index 0000000..b337154 --- /dev/null +++ b/funkwhale_api/playlists/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin + +from . import models + + +@admin.register(models.Playlist) +class PlaylistAdmin(admin.ModelAdmin): + list_display = ['name', 'user', 'is_public', 'creation_date'] + search_fields = ['name', ] + list_select_related = True + + +@admin.register(models.PlaylistTrack) +class PlaylistTrackAdmin(admin.ModelAdmin): + list_display = ['playlist', 'track', 'position', ] + search_fields = ['track__name', 'playlist__name'] + list_select_related = True diff --git a/funkwhale_api/playlists/migrations/0001_initial.py b/funkwhale_api/playlists/migrations/0001_initial.py new file mode 100644 index 0000000..8390148 --- /dev/null +++ b/funkwhale_api/playlists/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +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): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('music', '0011_auto_20161122_1905'), + ] + + operations = [ + migrations.CreateModel( + name='Playlist', + fields=[ + ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), + ('name', models.CharField(max_length=50)), + ('is_public', models.BooleanField(default=False)), + ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='playlists')), + ], + ), + migrations.CreateModel( + name='PlaylistTrack', + fields=[ + ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), + ('lft', models.PositiveIntegerField(db_index=True, editable=False)), + ('rght', models.PositiveIntegerField(db_index=True, editable=False)), + ('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')), + ('previous', mptt.fields.TreeOneToOneField(null=True, to='playlists.PlaylistTrack', related_name='next', blank=True)), + ('track', models.ForeignKey(to='music.Track', related_name='playlist_tracks')), + ], + options={ + 'ordering': ('-playlist', 'position'), + }, + ), + ] diff --git a/funkwhale_api/playlists/migrations/__init__.py b/funkwhale_api/playlists/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/funkwhale_api/playlists/models.py b/funkwhale_api/playlists/models.py index c869ab8..35a30a7 100644 --- a/funkwhale_api/playlists/models.py +++ b/funkwhale_api/playlists/models.py @@ -1,6 +1,8 @@ from django.db import models from django.utils import timezone +from mptt.models import MPTTModel, TreeOneToOneField + class Playlist(models.Model): name = models.CharField(max_length=50) @@ -8,24 +10,24 @@ class Playlist(models.Model): user = models.ForeignKey('users.User', related_name="playlists") creation_date = models.DateTimeField(default=timezone.now) - def add_tracks(self, tracks): - try: - last_position = self.playlist_tracks.objects.last().position - except AttributeError: - last_position = -1 - - playlist_tracks = [] - for idx, track in enumerate(tracks): - plt = PlaylistTrack(position=last_position + idx +1, track=track, playlist=self) - plt.save() - playlist_tracks.append(plt) - return playlist_tracks - -class PlaylistTrack(models.Model): - position = models.PositiveIntegerField(default=0) + def __str__(self): + return self.name + + def add_track(self, track, previous=None): + plt = PlaylistTrack(previous=previous, track=track, playlist=self) + plt.save() + + return plt + + +class PlaylistTrack(MPTTModel): track = models.ForeignKey('music.Track', related_name='playlist_tracks') + previous = TreeOneToOneField('self', blank=True, null=True, related_name='next') playlist = models.ForeignKey(Playlist, related_name='playlist_tracks') + class MPTTMeta: + level_attr = 'position' + parent_attr = 'previous' + class Meta: - unique_together = ('playlist', 'position') ordering = ('-playlist', 'position') diff --git a/funkwhale_api/playlists/serializers.py b/funkwhale_api/playlists/serializers.py new file mode 100644 index 0000000..359e787 --- /dev/null +++ b/funkwhale_api/playlists/serializers.py @@ -0,0 +1,22 @@ +from rest_framework import serializers +from taggit.models import Tag + +from funkwhale_api.music.serializers import TrackSerializerNested + +from . import models + + +class PlaylistTrackSerializer(serializers.ModelSerializer): + track = TrackSerializerNested() + + class Meta: + model = models.PlaylistTrack + fields = ('id', 'track', 'playlist', 'position') + + +class PlaylistSerializer(serializers.ModelSerializer): + playlist_tracks = PlaylistTrackSerializer(many=True) + + class Meta: + model = models.Playlist + fields = ('id', 'name', 'is_public', 'creation_date', 'playlist_tracks') diff --git a/funkwhale_api/playlists/tests/test_playlists.py b/funkwhale_api/playlists/tests/test_playlists.py index ab056d9..8a025b4 100644 --- a/funkwhale_api/playlists/tests/test_playlists.py +++ b/funkwhale_api/playlists/tests/test_playlists.py @@ -8,6 +8,8 @@ from model_mommy import mommy from funkwhale_api.users.models import User from funkwhale_api.playlists import models +from funkwhale_api.playlists.serializers import PlaylistSerializer + class TestPlayLists(TestCase): @@ -16,14 +18,49 @@ class TestPlayLists(TestCase): self.user = User.objects.create_user(username='test', email='test@test.com', password='test') def test_can_create_playlist(self): - tracks = mommy.make('music.Track', _quantity=5) + tracks = list(mommy.make('music.Track', _quantity=5)) playlist = models.Playlist.objects.create(user=self.user, name="test") - playlist.add_tracks(tracks) + previous = None + for i in range(len(tracks)): + previous = playlist.add_track(tracks[i], previous=previous) playlist_tracks = list(playlist.playlist_tracks.all()) + previous = None for idx, track in enumerate(tracks): - self.assertEqual(playlist_tracks[idx].position, idx) - self.assertEqual(playlist_tracks[idx].track, track) - self.assertEqual(playlist_tracks[idx].playlist, playlist) + plt = playlist_tracks[idx] + self.assertEqual(plt.position, idx) + self.assertEqual(plt.track, track) + if previous: + self.assertEqual(playlist_tracks[idx + 1], previous) + self.assertEqual(plt.playlist, playlist) + + def test_can_create_playlist_via_api(self): + self.client.login(username=self.user.username, password='test') + url = reverse('api:playlists-list') + data = { + 'name': 'test', + } + + response = self.client.post(url, data) + + playlist = self.user.playlists.latest('id') + self.assertEqual(playlist.name, 'test') + + def test_can_add_playlist_track_via_api(self): + tracks = list(mommy.make('music.Track', _quantity=5)) + playlist = models.Playlist.objects.create(user=self.user, name="test") + + self.client.login(username=self.user.username, password='test') + + url = reverse('api:playlist-tracks-list') + data = { + 'playlist': playlist.pk, + 'track': tracks[0].pk + } + + response = self.client.post(url, data) + + plts = self.user.playlists.latest('id').playlist_tracks.all() + self.assertEqual(plts.first().track, tracks[0]) diff --git a/funkwhale_api/playlists/views.py b/funkwhale_api/playlists/views.py new file mode 100644 index 0000000..4b720b9 --- /dev/null +++ b/funkwhale_api/playlists/views.py @@ -0,0 +1,57 @@ +from rest_framework import generics, mixins, viewsets +from rest_framework import status +from rest_framework.response import Response + +from funkwhale_api.music.models import Track +from funkwhale_api.common.permissions import ConditionalAuthentication + +from . import models +from . import serializers + + +class PlaylistViewSet( + mixins.RetrieveModelMixin, + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet): + + serializer_class = serializers.PlaylistSerializer + queryset = (models.Playlist.objects.all()) + permission_classes = [ConditionalAuthentication] + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(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_queryset(self): + return self.queryset.filter(user=self.request.user) + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + +class PlaylistTrackViewSet( + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet): + + serializer_class = serializers.PlaylistTrackSerializer + queryset = (models.PlaylistTrack.objects.all()) + permission_classes = [ConditionalAuthentication] + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(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_queryset(self): + return self.queryset.filter(playlist__user=self.request.user) diff --git a/requirements/base.txt b/requirements/base.txt index 2599948..4e9dd95 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -48,6 +48,7 @@ youtube_dl>=2015.12.21 djangorestframework djangorestframework-jwt django-celery +django-mptt google-api-python-client arrow django-taggit -- GitLab