kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
				
				
				
			Merge branch '206-settings-admin' into 'develop'
Resolve "Add a dedicated front-end to manage instance preferences" Closes #206 See merge request funkwhale/funkwhale!199merge-requests/237/head
						commit
						23e27e0dd9
					
				|  | @ -16,5 +16,5 @@ class APIAutenticationRequired( | ||||||
|     help_text = ( |     help_text = ( | ||||||
|         'If disabled, anonymous users will be able to query the API' |         'If disabled, anonymous users will be able to query the API' | ||||||
|         'and access music data (as well as other data exposed in the API ' |         'and access music data (as well as other data exposed in the API ' | ||||||
|         'without specific permissions)' |         'without specific permissions).' | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  | @ -19,6 +19,9 @@ class MusicCacheDuration(types.IntPreference): | ||||||
|         'locally? Federated files that were not listened in this interval ' |         'locally? Federated files that were not listened in this interval ' | ||||||
|         'will be erased and refetched from the remote on the next listening.' |         'will be erased and refetched from the remote on the next listening.' | ||||||
|     ) |     ) | ||||||
|  |     field_kwargs = { | ||||||
|  |         'required': False, | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @global_preferences_registry.register | @global_preferences_registry.register | ||||||
|  | @ -29,7 +32,7 @@ class Enabled(preferences.DefaultFromSettingMixin, types.BooleanPreference): | ||||||
|     verbose_name = 'Federation enabled' |     verbose_name = 'Federation enabled' | ||||||
|     help_text = ( |     help_text = ( | ||||||
|         'Use this setting to enable or disable federation logic and API' |         'Use this setting to enable or disable federation logic and API' | ||||||
|         ' globally' |         ' globally.' | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -41,8 +44,11 @@ class CollectionPageSize( | ||||||
|     setting = 'FEDERATION_COLLECTION_PAGE_SIZE' |     setting = 'FEDERATION_COLLECTION_PAGE_SIZE' | ||||||
|     verbose_name = 'Federation collection page size' |     verbose_name = 'Federation collection page size' | ||||||
|     help_text = ( |     help_text = ( | ||||||
|         'How much items to display in ActivityPub collections' |         'How much items to display in ActivityPub collections.' | ||||||
|     ) |     ) | ||||||
|  |     field_kwargs = { | ||||||
|  |         'required': False, | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @global_preferences_registry.register | @global_preferences_registry.register | ||||||
|  | @ -54,8 +60,11 @@ class ActorFetchDelay( | ||||||
|     verbose_name = 'Federation actor fetch delay' |     verbose_name = 'Federation actor fetch delay' | ||||||
|     help_text = ( |     help_text = ( | ||||||
|         'How much minutes to wait before refetching actors on ' |         'How much minutes to wait before refetching actors on ' | ||||||
|         'request authentication' |         'request authentication.' | ||||||
|     ) |     ) | ||||||
|  |     field_kwargs = { | ||||||
|  |         'required': False, | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @global_preferences_registry.register | @global_preferences_registry.register | ||||||
|  | @ -66,6 +75,6 @@ class MusicNeedsApproval( | ||||||
|     setting = 'FEDERATION_MUSIC_NEEDS_APPROVAL' |     setting = 'FEDERATION_MUSIC_NEEDS_APPROVAL' | ||||||
|     verbose_name = 'Federation music needs approval' |     verbose_name = 'Federation music needs approval' | ||||||
|     help_text = ( |     help_text = ( | ||||||
|         'When true, other federation actors will require your approval' |         'When true, other federation actors will need your approval' | ||||||
|         ' before being able to browse your library.' |         ' before being able to browse your library.' | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  | @ -13,8 +13,11 @@ class InstanceName(types.StringPreference): | ||||||
|     section = instance |     section = instance | ||||||
|     name = 'name' |     name = 'name' | ||||||
|     default = '' |     default = '' | ||||||
|     help_text = 'Instance public name' |     verbose_name = 'Public name' | ||||||
|     verbose_name = 'The public name of your instance' |     help_text = 'The public name of your instance, displayed in the about page.' | ||||||
|  |     field_kwargs = { | ||||||
|  |         'required': False, | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @global_preferences_registry.register | @global_preferences_registry.register | ||||||
|  | @ -23,7 +26,11 @@ class InstanceShortDescription(types.StringPreference): | ||||||
|     section = instance |     section = instance | ||||||
|     name = 'short_description' |     name = 'short_description' | ||||||
|     default = '' |     default = '' | ||||||
|     verbose_name = 'Instance succinct description' |     verbose_name = 'Short description' | ||||||
|  |     help_text = 'Instance succinct description, displayed in the about page.' | ||||||
|  |     field_kwargs = { | ||||||
|  |         'required': False, | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @global_preferences_registry.register | @global_preferences_registry.register | ||||||
|  | @ -31,31 +38,31 @@ class InstanceLongDescription(types.StringPreference): | ||||||
|     show_in_api = True |     show_in_api = True | ||||||
|     section = instance |     section = instance | ||||||
|     name = 'long_description' |     name = 'long_description' | ||||||
|  |     verbose_name = 'Long description' | ||||||
|     default = '' |     default = '' | ||||||
|     help_text = 'Instance long description (markdown allowed)' |     help_text = 'Instance long description, displayed in the about page (markdown allowed).' | ||||||
|  |     widget = widgets.Textarea | ||||||
|     field_kwargs = { |     field_kwargs = { | ||||||
|         'widget': widgets.Textarea |         'required': False, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| @global_preferences_registry.register | @global_preferences_registry.register | ||||||
| class RavenDSN(types.StringPreference): | class RavenDSN(types.StringPreference): | ||||||
|     show_in_api = True |     show_in_api = True | ||||||
|     section = raven |     section = raven | ||||||
|     name = 'front_dsn' |     name = 'front_dsn' | ||||||
|     default = 'https://9e0562d46b09442bb8f6844e50cbca2b@sentry.eliotberriot.com/4' |     default = 'https://9e0562d46b09442bb8f6844e50cbca2b@sentry.eliotberriot.com/4' | ||||||
|     verbose_name = ( |     verbose_name = 'Raven DSN key (front-end)' | ||||||
|         'A raven DSN key used to report front-ent errors to ' | 
 | ||||||
|         'a sentry instance' |  | ||||||
|     ) |  | ||||||
|     help_text = ( |     help_text = ( | ||||||
|         'Keeping the default one will report errors to funkwhale developers' |         'A Raven DSN key used to report front-ent errors to ' | ||||||
|  |         'a sentry instance. Keeping the default one will report errors to ' | ||||||
|  |         'Funkwhale developers.' | ||||||
|     ) |     ) | ||||||
| 
 |     field_kwargs = { | ||||||
| 
 |         'required': False, | ||||||
| SENTRY_HELP_TEXT = ( |     } | ||||||
|     'Error reporting is disabled by default but you can enable it if' |  | ||||||
|     ' you want to help us improve funkwhale' |  | ||||||
| ) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @global_preferences_registry.register | @global_preferences_registry.register | ||||||
|  | @ -65,8 +72,7 @@ class RavenEnabled(types.BooleanPreference): | ||||||
|     name = 'front_enabled' |     name = 'front_enabled' | ||||||
|     default = False |     default = False | ||||||
|     verbose_name = ( |     verbose_name = ( | ||||||
|         'Wether error reporting to a Sentry instance using raven is enabled' |         'Report front-end errors with Raven' | ||||||
|         ' for front-end errors' |  | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -78,7 +84,7 @@ class InstanceNodeinfoEnabled(types.BooleanPreference): | ||||||
|     default = True |     default = True | ||||||
|     verbose_name = 'Enable nodeinfo endpoint' |     verbose_name = 'Enable nodeinfo endpoint' | ||||||
|     help_text = ( |     help_text = ( | ||||||
|         'This endpoint is needed for your about page to work.' |         'This endpoint is needed for your about page to work. ' | ||||||
|         'It\'s also helpful for the various monitoring ' |         'It\'s also helpful for the various monitoring ' | ||||||
|         'tools that map and analyzize the fediverse, ' |         'tools that map and analyzize the fediverse, ' | ||||||
|         'but you can disable it completely if needed.' |         'but you can disable it completely if needed.' | ||||||
|  | @ -91,10 +97,10 @@ class InstanceNodeinfoPrivate(types.BooleanPreference): | ||||||
|     section = instance |     section = instance | ||||||
|     name = 'nodeinfo_private' |     name = 'nodeinfo_private' | ||||||
|     default = False |     default = False | ||||||
|     verbose_name = 'Enable nodeinfo endpoint' |     verbose_name = 'Private mode in nodeinfo' | ||||||
|     help_text = ( |     help_text = ( | ||||||
|         'Indicate in the nodeinfo endpoint that you do not want your instance' |         'Indicate in the nodeinfo endpoint that you do not want your instance ' | ||||||
|         'to be tracked by third-party services.' |         'to be tracked by third-party services. ' | ||||||
|         'There is no guarantee these tools will honor this setting though.' |         'There is no guarantee these tools will honor this setting though.' | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|  | @ -107,6 +113,6 @@ class InstanceNodeinfoStatsEnabled(types.BooleanPreference): | ||||||
|     default = True |     default = True | ||||||
|     verbose_name = 'Enable usage and library stats in nodeinfo endpoint' |     verbose_name = 'Enable usage and library stats in nodeinfo endpoint' | ||||||
|     help_text = ( |     help_text = ( | ||||||
|         'Disable this f you don\'t want to share usage and library statistics' |         'Disable this if you don\'t want to share usage and library statistics ' | ||||||
|         'in the nodeinfo endpoint but don\'t want to disable it completely.' |         'in the nodeinfo endpoint but don\'t want to disable it completely.' | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  | @ -1,9 +1,11 @@ | ||||||
| from django.conf.urls import url | from django.conf.urls import url | ||||||
|  | from rest_framework import routers | ||||||
| 
 | 
 | ||||||
| from . import views | from . import views | ||||||
| 
 | admin_router = routers.SimpleRouter() | ||||||
|  | admin_router.register(r'admin/settings', views.AdminSettings, 'admin-settings') | ||||||
| 
 | 
 | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     url(r'^nodeinfo/2.0/$', views.NodeInfo.as_view(), name='nodeinfo-2.0'), |     url(r'^nodeinfo/2.0/$', views.NodeInfo.as_view(), name='nodeinfo-2.0'), | ||||||
|     url(r'^settings/$', views.InstanceSettings.as_view(), name='settings'), |     url(r'^settings/$', views.InstanceSettings.as_view(), name='settings'), | ||||||
| ] | ] + admin_router.urls | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ from rest_framework import views | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| 
 | 
 | ||||||
| from dynamic_preferences.api import serializers | from dynamic_preferences.api import serializers | ||||||
|  | from dynamic_preferences.api import viewsets as preferences_viewsets | ||||||
| from dynamic_preferences.registries import global_preferences_registry | from dynamic_preferences.registries import global_preferences_registry | ||||||
| 
 | 
 | ||||||
| from funkwhale_api.common import preferences | from funkwhale_api.common import preferences | ||||||
|  | @ -15,6 +16,10 @@ NODEINFO_2_CONTENT_TYPE = ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class AdminSettings(preferences_viewsets.GlobalPreferencesViewSet): | ||||||
|  |     pagination_class = None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class InstanceSettings(views.APIView): | class InstanceSettings(views.APIView): | ||||||
|     permission_classes = [] |     permission_classes = [] | ||||||
|     authentication_classes = [] |     authentication_classes = [] | ||||||
|  |  | ||||||
|  | @ -13,3 +13,6 @@ class MaxTracks(preferences.DefaultFromSettingMixin, types.IntegerPreference): | ||||||
|     name = 'max_tracks' |     name = 'max_tracks' | ||||||
|     verbose_name = 'Max tracks per playlist' |     verbose_name = 'Max tracks per playlist' | ||||||
|     setting = 'PLAYLISTS_MAX_TRACKS' |     setting = 'PLAYLISTS_MAX_TRACKS' | ||||||
|  |     field_kwargs = { | ||||||
|  |         'required': False, | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | @ -1,3 +1,5 @@ | ||||||
|  | from django import forms | ||||||
|  | 
 | ||||||
| from dynamic_preferences.types import StringPreference, Section | from dynamic_preferences.types import StringPreference, Section | ||||||
| from dynamic_preferences.registries import global_preferences_registry | from dynamic_preferences.registries import global_preferences_registry | ||||||
| 
 | 
 | ||||||
|  | @ -11,3 +13,7 @@ class APIKey(StringPreference): | ||||||
|     default = '' |     default = '' | ||||||
|     verbose_name = 'Acoustid API key' |     verbose_name = 'Acoustid API key' | ||||||
|     help_text = 'The API key used to query AcoustID. Get one at https://acoustid.org/new-application.' |     help_text = 'The API key used to query AcoustID. Get one at https://acoustid.org/new-application.' | ||||||
|  |     widget = forms.PasswordInput | ||||||
|  |     field_kwargs = { | ||||||
|  |         'required': False, | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | @ -1,3 +1,5 @@ | ||||||
|  | from django import forms | ||||||
|  | 
 | ||||||
| from dynamic_preferences.types import StringPreference, Section | from dynamic_preferences.types import StringPreference, Section | ||||||
| from dynamic_preferences.registries import global_preferences_registry | from dynamic_preferences.registries import global_preferences_registry | ||||||
| 
 | 
 | ||||||
|  | @ -11,3 +13,7 @@ class APIKey(StringPreference): | ||||||
|     default = 'CHANGEME' |     default = 'CHANGEME' | ||||||
|     verbose_name = 'YouTube API key' |     verbose_name = 'YouTube API key' | ||||||
|     help_text = 'The API key used to query YouTube. Get one at https://console.developers.google.com/.' |     help_text = 'The API key used to query YouTube. Get one at https://console.developers.google.com/.' | ||||||
|  |     widget = forms.PasswordInput | ||||||
|  |     field_kwargs = { | ||||||
|  |         'required': False, | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ class RegistrationEnabled(types.BooleanPreference): | ||||||
|     section = users |     section = users | ||||||
|     name = 'registration_enabled' |     name = 'registration_enabled' | ||||||
|     default = False |     default = False | ||||||
|     verbose_name = ( |     verbose_name = 'Open registrations to new users' | ||||||
|         'Can visitors open a new account on this instance?' |     help_text = ( | ||||||
|  |         'When enabled, new users will be able to register on this instance.' | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ import os | ||||||
| import uuid | import uuid | ||||||
| 
 | 
 | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.contrib.auth.models import AbstractUser | from django.contrib.auth.models import AbstractUser, Permission | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.db import models | from django.db import models | ||||||
| from django.utils.encoding import python_2_unicode_compatible | from django.utils.encoding import python_2_unicode_compatible | ||||||
|  | @ -55,6 +55,10 @@ class User(AbstractUser): | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return self.username |         return self.username | ||||||
| 
 | 
 | ||||||
|  |     def add_permission(self, codename): | ||||||
|  |         p = Permission.objects.get(codename=codename) | ||||||
|  |         self.user_permissions.add(p) | ||||||
|  | 
 | ||||||
|     def get_absolute_url(self): |     def get_absolute_url(self): | ||||||
|         return reverse('users:detail', kwargs={'username': self.username}) |         return reverse('users:detail', kwargs={'username': self.username}) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -21,3 +21,31 @@ def test_nodeinfo_endpoint_disabled(db, api_client, preferences): | ||||||
|     response = api_client.get(url) |     response = api_client.get(url) | ||||||
| 
 | 
 | ||||||
|     assert response.status_code == 404 |     assert response.status_code == 404 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_settings_only_list_public_settings(db, api_client, preferences): | ||||||
|  |     url = reverse('api:v1:instance:settings') | ||||||
|  |     response = api_client.get(url) | ||||||
|  | 
 | ||||||
|  |     for conf in response.data: | ||||||
|  |         p = preferences.model.objects.get( | ||||||
|  |             section=conf['section'], name=conf['name']) | ||||||
|  |         assert p.preference.show_in_api is True | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_admin_settings_restrict_access(db, logged_in_api_client, preferences): | ||||||
|  |     url = reverse('api:v1:instance:admin-settings-list') | ||||||
|  |     response = logged_in_api_client.get(url) | ||||||
|  | 
 | ||||||
|  |     assert response.status_code == 403 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_admin_settings_correct_permission( | ||||||
|  |         db, logged_in_api_client, preferences): | ||||||
|  |     user = logged_in_api_client.user | ||||||
|  |     user.add_permission('change_globalpreferencemodel') | ||||||
|  |     url = reverse('api:v1:instance:admin-settings-list') | ||||||
|  |     response = logged_in_api_client.get(url) | ||||||
|  | 
 | ||||||
|  |     assert response.status_code == 200 | ||||||
|  |     assert len(response.data) == len(preferences.all()) | ||||||
|  |  | ||||||
|  | @ -0,0 +1,17 @@ | ||||||
|  | We now have a brand new instance settings interface in the front-end (#206) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Instance settings interface | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  | 
 | ||||||
|  | Prior to this release, the only way to update instance settings (such as | ||||||
|  | instance description, signup policy, federation configuration, etc.) was using | ||||||
|  | the admin interface provided by Django (the back-end framework which power the API). | ||||||
|  | 
 | ||||||
|  | This interface worked, but was not really-user friendly and intuitive. | ||||||
|  | 
 | ||||||
|  | Starting from this release, we now offer a dedicated interface directly | ||||||
|  | in the front-end. You can view and edit all your instance settings from here, | ||||||
|  | assuming you have the required permissions. | ||||||
|  | 
 | ||||||
|  | This interface is available at ``/manage/settings` and via link in the sidebar. | ||||||
|  | @ -27,15 +27,24 @@ Those settings are stored in database and do not require a restart of your | ||||||
| instance after modification. They typically relate to higher level configuration, | instance after modification. They typically relate to higher level configuration, | ||||||
| such your instance description, signup policy and so on. | such your instance description, signup policy and so on. | ||||||
| 
 | 
 | ||||||
| There is no polished interface for those settings, yet, but you can view update | You can edit those settings directly from the web application, assuming | ||||||
| them using the administration interface provided by Django (the framework funkwhale is built on). | you have the required permissions. The URL is ``/manage/settings``, and | ||||||
| 
 | you will also find a link to this page in the sidebar. | ||||||
| The URL should be ``/api/admin/dynamic_preferences/globalpreferencemodel/`` (prepend your domain in front of it, of course). |  | ||||||
| 
 | 
 | ||||||
| If you plan to use acoustid and external imports | If you plan to use acoustid and external imports | ||||||
| (e.g. with the youtube backends), you should edit the corresponding | (e.g. with the youtube backends), you should edit the corresponding | ||||||
| settings in this interface. | settings in this interface. | ||||||
| 
 | 
 | ||||||
|  | .. note:: | ||||||
|  | 
 | ||||||
|  |     If you have any issue with the web application, a management interface is also | ||||||
|  |     available for those settings from Django's administration interface. It's | ||||||
|  |     less user friendly, though, and we recommend you use the web app interface | ||||||
|  |     whenever possible. | ||||||
|  | 
 | ||||||
|  |     The URL should be ``/api/admin/dynamic_preferences/globalpreferencemodel/`` (prepend your domain in front of it, of course). | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| Configuration reference | Configuration reference | ||||||
| ----------------------- | ----------------------- | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -13,6 +13,12 @@ | ||||||
|       <p v-if="!instance.short_description.value && !instance.long_description.value"> |       <p v-if="!instance.short_description.value && !instance.long_description.value"> | ||||||
|         {{ $t('Unfortunately, owners of this instance did not yet take the time to complete this page.') }} |         {{ $t('Unfortunately, owners of this instance did not yet take the time to complete this page.') }} | ||||||
|       </p> |       </p> | ||||||
|  |       <router-link | ||||||
|  |         class="ui button" | ||||||
|  |         v-if="$store.state.auth.availablePermissions['settings.change']" | ||||||
|  |         :to="{path: '/manage/settings', hash: 'instance'}"> | ||||||
|  |         <i class="pencil icon"></i>{{ $t('Edit instance info') }} | ||||||
|  |       </router-link> | ||||||
|       <div |       <div | ||||||
|         v-if="instance.short_description.value" |         v-if="instance.short_description.value" | ||||||
|         class="ui middle aligned stackable text container"> |         class="ui middle aligned stackable text container"> | ||||||
|  |  | ||||||
|  | @ -78,6 +78,12 @@ | ||||||
|                 :title="$t('Pending follow requests')"> |                 :title="$t('Pending follow requests')"> | ||||||
|                 {{ notifications.federation }}</div> |                 {{ notifications.federation }}</div> | ||||||
|             </router-link> |             </router-link> | ||||||
|  |             <router-link | ||||||
|  |               class="item" | ||||||
|  |               v-if="$store.state.auth.availablePermissions['settings.change']" | ||||||
|  |               :to="{path: '/manage/settings'}"> | ||||||
|  |               <i class="settings icon"></i>{{ $t('Settings') }} | ||||||
|  |             </router-link> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  | @ -217,7 +223,6 @@ export default { | ||||||
|       } |       } | ||||||
|       let self = this |       let self = this | ||||||
|       axios.get('requests/import-requests/', {params: {status: 'pending'}}).then(response => { |       axios.get('requests/import-requests/', {params: {status: 'pending'}}).then(response => { | ||||||
|         console.log('YOLo') |  | ||||||
|         self.notifications.importRequests = response.data.count |         self.notifications.importRequests = response.data.count | ||||||
|       }) |       }) | ||||||
|     }, |     }, | ||||||
|  | @ -256,7 +261,6 @@ export default { | ||||||
|     }, |     }, | ||||||
|     '$store.state.availablePermissions': { |     '$store.state.availablePermissions': { | ||||||
|       handler () { |       handler () { | ||||||
|         console.log('YOLO') |  | ||||||
|         this.fetchNotificationsCount() |         this.fetchNotificationsCount() | ||||||
|       }, |       }, | ||||||
|       deep: true |       deep: true | ||||||
|  |  | ||||||
|  | @ -0,0 +1,120 @@ | ||||||
|  | <template> | ||||||
|  |   <form :id="group.id" class="ui form" @submit.prevent="save"> | ||||||
|  |     <div class="ui divider" /> | ||||||
|  |     <h3 class="ui header">{{ group.label }}</h3> | ||||||
|  |     <div v-if="errors.length > 0" class="ui negative message"> | ||||||
|  |       <div class="header">{{ $t('Error while saving settings') }}</div> | ||||||
|  |       <ul class="list"> | ||||||
|  |         <li v-for="error in errors">{{ error }}</li> | ||||||
|  |       </ul> | ||||||
|  |     </div> | ||||||
|  |     <div v-if="result" class="ui positive message"> | ||||||
|  |       {{ $t('Settings updated successfully.') }} | ||||||
|  |     </div> | ||||||
|  |     <p v-if="group.help">{{ group.help }}</p> | ||||||
|  |     <div v-for="setting in settings" class="ui field"> | ||||||
|  |       <template v-if="setting.field.widget.class !== 'CheckboxInput'"> | ||||||
|  |         <label :for="setting.identifier">{{ setting.verbose_name }}</label> | ||||||
|  |         <p v-if="setting.help_text">{{ setting.help_text }}</p> | ||||||
|  |       </template> | ||||||
|  |       <input | ||||||
|  |         :id="setting.identifier" | ||||||
|  |         v-if="setting.field.widget.class === 'PasswordInput'" | ||||||
|  |         type="password" | ||||||
|  |         class="ui input" | ||||||
|  |         v-model="values[setting.identifier]" /> | ||||||
|  |       <input | ||||||
|  |         :id="setting.identifier" | ||||||
|  |         v-if="setting.field.widget.class === 'TextInput'" | ||||||
|  |         type="text" | ||||||
|  |         class="ui input" | ||||||
|  |         v-model="values[setting.identifier]" /> | ||||||
|  |       <input | ||||||
|  |         :id="setting.identifier" | ||||||
|  |         v-if="setting.field.class === 'IntegerField'" | ||||||
|  |         type="number" | ||||||
|  |         class="ui input" | ||||||
|  |         v-model.number="values[setting.identifier]" /> | ||||||
|  |       <textarea | ||||||
|  |         :id="setting.identifier" | ||||||
|  |         v-else-if="setting.field.widget.class === 'Textarea'" | ||||||
|  |         type="text" | ||||||
|  |         class="ui input" | ||||||
|  |         v-model="values[setting.identifier]" /> | ||||||
|  |       <div v-else-if="setting.field.widget.class === 'CheckboxInput'" class="ui toggle checkbox"> | ||||||
|  |         <input | ||||||
|  |           :id="setting.identifier" | ||||||
|  |           :name="setting.identifier" | ||||||
|  |           v-model="values[setting.identifier]" | ||||||
|  |           type="checkbox" /> | ||||||
|  |         <label :for="setting.identifier">{{ setting.verbose_name }}</label> | ||||||
|  |         <p v-if="setting.help_text">{{ setting.help_text }}</p> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |     <button | ||||||
|  |       type="submit" | ||||||
|  |       :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']"> | ||||||
|  |         {{ $t('Save') }} | ||||||
|  |     </button> | ||||||
|  |   </form> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import axios from 'axios' | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     group: {type: Object, required: true}, | ||||||
|  |     settingsData: {type: Array, required: true} | ||||||
|  |   }, | ||||||
|  |   data () { | ||||||
|  |     return { | ||||||
|  |       values: {}, | ||||||
|  |       result: null, | ||||||
|  |       errors: [], | ||||||
|  |       isLoading: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   created () { | ||||||
|  |     let self = this | ||||||
|  |     this.settings.forEach(e => { | ||||||
|  |       self.values[e.identifier] = e.value | ||||||
|  |     }) | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     save () { | ||||||
|  |       let self = this | ||||||
|  |       this.isLoading = true | ||||||
|  |       self.errors = [] | ||||||
|  |       self.result = null | ||||||
|  |       axios.post('instance/admin/settings/bulk/', self.values).then((response) => { | ||||||
|  |         self.result = true | ||||||
|  |         self.isLoading = false | ||||||
|  |         self.$store.dispatch('instance/fetchSettings') | ||||||
|  |       }, error => { | ||||||
|  |         self.isLoading = false | ||||||
|  |         self.errors = error.backendErrors | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     settings () { | ||||||
|  |       let byIdentifier = {} | ||||||
|  |       this.settingsData.forEach(e => { | ||||||
|  |         byIdentifier[e.identifier] = e | ||||||
|  |       }) | ||||||
|  |       return this.group.settings.map(e => { | ||||||
|  |         return byIdentifier[e] | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <!-- Add "scoped" attribute to limit CSS to this component only --> | ||||||
|  | <style scoped> | ||||||
|  | 
 | ||||||
|  | .ui.checkbox p { | ||||||
|  |   margin-top: 1rem; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -35,8 +35,26 @@ Vue.use(VueMasonryPlugin) | ||||||
| Vue.use(VueLazyload) | Vue.use(VueLazyload) | ||||||
| Vue.config.productionTip = false | Vue.config.productionTip = false | ||||||
| Vue.directive('title', { | Vue.directive('title', { | ||||||
|   inserted: (el, binding) => { document.title = binding.value + ' - Funkwhale' }, |   inserted: (el, binding) => { | ||||||
|   updated: (el, binding) => { document.title = binding.value + ' - Funkwhale' } |     let parts = [] | ||||||
|  |     let instanceName = store.state.instance.settings.instance.name.value | ||||||
|  |     if (instanceName.length === 0) { | ||||||
|  |       instanceName = 'Funkwhale' | ||||||
|  |     } | ||||||
|  |     parts.unshift(instanceName) | ||||||
|  |     parts.unshift(binding.value) | ||||||
|  |     document.title = parts.join(' - ') | ||||||
|  |   }, | ||||||
|  |   updated: (el, binding) => { | ||||||
|  |     let parts = [] | ||||||
|  |     let instanceName = store.state.instance.settings.instance.name.value | ||||||
|  |     if (instanceName.length === 0) { | ||||||
|  |       instanceName = 'Funkwhale' | ||||||
|  |     } | ||||||
|  |     parts.unshift(instanceName) | ||||||
|  |     parts.unshift(binding.value) | ||||||
|  |     document.title = parts.join(' - ') | ||||||
|  |   } | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| axios.defaults.baseURL = config.API_URL | axios.defaults.baseURL = config.API_URL | ||||||
|  |  | ||||||
|  | @ -28,6 +28,7 @@ import RequestsList from '@/components/requests/RequestsList' | ||||||
| import PlaylistDetail from '@/views/playlists/Detail' | import PlaylistDetail from '@/views/playlists/Detail' | ||||||
| import PlaylistList from '@/views/playlists/List' | import PlaylistList from '@/views/playlists/List' | ||||||
| import Favorites from '@/components/favorites/List' | import Favorites from '@/components/favorites/List' | ||||||
|  | import AdminSettings from '@/views/admin/Settings' | ||||||
| import FederationBase from '@/views/federation/Base' | import FederationBase from '@/views/federation/Base' | ||||||
| import FederationScan from '@/views/federation/Scan' | import FederationScan from '@/views/federation/Scan' | ||||||
| import FederationLibraryDetail from '@/views/federation/LibraryDetail' | import FederationLibraryDetail from '@/views/federation/LibraryDetail' | ||||||
|  | @ -117,6 +118,11 @@ export default new Router({ | ||||||
|         defaultPaginateBy: route.query.paginateBy |         defaultPaginateBy: route.query.paginateBy | ||||||
|       }) |       }) | ||||||
|     }, |     }, | ||||||
|  |     { | ||||||
|  |       path: '/manage/settings', | ||||||
|  |       name: 'manage.settings', | ||||||
|  |       component: AdminSettings | ||||||
|  |     }, | ||||||
|     { |     { | ||||||
|       path: '/manage/federation', |       path: '/manage/federation', | ||||||
|       component: FederationBase, |       component: FederationBase, | ||||||
|  |  | ||||||
|  | @ -0,0 +1,155 @@ | ||||||
|  | <template> | ||||||
|  |   <div class="main pusher"  v-title="$t('Instance settings')"> | ||||||
|  |     <div class="ui vertical stripe segment"> | ||||||
|  |       <div class="ui text container"> | ||||||
|  |         <div :class="['ui', {'loading': isLoading}, 'form']"></div> | ||||||
|  |         <div id="settings-grid" v-if="settingsData" class="ui grid"> | ||||||
|  |           <div class="twelve wide stretched column"> | ||||||
|  |             <settings-group | ||||||
|  |               :settings-data="settingsData" | ||||||
|  |               :group="group" | ||||||
|  |               :key="group.title" | ||||||
|  |               v-for="group in groups" /> | ||||||
|  |           </div> | ||||||
|  |           <div class="four wide column"> | ||||||
|  |             <div class="ui sticky vertical secondary menu"> | ||||||
|  |               <div class="header item">{{ $t('Sections') }}</div> | ||||||
|  |               <a :class="['menu', {active: group.id === current}, 'item']" | ||||||
|  |                 @click.prevent="scrollTo(group.id)" | ||||||
|  |                 :href="'#' + group.id" | ||||||
|  |                 v-for="group in groups">{{ group.label }}</a> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import axios from 'axios' | ||||||
|  | import $ from 'jquery' | ||||||
|  | 
 | ||||||
|  | import SettingsGroup from '@/components/admin/SettingsGroup' | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   components: { | ||||||
|  |     SettingsGroup | ||||||
|  |   }, | ||||||
|  |   data () { | ||||||
|  |     return { | ||||||
|  |       isLoading: false, | ||||||
|  |       settingsData: null, | ||||||
|  |       current: null | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   created () { | ||||||
|  |     let self = this | ||||||
|  |     this.fetchSettings().then(r => { | ||||||
|  |       self.$nextTick(() => { | ||||||
|  |         if (self.$store.state.route.hash) { | ||||||
|  |           self.scrollTo(self.$store.state.route.hash.substr(1)) | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     scrollTo (id) { | ||||||
|  |       console.log(id, 'hello') | ||||||
|  |       this.current = id | ||||||
|  |       document.getElementById(id).scrollIntoView() | ||||||
|  |     }, | ||||||
|  |     fetchSettings () { | ||||||
|  |       let self = this | ||||||
|  |       self.isLoading = true | ||||||
|  |       return axios.get('instance/admin/settings/').then((response) => { | ||||||
|  |         self.settingsData = response.data | ||||||
|  |         self.isLoading = false | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     groups () { | ||||||
|  |       return [ | ||||||
|  |         { | ||||||
|  |           label: this.$t('Instance information'), | ||||||
|  |           id: 'instance', | ||||||
|  |           settings: [ | ||||||
|  |             'instance__name', | ||||||
|  |             'instance__short_description', | ||||||
|  |             'instance__long_description' | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           label: this.$t('Users'), | ||||||
|  |           id: 'users', | ||||||
|  |           settings: [ | ||||||
|  |             'users__registration_enabled', | ||||||
|  |             'common__api_authentication_required' | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           label: this.$t('Imports'), | ||||||
|  |           id: 'imports', | ||||||
|  |           settings: [ | ||||||
|  |             'providers_youtube__api_key', | ||||||
|  |             'providers_acoustid__api_key' | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           label: this.$t('Playlists'), | ||||||
|  |           id: 'playlists', | ||||||
|  |           settings: [ | ||||||
|  |             'playlists__max_tracks' | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           label: this.$t('Federation'), | ||||||
|  |           id: 'federation', | ||||||
|  |           settings: [ | ||||||
|  |             'federation__enabled', | ||||||
|  |             'federation__music_needs_approval', | ||||||
|  |             'federation__collection_page_size', | ||||||
|  |             'federation__music_cache_duration', | ||||||
|  |             'federation__actor_fetch_delay' | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           label: this.$t('Subsonic'), | ||||||
|  |           id: 'subsonic', | ||||||
|  |           settings: [ | ||||||
|  |             'subsonic__enabled' | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           label: this.$t('Statistics'), | ||||||
|  |           id: 'statistics', | ||||||
|  |           settings: [ | ||||||
|  |             'instance__nodeinfo_enabled', | ||||||
|  |             'instance__nodeinfo_stats_enabled', | ||||||
|  |             'instance__nodeinfo_private' | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           label: this.$t('Error reporting'), | ||||||
|  |           id: 'reporting', | ||||||
|  |           settings: [ | ||||||
|  |             'raven__front_enabled', | ||||||
|  |             'raven__front_dsn' | ||||||
|  | 
 | ||||||
|  |           ] | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     settingsData () { | ||||||
|  |       let self = this | ||||||
|  |       this.$nextTick(() => { | ||||||
|  |         $(self.$el).find('.sticky').sticky({context: '#settings-grid'}) | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | @ -76,7 +76,6 @@ export default { | ||||||
|     Pagination |     Pagination | ||||||
|   }, |   }, | ||||||
|   data () { |   data () { | ||||||
|     console.log('YOLO', this.$t) |  | ||||||
|     let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') |     let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') | ||||||
|     return { |     return { | ||||||
|       isLoading: true, |       isLoading: true, | ||||||
|  |  | ||||||
		Ładowanie…
	
		Reference in New Issue
	
	 Eliot Berriot
						Eliot Berriot