From f6a4e4b7a3b7f6dcd934b4a91625e3e7fdd119b5 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 18 Feb 2018 15:08:18 +0100 Subject: [PATCH 01/49] Now reset player colors when track has no coverNow reset player colors when track has no cover --- CHANGELOG | 1 + front/src/components/audio/Player.vue | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 2d005e1a3..03665014f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ Changelog 0.5 (Unreleased) ---------------- +- Front: Now reset player colors when track has no cover (#46) 0.4 (2018-02-18) ---------------- diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue index e44a92d4f..5e9965158 100644 --- a/front/src/components/audio/Player.vue +++ b/front/src/components/audio/Player.vue @@ -232,7 +232,7 @@ export default { }, watch: { currentTrack (newValue) { - if (!newValue) { + if (!newValue || !newValue.album.cover) { this.ambiantColors = this.defaultAmbiantColors } }, From fc0d9dfd73cde5c411cdb08a7495a47326011d98 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 18 Feb 2018 15:31:37 +0100 Subject: [PATCH 02/49] Removed useless log --- front/src/components/Raven.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/front/src/components/Raven.vue b/front/src/components/Raven.vue index e5e125b81..f45d0ed22 100644 --- a/front/src/components/Raven.vue +++ b/front/src/components/Raven.vue @@ -22,7 +22,6 @@ export default { Raven.uninstall() logger.default.info('Installing raven...') Raven.config(this.dsn).addPlugin(RavenVue, Vue).install() - console.log({}.test.test) } }, watch: { From 4992029c8a7845313045ac346634ac2aae242868 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 18 Feb 2018 22:05:09 +0100 Subject: [PATCH 03/49] Ignore useless files --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c1b8300f2..66ec5a41d 100644 --- a/.gitignore +++ b/.gitignore @@ -72,7 +72,7 @@ api/music api/media api/staticfiles api/static - +api/.pytest_cache # Front front/node_modules/ From 937c55fdd531a858eba1b8f4d1a8dafec380ed46 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 18 Feb 2018 22:06:10 +0100 Subject: [PATCH 04/49] Install ffmpeg and magic --- api/Dockerfile | 2 +- api/docker/Dockerfile.test | 3 ++- api/requirements.apt | 3 ++- api/requirements/base.txt | 2 ++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/api/Dockerfile b/api/Dockerfile index 3281e6f56..5d4e85857 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -3,7 +3,7 @@ FROM python:3.5 ENV PYTHONUNBUFFERED 1 # Requirements have to be pulled and installed here, otherwise caching won't work - +RUN echo 'deb http://httpredir.debian.org/debian/ jessie-backports main' > /etc/apt/sources.list.d/ffmpeg.list COPY ./requirements.apt /requirements.apt RUN apt-get update -qq && grep "^[^#;]" requirements.apt | xargs apt-get install -y RUN curl -L https://github.com/acoustid/chromaprint/releases/download/v1.4.2/chromaprint-fpcalc-1.4.2-linux-x86_64.tar.gz | tar -xz -C /usr/local/bin --strip 1 diff --git a/api/docker/Dockerfile.test b/api/docker/Dockerfile.test index 08b437cf2..069b89c2f 100644 --- a/api/docker/Dockerfile.test +++ b/api/docker/Dockerfile.test @@ -1,9 +1,10 @@ FROM python:3.5 ENV PYTHONUNBUFFERED 1 -ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONDONTWRITEBYTECODE 1 # Requirements have to be pulled and installed here, otherwise caching won't work +RUN echo 'deb http://httpredir.debian.org/debian/ jessie-backports main' > /etc/apt/sources.list.d/ffmpeg.list COPY ./requirements.apt /requirements.apt COPY ./install_os_dependencies.sh /install_os_dependencies.sh RUN bash install_os_dependencies.sh install diff --git a/api/requirements.apt b/api/requirements.apt index e28360b56..462a5a705 100644 --- a/api/requirements.apt +++ b/api/requirements.apt @@ -5,6 +5,7 @@ libjpeg-dev zlib1g-dev libpq-dev postgresql-client -libav-tools +libmagic-dev +ffmpeg python3-dev curl diff --git a/api/requirements/base.txt b/api/requirements/base.txt index f38da9629..133fcc0cb 100644 --- a/api/requirements/base.txt +++ b/api/requirements/base.txt @@ -57,3 +57,5 @@ git+https://github.com/EliotBerriot/django-cachalot.git@django-2 django-dynamic-preferences>=1.5,<1.6 pyacoustid>=1.1.5,<1.2 raven>=6.5,<7 +python-magic==0.4.15 +ffmpeg-python==0.1.10 From ddea5f182570f9d558e195fcfe55ba533e547a12 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 18 Feb 2018 23:46:15 +0100 Subject: [PATCH 05/49] Now store track file mimetype in database --- .../migrations/0018_auto_20180218_1554.py | 28 +++++++++++++++ .../migrations/0019_populate_mimetypes.py | 34 +++++++++++++++++++ api/funkwhale_api/music/models.py | 6 ++++ api/funkwhale_api/music/serializers.py | 9 ++++- api/funkwhale_api/music/utils.py | 8 +++++ api/tests/music/test_models.py | 15 ++++++++ 6 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 api/funkwhale_api/music/migrations/0018_auto_20180218_1554.py create mode 100644 api/funkwhale_api/music/migrations/0019_populate_mimetypes.py diff --git a/api/funkwhale_api/music/migrations/0018_auto_20180218_1554.py b/api/funkwhale_api/music/migrations/0018_auto_20180218_1554.py new file mode 100644 index 000000000..c45298798 --- /dev/null +++ b/api/funkwhale_api/music/migrations/0018_auto_20180218_1554.py @@ -0,0 +1,28 @@ +# Generated by Django 2.0.2 on 2018-02-18 15:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('music', '0017_auto_20171227_1728'), + ] + + operations = [ + migrations.AddField( + model_name='trackfile', + name='mimetype', + field=models.CharField(blank=True, max_length=200, null=True), + ), + migrations.AlterField( + model_name='importjob', + name='source', + field=models.CharField(max_length=500), + ), + migrations.AlterField( + model_name='importjob', + 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/0019_populate_mimetypes.py b/api/funkwhale_api/music/migrations/0019_populate_mimetypes.py new file mode 100644 index 000000000..127aa5e69 --- /dev/null +++ b/api/funkwhale_api/music/migrations/0019_populate_mimetypes.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import os + +from django.db import migrations, models +from funkwhale_api.music.utils import guess_mimetype + + +def populate_mimetype(apps, schema_editor): + TrackFile = apps.get_model("music", "TrackFile") + + for tf in TrackFile.objects.filter(audio_file__isnull=False, mimetype__isnull=True).only('audio_file'): + try: + tf.mimetype = guess_mimetype(tf.audio_file) + except Exception as e: + print('Error on track file {}: {}'.format(tf.pk, e)) + continue + print('Track file {}: {}'.format(tf.pk, tf.mimetype)) + tf.save(update_fields=['mimetype']) + + +def rewind(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('music', '0018_auto_20180218_1554'), + ] + + operations = [ + migrations.RunPython(populate_mimetype, rewind), + ] diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index f8373ab4d..3ebd07419 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -18,6 +18,7 @@ from versatileimagefield.fields import VersatileImageField from funkwhale_api import downloader from funkwhale_api import musicbrainz from . import importers +from . import utils class APIModelMixin(models.Model): @@ -364,6 +365,7 @@ class TrackFile(models.Model): source = models.URLField(null=True, blank=True) duration = models.IntegerField(null=True, blank=True) acoustid_track_id = models.UUIDField(null=True, blank=True) + mimetype = models.CharField(null=True, blank=True, max_length=200) def download_file(self): # import the track file, since there is not any @@ -393,6 +395,10 @@ class TrackFile(models.Model): self.track.full_name, os.path.splitext(self.audio_file.name)[-1]) + def save(self, **kwargs): + if not self.mimetype and self.audio_file: + self.mimetype = utils.guess_mimetype(self.audio_file) + return super().save(**kwargs) class ImportBatch(models.Model): IMPORT_BATCH_SOURCES = [ diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index 506893a4d..41de30f10 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -28,7 +28,14 @@ class TrackFileSerializer(serializers.ModelSerializer): class Meta: model = models.TrackFile - fields = ('id', 'path', 'duration', 'source', 'filename', 'track') + fields = ( + 'id', + 'path', + 'duration', + 'source', + 'filename', + 'mimetype', + 'track') def get_path(self, o): url = o.path diff --git a/api/funkwhale_api/music/utils.py b/api/funkwhale_api/music/utils.py index 32b1aeb47..0e4318e56 100644 --- a/api/funkwhale_api/music/utils.py +++ b/api/funkwhale_api/music/utils.py @@ -1,7 +1,9 @@ +import magic import re from django.db.models import Q + def normalize_query(query_string, findterms=re.compile(r'"([^"]+)"|(\S+)').findall, normspace=re.compile(r'\s{2,}').sub): @@ -15,6 +17,7 @@ def normalize_query(query_string, ''' return [normspace(' ', (t[0] or t[1]).strip()) for t in findterms(query_string)] + def get_query(query_string, search_fields): ''' Returns a query, that is a combination of Q objects. That combination aims to search keywords within a model by testing the given search fields. @@ -35,3 +38,8 @@ def get_query(query_string, search_fields): else: query = query & or_query return query + + +def guess_mimetype(f): + b = min(100000, f.size) + return magic.from_buffer(f.read(b), mime=True) diff --git a/api/tests/music/test_models.py b/api/tests/music/test_models.py index 165415465..2eb1f2763 100644 --- a/api/tests/music/test_models.py +++ b/api/tests/music/test_models.py @@ -1,9 +1,12 @@ +import os import pytest from funkwhale_api.music import models from funkwhale_api.music import importers from funkwhale_api.music import tasks +DATA_DIR = os.path.dirname(os.path.abspath(__file__)) + def test_can_store_release_group_id_on_album(factories): album = factories['music.Album']() @@ -48,3 +51,15 @@ def test_import_job_is_bound_to_track_file(factories, mocker): tasks.import_job_run(import_job_id=job.pk) job.refresh_from_db() assert job.track_file.track == track + +@pytest.mark.parametrize('extention,mimetype', [ + ('ogg', 'audio/ogg'), + ('mp3', 'audio/mpeg'), +]) +def test_audio_track_mime_type(extention, mimetype, factories): + + name = '.'.join(['test', extention]) + path = os.path.join(DATA_DIR, name) + tf = factories['music.TrackFile'](audio_file__from_path=path) + + assert tf.mimetype == mimetype From 1cfdf31e00dfad0bc2cdd203edfdc1832047cb3f Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 18 Feb 2018 23:49:42 +0100 Subject: [PATCH 06/49] Can now stream transcoded version of audio tracks \o/ --- api/funkwhale_api/music/forms.py | 23 ++++++++++++++++++ api/funkwhale_api/music/views.py | 40 ++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 api/funkwhale_api/music/forms.py diff --git a/api/funkwhale_api/music/forms.py b/api/funkwhale_api/music/forms.py new file mode 100644 index 000000000..04e4bfe05 --- /dev/null +++ b/api/funkwhale_api/music/forms.py @@ -0,0 +1,23 @@ +from django import forms + +from . import models + + +class TranscodeForm(forms.Form): + FORMAT_CHOICES = [ + ('ogg', 'ogg'), + ('mp3', 'mp3'), + ] + + to = forms.ChoiceField(choices=FORMAT_CHOICES) + BITRATE_CHOICES = [ + (64, '64'), + (128, '128'), + (256, '256'), + ] + bitrate = forms.ChoiceField( + choices=BITRATE_CHOICES, required=False) + + track_file = forms.ModelChoiceField( + queryset=models.TrackFile.objects.all() + ) diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 2395454c4..8e46cbd71 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -1,11 +1,16 @@ +import ffmpeg import os import json +import subprocess import unicodedata import urllib + from django.urls import reverse from django.db import models, transaction from django.db.models.functions import Length from django.conf import settings +from django.http import StreamingHttpResponse + from rest_framework import viewsets, views, mixins from rest_framework.decorators import detail_route, list_route from rest_framework.response import Response @@ -19,6 +24,7 @@ from funkwhale_api.common.permissions import ( ConditionalAuthentication, HasModelPermission) from taggit.models import Tag +from . import forms from . import models from . import serializers from . import importers @@ -183,6 +189,40 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet): f.audio_file.url) return response + @list_route(methods=['get']) + def viewable(self, request, *args, **kwargs): + return Response({}, status=200) + + @list_route(methods=['get']) + def transcode(self, request, *args, **kwargs): + form = forms.TranscodeForm(request.GET) + if not form.is_valid(): + return Response(form.errors, status=400) + + f = form.cleaned_data['track_file'] + output_kwargs = { + 'format': form.cleaned_data['to'] + } + args = (ffmpeg + .input(f.audio_file.path) + .output('pipe:', **output_kwargs) + .get_args() + ) + # we use a generator here so the view return immediatly and send + # file chunk to the browser, instead of blocking a few seconds + def _transcode(): + p = subprocess.Popen( + ['ffmpeg'] + args, + stdout=subprocess.PIPE) + for line in p.stdout: + yield line + + response = StreamingHttpResponse( + _transcode(), status=200, + content_type=form.cleaned_data['to']) + + return response + class TagViewSet(viewsets.ReadOnlyModelViewSet): queryset = Tag.objects.all().order_by('name') From d15fefe730484f4bb042547eeab20844a6f1f9db Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 18 Feb 2018 23:50:08 +0100 Subject: [PATCH 07/49] Leverage new transcode endpoint in player --- front/src/audio/formats.js | 10 +++++++++ front/src/components/audio/Track.vue | 33 ++++++++++++++++++---------- 2 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 front/src/audio/formats.js diff --git a/front/src/audio/formats.js b/front/src/audio/formats.js new file mode 100644 index 000000000..f6e2157a1 --- /dev/null +++ b/front/src/audio/formats.js @@ -0,0 +1,10 @@ +export default { + formats: [ + // 'audio/ogg', + 'audio/mpeg' + ], + formatsMap: { + 'audio/ogg': 'ogg', + 'audio/mpeg': 'mp3' + } +} diff --git a/front/src/components/audio/Track.vue b/front/src/components/audio/Track.vue index a513c468f..d8dcaff9b 100644 --- a/front/src/components/audio/Track.vue +++ b/front/src/components/audio/Track.vue @@ -1,21 +1,20 @@ + + From 64290465e77db880e75be21b40b63359e57ec518 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 21 Feb 2018 19:44:23 +0100 Subject: [PATCH 22/49] 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 23/49] 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 24/49] 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 25/49] 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 26/49] 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 27/49] 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 28/49] 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 29/49] 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 30/49] 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 31/49] 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 32/49] 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 34/49] 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 35/49] 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 } From 3dd1a20b68f5fc89d2381ffee6e97df0836f5aa1 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 24 Feb 2018 12:20:42 +0100 Subject: [PATCH 36/49] Fix #58: enabling registrations is now done via a preference --- api/funkwhale_api/users/adapters.py | 11 +++-------- .../users/dynamic_preferences_registry.py | 15 +++++++++++++++ api/tests/users/test_views.py | 8 ++++---- 3 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 api/funkwhale_api/users/dynamic_preferences_registry.py diff --git a/api/funkwhale_api/users/adapters.py b/api/funkwhale_api/users/adapters.py index 792b4860f..96d1b8b1d 100644 --- a/api/funkwhale_api/users/adapters.py +++ b/api/funkwhale_api/users/adapters.py @@ -1,15 +1,10 @@ from allauth.account.adapter import DefaultAccountAdapter -from django.conf import settings +from dynamic_preferences.registries import global_preferences_registry class FunkwhaleAccountAdapter(DefaultAccountAdapter): def is_open_for_signup(self, request): - - if settings.REGISTRATION_MODE == "disabled": - return False - if settings.REGISTRATION_MODE == "public": - return True - - return False + manager = global_preferences_registry.manager() + return manager['users__registration_enabled'] diff --git a/api/funkwhale_api/users/dynamic_preferences_registry.py b/api/funkwhale_api/users/dynamic_preferences_registry.py new file mode 100644 index 000000000..16d79da14 --- /dev/null +++ b/api/funkwhale_api/users/dynamic_preferences_registry.py @@ -0,0 +1,15 @@ +from dynamic_preferences import types +from dynamic_preferences.registries import global_preferences_registry + +users = types.Section('users') + + +@global_preferences_registry.register +class RegistrationEnabled(types.BooleanPreference): + show_in_api = True + section = users + name = 'registration_enabled' + default = False + verbose_name = ( + 'Can visitors open a new account on this instance?' + ) diff --git a/api/tests/users/test_views.py b/api/tests/users/test_views.py index 1eb8ef222..569acbd15 100644 --- a/api/tests/users/test_views.py +++ b/api/tests/users/test_views.py @@ -6,7 +6,7 @@ from django.urls import reverse from funkwhale_api.users.models import User -def test_can_create_user_via_api(settings, client, db): +def test_can_create_user_via_api(preferences, client, db): url = reverse('rest_register') data = { 'username': 'test1', @@ -14,7 +14,7 @@ def test_can_create_user_via_api(settings, client, db): 'password1': 'testtest', 'password2': 'testtest', } - settings.REGISTRATION_MODE = "public" + preferences['users__registration_enabled'] = True response = client.post(url, data) assert response.status_code == 201 @@ -22,7 +22,7 @@ def test_can_create_user_via_api(settings, client, db): assert u.username == 'test1' -def test_can_disable_registration_view(settings, client, db): +def test_can_disable_registration_view(preferences, client, db): url = reverse('rest_register') data = { 'username': 'test1', @@ -30,7 +30,7 @@ def test_can_disable_registration_view(settings, client, db): 'password1': 'testtest', 'password2': 'testtest', } - settings.REGISTRATION_MODE = "disabled" + preferences['users__registration_enabled'] = False response = client.post(url, data) assert response.status_code == 403 From d1b5895b784a9952a017a1fe4faaa29e0b2e1217 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 24 Feb 2018 12:21:02 +0100 Subject: [PATCH 37/49] Removed now useless REGISTRATION_MODE setting --- api/config/settings/common.py | 1 - deploy/env.prod.sample | 5 ----- 2 files changed, 6 deletions(-) diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 5fe55e53a..927e754d0 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -317,7 +317,6 @@ CORS_ORIGIN_ALLOW_ALL = True # ) CORS_ALLOW_CREDENTIALS = True API_AUTHENTICATION_REQUIRED = env.bool("API_AUTHENTICATION_REQUIRED", True) -REGISTRATION_MODE = env('REGISTRATION_MODE', default='disabled') REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', diff --git a/deploy/env.prod.sample b/deploy/env.prod.sample index 5bdfeb9c6..6a4b15b67 100644 --- a/deploy/env.prod.sample +++ b/deploy/env.prod.sample @@ -74,11 +74,6 @@ DJANGO_SECRET_KEY= # If True, unauthenticated users won't be able to query the API API_AUTHENTICATION_REQUIRED=True -# What is the workflow for registration on funkwhale ? Possible values: -# public: anybody can register an account -# disabled: nobody can register an account -REGISTRATION_MODE=disabled - # Sentry/Raven error reporting (server side) # Enable Raven if you want to help improve funkwhale by # automatically sending error reports our Sentry instance. From a91670f379430b160205f25181ee5e244060d7f4 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 24 Feb 2018 12:21:56 +0100 Subject: [PATCH 38/49] Changelog for #58 --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 07d1dedbd..d5311057a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ Changelog - Front: Now reset player colors when track has no cover (#46) - Front: play button now disabled for unplayable tracks +- API: You can now enable or disable registration on the fly, via a preference (#58) Transcoding: From a14e0acbe9cdcf1dc2004988b07de8ffed845b0d Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 24 Feb 2018 14:05:27 +0100 Subject: [PATCH 39/49] Correct path for rest auth urls --- api/config/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/config/urls.py b/api/config/urls.py index de67ebb57..8f7e37bc2 100644 --- a/api/config/urls.py +++ b/api/config/urls.py @@ -13,8 +13,8 @@ urlpatterns = [ url(settings.ADMIN_URL, admin.site.urls), url(r'^api/', include(("config.api_urls", 'api'), namespace="api")), - url(r'^api/auth/', include('rest_auth.urls')), - url(r'^api/auth/registration/', include('funkwhale_api.users.rest_auth_urls')), + url(r'^api/v1/auth/', include('rest_auth.urls')), + url(r'^api/v1/auth/registration/', include('funkwhale_api.users.rest_auth_urls')), url(r'^accounts/', include('allauth.urls')), # Your stuff: custom urls includes go here From 0ed3f68305da35d54a4dbf929b87bc5b8436a4c0 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 24 Feb 2018 14:27:50 +0100 Subject: [PATCH 40/49] Allow login with email or username --- api/config/settings/common.py | 2 +- front/src/components/auth/Login.vue | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 927e754d0..491babdd1 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -264,7 +264,7 @@ AUTHENTICATION_BACKENDS = ( ) # Some really nice defaults -ACCOUNT_AUTHENTICATION_METHOD = 'username' +ACCOUNT_AUTHENTICATION_METHOD = 'username_email' ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_VERIFICATION = 'mandatory' diff --git a/front/src/components/auth/Login.vue b/front/src/components/auth/Login.vue index 99b439af8..b02a3ed07 100644 --- a/front/src/components/auth/Login.vue +++ b/front/src/components/auth/Login.vue @@ -12,13 +12,13 @@
- +
From 55b38a3f6e2a126362fdc58e93c6a9acdbf045f5 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 24 Feb 2018 14:28:48 +0100 Subject: [PATCH 41/49] Fixed broken error handling on user login and settings --- front/src/components/auth/Login.vue | 6 +++--- front/src/components/auth/Settings.vue | 13 ++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/front/src/components/auth/Login.vue b/front/src/components/auth/Login.vue index b02a3ed07..f84ad9295 100644 --- a/front/src/components/auth/Login.vue +++ b/front/src/components/auth/Login.vue @@ -73,9 +73,9 @@ export default { // to properly make use of http in the auth service this.$store.dispatch('auth/login', { credentials, - next: this.next, - onError: response => { - if (response.status === 400) { + next: '/library', + onError: error => { + if (error.response.status === 400) { self.error = 'invalid_credentials' } else { self.error = 'unknown_error' diff --git a/front/src/components/auth/Settings.vue b/front/src/components/auth/Settings.vue index f090581ef..4e8f33289 100644 --- a/front/src/components/auth/Settings.vue +++ b/front/src/components/auth/Settings.vue @@ -37,7 +37,6 @@ + + + diff --git a/front/src/router/index.js b/front/src/router/index.js index ea8854bbe..c1d03e059 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -3,6 +3,7 @@ import Router from 'vue-router' import PageNotFound from '@/components/PageNotFound' import Home from '@/components/Home' import Login from '@/components/auth/Login' +import Signup from '@/components/auth/Signup' import Profile from '@/components/auth/Profile' import Settings from '@/components/auth/Settings' import Logout from '@/components/auth/Logout' @@ -38,6 +39,11 @@ export default new Router({ component: Login, props: (route) => ({ next: route.query.next || '/library' }) }, + { + path: '/signup', + name: 'signup', + component: Signup + }, { path: '/logout', name: 'logout', diff --git a/front/src/store/instance.js b/front/src/store/instance.js index a0071f096..dd20a8b1d 100644 --- a/front/src/store/instance.js +++ b/front/src/store/instance.js @@ -6,6 +6,11 @@ export default { namespaced: true, state: { settings: { + users: { + registration_enabled: { + value: true + } + }, raven: { front_enabled: { value: false @@ -23,7 +28,7 @@ export default { }, actions: { // Send a request to the login URL and save the returned JWT - fetchSettings ({commit}) { + fetchSettings ({commit}, {callback}) { return axios.get('instance/settings/').then(response => { logger.default.info('Successfully fetched instance settings') let sections = {} @@ -34,6 +39,9 @@ export default { sections[e.section][e.name] = e }) commit('settings', sections) + if (callback) { + callback() + } }, response => { logger.default.error('Error while fetching settings', response.data) }) From ea4d8b1010f964dc189755a037d2e77b47bb05c0 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 24 Feb 2018 14:40:55 +0100 Subject: [PATCH 43/49] Changelog --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index d5311057a..d010c076e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,9 @@ Changelog - Front: Now reset player colors when track has no cover (#46) - Front: play button now disabled for unplayable tracks - API: You can now enable or disable registration on the fly, via a preference (#58) +- Front: can now signup via the web interface (#35) +- Front: Fixed broken redirection on login +- Front: Fixed broken error handling on settings and login form Transcoding: From f398d40ecc9a85b6b7822ad1b81d6f3c42cdb479 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 24 Feb 2018 14:55:08 +0100 Subject: [PATCH 44/49] Fixed failing test --- front/src/store/instance.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front/src/store/instance.js b/front/src/store/instance.js index dd20a8b1d..80003db0d 100644 --- a/front/src/store/instance.js +++ b/front/src/store/instance.js @@ -28,7 +28,7 @@ export default { }, actions: { // Send a request to the login URL and save the returned JWT - fetchSettings ({commit}, {callback}) { + fetchSettings ({commit}, payload) { return axios.get('instance/settings/').then(response => { logger.default.info('Successfully fetched instance settings') let sections = {} @@ -39,7 +39,7 @@ export default { sections[e.section][e.name] = e }) commit('settings', sections) - if (callback) { + if (payload && payload.callback) { callback() } }, response => { From b4ace3c9dd29972440a974a16514a06e0b046a62 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 24 Feb 2018 15:10:47 +0100 Subject: [PATCH 45/49] Typo --- front/src/store/instance.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/src/store/instance.js b/front/src/store/instance.js index 80003db0d..ed7d653fc 100644 --- a/front/src/store/instance.js +++ b/front/src/store/instance.js @@ -40,7 +40,7 @@ export default { }) commit('settings', sections) if (payload && payload.callback) { - callback() + payload.callback() } }, response => { logger.default.error('Error while fetching settings', response.data) From cdabb26989ebc233520051dab816220942649922 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 24 Feb 2018 15:27:51 +0100 Subject: [PATCH 46/49] Fix #30: added instance preferences for name and descriptions --- .../instance/dynamic_preferences_registry.py | 33 +++++++++++++++++++ api/tests/instance/test_preferences.py | 13 ++++++++ 2 files changed, 46 insertions(+) diff --git a/api/funkwhale_api/instance/dynamic_preferences_registry.py b/api/funkwhale_api/instance/dynamic_preferences_registry.py index 1d93c383e..1d11a2988 100644 --- a/api/funkwhale_api/instance/dynamic_preferences_registry.py +++ b/api/funkwhale_api/instance/dynamic_preferences_registry.py @@ -1,9 +1,42 @@ +from django.forms import widgets + from dynamic_preferences import types from dynamic_preferences.registries import global_preferences_registry raven = types.Section('raven') +instance = types.Section('instance') +@global_preferences_registry.register +class InstanceName(types.StringPreference): + show_in_api = True + section = instance + name = 'name' + default = '' + help_text = 'Instance public name' + verbose_name = 'The public name of your instance' + + +@global_preferences_registry.register +class InstanceShortDescription(types.StringPreference): + show_in_api = True + section = instance + name = 'short_description' + default = '' + verbose_name = 'Instance succinct description' + + +@global_preferences_registry.register +class InstanceLongDescription(types.StringPreference): + show_in_api = True + section = instance + name = 'long_description' + default = '' + help_text = 'Instance long description (markdown allowed)' + field_kwargs = { + 'widget': widgets.Textarea + } + @global_preferences_registry.register class RavenDSN(types.StringPreference): show_in_api = True diff --git a/api/tests/instance/test_preferences.py b/api/tests/instance/test_preferences.py index c89bfa349..beb8e6d33 100644 --- a/api/tests/instance/test_preferences.py +++ b/api/tests/instance/test_preferences.py @@ -1,3 +1,5 @@ +import pytest + from django.urls import reverse from dynamic_preferences.api import serializers @@ -20,3 +22,14 @@ def test_can_list_settings_via_api(preferences, api_client): for p in response.data: i = '__'.join([p['section'], p['name']]) assert i in expected_preferences + + +@pytest.mark.parametrize('pref,value', [ + ('instance__name', 'My instance'), + ('instance__short_description', 'For music lovers'), + ('instance__long_description', 'For real music lovers'), +]) +def test_instance_settings(pref, value, preferences): + preferences[pref] = value + + assert preferences[pref] == value From ab2af7a548dd5cfdb89ca9dae142fa4470e236ea Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 24 Feb 2018 15:28:09 +0100 Subject: [PATCH 47/49] About page for instance based on instance descriptions --- front/src/App.vue | 3 +++ front/src/components/About.vue | 45 ++++++++++++++++++++++++++++++++++ front/src/components/Home.vue | 4 +++ front/src/router/index.js | 6 +++++ front/src/store/instance.js | 11 +++++++++ 5 files changed, 69 insertions(+) create mode 100644 front/src/components/About.vue diff --git a/front/src/App.vue b/front/src/App.vue index 98ad48d3f..8453aa339 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -9,6 +9,9 @@

Links