Merge branch '996-users-settings' into 'develop'

Fix #996: Persist theme and language settings accross sessions

Closes #996

See merge request funkwhale/funkwhale!1158
This commit is contained in:
Agate 2020-07-05 11:28:19 +02:00
commit fd25d28a8d
11 changed files with 108 additions and 3 deletions

View File

@ -0,0 +1,26 @@
# Generated by Django 3.0.8 on 2020-07-05 08:29
import django.contrib.postgres.fields.jsonb
from django.db import migrations
import funkwhale_api.users.models
class Migration(migrations.Migration):
dependencies = [
('users', '0017_actor_avatar'),
]
operations = [
migrations.AlterModelManagers(
name='user',
managers=[
('objects', funkwhale_api.users.models.UserManager()),
],
),
migrations.AddField(
model_name='user',
name='settings',
field=django.contrib.postgres.fields.jsonb.JSONField(default=None, null=True, blank=True, max_length=50000),
),
]

View File

@ -10,7 +10,8 @@ import uuid
from django.conf import settings
from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager
from django.db import models
from django.contrib.postgres.fields import JSONField
from django.db import models, transaction
from django.dispatch import receiver
from django.urls import reverse
from django.utils import timezone
@ -191,6 +192,7 @@ class User(AbstractUser):
null=True,
blank=True,
)
settings = JSONField(default=None, null=True, blank=True, max_length=50000)
objects = UserManager()
@ -213,6 +215,16 @@ class User(AbstractUser):
def all_permissions(self):
return self.get_permissions()
@transaction.atomic
def set_settings(self, **settings):
u = self.__class__.objects.select_for_update().get(pk=self.pk)
if not u.settings:
u.settings = {}
for key, value in settings.items():
u.settings[key] = value
u.save(update_fields=["settings"])
self.settings = u.settings
def has_permissions(self, *perms, **kwargs):
operator = kwargs.pop("operator", "and")
if operator not in ["and", "or"]:

View File

@ -232,6 +232,7 @@ class MeSerializer(UserReadSerializer):
"funkwhale_support_message_display_date",
"summary",
"tokens",
"settings",
]
def get_quota_status(self, o):

View File

@ -80,6 +80,13 @@ class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
serializer = serializers.MeSerializer(request.user)
return Response(serializer.data)
@action(methods=["post"], detail=False, url_name="settings", url_path="settings")
def set_settings(self, request, *args, **kwargs):
"""Return information about the current user or delete it"""
new_settings = request.data
request.user.set_settings(**new_settings)
return Response(request.user.settings)
@action(
methods=["get", "post", "delete"],
required_scope="security",

View File

@ -259,3 +259,16 @@ def test_get_by_natural_key_annotates_primary_email_verified_false(factories):
user = models.User.objects.get_by_natural_key(user.username)
assert user.has_verified_primary_email is False
def test_set_settings(factories):
user = factories["users.User"]()
assert user.settings is None
user.set_settings(foo="bar", hello="world")
user.refresh_from_db()
assert user.settings == {
"foo": "bar",
"hello": "world",
}

View File

@ -57,3 +57,10 @@ def test_me_serializer_includes_tokens(factories, mocker):
generate_scoped_token.assert_called_once_with(
user_id=user.pk, user_secret=user.secret_key, scopes=["read:libraries"]
)
def test_me_serializer_includes_settings(factories):
user = factories["users.User"](settings={"foo": "bar"})
serializer = serializers.MeSerializer(user)
assert serializer.data["settings"] == {"foo": "bar"}

View File

@ -556,3 +556,15 @@ def test_logout(api_client, factories, mocker):
response = api_client.post(url)
assert response.status_code == 200
assert auth_logout.call_count == 1
def test_update_settings(logged_in_api_client, factories):
logged_in_api_client.user.set_settings(foo="bar")
url = reverse("api:v1:users:users-settings")
payload = {"theme": "dark"}
response = logged_in_api_client.post(url, payload, format="json")
assert response.status_code == 200
logged_in_api_client.user.refresh_from_db()
assert logged_in_api_client.user.settings == {"foo": "bar", "theme": "dark"}

View File

@ -0,0 +1 @@
Persist theme and language settings accross sessions (#996)

View File

@ -23,7 +23,7 @@
<div class="ui form">
<div class="ui field">
<label><translate translate-context="Footer/Settings/Dropdown.Label/Short, Verb">Change language</translate></label>
<select class="ui dropdown" :value="$language.current" @change="$store.commit('ui/currentLanguage', $event.target.value)">
<select class="ui dropdown" :value="$language.current" @change="$store.dispatch('ui/currentLanguage', $event.target.value)">
<option v-for="(language, key) in $language.available" :key="key" :value="key">{{ language }}</option>
</select>
</div>
@ -39,7 +39,7 @@
<div class="ui form">
<div class="ui field">
<label><translate translate-context="Footer/Settings/Dropdown.Label/Short, Verb">Change theme</translate></label>
<select class="ui dropdown" :value="$store.state.ui.theme" @change="$store.commit('ui/theme', $event.target.value)">
<select class="ui dropdown" :value="$store.state.ui.theme" @change="$store.dispatch('ui/theme', $event.target.value)">
<option v-for="theme in themes" :key="theme.key" :value="theme.key">{{ theme.name }}</option>
</select>
</div>

View File

@ -184,9 +184,11 @@ export default {
return new Promise((resolve, reject) => {
axios.get('users/users/me/').then((response) => {
logger.default.info('Successfully fetched user profile')
dispatch('ui/initSettings', response.data.settings, { root: true })
dispatch('updateProfile', response.data).then(() => {
resolve(response.data)
})
dispatch('ui/fetchUnreadNotifications', null, { root: true })
if (response.data.permissions.library) {
dispatch('ui/fetchPendingReviewEdits', null, { root: true })

View File

@ -278,6 +278,30 @@ export default {
commit('notifications', {type: 'pendingReviewRequests', count: response.data.count})
})
},
async currentLanguage ({state, commit, rootState}, value) {
commit("currentLanguage", value)
if (rootState.auth.authenticated) {
await axios.post("users/settings", {"language": value})
}
},
async theme ({state, commit, rootState}, value) {
commit("theme", value)
if (rootState.auth.authenticated) {
await axios.post("users/settings", {"theme": value})
}
},
async initSettings ({commit}, settings) {
settings = settings || {}
if (settings.language) {
commit("currentLanguage", settings.language)
}
if (settings.theme) {
commit("theme", settings.theme)
}
},
websocketEvent ({state}, event) {
let handlers = state.websocketEventsHandlers[event.type]
console.log('Dispatching websocket event', event, handlers)