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