Merge branch 'release/0.2.5'
This commit is contained in:
commit
20eaa5e615
|
@ -13,14 +13,22 @@ stages:
|
||||||
test_api:
|
test_api:
|
||||||
stage: test
|
stage: test
|
||||||
image: funkwhale/funkwhale:base
|
image: funkwhale/funkwhale:base
|
||||||
|
variables:
|
||||||
|
PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache"
|
||||||
|
DATABASE_URL: "sqlite://"
|
||||||
before_script:
|
before_script:
|
||||||
|
- python3 -m venv --copies virtualenv
|
||||||
|
- source virtualenv/bin/activate
|
||||||
- cd api
|
- cd api
|
||||||
|
- pip install -r requirements/base.txt
|
||||||
|
- pip install -r requirements/local.txt
|
||||||
- pip install -r requirements/test.txt
|
- pip install -r requirements/test.txt
|
||||||
script:
|
script:
|
||||||
- pytest
|
- pytest
|
||||||
variables:
|
cache:
|
||||||
DATABASE_URL: "sqlite://"
|
key: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME"
|
||||||
|
paths:
|
||||||
|
- "$CI_PROJECT_DIR/pip-cache"
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- docker
|
||||||
|
|
||||||
|
|
22
CHANGELOG
22
CHANGELOG
|
@ -2,10 +2,30 @@ Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
|
||||||
0.2.5 (unreleased)
|
0.2.6 (Unreleased)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
0.2.5 (2017-12-15)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
- Import: can now specify search template when querying import sources (#45)
|
||||||
|
- Login form: now redirect to previous page after login (#2)
|
||||||
|
- 404: a decent 404 template, at least (#48)
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
- Player: better handling of errors when fetching the audio file (#46)
|
||||||
|
- Csrf: default CSRF_TRUSTED_ORIGINS to ALLOWED_HOSTS to avoid Csrf issues on admin (#49)
|
||||||
|
|
||||||
|
Tech:
|
||||||
|
|
||||||
|
- Django 2 compatibility, lot of packages upgrades (#47)
|
||||||
|
|
||||||
|
|
||||||
0.2.4 (2017-12-14)
|
0.2.4 (2017-12-14)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,9 @@ COPY ./requirements.apt /requirements.apt
|
||||||
RUN apt-get update -qq && grep "^[^#;]" requirements.apt | xargs apt-get install -y
|
RUN apt-get update -qq && grep "^[^#;]" requirements.apt | xargs apt-get install -y
|
||||||
|
|
||||||
|
|
||||||
COPY ./requirements /requirements
|
COPY ./requirements/base.txt /requirements
|
||||||
|
RUN pip install -r /requirements/base.txt
|
||||||
|
COPY ./requirements/production.txt /requirements
|
||||||
RUN pip install -r /requirements/production.txt
|
RUN pip install -r /requirements/production.txt
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
|
|
@ -25,22 +25,32 @@ v1_patterns = router.urls
|
||||||
|
|
||||||
v1_patterns += [
|
v1_patterns += [
|
||||||
url(r'^providers/',
|
url(r'^providers/',
|
||||||
include('funkwhale_api.providers.urls', namespace='providers')),
|
include(
|
||||||
|
('funkwhale_api.providers.urls', 'providers'),
|
||||||
|
namespace='providers')),
|
||||||
url(r'^favorites/',
|
url(r'^favorites/',
|
||||||
include('funkwhale_api.favorites.urls', namespace='favorites')),
|
include(
|
||||||
|
('funkwhale_api.favorites.urls', 'favorites'),
|
||||||
|
namespace='favorites')),
|
||||||
url(r'^search$',
|
url(r'^search$',
|
||||||
views.Search.as_view(), name='search'),
|
views.Search.as_view(), name='search'),
|
||||||
url(r'^radios/',
|
url(r'^radios/',
|
||||||
include('funkwhale_api.radios.urls', namespace='radios')),
|
include(
|
||||||
|
('funkwhale_api.radios.urls', 'radios'),
|
||||||
|
namespace='radios')),
|
||||||
url(r'^history/',
|
url(r'^history/',
|
||||||
include('funkwhale_api.history.urls', namespace='history')),
|
include(
|
||||||
|
('funkwhale_api.history.urls', 'history'),
|
||||||
|
namespace='history')),
|
||||||
url(r'^users/',
|
url(r'^users/',
|
||||||
include('funkwhale_api.users.api_urls', namespace='users')),
|
include(
|
||||||
|
('funkwhale_api.users.api_urls', 'users'),
|
||||||
|
namespace='users')),
|
||||||
url(r'^token/',
|
url(r'^token/',
|
||||||
jwt_views.obtain_jwt_token),
|
jwt_views.obtain_jwt_token),
|
||||||
url(r'^token/refresh/', jwt_views.refresh_jwt_token),
|
url(r'^token/refresh/', jwt_views.refresh_jwt_token),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^v1/', include(v1_patterns, namespace='v1'))
|
url(r'^v1/', include((v1_patterns, 'v1'), namespace='v1'))
|
||||||
]
|
]
|
||||||
|
|
|
@ -75,7 +75,7 @@ INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
||||||
|
|
||||||
# MIDDLEWARE CONFIGURATION
|
# MIDDLEWARE CONFIGURATION
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
MIDDLEWARE_CLASSES = (
|
MIDDLEWARE = (
|
||||||
# Make sure djangosecure.middleware.SecurityMiddleware is listed first
|
# Make sure djangosecure.middleware.SecurityMiddleware is listed first
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'funkwhale_api.users.middleware.AnonymousSessionMiddleware',
|
'funkwhale_api.users.middleware.AnonymousSessionMiddleware',
|
||||||
|
|
|
@ -31,7 +31,7 @@ EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND',
|
||||||
|
|
||||||
# django-debug-toolbar
|
# django-debug-toolbar
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
|
MIDDLEWARE += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
|
||||||
|
|
||||||
# INTERNAL_IPS = ('127.0.0.1', '10.0.2.2',)
|
# INTERNAL_IPS = ('127.0.0.1', '10.0.2.2',)
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
# # Make sure djangosecure.middleware.SecurityMiddleware is listed first
|
# # Make sure djangosecure.middleware.SecurityMiddleware is listed first
|
||||||
# MIDDLEWARE_CLASSES = SECURITY_MIDDLEWARE + MIDDLEWARE_CLASSES
|
# MIDDLEWARE = SECURITY_MIDDLEWARE + MIDDLEWARE
|
||||||
#
|
#
|
||||||
# # set this to 60 seconds and then to 518400 when you can prove it works
|
# # set this to 60 seconds and then to 518400 when you can prove it works
|
||||||
# SECURE_HSTS_SECONDS = 60
|
# SECURE_HSTS_SECONDS = 60
|
||||||
|
@ -55,6 +55,8 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
# Hosts/domain names that are valid for this site
|
# Hosts/domain names that are valid for this site
|
||||||
# See https://docs.djangoproject.com/en/1.6/ref/settings/#allowed-hosts
|
# See https://docs.djangoproject.com/en/1.6/ref/settings/#allowed-hosts
|
||||||
ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS')
|
ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS')
|
||||||
|
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
|
||||||
|
|
||||||
# END SITE CONFIGURATION
|
# END SITE CONFIGURATION
|
||||||
|
|
||||||
INSTALLED_APPS += ("gunicorn", )
|
INSTALLED_APPS += ("gunicorn", )
|
||||||
|
|
|
@ -22,8 +22,8 @@ CACHES = {
|
||||||
'LOCATION': ''
|
'LOCATION': ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
INSTALLED_APPS += ('kombu.transport.django',)
|
|
||||||
BROKER_URL = 'django://'
|
BROKER_URL = 'memory://'
|
||||||
|
|
||||||
# TESTING
|
# TESTING
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -10,9 +10,9 @@ from django.views import defaults as default_views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Django Admin, use {% url 'admin:index' %}
|
# Django Admin, use {% url 'admin:index' %}
|
||||||
url(settings.ADMIN_URL, include(admin.site.urls)),
|
url(settings.ADMIN_URL, admin.site.urls),
|
||||||
|
|
||||||
url(r'^api/', include("config.api_urls", namespace="api")),
|
url(r'^api/', include(("config.api_urls", 'api'), namespace="api")),
|
||||||
url(r'^api/auth/', include('rest_auth.urls')),
|
url(r'^api/auth/', include('rest_auth.urls')),
|
||||||
url(r'^api/auth/registration/', include('funkwhale_api.users.rest_auth_urls')),
|
url(r'^api/auth/registration/', include('funkwhale_api.users.rest_auth_urls')),
|
||||||
url(r'^accounts/', include('allauth.urls')),
|
url(r'^accounts/', include('allauth.urls')),
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
__version__ = '0.2.4'
|
__version__ = '0.2.5'
|
||||||
__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])
|
__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])
|
||||||
|
|
|
@ -7,5 +7,5 @@ class ConditionalAuthentication(BasePermission):
|
||||||
|
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
if settings.API_AUTHENTICATION_REQUIRED:
|
if settings.API_AUTHENTICATION_REQUIRED:
|
||||||
return request.user and request.user.is_authenticated()
|
return request.user and request.user.is_authenticated
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -25,7 +25,7 @@ class Migration(migrations.Migration):
|
||||||
'ordering': ('domain',),
|
'ordering': ('domain',),
|
||||||
},
|
},
|
||||||
managers=[
|
managers=[
|
||||||
(b'objects', django.contrib.sites.models.SiteManager()),
|
('objects', django.contrib.sites.models.SiteManager()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -19,8 +19,8 @@ class Migration(migrations.Migration):
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)),
|
('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)),
|
||||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
('track', models.ForeignKey(related_name='track_favorites', to='music.Track')),
|
('track', models.ForeignKey(related_name='track_favorites', to='music.Track', on_delete=models.CASCADE)),
|
||||||
('user', models.ForeignKey(related_name='track_favorites', to=settings.AUTH_USER_MODEL)),
|
('user', models.ForeignKey(related_name='track_favorites', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'ordering': ('-creation_date',),
|
'ordering': ('-creation_date',),
|
||||||
|
|
|
@ -5,8 +5,10 @@ from funkwhale_api.music.models import Track
|
||||||
|
|
||||||
class TrackFavorite(models.Model):
|
class TrackFavorite(models.Model):
|
||||||
creation_date = models.DateTimeField(default=timezone.now)
|
creation_date = models.DateTimeField(default=timezone.now)
|
||||||
user = models.ForeignKey('users.User', related_name='track_favorites')
|
user = models.ForeignKey(
|
||||||
track = models.ForeignKey(Track, related_name='track_favorites')
|
'users.User', related_name='track_favorites', on_delete=models.CASCADE)
|
||||||
|
track = models.ForeignKey(
|
||||||
|
Track, related_name='track_favorites', on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('track', 'user')
|
unique_together = ('track', 'user')
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import json
|
import json
|
||||||
from test_plus.test import TestCase
|
from test_plus.test import TestCase
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from funkwhale_api.music.models import Track, Artist
|
from funkwhale_api.music.models import Track, Artist
|
||||||
from funkwhale_api.favorites.models import TrackFavorite
|
from funkwhale_api.favorites.models import TrackFavorite
|
||||||
|
|
|
@ -20,8 +20,8 @@ class Migration(migrations.Migration):
|
||||||
('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
|
('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
|
||||||
('end_date', models.DateTimeField(null=True, blank=True, default=django.utils.timezone.now)),
|
('end_date', models.DateTimeField(null=True, blank=True, default=django.utils.timezone.now)),
|
||||||
('session_key', models.CharField(null=True, blank=True, max_length=100)),
|
('session_key', models.CharField(null=True, blank=True, max_length=100)),
|
||||||
('track', models.ForeignKey(related_name='listenings', to='music.Track')),
|
('track', models.ForeignKey(related_name='listenings', to='music.Track', on_delete=models.CASCADE)),
|
||||||
('user', models.ForeignKey(blank=True, null=True, related_name='listenings', to=settings.AUTH_USER_MODEL)),
|
('user', models.ForeignKey(blank=True, null=True, related_name='listenings', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'ordering': ('-end_date',),
|
'ordering': ('-end_date',),
|
||||||
|
|
|
@ -7,8 +7,14 @@ from funkwhale_api.music.models import Track
|
||||||
|
|
||||||
class Listening(models.Model):
|
class Listening(models.Model):
|
||||||
end_date = models.DateTimeField(default=timezone.now, null=True, blank=True)
|
end_date = models.DateTimeField(default=timezone.now, null=True, blank=True)
|
||||||
track = models.ForeignKey(Track, related_name="listenings")
|
track = models.ForeignKey(
|
||||||
user = models.ForeignKey('users.User', related_name="listenings", null=True, blank=True)
|
Track, related_name="listenings", on_delete=models.CASCADE)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
'users.User',
|
||||||
|
related_name="listenings",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.CASCADE)
|
||||||
session_key = models.CharField(max_length=100, null=True, blank=True)
|
session_key = models.CharField(max_length=100, null=True, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import factory
|
||||||
|
from funkwhale_api.music.tests import factories
|
||||||
|
|
||||||
|
from funkwhale_api.users.tests.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
|
class ListeningFactory(factory.django.DjangoModelFactory):
|
||||||
|
user = factory.SubFactory(UserFactory)
|
||||||
|
track = factory.SubFactory(factories.TrackFactory)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = 'history.Listening'
|
|
@ -1,15 +1,16 @@
|
||||||
import random
|
import random
|
||||||
import json
|
import json
|
||||||
from test_plus.test import TestCase
|
from test_plus.test import TestCase
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from model_mommy import mommy
|
from funkwhale_api.music.tests.factories import TrackFactory
|
||||||
|
|
||||||
from funkwhale_api.users.models import User
|
from funkwhale_api.users.models import User
|
||||||
from funkwhale_api.history import models
|
from funkwhale_api.history import models
|
||||||
|
|
||||||
|
|
||||||
class TestHistory(TestCase):
|
class TestHistory(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -17,12 +18,12 @@ class TestHistory(TestCase):
|
||||||
self.user = User.objects.create_user(username='test', email='test@test.com', password='test')
|
self.user = User.objects.create_user(username='test', email='test@test.com', password='test')
|
||||||
|
|
||||||
def test_can_create_listening(self):
|
def test_can_create_listening(self):
|
||||||
track = mommy.make('music.Track')
|
track = TrackFactory()
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
l = models.Listening.objects.create(user=self.user, track=track)
|
l = models.Listening.objects.create(user=self.user, track=track)
|
||||||
|
|
||||||
def test_anonymous_user_can_create_listening_via_api(self):
|
def test_anonymous_user_can_create_listening_via_api(self):
|
||||||
track = mommy.make('music.Track')
|
track = TrackFactory()
|
||||||
url = self.reverse('api:v1:history:listenings-list')
|
url = self.reverse('api:v1:history:listenings-list')
|
||||||
response = self.client.post(url, {
|
response = self.client.post(url, {
|
||||||
'track': track.pk,
|
'track': track.pk,
|
||||||
|
@ -34,7 +35,7 @@ class TestHistory(TestCase):
|
||||||
self.assertIsNotNone(listening.session_key)
|
self.assertIsNotNone(listening.session_key)
|
||||||
|
|
||||||
def test_logged_in_user_can_create_listening_via_api(self):
|
def test_logged_in_user_can_create_listening_via_api(self):
|
||||||
track = mommy.make('music.Track')
|
track = TrackFactory()
|
||||||
|
|
||||||
self.client.login(username=self.user.username, password='test')
|
self.client.login(username=self.user.username, password='test')
|
||||||
|
|
||||||
|
|
|
@ -22,14 +22,14 @@ class ListeningViewSet(mixins.CreateModelMixin,
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
if self.request.user.is_authenticated():
|
if self.request.user.is_authenticated:
|
||||||
return queryset.filter(user=self.request.user)
|
return queryset.filter(user=self.request.user)
|
||||||
else:
|
else:
|
||||||
return queryset.filter(session_key=self.request.session.session_key)
|
return queryset.filter(session_key=self.request.session.session_key)
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
context = super().get_serializer_context()
|
context = super().get_serializer_context()
|
||||||
if self.request.user.is_authenticated():
|
if self.request.user.is_authenticated:
|
||||||
context['user'] = self.request.user
|
context['user'] = self.request.user
|
||||||
else:
|
else:
|
||||||
context['session_key'] = self.request.session.session_key
|
context['session_key'] = self.request.session.session_key
|
||||||
|
|
|
@ -44,7 +44,7 @@ class Migration(migrations.Migration):
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')),
|
||||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
('submitted_by', models.ForeignKey(related_name='imports', to=settings.AUTH_USER_MODEL)),
|
('submitted_by', models.ForeignKey(related_name='imports', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
|
@ -54,7 +54,7 @@ class Migration(migrations.Migration):
|
||||||
('source', models.URLField()),
|
('source', models.URLField()),
|
||||||
('mbid', models.UUIDField(editable=False)),
|
('mbid', models.UUIDField(editable=False)),
|
||||||
('status', models.CharField(default='pending', choices=[('pending', 'Pending'), ('finished', 'finished')], max_length=30)),
|
('status', models.CharField(default='pending', choices=[('pending', 'Pending'), ('finished', 'finished')], max_length=30)),
|
||||||
('batch', models.ForeignKey(related_name='jobs', to='music.ImportBatch')),
|
('batch', models.ForeignKey(related_name='jobs', to='music.ImportBatch', on_delete=models.CASCADE)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
|
@ -64,8 +64,8 @@ class Migration(migrations.Migration):
|
||||||
('mbid', models.UUIDField(editable=False, blank=True, null=True)),
|
('mbid', models.UUIDField(editable=False, blank=True, null=True)),
|
||||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
('title', models.CharField(max_length=255)),
|
('title', models.CharField(max_length=255)),
|
||||||
('album', models.ForeignKey(related_name='tracks', blank=True, null=True, to='music.Album')),
|
('album', models.ForeignKey(related_name='tracks', blank=True, null=True, to='music.Album', on_delete=models.CASCADE)),
|
||||||
('artist', models.ForeignKey(related_name='tracks', to='music.Artist')),
|
('artist', models.ForeignKey(related_name='tracks', to='music.Artist', on_delete=models.CASCADE)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
'abstract': False,
|
||||||
|
@ -78,12 +78,12 @@ class Migration(migrations.Migration):
|
||||||
('audio_file', models.FileField(upload_to='tracks')),
|
('audio_file', models.FileField(upload_to='tracks')),
|
||||||
('source', models.URLField(blank=True, null=True)),
|
('source', models.URLField(blank=True, null=True)),
|
||||||
('duration', models.IntegerField(blank=True, null=True)),
|
('duration', models.IntegerField(blank=True, null=True)),
|
||||||
('track', models.ForeignKey(related_name='files', to='music.Track')),
|
('track', models.ForeignKey(related_name='files', to='music.Track', on_delete=models.CASCADE)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='album',
|
model_name='album',
|
||||||
name='artist',
|
name='artist',
|
||||||
field=models.ForeignKey(related_name='albums', to='music.Artist'),
|
field=models.ForeignKey(related_name='albums', to='music.Artist', on_delete=models.CASCADE),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -39,11 +39,11 @@ class Migration(migrations.Migration):
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='lyrics',
|
model_name='lyrics',
|
||||||
name='work',
|
name='work',
|
||||||
field=models.ForeignKey(related_name='lyrics', to='music.Work', blank=True, null=True),
|
field=models.ForeignKey(related_name='lyrics', to='music.Work', blank=True, null=True, on_delete=models.CASCADE),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='track',
|
model_name='track',
|
||||||
name='work',
|
name='work',
|
||||||
field=models.ForeignKey(related_name='tracks', to='music.Work', blank=True, null=True),
|
field=models.ForeignKey(related_name='tracks', to='music.Work', blank=True, null=True, on_delete=models.CASCADE),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -10,7 +10,7 @@ from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
from versatileimagefield.fields import VersatileImageField
|
from versatileimagefield.fields import VersatileImageField
|
||||||
|
@ -108,7 +108,8 @@ def import_tracks(instance, cleaned_data, raw_data):
|
||||||
|
|
||||||
class Album(APIModelMixin):
|
class Album(APIModelMixin):
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(max_length=255)
|
||||||
artist = models.ForeignKey(Artist, related_name='albums')
|
artist = models.ForeignKey(
|
||||||
|
Artist, related_name='albums', on_delete=models.CASCADE)
|
||||||
release_date = models.DateField(null=True)
|
release_date = models.DateField(null=True)
|
||||||
release_group_id = models.UUIDField(null=True, blank=True)
|
release_group_id = models.UUIDField(null=True, blank=True)
|
||||||
cover = VersatileImageField(upload_to='albums/covers/%Y/%m/%d', null=True, blank=True)
|
cover = VersatileImageField(upload_to='albums/covers/%Y/%m/%d', null=True, blank=True)
|
||||||
|
@ -245,7 +246,12 @@ class Work(APIModelMixin):
|
||||||
|
|
||||||
|
|
||||||
class Lyrics(models.Model):
|
class Lyrics(models.Model):
|
||||||
work = models.ForeignKey(Work, related_name='lyrics', null=True, blank=True)
|
work = models.ForeignKey(
|
||||||
|
Work,
|
||||||
|
related_name='lyrics',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.CASCADE)
|
||||||
url = models.URLField(unique=True)
|
url = models.URLField(unique=True)
|
||||||
content = models.TextField(null=True, blank=True)
|
content = models.TextField(null=True, blank=True)
|
||||||
|
|
||||||
|
@ -268,10 +274,21 @@ class Lyrics(models.Model):
|
||||||
|
|
||||||
class Track(APIModelMixin):
|
class Track(APIModelMixin):
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(max_length=255)
|
||||||
artist = models.ForeignKey(Artist, related_name='tracks')
|
artist = models.ForeignKey(
|
||||||
|
Artist, related_name='tracks', on_delete=models.CASCADE)
|
||||||
position = models.PositiveIntegerField(null=True, blank=True)
|
position = models.PositiveIntegerField(null=True, blank=True)
|
||||||
album = models.ForeignKey(Album, related_name='tracks', null=True, blank=True)
|
album = models.ForeignKey(
|
||||||
work = models.ForeignKey(Work, related_name='tracks', null=True, blank=True)
|
Album,
|
||||||
|
related_name='tracks',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
work = models.ForeignKey(
|
||||||
|
Work,
|
||||||
|
related_name='tracks',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
|
||||||
musicbrainz_model = 'recording'
|
musicbrainz_model = 'recording'
|
||||||
api = musicbrainz.api.recordings
|
api = musicbrainz.api.recordings
|
||||||
|
@ -340,7 +357,8 @@ class Track(APIModelMixin):
|
||||||
|
|
||||||
|
|
||||||
class TrackFile(models.Model):
|
class TrackFile(models.Model):
|
||||||
track = models.ForeignKey(Track, related_name='files')
|
track = models.ForeignKey(
|
||||||
|
Track, related_name='files', on_delete=models.CASCADE)
|
||||||
audio_file = models.FileField(upload_to='tracks/%Y/%m/%d', max_length=255)
|
audio_file = models.FileField(upload_to='tracks/%Y/%m/%d', max_length=255)
|
||||||
source = models.URLField(null=True, blank=True)
|
source = models.URLField(null=True, blank=True)
|
||||||
duration = models.IntegerField(null=True, blank=True)
|
duration = models.IntegerField(null=True, blank=True)
|
||||||
|
@ -376,7 +394,8 @@ class TrackFile(models.Model):
|
||||||
|
|
||||||
class ImportBatch(models.Model):
|
class ImportBatch(models.Model):
|
||||||
creation_date = models.DateTimeField(default=timezone.now)
|
creation_date = models.DateTimeField(default=timezone.now)
|
||||||
submitted_by = models.ForeignKey('users.User', related_name='imports')
|
submitted_by = models.ForeignKey(
|
||||||
|
'users.User', related_name='imports', on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['-creation_date']
|
ordering = ['-creation_date']
|
||||||
|
@ -392,9 +411,14 @@ class ImportBatch(models.Model):
|
||||||
return 'finished'
|
return 'finished'
|
||||||
|
|
||||||
class ImportJob(models.Model):
|
class ImportJob(models.Model):
|
||||||
batch = models.ForeignKey(ImportBatch, related_name='jobs')
|
batch = models.ForeignKey(
|
||||||
|
ImportBatch, related_name='jobs', on_delete=models.CASCADE)
|
||||||
track_file = models.ForeignKey(
|
track_file = models.ForeignKey(
|
||||||
TrackFile, related_name='jobs', null=True, blank=True)
|
TrackFile,
|
||||||
|
related_name='jobs',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.CASCADE)
|
||||||
source = models.URLField()
|
source = models.URLField()
|
||||||
mbid = models.UUIDField(editable=False)
|
mbid = models.UUIDField(editable=False)
|
||||||
STATUS_CHOICES = (
|
STATUS_CHOICES = (
|
||||||
|
|
|
@ -59,3 +59,30 @@ class ImportJobFactory(factory.django.DjangoModelFactory):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = 'music.ImportJob'
|
model = 'music.ImportJob'
|
||||||
|
|
||||||
|
|
||||||
|
class WorkFactory(factory.django.DjangoModelFactory):
|
||||||
|
mbid = factory.Faker('uuid4')
|
||||||
|
language = 'eng'
|
||||||
|
nature = 'song'
|
||||||
|
title = factory.Faker('sentence', nb_words=3)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = 'music.Work'
|
||||||
|
|
||||||
|
|
||||||
|
class LyricsFactory(factory.django.DjangoModelFactory):
|
||||||
|
work = factory.SubFactory(WorkFactory)
|
||||||
|
url = factory.Faker('url')
|
||||||
|
content = factory.Faker('paragraphs', nb=4)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = 'music.Lyrics'
|
||||||
|
|
||||||
|
|
||||||
|
class TagFactory(factory.django.DjangoModelFactory):
|
||||||
|
name = factory.SelfAttribute('slug')
|
||||||
|
slug = factory.Faker('slug')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = 'taggit.Tag'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import json
|
import json
|
||||||
import unittest
|
import unittest
|
||||||
from test_plus.test import TestCase
|
from test_plus.test import TestCase
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from funkwhale_api.music import models
|
from funkwhale_api.music import models
|
||||||
from funkwhale_api.utils.tests import TMPDirTestCaseMixin
|
from funkwhale_api.utils.tests import TMPDirTestCaseMixin
|
||||||
|
|
|
@ -1,25 +1,26 @@
|
||||||
import json
|
import json
|
||||||
import unittest
|
import unittest
|
||||||
from test_plus.test import TestCase
|
from test_plus.test import TestCase
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from model_mommy import mommy
|
|
||||||
|
|
||||||
from funkwhale_api.music import models
|
from funkwhale_api.music import models
|
||||||
from funkwhale_api.musicbrainz import api
|
from funkwhale_api.musicbrainz import api
|
||||||
from funkwhale_api.music import serializers
|
from funkwhale_api.music import serializers
|
||||||
from funkwhale_api.users.models import User
|
from funkwhale_api.users.models import User
|
||||||
|
from funkwhale_api.music import lyrics as lyrics_utils
|
||||||
|
|
||||||
from .mocking import lyricswiki
|
from .mocking import lyricswiki
|
||||||
|
from . import factories
|
||||||
from . import data as api_data
|
from . import data as api_data
|
||||||
from funkwhale_api.music import lyrics as lyrics_utils
|
|
||||||
|
|
||||||
|
|
||||||
class TestLyrics(TestCase):
|
class TestLyrics(TestCase):
|
||||||
|
|
||||||
@unittest.mock.patch('funkwhale_api.music.lyrics._get_html',
|
@unittest.mock.patch('funkwhale_api.music.lyrics._get_html',
|
||||||
return_value=lyricswiki.content)
|
return_value=lyricswiki.content)
|
||||||
def test_works_import_lyrics_if_any(self, *mocks):
|
def test_works_import_lyrics_if_any(self, *mocks):
|
||||||
lyrics = mommy.make(
|
lyrics = factories.LyricsFactory(
|
||||||
models.Lyrics,
|
|
||||||
url='http://lyrics.wikia.com/System_Of_A_Down:Chop_Suey!')
|
url='http://lyrics.wikia.com/System_Of_A_Down:Chop_Suey!')
|
||||||
|
|
||||||
lyrics.fetch_content()
|
lyrics.fetch_content()
|
||||||
|
@ -42,7 +43,7 @@ Is it me you're looking for?
|
||||||
content = """Hello
|
content = """Hello
|
||||||
Is it me you're looking for?"""
|
Is it me you're looking for?"""
|
||||||
|
|
||||||
l = mommy.make(models.Lyrics, content=content)
|
l = factories.LyricsFactory(content=content)
|
||||||
|
|
||||||
expected = "<p>Hello<br />Is it me you're looking for?</p>"
|
expected = "<p>Hello<br />Is it me you're looking for?</p>"
|
||||||
self.assertHTMLEqual(expected, l.content_rendered)
|
self.assertHTMLEqual(expected, l.content_rendered)
|
||||||
|
@ -54,8 +55,7 @@ Is it me you're looking for?"""
|
||||||
@unittest.mock.patch('funkwhale_api.music.lyrics._get_html',
|
@unittest.mock.patch('funkwhale_api.music.lyrics._get_html',
|
||||||
return_value=lyricswiki.content)
|
return_value=lyricswiki.content)
|
||||||
def test_works_import_lyrics_if_any(self, *mocks):
|
def test_works_import_lyrics_if_any(self, *mocks):
|
||||||
track = mommy.make(
|
track = factories.TrackFactory(
|
||||||
models.Track,
|
|
||||||
work=None,
|
work=None,
|
||||||
mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
|
mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,11 @@ from test_plus.test import TestCase
|
||||||
import unittest.mock
|
import unittest.mock
|
||||||
from funkwhale_api.music import models
|
from funkwhale_api.music import models
|
||||||
import datetime
|
import datetime
|
||||||
from model_mommy import mommy
|
|
||||||
|
from . import factories
|
||||||
from . import data as api_data
|
from . import data as api_data
|
||||||
from .cover import binary_data
|
from .cover import binary_data
|
||||||
|
|
||||||
def prettyprint(d):
|
|
||||||
import json
|
|
||||||
print(json.dumps(d, sort_keys=True, indent=4))
|
|
||||||
|
|
||||||
class TestMusic(TestCase):
|
class TestMusic(TestCase):
|
||||||
|
|
||||||
|
@ -79,9 +77,9 @@ class TestMusic(TestCase):
|
||||||
self.assertEqual(track, track2)
|
self.assertEqual(track, track2)
|
||||||
|
|
||||||
def test_album_tags_deduced_from_tracks_tags(self):
|
def test_album_tags_deduced_from_tracks_tags(self):
|
||||||
tag = mommy.make('taggit.Tag')
|
tag = factories.TagFactory()
|
||||||
album = mommy.make('music.Album')
|
album = factories.AlbumFactory()
|
||||||
tracks = mommy.make('music.Track', album=album, _quantity=5)
|
tracks = factories.TrackFactory.create_batch(album=album, size=5)
|
||||||
|
|
||||||
for track in tracks:
|
for track in tracks:
|
||||||
track.tags.add(tag)
|
track.tags.add(tag)
|
||||||
|
@ -92,10 +90,10 @@ class TestMusic(TestCase):
|
||||||
self.assertIn(tag, album.tags)
|
self.assertIn(tag, album.tags)
|
||||||
|
|
||||||
def test_artist_tags_deduced_from_album_tags(self):
|
def test_artist_tags_deduced_from_album_tags(self):
|
||||||
tag = mommy.make('taggit.Tag')
|
tag = factories.TagFactory()
|
||||||
artist = mommy.make('music.Artist')
|
artist = factories.ArtistFactory()
|
||||||
album = mommy.make('music.Album', artist=artist)
|
album = factories.AlbumFactory(artist=artist)
|
||||||
tracks = mommy.make('music.Track', album=album, _quantity=5)
|
tracks = factories.TrackFactory.create_batch(album=album, size=5)
|
||||||
|
|
||||||
for track in tracks:
|
for track in tracks:
|
||||||
track.tags.add(tag)
|
track.tags.add(tag)
|
||||||
|
@ -108,7 +106,7 @@ class TestMusic(TestCase):
|
||||||
@unittest.mock.patch('funkwhale_api.musicbrainz.api.images.get_front', return_value=binary_data)
|
@unittest.mock.patch('funkwhale_api.musicbrainz.api.images.get_front', return_value=binary_data)
|
||||||
def test_can_download_image_file_for_album(self, *mocks):
|
def test_can_download_image_file_for_album(self, *mocks):
|
||||||
# client._api.get_image_front('55ea4f82-b42b-423e-a0e5-290ccdf443ed')
|
# client._api.get_image_front('55ea4f82-b42b-423e-a0e5-290ccdf443ed')
|
||||||
album = mommy.make('music.Album', mbid='55ea4f82-b42b-423e-a0e5-290ccdf443ed')
|
album = factories.AlbumFactory(mbid='55ea4f82-b42b-423e-a0e5-290ccdf443ed')
|
||||||
album.get_image()
|
album.get_image()
|
||||||
album.save()
|
album.save()
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,24 @@
|
||||||
import json
|
import json
|
||||||
import unittest
|
import unittest
|
||||||
from test_plus.test import TestCase
|
from test_plus.test import TestCase
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from model_mommy import mommy
|
|
||||||
|
|
||||||
from funkwhale_api.music import models
|
from funkwhale_api.music import models
|
||||||
from funkwhale_api.musicbrainz import api
|
from funkwhale_api.musicbrainz import api
|
||||||
from funkwhale_api.music import serializers
|
from funkwhale_api.music import serializers
|
||||||
|
from funkwhale_api.music.tests import factories
|
||||||
from funkwhale_api.users.models import User
|
from funkwhale_api.users.models import User
|
||||||
|
|
||||||
from . import data as api_data
|
from . import data as api_data
|
||||||
|
|
||||||
|
|
||||||
class TestWorks(TestCase):
|
class TestWorks(TestCase):
|
||||||
|
|
||||||
@unittest.mock.patch('funkwhale_api.musicbrainz.api.works.get',
|
@unittest.mock.patch('funkwhale_api.musicbrainz.api.works.get',
|
||||||
return_value=api_data.works['get']['chop_suey'])
|
return_value=api_data.works['get']['chop_suey'])
|
||||||
def test_can_import_work(self, *mocks):
|
def test_can_import_work(self, *mocks):
|
||||||
recording = mommy.make(
|
recording = factories.TrackFactory(
|
||||||
models.Track, mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
|
mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
|
||||||
mbid = 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5'
|
mbid = 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5'
|
||||||
work = models.Work.create_from_api(id=mbid)
|
work = models.Work.create_from_api(id=mbid)
|
||||||
|
|
||||||
|
@ -36,8 +37,7 @@ class TestWorks(TestCase):
|
||||||
@unittest.mock.patch('funkwhale_api.musicbrainz.api.recordings.get',
|
@unittest.mock.patch('funkwhale_api.musicbrainz.api.recordings.get',
|
||||||
return_value=api_data.tracks['get']['chop_suey'])
|
return_value=api_data.tracks['get']['chop_suey'])
|
||||||
def test_can_get_work_from_recording(self, *mocks):
|
def test_can_get_work_from_recording(self, *mocks):
|
||||||
recording = mommy.make(
|
recording = factories.TrackFactory(
|
||||||
models.Track,
|
|
||||||
work=None,
|
work=None,
|
||||||
mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
|
mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
|
||||||
mbid = 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5'
|
mbid = 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5'
|
||||||
|
|
|
@ -2,7 +2,7 @@ import os
|
||||||
import json
|
import json
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import urllib
|
import urllib
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models.functions import Length
|
from django.db.models.functions import Length
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -102,7 +102,7 @@ class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
filter_favorites = self.request.GET.get('favorites', None)
|
filter_favorites = self.request.GET.get('favorites', None)
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
if user.is_authenticated() and filter_favorites == 'true':
|
if user.is_authenticated and filter_favorites == 'true':
|
||||||
queryset = queryset.filter(track_favorites__user=user)
|
queryset = queryset.filter(track_favorites__user=user)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import json
|
import json
|
||||||
import unittest
|
import unittest
|
||||||
from test_plus.test import TestCase
|
from test_plus.test import TestCase
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from funkwhale_api.musicbrainz import api
|
from funkwhale_api.musicbrainz import api
|
||||||
from . import data as api_data
|
from . import data as api_data
|
||||||
|
|
|
@ -22,7 +22,7 @@ class Migration(migrations.Migration):
|
||||||
('name', models.CharField(max_length=50)),
|
('name', models.CharField(max_length=50)),
|
||||||
('is_public', models.BooleanField(default=False)),
|
('is_public', models.BooleanField(default=False)),
|
||||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='playlists')),
|
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='playlists', on_delete=models.CASCADE)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
|
@ -33,9 +33,9 @@ class Migration(migrations.Migration):
|
||||||
('rght', models.PositiveIntegerField(db_index=True, editable=False)),
|
('rght', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||||
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
|
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||||
('position', 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')),
|
('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)),
|
('previous', mptt.fields.TreeOneToOneField(null=True, to='playlists.PlaylistTrack', related_name='next', blank=True, on_delete=models.CASCADE)),
|
||||||
('track', models.ForeignKey(to='music.Track', related_name='playlist_tracks')),
|
('track', models.ForeignKey(to='music.Track', related_name='playlist_tracks', on_delete=models.CASCADE)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'ordering': ('-playlist', 'position'),
|
'ordering': ('-playlist', 'position'),
|
||||||
|
|
|
@ -7,7 +7,8 @@ from mptt.models import MPTTModel, TreeOneToOneField
|
||||||
class Playlist(models.Model):
|
class Playlist(models.Model):
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
is_public = models.BooleanField(default=False)
|
is_public = models.BooleanField(default=False)
|
||||||
user = models.ForeignKey('users.User', related_name="playlists")
|
user = models.ForeignKey(
|
||||||
|
'users.User', related_name="playlists", on_delete=models.CASCADE)
|
||||||
creation_date = models.DateTimeField(default=timezone.now)
|
creation_date = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -21,9 +22,18 @@ class Playlist(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class PlaylistTrack(MPTTModel):
|
class PlaylistTrack(MPTTModel):
|
||||||
track = models.ForeignKey('music.Track', related_name='playlist_tracks')
|
track = models.ForeignKey(
|
||||||
previous = TreeOneToOneField('self', blank=True, null=True, related_name='next')
|
'music.Track',
|
||||||
playlist = models.ForeignKey(Playlist, related_name='playlist_tracks')
|
related_name='playlist_tracks',
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
previous = TreeOneToOneField(
|
||||||
|
'self',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
related_name='next',
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
playlist = models.ForeignKey(
|
||||||
|
Playlist, related_name='playlist_tracks', on_delete=models.CASCADE)
|
||||||
|
|
||||||
class MPTTMeta:
|
class MPTTMeta:
|
||||||
level_attr = 'position'
|
level_attr = 'position'
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import json
|
import json
|
||||||
from test_plus.test import TestCase
|
from test_plus.test import TestCase
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from model_mommy import mommy
|
from funkwhale_api.music.tests import factories
|
||||||
|
|
||||||
from funkwhale_api.users.models import User
|
from funkwhale_api.users.models import User
|
||||||
from funkwhale_api.playlists import models
|
from funkwhale_api.playlists import models
|
||||||
from funkwhale_api.playlists.serializers import PlaylistSerializer
|
from funkwhale_api.playlists.serializers import PlaylistSerializer
|
||||||
|
@ -18,7 +17,7 @@ class TestPlayLists(TestCase):
|
||||||
self.user = User.objects.create_user(username='test', email='test@test.com', password='test')
|
self.user = User.objects.create_user(username='test', email='test@test.com', password='test')
|
||||||
|
|
||||||
def test_can_create_playlist(self):
|
def test_can_create_playlist(self):
|
||||||
tracks = list(mommy.make('music.Track', _quantity=5))
|
tracks = factories.TrackFactory.create_batch(size=5)
|
||||||
playlist = models.Playlist.objects.create(user=self.user, name="test")
|
playlist = models.Playlist.objects.create(user=self.user, name="test")
|
||||||
|
|
||||||
previous = None
|
previous = None
|
||||||
|
@ -49,7 +48,7 @@ class TestPlayLists(TestCase):
|
||||||
self.assertEqual(playlist.name, 'test')
|
self.assertEqual(playlist.name, 'test')
|
||||||
|
|
||||||
def test_can_add_playlist_track_via_api(self):
|
def test_can_add_playlist_track_via_api(self):
|
||||||
tracks = list(mommy.make('music.Track', _quantity=5))
|
tracks = factories.TrackFactory.create_batch(size=5)
|
||||||
playlist = models.Playlist.objects.create(user=self.user, name="test")
|
playlist = models.Playlist.objects.create(user=self.user, name="test")
|
||||||
|
|
||||||
self.client.login(username=self.user.username, password='test')
|
self.client.login(username=self.user.username, password='test')
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from funkwhale_api.music import views
|
from funkwhale_api.music import views
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^youtube/', include('funkwhale_api.providers.youtube.urls', namespace='youtube')),
|
url(r'^youtube/', include(
|
||||||
url(r'^musicbrainz/', include('funkwhale_api.musicbrainz.urls', namespace='musicbrainz')),
|
('funkwhale_api.providers.youtube.urls', 'youtube'),
|
||||||
|
namespace='youtube')),
|
||||||
|
url(r'^musicbrainz/', include(
|
||||||
|
('funkwhale_api.musicbrainz.urls', 'musicbrainz'),
|
||||||
|
namespace='musicbrainz')),
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,7 +2,7 @@ import json
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
import unittest
|
import unittest
|
||||||
from test_plus.test import TestCase
|
from test_plus.test import TestCase
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from funkwhale_api.providers.youtube.client import client
|
from funkwhale_api.providers.youtube.client import client
|
||||||
|
|
||||||
from . import data as api_data
|
from . import data as api_data
|
||||||
|
|
|
@ -20,7 +20,7 @@ class Migration(migrations.Migration):
|
||||||
('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
|
('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
|
||||||
('radio_type', models.CharField(max_length=50)),
|
('radio_type', models.CharField(max_length=50)),
|
||||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
('user', models.ForeignKey(related_name='radio_sessions', blank=True, to=settings.AUTH_USER_MODEL, null=True)),
|
('user', models.ForeignKey(related_name='radio_sessions', blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
|
@ -28,8 +28,8 @@ class Migration(migrations.Migration):
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
|
('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
|
||||||
('position', models.IntegerField(default=1)),
|
('position', models.IntegerField(default=1)),
|
||||||
('session', models.ForeignKey(to='radios.RadioSession', related_name='session_tracks')),
|
('session', models.ForeignKey(to='radios.RadioSession', related_name='session_tracks', on_delete=models.CASCADE)),
|
||||||
('track', models.ForeignKey(to='music.Track', related_name='radio_session_tracks')),
|
('track', models.ForeignKey(to='music.Track', related_name='radio_session_tracks', on_delete=models.CASCADE)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'ordering': ('session', 'position'),
|
'ordering': ('session', 'position'),
|
||||||
|
|
|
@ -15,7 +15,7 @@ class Migration(migrations.Migration):
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='radiosession',
|
model_name='radiosession',
|
||||||
name='related_object_content_type',
|
name='related_object_content_type',
|
||||||
field=models.ForeignKey(null=True, to='contenttypes.ContentType', blank=True),
|
field=models.ForeignKey(null=True, to='contenttypes.ContentType', blank=True, on_delete=models.CASCADE),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='radiosession',
|
model_name='radiosession',
|
||||||
|
|
|
@ -7,11 +7,20 @@ from django.contrib.contenttypes.models import ContentType
|
||||||
from funkwhale_api.music.models import Track
|
from funkwhale_api.music.models import Track
|
||||||
|
|
||||||
class RadioSession(models.Model):
|
class RadioSession(models.Model):
|
||||||
user = models.ForeignKey('users.User', related_name='radio_sessions', null=True, blank=True)
|
user = models.ForeignKey(
|
||||||
|
'users.User',
|
||||||
|
related_name='radio_sessions',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.CASCADE)
|
||||||
session_key = models.CharField(max_length=100, null=True, blank=True)
|
session_key = models.CharField(max_length=100, null=True, blank=True)
|
||||||
radio_type = models.CharField(max_length=50)
|
radio_type = models.CharField(max_length=50)
|
||||||
creation_date = models.DateTimeField(default=timezone.now)
|
creation_date = models.DateTimeField(default=timezone.now)
|
||||||
related_object_content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, blank=True, null=True)
|
related_object_content_type = models.ForeignKey(
|
||||||
|
ContentType,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.CASCADE)
|
||||||
related_object_id = models.PositiveIntegerField(blank=True, null=True)
|
related_object_id = models.PositiveIntegerField(blank=True, null=True)
|
||||||
related_object = GenericForeignKey('related_object_content_type', 'related_object_id')
|
related_object = GenericForeignKey('related_object_content_type', 'related_object_id')
|
||||||
|
|
||||||
|
@ -43,9 +52,11 @@ class RadioSession(models.Model):
|
||||||
return registry[self.radio_type](session=self)
|
return registry[self.radio_type](session=self)
|
||||||
|
|
||||||
class RadioSessionTrack(models.Model):
|
class RadioSessionTrack(models.Model):
|
||||||
session = models.ForeignKey(RadioSession, related_name='session_tracks')
|
session = models.ForeignKey(
|
||||||
|
RadioSession, related_name='session_tracks', on_delete=models.CASCADE)
|
||||||
position = models.IntegerField(default=1)
|
position = models.IntegerField(default=1)
|
||||||
track = models.ForeignKey(Track, related_name='radio_session_tracks')
|
track = models.ForeignKey(
|
||||||
|
Track, related_name='radio_session_tracks', on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('session', 'position')
|
ordering = ('session', 'position')
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import random
|
import random
|
||||||
import json
|
import json
|
||||||
from test_plus.test import TestCase
|
from test_plus.test import TestCase
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from model_mommy import mommy
|
|
||||||
|
|
||||||
from funkwhale_api.radios import radios
|
from funkwhale_api.radios import radios
|
||||||
from funkwhale_api.radios import models
|
from funkwhale_api.radios import models
|
||||||
from funkwhale_api.favorites.models import TrackFavorite
|
from funkwhale_api.favorites.models import TrackFavorite
|
||||||
from funkwhale_api.users.models import User
|
from funkwhale_api.users.models import User
|
||||||
from funkwhale_api.music.models import Artist
|
from funkwhale_api.music.models import Artist
|
||||||
|
from funkwhale_api.music.tests import factories
|
||||||
|
from funkwhale_api.history.tests.factories import ListeningFactory
|
||||||
|
|
||||||
|
|
||||||
class TestRadios(TestCase):
|
class TestRadios(TestCase):
|
||||||
|
|
||||||
|
@ -55,7 +57,7 @@ class TestRadios(TestCase):
|
||||||
self.assertTrue(picks[2] > picks[1])
|
self.assertTrue(picks[2] > picks[1])
|
||||||
|
|
||||||
def test_can_get_choices_for_favorites_radio(self):
|
def test_can_get_choices_for_favorites_radio(self):
|
||||||
tracks = mommy.make('music.Track', _quantity=100)
|
tracks = factories.TrackFactory.create_batch(size=100)
|
||||||
|
|
||||||
for i in range(20):
|
for i in range(20):
|
||||||
TrackFavorite.add(track=random.choice(tracks), user=self.user)
|
TrackFavorite.add(track=random.choice(tracks), user=self.user)
|
||||||
|
@ -73,7 +75,7 @@ class TestRadios(TestCase):
|
||||||
self.assertIn(pick, choices)
|
self.assertIn(pick, choices)
|
||||||
|
|
||||||
def test_can_use_radio_session_to_filter_choices(self):
|
def test_can_use_radio_session_to_filter_choices(self):
|
||||||
tracks = mommy.make('music.Track', _quantity=30)
|
tracks = factories.TrackFactory.create_batch(size=30)
|
||||||
radio = radios.RandomRadio()
|
radio = radios.RandomRadio()
|
||||||
session = radio.start_session(self.user)
|
session = radio.start_session(self.user)
|
||||||
|
|
||||||
|
@ -85,7 +87,7 @@ class TestRadios(TestCase):
|
||||||
self.assertEqual(len(set(tracks_id)), 30)
|
self.assertEqual(len(set(tracks_id)), 30)
|
||||||
|
|
||||||
def test_can_restore_radio_from_previous_session(self):
|
def test_can_restore_radio_from_previous_session(self):
|
||||||
tracks = mommy.make('music.Track', _quantity=30)
|
tracks = factories.TrackFactory.create_batch(size=30)
|
||||||
|
|
||||||
radio = radios.RandomRadio()
|
radio = radios.RandomRadio()
|
||||||
session = radio.start_session(self.user)
|
session = radio.start_session(self.user)
|
||||||
|
@ -115,7 +117,7 @@ class TestRadios(TestCase):
|
||||||
self.assertIsNotNone(session.session_key)
|
self.assertIsNotNone(session.session_key)
|
||||||
|
|
||||||
def test_can_get_track_for_session_from_api(self):
|
def test_can_get_track_for_session_from_api(self):
|
||||||
tracks = mommy.make('music.Track', _quantity=1)
|
tracks = factories.TrackFactory.create_batch(size=1)
|
||||||
|
|
||||||
self.client.login(username=self.user.username, password='test')
|
self.client.login(username=self.user.username, password='test')
|
||||||
url = reverse('api:v1:radios:sessions-list')
|
url = reverse('api:v1:radios:sessions-list')
|
||||||
|
@ -129,7 +131,7 @@ class TestRadios(TestCase):
|
||||||
self.assertEqual(data['track']['id'], tracks[0].id)
|
self.assertEqual(data['track']['id'], tracks[0].id)
|
||||||
self.assertEqual(data['position'], 1)
|
self.assertEqual(data['position'], 1)
|
||||||
|
|
||||||
next_track = mommy.make('music.Track')
|
next_track = factories.TrackFactory()
|
||||||
response = self.client.post(url, {'session': session.pk})
|
response = self.client.post(url, {'session': session.pk})
|
||||||
data = json.loads(response.content.decode('utf-8'))
|
data = json.loads(response.content.decode('utf-8'))
|
||||||
|
|
||||||
|
@ -148,9 +150,10 @@ class TestRadios(TestCase):
|
||||||
radio.start_session(self.user, related_object=self.user)
|
radio.start_session(self.user, related_object=self.user)
|
||||||
|
|
||||||
def test_can_start_artist_radio(self):
|
def test_can_start_artist_radio(self):
|
||||||
artist = mommy.make('music.Artist')
|
artist = factories.ArtistFactory()
|
||||||
wrong_tracks = mommy.make('music.Track', _quantity=30)
|
wrong_tracks = factories.TrackFactory.create_batch(size=30)
|
||||||
good_tracks = mommy.make('music.Track', artist=artist, _quantity=5)
|
good_tracks = factories.TrackFactory.create_batch(
|
||||||
|
artist=artist, size=5)
|
||||||
|
|
||||||
radio = radios.ArtistRadio()
|
radio = radios.ArtistRadio()
|
||||||
session = radio.start_session(self.user, related_object=artist)
|
session = radio.start_session(self.user, related_object=artist)
|
||||||
|
@ -159,9 +162,9 @@ class TestRadios(TestCase):
|
||||||
self.assertIn(radio.pick(), good_tracks)
|
self.assertIn(radio.pick(), good_tracks)
|
||||||
|
|
||||||
def test_can_start_tag_radio(self):
|
def test_can_start_tag_radio(self):
|
||||||
tag = mommy.make('taggit.Tag')
|
tag = factories.TagFactory()
|
||||||
wrong_tracks = mommy.make('music.Track', _quantity=30)
|
wrong_tracks = factories.TrackFactory.create_batch(size=30)
|
||||||
good_tracks = mommy.make('music.Track', _quantity=5)
|
good_tracks = factories.TrackFactory.create_batch(size=5)
|
||||||
for track in good_tracks:
|
for track in good_tracks:
|
||||||
track.tags.add(tag)
|
track.tags.add(tag)
|
||||||
|
|
||||||
|
@ -172,7 +175,7 @@ class TestRadios(TestCase):
|
||||||
self.assertIn(radio.pick(), good_tracks)
|
self.assertIn(radio.pick(), good_tracks)
|
||||||
|
|
||||||
def test_can_start_artist_radio_from_api(self):
|
def test_can_start_artist_radio_from_api(self):
|
||||||
artist = mommy.make('music.Artist')
|
artist = factories.ArtistFactory()
|
||||||
url = reverse('api:v1:radios:sessions-list')
|
url = reverse('api:v1:radios:sessions-list')
|
||||||
|
|
||||||
response = self.client.post(url, {'radio_type': 'artist', 'related_object_id': artist.id})
|
response = self.client.post(url, {'radio_type': 'artist', 'related_object_id': artist.id})
|
||||||
|
@ -181,10 +184,10 @@ class TestRadios(TestCase):
|
||||||
self.assertEqual(session.related_object, artist)
|
self.assertEqual(session.related_object, artist)
|
||||||
|
|
||||||
def test_can_start_less_listened_radio(self):
|
def test_can_start_less_listened_radio(self):
|
||||||
history = mommy.make('history.Listening', _quantity=5, user=self.user)
|
history = ListeningFactory.create_batch(size=5, user=self.user)
|
||||||
wrong_tracks = [h.track for h in history]
|
wrong_tracks = [h.track for h in history]
|
||||||
|
|
||||||
good_tracks = mommy.make('music.Track', _quantity=30)
|
good_tracks = factories.TrackFactory.create_batch(size=30)
|
||||||
|
|
||||||
radio = radios.LessListenedRadio()
|
radio = radios.LessListenedRadio()
|
||||||
session = radio.start_session(self.user)
|
session = radio.start_session(self.user)
|
||||||
|
|
|
@ -6,4 +6,5 @@ router = routers.SimpleRouter()
|
||||||
router.register(r'sessions', views.RadioSessionViewSet, 'sessions')
|
router.register(r'sessions', views.RadioSessionViewSet, 'sessions')
|
||||||
router.register(r'tracks', views.RadioSessionTrackViewSet, 'tracks')
|
router.register(r'tracks', views.RadioSessionTrackViewSet, 'tracks')
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = router.urls
|
urlpatterns = router.urls
|
||||||
|
|
|
@ -19,14 +19,14 @@ class RadioSessionViewSet(mixins.CreateModelMixin,
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
if self.request.user.is_authenticated():
|
if self.request.user.is_authenticated:
|
||||||
return queryset.filter(user=self.request.user)
|
return queryset.filter(user=self.request.user)
|
||||||
else:
|
else:
|
||||||
return queryset.filter(session_key=self.request.session.session_key)
|
return queryset.filter(session_key=self.request.session.session_key)
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
context = super().get_serializer_context()
|
context = super().get_serializer_context()
|
||||||
if self.request.user.is_authenticated():
|
if self.request.user.is_authenticated:
|
||||||
context['user'] = self.request.user
|
context['user'] = self.request.user
|
||||||
else:
|
else:
|
||||||
context['session_key'] = self.request.session.session_key
|
context['session_key'] = self.request.session.session_key
|
||||||
|
@ -44,7 +44,7 @@ class RadioSessionTrackViewSet(mixins.CreateModelMixin,
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
session = serializer.validated_data['session']
|
session = serializer.validated_data['session']
|
||||||
try:
|
try:
|
||||||
if request.user.is_authenticated():
|
if request.user.is_authenticated:
|
||||||
assert request.user == session.user
|
assert request.user == session.user
|
||||||
else:
|
else:
|
||||||
assert request.session.session_key == session.session_key
|
assert request.session.session_key == session.session_key
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
|
||||||
|
|
||||||
class AnonymousSessionMiddleware(object):
|
class AnonymousSessionMiddleware:
|
||||||
def process_request(self, request):
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
if not request.session.session_key:
|
if not request.session.session_key:
|
||||||
request.session.save()
|
request.session.save()
|
||||||
|
response = self.get_response(request)
|
||||||
|
return response
|
||||||
|
|
|
@ -38,7 +38,7 @@ class Migration(migrations.Migration):
|
||||||
'verbose_name_plural': 'users',
|
'verbose_name_plural': 'users',
|
||||||
},
|
},
|
||||||
managers=[
|
managers=[
|
||||||
(b'objects', django.contrib.auth.models.UserManager()),
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
|
@ -1,59 +1,59 @@
|
||||||
# Bleeding edge Django
|
# Bleeding edge Django
|
||||||
django==1.11
|
django>=2.0,<2.1
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
django-environ==0.4.0
|
django-environ>=0.4,<0.5
|
||||||
django-secure==1.0.1
|
whitenoise>=3.3,<3.4
|
||||||
whitenoise==2.0.6
|
|
||||||
|
|
||||||
# Models
|
|
||||||
django-model-utils==2.3.1
|
|
||||||
|
|
||||||
# Images
|
# Images
|
||||||
Pillow==3.0.0
|
Pillow>=4.3,<4.4
|
||||||
|
|
||||||
# For user registration, either via email or social
|
# For user registration, either via email or social
|
||||||
# Well-built with regular release cycles!
|
# Well-built with regular release cycles!
|
||||||
django-allauth==0.24.1
|
django-allauth>=0.34,<0.35
|
||||||
|
|
||||||
|
|
||||||
# Python-PostgreSQL Database Adapter
|
# Python-PostgreSQL Database Adapter
|
||||||
psycopg2==2.6.1
|
psycopg2>=2.7,<=2.8
|
||||||
|
|
||||||
# Time zones support
|
# Time zones support
|
||||||
pytz==2015.7
|
pytz==2017.3
|
||||||
|
|
||||||
# Redis support
|
# Redis support
|
||||||
django-redis==4.3.0
|
django-redis>=4.5,<4.6
|
||||||
redis>=2.10.0
|
redis>=2.10,<2.11
|
||||||
|
|
||||||
|
|
||||||
celery==3.1.19
|
celery>=3.1,<3.2
|
||||||
|
|
||||||
|
|
||||||
# Your custom requirements go here
|
# Your custom requirements go here
|
||||||
django-cors-headers==2.1.0
|
django-cors-headers>=2.1,<2.2
|
||||||
musicbrainzngs==0.6
|
musicbrainzngs==0.6
|
||||||
youtube_dl>=2015.12.21
|
youtube_dl>=2017.12.14
|
||||||
djangorestframework==3.6.3
|
djangorestframework>=3.7,<3.8
|
||||||
djangorestframework-jwt==1.11.0
|
djangorestframework-jwt>=1.11,<1.12
|
||||||
django-celery==3.2.1
|
django-celery>=3.2,<3.3
|
||||||
django-mptt==0.8.7
|
django-mptt>=0.9,<0.10
|
||||||
google-api-python-client==1.6.2
|
google-api-python-client>=1.6,<1.7
|
||||||
arrow==0.10.0
|
arrow>=0.12,<0.13
|
||||||
django-taggit==0.22.1
|
persisting-theory>=0.2,<0.3
|
||||||
persisting-theory==0.2.1
|
django-versatileimagefield>=1.8,<1.9
|
||||||
django-versatileimagefield==1.7.1
|
django-filter>=1.1,<1.2
|
||||||
django-cachalot==1.5.0
|
django-rest-auth>=0.9,<0.10
|
||||||
django-filter==1.1
|
beautifulsoup4>=4.6,<4.7
|
||||||
django-rest-auth==0.9.1
|
Markdown>=2.6,<2.7
|
||||||
beautifulsoup4==4.6.0
|
ipython>=6,<7
|
||||||
Markdown==2.6.8
|
mutagen>=1.39,<1.40
|
||||||
ipython==6.1.0
|
|
||||||
mutagen==1.38
|
|
||||||
|
|
||||||
|
|
||||||
|
# Until this is merged
|
||||||
|
#django-taggit>=0.22,<0.23
|
||||||
|
git+https://github.com/jdufresne/django-taggit.git@e8f7f216f04c9781bebc84363ab24d575f948ede
|
||||||
# Until this is merged
|
# Until this is merged
|
||||||
git+https://github.com/EliotBerriot/PyMemoize.git@django
|
git+https://github.com/EliotBerriot/PyMemoize.git@django
|
||||||
|
# Until this is merged
|
||||||
|
#django-cachalot==1.5.0
|
||||||
|
git+https://github.com/EliotBerriot/django-cachalot.git@django-2
|
||||||
|
|
||||||
django-dynamic-preferences>=1.3,<1.4
|
django-dynamic-preferences>=1.5,<1.6
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
# Local development dependencies go here
|
# Local development dependencies go here
|
||||||
-r base.txt
|
|
||||||
coverage==4.0.3
|
coverage>=4.4,<4.5
|
||||||
django_coverage_plugin==1.1
|
django_coverage_plugin>=1.5,<1.6
|
||||||
Sphinx==1.6.2
|
Sphinx>=1.6,<1.7
|
||||||
django-extensions==1.5.9
|
django-extensions>=1.9,<1.10
|
||||||
Werkzeug==0.11.2
|
Werkzeug>=0.13,<0.14
|
||||||
django-test-plus==1.0.11
|
django-test-plus>=1.0.20
|
||||||
factory_boy>=2.8.1
|
factory_boy>=2.8.1
|
||||||
|
|
||||||
# django-debug-toolbar that works with Django 1.5+
|
# django-debug-toolbar that works with Django 1.5+
|
||||||
django-debug-toolbar>=1.5,<1.6
|
django-debug-toolbar>=1.9,<1.10
|
||||||
|
|
||||||
# improved REPL
|
# improved REPL
|
||||||
ipdb==0.8.1
|
ipdb==0.8.1
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
# Pro-tip: Try not to put anything here. There should be no dependency in
|
# Pro-tip: Try not to put anything here. There should be no dependency in
|
||||||
# production that isn't in development.
|
# production that isn't in development.
|
||||||
-r base.txt
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# WSGI Handler
|
# WSGI Handler
|
||||||
# ------------------------------------------------
|
# ------------------------------------------------
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
# Test dependencies go here.
|
# Test dependencies go here.
|
||||||
-r local.txt
|
|
||||||
|
|
||||||
|
flake8
|
||||||
flake8==2.5.0
|
|
||||||
model-mommy==1.3.2
|
|
||||||
pytest
|
pytest
|
||||||
pytest-django
|
pytest-django
|
||||||
pytest-mock
|
pytest-mock
|
||||||
|
|
|
@ -26,6 +26,7 @@ class Audio {
|
||||||
if (options.onEnded) {
|
if (options.onEnded) {
|
||||||
this.onEnded = options.onEnded
|
this.onEnded = options.onEnded
|
||||||
}
|
}
|
||||||
|
this.onError = options.onError
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
preload: preload,
|
preload: preload,
|
||||||
|
@ -60,8 +61,12 @@ class Audio {
|
||||||
init (src, options = {}) {
|
init (src, options = {}) {
|
||||||
if (!src) throw Error('src must be required')
|
if (!src) throw Error('src must be required')
|
||||||
this.state.startLoad = true
|
this.state.startLoad = true
|
||||||
if (this.state.tried === this.state.try) {
|
if (this.state.tried >= this.state.try) {
|
||||||
this.state.failed = true
|
this.state.failed = true
|
||||||
|
logger.default.error('Cannot fetch audio', src)
|
||||||
|
if (this.onError) {
|
||||||
|
this.onError(src)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$Audio = new window.Audio(src)
|
this.$Audio = new window.Audio(src)
|
||||||
|
|
|
@ -140,7 +140,6 @@ class Queue {
|
||||||
} else {
|
} else {
|
||||||
index = index || this.tracks.length
|
index = index || this.tracks.length
|
||||||
}
|
}
|
||||||
console.log('INDEEEEEX', index)
|
|
||||||
tracks.forEach((t) => {
|
tracks.forEach((t) => {
|
||||||
self.append(t, index, true)
|
self.append(t, index, true)
|
||||||
index += 1
|
index += 1
|
||||||
|
@ -243,7 +242,11 @@ class Queue {
|
||||||
rate: 1,
|
rate: 1,
|
||||||
loop: false,
|
loop: false,
|
||||||
volume: this.state.volume,
|
volume: this.state.volume,
|
||||||
onEnded: this.handleAudioEnded.bind(this)
|
onEnded: this.handleAudioEnded.bind(this),
|
||||||
|
onError: function (src) {
|
||||||
|
self.errored = true
|
||||||
|
self.next()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
this.audio = audio
|
this.audio = audio
|
||||||
audio.updateHook('playState', function (e) {
|
audio.updateHook('playState', function (e) {
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
<template>
|
||||||
|
<div class="main pusher">
|
||||||
|
<div class="ui vertical stripe segment">
|
||||||
|
<div class="ui text container">
|
||||||
|
<h1 class="ui huge header">
|
||||||
|
<i class="warning icon"></i>
|
||||||
|
<div class="content">
|
||||||
|
<strike>Whale</strike> Page not found!
|
||||||
|
</div>
|
||||||
|
</h1>
|
||||||
|
<p>We're sorry, the page you asked for does not exists.</p>
|
||||||
|
<p>Requested URL: <a :href="path">{{ path }}</a></p>
|
||||||
|
<router-link class="ui icon button" to="/">
|
||||||
|
Go to home page
|
||||||
|
<i class="right arrow icon"></i>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
path: window.location.href
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
|
@ -43,6 +43,9 @@ import auth from '@/auth'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'login',
|
name: 'login',
|
||||||
|
props: {
|
||||||
|
next: {type: String}
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
// We need to initialize the component with any
|
// We need to initialize the component with any
|
||||||
|
@ -69,7 +72,7 @@ export default {
|
||||||
}
|
}
|
||||||
// We need to pass the component's this context
|
// We need to pass the component's this context
|
||||||
// to properly make use of http in the auth service
|
// to properly make use of http in the auth service
|
||||||
auth.login(this, credentials, {path: '/library'}, function (response) {
|
auth.login(this, credentials, {path: this.next}, function (response) {
|
||||||
// error callback
|
// error callback
|
||||||
if (response.status === 400) {
|
if (response.status === 400) {
|
||||||
self.error = 'invalid_credentials'
|
self.error = 'invalid_credentials'
|
||||||
|
|
|
@ -12,6 +12,10 @@
|
||||||
<label>{{ t }}</label>
|
<label>{{ t }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Query template</label>
|
||||||
|
<input v-model="customQueryTemplate" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<template
|
<template
|
||||||
|
@ -22,6 +26,7 @@
|
||||||
:backends="backends"
|
:backends="backends"
|
||||||
:defaultEnabled="false"
|
:defaultEnabled="false"
|
||||||
:default-backend-id="defaultBackendId"
|
:default-backend-id="defaultBackendId"
|
||||||
|
:query-template="customQueryTemplate"
|
||||||
@import-data-changed="recordReleaseData"
|
@import-data-changed="recordReleaseData"
|
||||||
@enabled="recordReleaseEnabled"
|
@enabled="recordReleaseEnabled"
|
||||||
></release-import>
|
></release-import>
|
||||||
|
|
|
@ -13,10 +13,12 @@ export default {
|
||||||
metadata: {type: Object, required: true},
|
metadata: {type: Object, required: true},
|
||||||
defaultEnabled: {type: Boolean, default: true},
|
defaultEnabled: {type: Boolean, default: true},
|
||||||
backends: {type: Array},
|
backends: {type: Array},
|
||||||
defaultBackendId: {type: String}
|
defaultBackendId: {type: String},
|
||||||
|
queryTemplate: {type: String, default: '$artist $title'}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
customQueryTemplate: this.queryTemplate,
|
||||||
currentBackendId: this.defaultBackendId,
|
currentBackendId: this.defaultBackendId,
|
||||||
isImporting: false,
|
isImporting: false,
|
||||||
enabled: this.defaultEnabled
|
enabled: this.defaultEnabled
|
||||||
|
@ -56,6 +58,9 @@ export default {
|
||||||
return this.backends.filter(b => {
|
return this.backends.filter(b => {
|
||||||
return b.id === self.currentBackendId
|
return b.id === self.currentBackendId
|
||||||
})[0]
|
})[0]
|
||||||
|
},
|
||||||
|
realQueryTemplate () {
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -70,6 +75,14 @@ export default {
|
||||||
},
|
},
|
||||||
enabled (newValue) {
|
enabled (newValue) {
|
||||||
this.$emit('enabled', this.importData, newValue)
|
this.$emit('enabled', this.importData, newValue)
|
||||||
|
},
|
||||||
|
queryTemplate (newValue, oldValue) {
|
||||||
|
// we inherit from the prop template unless the component changed
|
||||||
|
// the value
|
||||||
|
if (oldValue === this.customQueryTemplate) {
|
||||||
|
// no changed from prop, we keep the sync
|
||||||
|
this.customQueryTemplate = newValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
:release-metadata="metadata"
|
:release-metadata="metadata"
|
||||||
:backends="backends"
|
:backends="backends"
|
||||||
:default-backend-id="defaultBackendId"
|
:default-backend-id="defaultBackendId"
|
||||||
|
:query-template="customQueryTemplate"
|
||||||
@import-data-changed="recordTrackData"
|
@import-data-changed="recordTrackData"
|
||||||
@enabled="recordTrackEnabled"
|
@enabled="recordTrackEnabled"
|
||||||
></track-import>
|
></track-import>
|
||||||
|
|
|
@ -92,13 +92,7 @@ export default Vue.extend({
|
||||||
releaseMetadata: {type: Object, required: true}
|
releaseMetadata: {type: Object, required: true}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
let queryParts = [
|
|
||||||
this.releaseMetadata['artist-credit'][0]['artist']['name'],
|
|
||||||
this.releaseMetadata['title'],
|
|
||||||
this.metadata['recording']['title']
|
|
||||||
]
|
|
||||||
return {
|
return {
|
||||||
query: queryParts.join(' '),
|
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [],
|
results: [],
|
||||||
currentResultIndex: 0,
|
currentResultIndex: 0,
|
||||||
|
@ -151,6 +145,18 @@ export default Vue.extend({
|
||||||
mbid: this.metadata.recording.id,
|
mbid: this.metadata.recording.id,
|
||||||
source: this.importedUrl
|
source: this.importedUrl
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
query () {
|
||||||
|
let queryMapping = [
|
||||||
|
['artist', this.releaseMetadata['artist-credit'][0]['artist']['name']],
|
||||||
|
['album', this.releaseMetadata['title']],
|
||||||
|
['title', this.metadata['recording']['title']]
|
||||||
|
]
|
||||||
|
let query = this.customQueryTemplate
|
||||||
|
queryMapping.forEach(e => {
|
||||||
|
query = query.split('$' + e[0]).join(e[1])
|
||||||
|
})
|
||||||
|
return query
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
|
@ -32,7 +32,7 @@ Vue.http.interceptors.push(function (request, next) {
|
||||||
// redirect to login form when we get unauthorized response from server
|
// redirect to login form when we get unauthorized response from server
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
logger.default.warn('Received 401 response from API, redirecting to login form')
|
logger.default.warn('Received 401 response from API, redirecting to login form')
|
||||||
router.push({name: 'login'})
|
router.push({name: 'login', query: {next: router.currentRoute.fullPath}})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Router from 'vue-router'
|
import Router from 'vue-router'
|
||||||
|
import PageNotFound from '@/components/PageNotFound'
|
||||||
import Home from '@/components/Home'
|
import Home from '@/components/Home'
|
||||||
import Login from '@/components/auth/Login'
|
import Login from '@/components/auth/Login'
|
||||||
import Profile from '@/components/auth/Profile'
|
import Profile from '@/components/auth/Profile'
|
||||||
|
@ -30,7 +31,8 @@ export default new Router({
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'login',
|
name: 'login',
|
||||||
component: Login
|
component: Login,
|
||||||
|
props: (route) => ({ next: route.query.next || '/library' })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/logout',
|
path: '/logout',
|
||||||
|
@ -71,7 +73,7 @@ export default new Router({
|
||||||
},
|
},
|
||||||
{ path: 'import/batches/:id', name: 'library.import.batches.detail', component: BatchDetail, props: true }
|
{ path: 'import/batches/:id', name: 'library.import.batches.detail', component: BatchDetail, props: true }
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
{ path: '*', component: PageNotFound }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue