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:
commit
fd25d28a8d
|
@ -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.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"]:
|
||||
|
|
|
@ -232,6 +232,7 @@ class MeSerializer(UserReadSerializer):
|
|||
"funkwhale_support_message_display_date",
|
||||
"summary",
|
||||
"tokens",
|
||||
"settings",
|
||||
]
|
||||
|
||||
def get_quota_status(self, o):
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Persist theme and language settings accross sessions (#996)
|
|
@ -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>
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue