From 15bdf18705f73c0543980e34583eafe232dcb641 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 20 Feb 2018 23:59:50 +0100 Subject: [PATCH 01/23] logged in api client --- api/tests/conftest.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/api/tests/conftest.py b/api/tests/conftest.py index 4d7a6fa98..82fd2655a 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -56,6 +56,15 @@ def api_client(client): return APIClient() +@pytest.fixture +def logged_in_api_client(db, factories, api_client): + user = factories['users.User']() + assert api_client.login(username=user.username, password='test') + setattr(api_client, 'user', user) + yield api_client + delattr(api_client, 'user') + + @pytest.fixture def superuser_client(db, factories, client): user = factories['users.SuperUser']() From 24e25557937629c74e93d20cda62ee41aee6d47d Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 21 Feb 2018 00:02:09 +0100 Subject: [PATCH 02/23] Added status field to import batch, it's synced based on jobs --- .../migrations/0020_importbatch_status.py | 18 ++++++++ .../migrations/0021_populate_batch_status.py | 29 +++++++++++++ api/funkwhale_api/music/models.py | 43 +++++++++++-------- api/funkwhale_api/music/utils.py | 10 +++++ api/tests/music/test_models.py | 14 ++++++ 5 files changed, 97 insertions(+), 17 deletions(-) create mode 100644 api/funkwhale_api/music/migrations/0020_importbatch_status.py create mode 100644 api/funkwhale_api/music/migrations/0021_populate_batch_status.py diff --git a/api/funkwhale_api/music/migrations/0020_importbatch_status.py b/api/funkwhale_api/music/migrations/0020_importbatch_status.py new file mode 100644 index 000000000..265d1ba5d --- /dev/null +++ b/api/funkwhale_api/music/migrations/0020_importbatch_status.py @@ -0,0 +1,18 @@ +# 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), + ), + ] diff --git a/api/funkwhale_api/music/migrations/0021_populate_batch_status.py b/api/funkwhale_api/music/migrations/0021_populate_batch_status.py new file mode 100644 index 000000000..061d649b0 --- /dev/null +++ b/api/funkwhale_api/music/migrations/0021_populate_batch_status.py @@ -0,0 +1,29 @@ +# -*- 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), + ] diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 3ebd07419..3c6f60165 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -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,13 +453,9 @@ 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) diff --git a/api/funkwhale_api/music/utils.py b/api/funkwhale_api/music/utils.py index 0e4318e56..a75cf5de8 100644 --- a/api/funkwhale_api/music/utils.py +++ b/api/funkwhale_api/music/utils.py @@ -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' diff --git a/api/tests/music/test_models.py b/api/tests/music/test_models.py index 2eb1f2763..9f52ba887 100644 --- a/api/tests/music/test_models.py +++ b/api/tests/music/test_models.py @@ -52,6 +52,20 @@ def test_import_job_is_bound_to_track_file(factories, mocker): job.refresh_from_db() assert job.track_file.track == track + +@pytest.mark.parametrize('status', ['pending', 'errored', 'finished']) +def test_saving_job_updates_batch_status(status,factories, mocker): + batch = factories['music.ImportBatch']() + + assert batch.status == 'pending' + + job = factories['music.ImportJob'](batch=batch, status=status) + + batch.refresh_from_db() + + assert batch.status == status + + @pytest.mark.parametrize('extention,mimetype', [ ('ogg', 'audio/ogg'), ('mp3', 'audio/mpeg'), From 3fa7d0009e5e807b975f441847326b5791cef0a7 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 21 Feb 2018 00:02:48 +0100 Subject: [PATCH 03/23] Initial import request model --- api/funkwhale_api/requests/__init__.py | 0 .../requests/migrations/0001_initial.py | 31 +++++++++++++++++++ .../requests/migrations/__init__.py | 0 api/funkwhale_api/requests/models.py | 29 +++++++++++++++++ 4 files changed, 60 insertions(+) create mode 100644 api/funkwhale_api/requests/__init__.py create mode 100644 api/funkwhale_api/requests/migrations/0001_initial.py create mode 100644 api/funkwhale_api/requests/migrations/__init__.py create mode 100644 api/funkwhale_api/requests/models.py diff --git a/api/funkwhale_api/requests/__init__.py b/api/funkwhale_api/requests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/funkwhale_api/requests/migrations/0001_initial.py b/api/funkwhale_api/requests/migrations/0001_initial.py new file mode 100644 index 000000000..7c239b3c0 --- /dev/null +++ b/api/funkwhale_api/requests/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# 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)), + ], + ), + ] diff --git a/api/funkwhale_api/requests/migrations/__init__.py b/api/funkwhale_api/requests/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/funkwhale_api/requests/models.py b/api/funkwhale_api/requests/models.py new file mode 100644 index 000000000..c29852430 --- /dev/null +++ b/api/funkwhale_api/requests/models.py @@ -0,0 +1,29 @@ +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 8900f5581b5e4313dd414c7065449ccee6e7d971 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 21 Feb 2018 00:03:05 +0100 Subject: [PATCH 04/23] ImportRequest factory --- api/funkwhale_api/requests/factories.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 api/funkwhale_api/requests/factories.py diff --git a/api/funkwhale_api/requests/factories.py b/api/funkwhale_api/requests/factories.py new file mode 100644 index 000000000..2bcdeb6a9 --- /dev/null +++ b/api/funkwhale_api/requests/factories.py @@ -0,0 +1,15 @@ +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' From b492e133c628cbdd8c70fb2b05caeec60e3f8a53 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 21 Feb 2018 00:03:37 +0100 Subject: [PATCH 05/23] Job signal to update batch status --- api/funkwhale_api/music/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 3c6f60165..308bc43cc 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -461,3 +461,8 @@ class ImportJob(models.Model): class Meta: ordering = ('id', ) + + +@receiver(post_save, sender=ImportJob) +def update_batch_status(sender, instance, **kwargs): + instance.batch.update_status() From f3d77ef7d922fe48a93b74d6b207c50ded02b443 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 21 Feb 2018 00:04:30 +0100 Subject: [PATCH 06/23] Update request status based on batch status --- .../0022_importbatch_import_request.py | 20 ++++++++++++++++ api/funkwhale_api/music/models.py | 23 +++++++++++++++++++ api/tests/requests/test_models.py | 23 +++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 api/funkwhale_api/music/migrations/0022_importbatch_import_request.py create mode 100644 api/tests/requests/test_models.py diff --git a/api/funkwhale_api/music/migrations/0022_importbatch_import_request.py b/api/funkwhale_api/music/migrations/0022_importbatch_import_request.py new file mode 100644 index 000000000..d9f6f01d9 --- /dev/null +++ b/api/funkwhale_api/music/migrations/0022_importbatch_import_request.py @@ -0,0 +1,20 @@ +# 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'), + ), + ] diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 308bc43cc..97992fc8f 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -466,3 +466,26 @@ class ImportJob(models.Model): @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']) diff --git a/api/tests/requests/test_models.py b/api/tests/requests/test_models.py new file mode 100644 index 000000000..797656bd7 --- /dev/null +++ b/api/tests/requests/test_models.py @@ -0,0 +1,23 @@ +import pytest + +from django.forms import ValidationError + + +def test_can_bind_import_batch_to_request(factories): + request = factories['requests.ImportRequest']() + + assert request.status == 'pending' + + # when we create the import, we consider the request as accepted + batch = factories['music.ImportBatch'](import_request=request) + request.refresh_from_db() + + assert request.status == 'accepted' + + # now, the batch is finished, therefore the request status should be + # imported + batch.status = 'finished' + batch.save(update_fields=['status']) + request.refresh_from_db() + + assert request.status == 'imported' From 98b62caa24463c6d32c7f9762c132a60e3dd2ed7 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 21 Feb 2018 00:05:07 +0100 Subject: [PATCH 07/23] Import request viewset, serializer and url --- api/config/api_urls.py | 4 +++ api/config/settings/common.py | 2 ++ api/funkwhale_api/requests/api_urls.py | 11 +++++++++ api/funkwhale_api/requests/serializers.py | 27 ++++++++++++++++++++ api/funkwhale_api/requests/views.py | 30 +++++++++++++++++++++++ api/tests/requests/test_views.py | 26 ++++++++++++++++++++ 6 files changed, 100 insertions(+) create mode 100644 api/funkwhale_api/requests/api_urls.py create mode 100644 api/funkwhale_api/requests/serializers.py create mode 100644 api/funkwhale_api/requests/views.py create mode 100644 api/tests/requests/test_views.py diff --git a/api/config/api_urls.py b/api/config/api_urls.py index c7ebc4ed3..ff6db0d06 100644 --- a/api/config/api_urls.py +++ b/api/config/api_urls.py @@ -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'), ] diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 6d02cbbc1..5fe55e53a 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -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', diff --git a/api/funkwhale_api/requests/api_urls.py b/api/funkwhale_api/requests/api_urls.py new file mode 100644 index 000000000..37459a664 --- /dev/null +++ b/api/funkwhale_api/requests/api_urls.py @@ -0,0 +1,11 @@ +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 diff --git a/api/funkwhale_api/requests/serializers.py b/api/funkwhale_api/requests/serializers.py new file mode 100644 index 000000000..8e830d388 --- /dev/null +++ b/api/funkwhale_api/requests/serializers.py @@ -0,0 +1,27 @@ +from rest_framework import serializers + +from . import models + + +class ImportRequestSerializer(serializers.ModelSerializer): + + 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) diff --git a/api/funkwhale_api/requests/views.py b/api/funkwhale_api/requests/views.py new file mode 100644 index 000000000..b2dc78db0 --- /dev/null +++ b/api/funkwhale_api/requests/views.py @@ -0,0 +1,30 @@ +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 models +from . import serializers + + +class ImportRequestViewSet( + SearchMixin, + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet): + + serializer_class = serializers.ImportRequestSerializer + queryset = models.ImportRequest.objects.all() + search_fields = ['artist_name', 'album_name', 'comment'] + + 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 diff --git a/api/tests/requests/test_views.py b/api/tests/requests/test_views.py new file mode 100644 index 000000000..6c34f9ad1 --- /dev/null +++ b/api/tests/requests/test_views.py @@ -0,0 +1,26 @@ +from django.urls import reverse + + +def test_request_viewset_requires_auth(db, api_client): + url = reverse('api:v1:requests:import-requests-list') + response = api_client.get(url) + assert response.status_code == 401 + + +def test_user_can_create_request(logged_in_api_client): + url = reverse('api:v1:requests:import-requests-list') + user = logged_in_api_client.user + data = { + 'artist_name': 'System of a Down', + 'albums': 'All please!', + 'comment': 'Please, they rock!', + } + response = logged_in_api_client.post(url, data) + + assert response.status_code == 201 + + ir = user.import_requests.latest('id') + assert ir.status == 'pending' + assert ir.creation_date is not None + for field, value in data.items(): + assert getattr(ir, field) == value From 9d5e07872dcc8cfe6d0eea7b7d8a10657bcfce81 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 21 Feb 2018 00:05:19 +0100 Subject: [PATCH 08/23] Removed dead code --- api/funkwhale_api/history/views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/api/funkwhale_api/history/views.py b/api/funkwhale_api/history/views.py index 32bad6060..59dcbd26b 100644 --- a/api/funkwhale_api/history/views.py +++ b/api/funkwhale_api/history/views.py @@ -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: From 26e6459959bee550881d05f66362a0f9844c7172 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 21 Feb 2018 00:05:33 +0100 Subject: [PATCH 09/23] Request form component --- front/src/components/library/Home.vue | 10 ++- front/src/components/requests/Form.vue | 113 +++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 front/src/components/requests/Form.vue diff --git a/front/src/components/library/Home.vue b/front/src/components/library/Home.vue index cdcbe4b72..e4e22fc09 100644 --- a/front/src/components/library/Home.vue +++ b/front/src/components/library/Home.vue @@ -4,7 +4,7 @@
-
+

Latest artists

@@ -18,6 +18,10 @@
+
+

Music requests

+ +
@@ -30,6 +34,7 @@ import backend from '@/audio/backend' import logger from '@/logging' import ArtistCard from '@/components/audio/artist/Card' import RadioCard from '@/components/radios/Card' +import RequestForm from '@/components/requests/Form' const ARTISTS_URL = 'artists/' @@ -38,7 +43,8 @@ export default { components: { Search, ArtistCard, - RadioCard + RadioCard, + RequestForm }, data () { return { diff --git a/front/src/components/requests/Form.vue b/front/src/components/requests/Form.vue new file mode 100644 index 000000000..22d47d79c --- /dev/null +++ b/front/src/components/requests/Form.vue @@ -0,0 +1,113 @@ + + + + + From 64290465e77db880e75be21b40b63359e57ec518 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 21 Feb 2018 19:44:23 +0100 Subject: [PATCH 10/23] Fixed source not passed from query in import route --- front/src/components/library/import/Main.vue | 2 +- front/src/router/index.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/front/src/components/library/import/Main.vue b/front/src/components/library/import/Main.vue index 66ba8c661..75017c1e6 100644 --- a/front/src/components/library/import/Main.vue +++ b/front/src/components/library/import/Main.vue @@ -150,7 +150,7 @@ export default { currentType: this.mbType || 'artist', currentId: this.mbId, currentStep: 0, - currentSource: '', + currentSource: this.source, metadata: {}, isImporting: false, importData: { diff --git a/front/src/router/index.js b/front/src/router/index.js index 971ef05cd..bf59b8ee8 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -98,7 +98,10 @@ export default new Router({ path: 'import/launch', name: 'library.import.launch', component: LibraryImport, - props: (route) => ({ mbType: route.query.type, mbId: route.query.id }) + props: (route) => ({ + source: route.query.source, + mbType: route.query.type, + mbId: route.query.id }) }, { path: 'import/batches', From 999198b1c53264825be92551ffa26b5712d38ae5 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 22 Feb 2018 22:01:51 +0100 Subject: [PATCH 11/23] Serialize user with requests --- api/funkwhale_api/requests/serializers.py | 3 +++ api/funkwhale_api/requests/views.py | 2 +- api/funkwhale_api/users/serializers.py | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/api/funkwhale_api/requests/serializers.py b/api/funkwhale_api/requests/serializers.py index 8e830d388..51a709514 100644 --- a/api/funkwhale_api/requests/serializers.py +++ b/api/funkwhale_api/requests/serializers.py @@ -1,9 +1,12 @@ 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 diff --git a/api/funkwhale_api/requests/views.py b/api/funkwhale_api/requests/views.py index b2dc78db0..345ff6f3b 100644 --- a/api/funkwhale_api/requests/views.py +++ b/api/funkwhale_api/requests/views.py @@ -17,7 +17,7 @@ class ImportRequestViewSet( viewsets.GenericViewSet): serializer_class = serializers.ImportRequestSerializer - queryset = models.ImportRequest.objects.all() + queryset = models.ImportRequest.objects.all().select_related() search_fields = ['artist_name', 'album_name', 'comment'] def perform_create(self, serializer): diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py index 261873bdb..8c218b1c2 100644 --- a/api/funkwhale_api/users/serializers.py +++ b/api/funkwhale_api/users/serializers.py @@ -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() From 7ffff9000525bf9917102ab71a0a487af9e5e755 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 22 Feb 2018 22:02:19 +0100 Subject: [PATCH 12/23] Moment, markdown and truncate filters --- front/package.json | 2 ++ front/src/filters.js | 31 ++++++++++++++++ front/src/main.js | 1 + front/test/unit/specs/filters/filters.spec.js | 35 +++++++++++++++++++ 4 files changed, 69 insertions(+) create mode 100644 front/src/filters.js create mode 100644 front/test/unit/specs/filters/filters.spec.js diff --git a/front/package.json b/front/package.json index ac3895f6d..042e332d0 100644 --- a/front/package.json +++ b/front/package.json @@ -20,9 +20,11 @@ "js-logger": "^1.3.0", "jwt-decode": "^2.2.0", "lodash": "^4.17.4", + "moment": "^2.20.1", "moxios": "^0.4.0", "raven-js": "^3.22.3", "semantic-ui-css": "^2.2.10", + "showdown": "^1.8.6", "vue": "^2.3.3", "vue-lazyload": "^1.1.4", "vue-router": "^2.3.1", diff --git a/front/src/filters.js b/front/src/filters.js new file mode 100644 index 000000000..7695046e4 --- /dev/null +++ b/front/src/filters.js @@ -0,0 +1,31 @@ +import Vue from 'vue' + +import moment from 'moment' +import showdown from 'showdown' + +export function truncate (str, max, ellipsis) { + max = max || 100 + ellipsis = ellipsis || '…' + if (str.length <= max) { + return str + } + return str.slice(0, max) + ellipsis +} + +Vue.filter('truncate', truncate) + +export function markdown (str) { + const converter = new showdown.Converter() + return converter.makeHtml(str) +} + +Vue.filter('markdown', markdown) + +export function ago (date) { + const m = moment(date) + return m.fromNow() +} + +Vue.filter('ago', ago) + +export default {} diff --git a/front/src/main.js b/front/src/main.js index d1ff90c32..33e998ded 100644 --- a/front/src/main.js +++ b/front/src/main.js @@ -13,6 +13,7 @@ import VueLazyload from 'vue-lazyload' import store from './store' import config from './config' import { sync } from 'vuex-router-sync' +import filters from '@/filters' // eslint-disable-line sync(store, router) diff --git a/front/test/unit/specs/filters/filters.spec.js b/front/test/unit/specs/filters/filters.spec.js new file mode 100644 index 000000000..227d6c88b --- /dev/null +++ b/front/test/unit/specs/filters/filters.spec.js @@ -0,0 +1,35 @@ +import {truncate, markdown, ago} from '@/filters' + +describe('filters', () => { + describe('truncate', () => { + it('leave strings as it if correct size', () => { + const input = 'Hello world' + let output = truncate(input, 100) + expect(output).to.equal(input) + }) + it('returns shorter string with character', () => { + const input = 'Hello world' + let output = truncate(input, 5) + expect(output).to.equal('Hello…') + }) + it('custom ellipsis', () => { + const input = 'Hello world' + let output = truncate(input, 5, ' pouet') + expect(output).to.equal('Hello pouet') + }) + }) + describe('markdown', () => { + it('renders markdown', () => { + const input = 'Hello world' + let output = markdown(input) + expect(output).to.equal('

Hello world

') + }) + }) + describe('ago', () => { + it('works', () => { + const input = new Date() + let output = ago(input) + expect(output).to.equal('a few seconds ago') + }) + }) +}) From a73a4e248d5b73152023707b0442a1a1330728e1 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 22 Feb 2018 22:02:32 +0100 Subject: [PATCH 13/23] Comment component --- front/src/components/discussion/Comment.vue | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 front/src/components/discussion/Comment.vue diff --git a/front/src/components/discussion/Comment.vue b/front/src/components/discussion/Comment.vue new file mode 100644 index 000000000..c10d13bc0 --- /dev/null +++ b/front/src/components/discussion/Comment.vue @@ -0,0 +1,49 @@ + + From dcb1915a7b0c7caef4a17f292c1823d09cecff5b Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 22 Feb 2018 23:33:29 +0100 Subject: [PATCH 14/23] Can now bind batch to request via API --- api/funkwhale_api/music/serializers.py | 2 +- api/funkwhale_api/music/views.py | 27 ++++++++++++++++--- api/tests/conftest.py | 9 +++++++ api/tests/music/test_import.py | 37 ++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 api/tests/music/test_import.py diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index 41de30f10..db6298a9e 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -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',) diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 8e46cbd71..bf9d39b1d 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -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]) diff --git a/api/tests/conftest.py b/api/tests/conftest.py index 82fd2655a..10d7c3235 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -65,6 +65,15 @@ def logged_in_api_client(db, factories, api_client): delattr(api_client, 'user') +@pytest.fixture +def superuser_api_client(db, factories, api_client): + user = factories['users.SuperUser']() + assert api_client.login(username=user.username, password='test') + setattr(api_client, 'user', user) + yield api_client + delattr(api_client, 'user') + + @pytest.fixture def superuser_client(db, factories, client): user = factories['users.SuperUser']() diff --git a/api/tests/music/test_import.py b/api/tests/music/test_import.py new file mode 100644 index 000000000..f2ca1abbd --- /dev/null +++ b/api/tests/music/test_import.py @@ -0,0 +1,37 @@ +import json + +from django.urls import reverse + +from . import data as api_data + + +def test_create_import_can_bind_to_request( + mocker, factories, superuser_api_client): + request = factories['requests.ImportRequest']() + + mocker.patch('funkwhale_api.music.tasks.import_job_run') + mocker.patch( + 'funkwhale_api.musicbrainz.api.artists.get', + return_value=api_data.artists['get']['soad']) + mocker.patch( + 'funkwhale_api.musicbrainz.api.images.get_front', + return_value=b'') + mocker.patch( + 'funkwhale_api.musicbrainz.api.releases.get', + return_value=api_data.albums['get_with_includes']['hypnotize']) + payload = { + 'releaseId': '47ae093f-1607-49a3-be11-a15d335ccc94', + 'importRequest': request.pk, + 'tracks': [ + { + 'mbid': '1968a9d6-8d92-4051-8f76-674e157b6eed', + 'source': 'https://www.youtube.com/watch?v=1111111111', + } + ] + } + url = reverse('api:v1:submit-album') + response = superuser_api_client.post( + url, json.dumps(payload), content_type='application/json') + batch = request.import_batches.latest('id') + + assert batch.import_request == request From dac47da584475c777738e4da940229e1017b5b7e Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 22 Feb 2018 23:33:59 +0100 Subject: [PATCH 15/23] Import request filter --- api/funkwhale_api/requests/filters.py | 14 ++++++++++++++ api/funkwhale_api/requests/views.py | 8 +++++++- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 api/funkwhale_api/requests/filters.py diff --git a/api/funkwhale_api/requests/filters.py b/api/funkwhale_api/requests/filters.py new file mode 100644 index 000000000..bf353e8ad --- /dev/null +++ b/api/funkwhale_api/requests/filters.py @@ -0,0 +1,14 @@ +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'], + } diff --git a/api/funkwhale_api/requests/views.py b/api/funkwhale_api/requests/views.py index 345ff6f3b..395fac66c 100644 --- a/api/funkwhale_api/requests/views.py +++ b/api/funkwhale_api/requests/views.py @@ -5,6 +5,7 @@ from rest_framework.decorators import detail_route from funkwhale_api.music.views import SearchMixin +from . import filters from . import models from . import serializers @@ -17,8 +18,13 @@ class ImportRequestViewSet( viewsets.GenericViewSet): serializer_class = serializers.ImportRequestSerializer - queryset = models.ImportRequest.objects.all().select_related() + 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) From 2cd90ff4bde91be7901f1ff78a65e5cc48620773 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 22 Feb 2018 23:34:24 +0100 Subject: [PATCH 16/23] Capitalize filter --- front/src/filters.js | 6 ++++++ front/test/unit/specs/filters/filters.spec.js | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/front/src/filters.js b/front/src/filters.js index 7695046e4..1ec4f2307 100644 --- a/front/src/filters.js +++ b/front/src/filters.js @@ -28,4 +28,10 @@ export function ago (date) { Vue.filter('ago', ago) +export function capitalize (str) { + return str.charAt(0).toUpperCase() + str.slice(1) +} + +Vue.filter('capitalize', capitalize) + export default {} diff --git a/front/test/unit/specs/filters/filters.spec.js b/front/test/unit/specs/filters/filters.spec.js index 227d6c88b..c2b43da44 100644 --- a/front/test/unit/specs/filters/filters.spec.js +++ b/front/test/unit/specs/filters/filters.spec.js @@ -1,4 +1,4 @@ -import {truncate, markdown, ago} from '@/filters' +import {truncate, markdown, ago, capitalize} from '@/filters' describe('filters', () => { describe('truncate', () => { @@ -32,4 +32,11 @@ describe('filters', () => { expect(output).to.equal('a few seconds ago') }) }) + describe('capitalize', () => { + it('works', () => { + const input = 'hello world' + let output = capitalize(input) + expect(output).to.equal('Hello world') + }) + }) }) From d0a9873a0709e1d6907854a4c21f081dc735ed0f Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 22 Feb 2018 23:34:31 +0100 Subject: [PATCH 17/23] Moment format filter --- front/src/filters.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/front/src/filters.js b/front/src/filters.js index 1ec4f2307..22d93149b 100644 --- a/front/src/filters.js +++ b/front/src/filters.js @@ -28,6 +28,13 @@ export function ago (date) { Vue.filter('ago', ago) +export function momentFormat (date, format) { + format = format || 'lll' + return moment(date).format(format) +} + +Vue.filter('moment', momentFormat) + export function capitalize (str) { return str.charAt(0).toUpperCase() + str.slice(1) } From 7808d14a49884f55fbeb3fd2d16eed9a34760ad7 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 22 Feb 2018 23:34:52 +0100 Subject: [PATCH 18/23] Human date component --- front/src/components/common/HumanDate.vue | 8 ++++++++ front/src/components/discussion/Comment.vue | 2 +- front/src/components/globals.js | 7 +++++++ front/src/main.js | 1 + 4 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 front/src/components/common/HumanDate.vue create mode 100644 front/src/components/globals.js diff --git a/front/src/components/common/HumanDate.vue b/front/src/components/common/HumanDate.vue new file mode 100644 index 000000000..ff6ff5c71 --- /dev/null +++ b/front/src/components/common/HumanDate.vue @@ -0,0 +1,8 @@ + + diff --git a/front/src/components/discussion/Comment.vue b/front/src/components/discussion/Comment.vue index c10d13bc0..a3c5176ec 100644 --- a/front/src/components/discussion/Comment.vue +++ b/front/src/components/discussion/Comment.vue @@ -3,7 +3,7 @@
{{ user.username }}
diff --git a/front/src/components/globals.js b/front/src/components/globals.js new file mode 100644 index 000000000..40315bc47 --- /dev/null +++ b/front/src/components/globals.js @@ -0,0 +1,7 @@ +import Vue from 'vue' + +import HumanDate from '@/components/common/HumanDate' + +Vue.component('human-date', HumanDate) + +export default {} diff --git a/front/src/main.js b/front/src/main.js index 33e998ded..2e351310a 100644 --- a/front/src/main.js +++ b/front/src/main.js @@ -14,6 +14,7 @@ import store from './store' import config from './config' import { sync } from 'vuex-router-sync' import filters from '@/filters' // eslint-disable-line +import globals from '@/components/globals' // eslint-disable-line sync(store, router) From d91f0ff9a663e371ec06559155e751e9f59566bf Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 22 Feb 2018 23:35:00 +0100 Subject: [PATCH 19/23] Better pagination (borderless) --- front/src/components/Pagination.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/front/src/components/Pagination.vue b/front/src/components/Pagination.vue index 3ac7c59af..83b386fde 100644 --- a/front/src/components/Pagination.vue +++ b/front/src/components/Pagination.vue @@ -49,4 +49,8 @@ export default { From bf7bc9a9bc7a0a3df01642da8f526d86c87ad405 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 22 Feb 2018 23:35:40 +0100 Subject: [PATCH 20/23] Display current request under import and send request to API --- .../components/library/import/ImportMixin.vue | 6 ++++- front/src/components/library/import/Main.vue | 25 ++++++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/front/src/components/library/import/ImportMixin.vue b/front/src/components/library/import/ImportMixin.vue index 33c6193bd..8b0757dcc 100644 --- a/front/src/components/library/import/ImportMixin.vue +++ b/front/src/components/library/import/ImportMixin.vue @@ -13,7 +13,8 @@ export default { defaultEnabled: {type: Boolean, default: true}, backends: {type: Array}, defaultBackendId: {type: String}, - queryTemplate: {type: String, default: '$artist $title'} + queryTemplate: {type: String, default: '$artist $title'}, + request: {type: Object, required: false} }, data () { return { @@ -32,6 +33,9 @@ export default { this.isImporting = true let url = 'submit/' + self.importType + '/' let payload = self.importData + if (this.request) { + payload.importRequest = this.request.id + } axios.post(url, payload).then((response) => { logger.default.info('launched import for', self.type, self.metadata.id) self.isImporting = false diff --git a/front/src/components/library/import/Main.vue b/front/src/components/library/import/Main.vue index 75017c1e6..0a1cc6df9 100644 --- a/front/src/components/library/import/Main.vue +++ b/front/src/components/library/import/Main.vue @@ -92,6 +92,7 @@ -
+
+

Music request

+

This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled.

+
@@ -121,6 +125,7 @@ From 2fb533c5f510c821d51fea4a24b6a12b1992184e Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 22 Feb 2018 23:36:53 +0100 Subject: [PATCH 22/23] Maxlength and truncation --- front/src/components/requests/Form.vue | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/front/src/components/requests/Form.vue b/front/src/components/requests/Form.vue index 22d47d79c..68c725ba7 100644 --- a/front/src/components/requests/Form.vue +++ b/front/src/components/requests/Form.vue @@ -4,16 +4,16 @@

Something's missing in the library? Let us know what you would like to listen!

- +

Leave this field empty if you're requesting the whole discography.

- +
- +
@@ -29,8 +29,10 @@
{{ request.artist_name }}
-
{{ truncate(request.albums, 50) }}
-
{{ truncate(request.comment, 50) }}
+
+ {{ request.albums|truncate }}
+
+ {{ request.comment|truncate }}
From fd60c968ba9114c75c3b2cf2edbab06aca606f3d Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 22 Feb 2018 23:37:11 +0100 Subject: [PATCH 23/23] Request list and card --- front/src/components/requests/Card.vue | 61 +++++++ .../src/components/requests/RequestsList.vue | 163 ++++++++++++++++++ front/src/router/index.js | 18 +- 3 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 front/src/components/requests/Card.vue create mode 100644 front/src/components/requests/RequestsList.vue diff --git a/front/src/components/requests/Card.vue b/front/src/components/requests/Card.vue new file mode 100644 index 000000000..deb9c3fe0 --- /dev/null +++ b/front/src/components/requests/Card.vue @@ -0,0 +1,61 @@ + + + + + + diff --git a/front/src/components/requests/RequestsList.vue b/front/src/components/requests/RequestsList.vue new file mode 100644 index 000000000..cb3e9af00 --- /dev/null +++ b/front/src/components/requests/RequestsList.vue @@ -0,0 +1,163 @@ + + + + + + diff --git a/front/src/router/index.js b/front/src/router/index.js index bf59b8ee8..ea8854bbe 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -17,6 +17,7 @@ import LibraryRadios from '@/components/library/Radios' import RadioBuilder from '@/components/library/radios/Builder' import BatchList from '@/components/library/import/BatchList' import BatchDetail from '@/components/library/import/BatchDetail' +import RequestsList from '@/components/requests/RequestsList' import Favorites from '@/components/favorites/List' @@ -100,6 +101,7 @@ export default new Router({ component: LibraryImport, props: (route) => ({ source: route.query.source, + request: route.query.request, mbType: route.query.type, mbId: route.query.id }) }, @@ -110,7 +112,21 @@ export default new Router({ children: [ ] }, - { 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: 'requests/', + name: 'library.requests', + component: RequestsList, + props: (route) => ({ + defaultOrdering: route.query.ordering, + defaultQuery: route.query.query, + defaultPaginateBy: route.query.paginateBy, + defaultPage: route.query.page, + defaultStatus: route.query.status || 'pending' + }), + children: [ + ] + } ] }, { path: '*', component: PageNotFound }