Fix #996: Persist theme and language settings accross sessions
This commit is contained in:
parent
f6a81a9ecf
commit
84d49754a7
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -10,7 +10,8 @@ import uuid
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager
|
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.dispatch import receiver
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -191,6 +192,7 @@ class User(AbstractUser):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
settings = JSONField(default=None, null=True, blank=True, max_length=50000)
|
||||||
|
|
||||||
objects = UserManager()
|
objects = UserManager()
|
||||||
|
|
||||||
|
@ -213,6 +215,16 @@ class User(AbstractUser):
|
||||||
def all_permissions(self):
|
def all_permissions(self):
|
||||||
return self.get_permissions()
|
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):
|
def has_permissions(self, *perms, **kwargs):
|
||||||
operator = kwargs.pop("operator", "and")
|
operator = kwargs.pop("operator", "and")
|
||||||
if operator not in ["and", "or"]:
|
if operator not in ["and", "or"]:
|
||||||
|
|
|
@ -232,6 +232,7 @@ class MeSerializer(UserReadSerializer):
|
||||||
"funkwhale_support_message_display_date",
|
"funkwhale_support_message_display_date",
|
||||||
"summary",
|
"summary",
|
||||||
"tokens",
|
"tokens",
|
||||||
|
"settings",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_quota_status(self, o):
|
def get_quota_status(self, o):
|
||||||
|
|
|
@ -80,6 +80,13 @@ class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
|
||||||
serializer = serializers.MeSerializer(request.user)
|
serializer = serializers.MeSerializer(request.user)
|
||||||
return Response(serializer.data)
|
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(
|
@action(
|
||||||
methods=["get", "post", "delete"],
|
methods=["get", "post", "delete"],
|
||||||
required_scope="security",
|
required_scope="security",
|
||||||
|
|
|
@ -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)
|
user = models.User.objects.get_by_natural_key(user.username)
|
||||||
|
|
||||||
assert user.has_verified_primary_email is False
|
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",
|
||||||
|
}
|
||||||
|
|
|
@ -57,3 +57,10 @@ def test_me_serializer_includes_tokens(factories, mocker):
|
||||||
generate_scoped_token.assert_called_once_with(
|
generate_scoped_token.assert_called_once_with(
|
||||||
user_id=user.pk, user_secret=user.secret_key, scopes=["read:libraries"]
|
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"}
|
||||||
|
|
|
@ -556,3 +556,15 @@ def test_logout(api_client, factories, mocker):
|
||||||
response = api_client.post(url)
|
response = api_client.post(url)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert auth_logout.call_count == 1
|
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"}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Persist theme and language settings accross sessions (#996)
|
|
@ -23,7 +23,7 @@
|
||||||
<div class="ui form">
|
<div class="ui form">
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
<label><translate translate-context="Footer/Settings/Dropdown.Label/Short, Verb">Change language</translate></label>
|
<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>
|
<option v-for="(language, key) in $language.available" :key="key" :value="key">{{ language }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
<div class="ui form">
|
<div class="ui form">
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
<label><translate translate-context="Footer/Settings/Dropdown.Label/Short, Verb">Change theme</translate></label>
|
<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>
|
<option v-for="theme in themes" :key="theme.key" :value="theme.key">{{ theme.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -184,9 +184,11 @@ export default {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
axios.get('users/users/me/').then((response) => {
|
axios.get('users/users/me/').then((response) => {
|
||||||
logger.default.info('Successfully fetched user profile')
|
logger.default.info('Successfully fetched user profile')
|
||||||
|
dispatch('ui/initSettings', response.data.settings, { root: true })
|
||||||
dispatch('updateProfile', response.data).then(() => {
|
dispatch('updateProfile', response.data).then(() => {
|
||||||
resolve(response.data)
|
resolve(response.data)
|
||||||
})
|
})
|
||||||
|
|
||||||
dispatch('ui/fetchUnreadNotifications', null, { root: true })
|
dispatch('ui/fetchUnreadNotifications', null, { root: true })
|
||||||
if (response.data.permissions.library) {
|
if (response.data.permissions.library) {
|
||||||
dispatch('ui/fetchPendingReviewEdits', null, { root: true })
|
dispatch('ui/fetchPendingReviewEdits', null, { root: true })
|
||||||
|
|
|
@ -278,6 +278,30 @@ export default {
|
||||||
commit('notifications', {type: 'pendingReviewRequests', count: response.data.count})
|
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) {
|
websocketEvent ({state}, event) {
|
||||||
let handlers = state.websocketEventsHandlers[event.type]
|
let handlers = state.websocketEventsHandlers[event.type]
|
||||||
console.log('Dispatching websocket event', event, handlers)
|
console.log('Dispatching websocket event', event, handlers)
|
||||||
|
|
Loading…
Reference in New Issue