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

Merge branch 'feature/music-requests' into 'develop'

Feature/music requests

Closes #9 and #25

See merge request funkwhale/funkwhale!48
parents 953d0ddc fd60c968
No related branches found
No related tags found
No related merge requests found
Showing
with 333 additions and 25 deletions
......@@ -52,6 +52,10 @@ v1_patterns += [
include(
('funkwhale_api.users.api_urls', 'users'),
namespace='users')),
url(r'^requests/',
include(
('funkwhale_api.requests.api_urls', 'requests'),
namespace='requests')),
url(r'^token/$', jwt_views.obtain_jwt_token, name='token'),
url(r'^token/refresh/$', jwt_views.refresh_jwt_token, name='token_refresh'),
]
......
......@@ -80,10 +80,12 @@ if RAVEN_ENABLED:
# Apps specific for this project go here.
LOCAL_APPS = (
'funkwhale_api.common',
'funkwhale_api.users', # custom users app
# Your stuff: custom apps go here
'funkwhale_api.instance',
'funkwhale_api.music',
'funkwhale_api.requests',
'funkwhale_api.favorites',
'funkwhale_api.radios',
'funkwhale_api.history',
......
......@@ -17,9 +17,6 @@ class ListeningViewSet(mixins.CreateModelMixin,
queryset = models.Listening.objects.all()
permission_classes = [ConditionalAuthentication]
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
def get_queryset(self):
queryset = super().get_queryset()
if self.request.user.is_authenticated:
......
# Generated by Django 2.0.2 on 2018-02-20 19:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('music', '0019_populate_mimetypes'),
]
operations = [
migrations.AddField(
model_name='importbatch',
name='status',
field=models.CharField(choices=[('pending', 'Pending'), ('finished', 'Finished'), ('errored', 'Errored'), ('skipped', 'Skipped')], default='pending', max_length=30),
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
from django.db import migrations, models
def populate_status(apps, schema_editor):
from funkwhale_api.music.utils import compute_status
ImportBatch = apps.get_model("music", "ImportBatch")
for ib in ImportBatch.objects.prefetch_related('jobs'):
ib.status = compute_status(ib.jobs.all())
ib.save(update_fields=['status'])
def rewind(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('music', '0020_importbatch_status'),
]
operations = [
migrations.RunPython(populate_status, rewind),
]
# Generated by Django 2.0.2 on 2018-02-20 22:48
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('requests', '__first__'),
('music', '0021_populate_batch_status'),
]
operations = [
migrations.AddField(
model_name='importbatch',
name='import_request',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='import_batches', to='requests.ImportRequest'),
),
]
......@@ -10,8 +10,11 @@ from django.conf import settings
from django.db import models
from django.core.files.base import ContentFile
from django.core.files import File
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.urls import reverse
from django.utils import timezone
from taggit.managers import TaggableManager
from versatileimagefield.fields import VersatileImageField
......@@ -400,6 +403,14 @@ class TrackFile(models.Model):
self.mimetype = utils.guess_mimetype(self.audio_file)
return super().save(**kwargs)
IMPORT_STATUS_CHOICES = (
('pending', 'Pending'),
('finished', 'Finished'),
('errored', 'Errored'),
('skipped', 'Skipped'),
)
class ImportBatch(models.Model):
IMPORT_BATCH_SOURCES = [
('api', 'api'),
......@@ -412,22 +423,24 @@ class ImportBatch(models.Model):
'users.User',
related_name='imports',
on_delete=models.CASCADE)
status = models.CharField(
choices=IMPORT_STATUS_CHOICES, default='pending', max_length=30)
import_request = models.ForeignKey(
'requests.ImportRequest',
related_name='import_batches',
null=True,
blank=True,
on_delete=models.CASCADE)
class Meta:
ordering = ['-creation_date']
def __str__(self):
return str(self.pk)
@property
def status(self):
pending = any([job.status == 'pending' for job in self.jobs.all()])
errored = any([job.status == 'errored' for job in self.jobs.all()])
if pending:
return 'pending'
if errored:
return 'errored'
return 'finished'
def update_status(self):
self.status = utils.compute_status(self.jobs.all())
self.save(update_fields=['status'])
class ImportJob(models.Model):
batch = models.ForeignKey(
......@@ -440,15 +453,39 @@ class ImportJob(models.Model):
on_delete=models.CASCADE)
source = models.CharField(max_length=500)
mbid = models.UUIDField(editable=False, null=True, blank=True)
STATUS_CHOICES = (
('pending', 'Pending'),
('finished', 'Finished'),
('errored', 'Errored'),
('skipped', 'Skipped'),
)
status = models.CharField(choices=STATUS_CHOICES, default='pending', max_length=30)
status = models.CharField(
choices=IMPORT_STATUS_CHOICES, default='pending', max_length=30)
audio_file = models.FileField(
upload_to='imports/%Y/%m/%d', max_length=255, null=True, blank=True)
class Meta:
ordering = ('id', )
@receiver(post_save, sender=ImportJob)
def update_batch_status(sender, instance, **kwargs):
instance.batch.update_status()
@receiver(post_save, sender=ImportBatch)
def update_request_status(sender, instance, created, **kwargs):
update_fields = kwargs.get('update_fields', []) or []
if not instance.import_request:
return
if not created and not 'status' in update_fields:
return
r_status = instance.import_request.status
status = instance.status
if status == 'pending' and r_status == 'pending':
# let's mark the request as accepted since we started an import
instance.import_request.status = 'accepted'
return instance.import_request.save(update_fields=['status'])
if status == 'finished' and r_status == 'accepted':
# let's mark the request as imported since the import is over
instance.import_request.status = 'imported'
return instance.import_request.save(update_fields=['status'])
......@@ -125,5 +125,5 @@ class ImportBatchSerializer(serializers.ModelSerializer):
jobs = ImportJobSerializer(many=True, read_only=True)
class Meta:
model = models.ImportBatch
fields = ('id', 'jobs', 'status', 'creation_date')
fields = ('id', 'jobs', 'status', 'creation_date', 'import_request')
read_only_fields = ('creation_date',)
......@@ -43,3 +43,13 @@ def get_query(query_string, search_fields):
def guess_mimetype(f):
b = min(100000, f.size)
return magic.from_buffer(f.read(b), mime=True)
def compute_status(jobs):
errored = any([job.status == 'errored' for job in jobs])
if errored:
return 'errored'
pending = any([job.status == 'pending' for job in jobs])
if pending:
return 'pending'
return 'finished'
......@@ -19,6 +19,7 @@ from musicbrainzngs import ResponseError
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from funkwhale_api.requests.models import ImportRequest
from funkwhale_api.musicbrainz import api
from funkwhale_api.common.permissions import (
ConditionalAuthentication, HasModelPermission)
......@@ -314,14 +315,28 @@ class SubmitViewSet(viewsets.ViewSet):
serializer = serializers.ImportBatchSerializer(batch)
return Response(serializer.data)
def get_import_request(self, data):
try:
raw = data['importRequest']
except KeyError:
return
pk = int(raw)
try:
return ImportRequest.objects.get(pk=pk)
except ImportRequest.DoesNotExist:
pass
@list_route(methods=['post'])
@transaction.non_atomic_requests
def album(self, request, *args, **kwargs):
data = json.loads(request.body.decode('utf-8'))
import_data, batch = self._import_album(data, request, batch=None)
import_request = self.get_import_request(data)
import_data, batch = self._import_album(
data, request, batch=None, import_request=import_request)
return Response(import_data)
def _import_album(self, data, request, batch=None):
def _import_album(self, data, request, batch=None, import_request=None):
# we import the whole album here to prevent race conditions that occurs
# when using get_or_create_from_api in tasks
album_data = api.releases.get(id=data['releaseId'], includes=models.Album.api_includes)['release']
......@@ -332,7 +347,9 @@ class SubmitViewSet(viewsets.ViewSet):
except ResponseError:
pass
if not batch:
batch = models.ImportBatch.objects.create(submitted_by=request.user)
batch = models.ImportBatch.objects.create(
submitted_by=request.user,
import_request=import_request)
for row in data['tracks']:
try:
models.TrackFile.objects.get(track__mbid=row['mbid'])
......@@ -346,6 +363,7 @@ class SubmitViewSet(viewsets.ViewSet):
@transaction.non_atomic_requests
def artist(self, request, *args, **kwargs):
data = json.loads(request.body.decode('utf-8'))
import_request = self.get_import_request(data)
artist_data = api.artists.get(id=data['artistId'])['artist']
cleaned_data = models.Artist.clean_musicbrainz_data(artist_data)
artist = importers.load(models.Artist, cleaned_data, artist_data, import_hooks=[])
......@@ -353,7 +371,8 @@ class SubmitViewSet(viewsets.ViewSet):
import_data = []
batch = None
for row in data['albums']:
row_data, batch = self._import_album(row, request, batch=batch)
row_data, batch = self._import_album(
row, request, batch=batch, import_request=import_request)
import_data.append(row_data)
return Response(import_data[0])
from django.conf.urls import include, url
from . import views
from rest_framework import routers
router = routers.SimpleRouter()
router.register(
r'import-requests',
views.ImportRequestViewSet,
'import-requests')
urlpatterns = router.urls
import factory
from funkwhale_api.factories import registry
from funkwhale_api.users.factories import UserFactory
@registry.register
class ImportRequestFactory(factory.django.DjangoModelFactory):
artist_name = factory.Faker('name')
albums = factory.Faker('sentence')
user = factory.SubFactory(UserFactory)
comment = factory.Faker('paragraph')
class Meta:
model = 'requests.ImportRequest'
import django_filters
from . import models
class ImportRequestFilter(django_filters.FilterSet):
class Meta:
model = models.ImportRequest
fields = {
'artist_name': ['exact', 'iexact', 'startswith', 'icontains'],
'status': ['exact'],
'user__username': ['exact'],
}
# Generated by Django 2.0.2 on 2018-02-20 22:49
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ImportRequest',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
('imported_date', models.DateTimeField(blank=True, null=True)),
('artist_name', models.CharField(max_length=250)),
('albums', models.CharField(blank=True, max_length=3000, null=True)),
('status', models.CharField(choices=[('pending', 'pending'), ('accepted', 'accepted'), ('imported', 'imported'), ('closed', 'closed')], default='pending', max_length=50)),
('comment', models.TextField(blank=True, max_length=3000, null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='import_requests', to=settings.AUTH_USER_MODEL)),
],
),
]
from django.db import models
from django.utils import timezone
NATURE_CHOICES = [
('artist', 'artist'),
('album', 'album'),
('track', 'track'),
]
STATUS_CHOICES = [
('pending', 'pending'),
('accepted', 'accepted'),
('imported', 'imported'),
('closed', 'closed'),
]
class ImportRequest(models.Model):
creation_date = models.DateTimeField(default=timezone.now)
imported_date = models.DateTimeField(null=True, blank=True)
user = models.ForeignKey(
'users.User',
related_name='import_requests',
on_delete=models.CASCADE)
artist_name = models.CharField(max_length=250)
albums = models.CharField(max_length=3000, null=True, blank=True)
status = models.CharField(
choices=STATUS_CHOICES, max_length=50, default='pending')
comment = models.TextField(null=True, blank=True, max_length=3000)
from rest_framework import serializers
from funkwhale_api.users.serializers import UserBasicSerializer
from . import models
class ImportRequestSerializer(serializers.ModelSerializer):
user = UserBasicSerializer(read_only=True)
class Meta:
model = models.ImportRequest
fields = (
'id',
'status',
'albums',
'artist_name',
'user',
'creation_date',
'imported_date',
'comment')
read_only_fields = (
'creation_date',
'imported_date',
'user',
'status')
def create(self, validated_data):
validated_data['user'] = self.context['user']
return super().create(validated_data)
from rest_framework import generics, mixins, viewsets
from rest_framework import status
from rest_framework.response import Response
from rest_framework.decorators import detail_route
from funkwhale_api.music.views import SearchMixin
from . import filters
from . import models
from . import serializers
class ImportRequestViewSet(
SearchMixin,
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet):
serializer_class = serializers.ImportRequestSerializer
queryset = (
models.ImportRequest.objects.all()
.select_related()
.order_by('-creation_date'))
search_fields = ['artist_name', 'album_name', 'comment']
filter_class = filters.ImportRequestFilter
ordering_fields = ('id', 'artist_name', 'creation_date', 'status')
def perform_create(self, serializer):
return serializer.save(user=self.request.user)
def get_serializer_context(self):
context = super().get_serializer_context()
if self.request.user.is_authenticated:
context['user'] = self.request.user
return context
......@@ -3,6 +3,12 @@ from rest_framework import serializers
from . import models
class UserBasicSerializer(serializers.ModelSerializer):
class Meta:
model = models.User
fields = ['id', 'username', 'name', 'date_joined']
class UserSerializer(serializers.ModelSerializer):
permissions = serializers.SerializerMethodField()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment