diff --git a/api/funkwhale_api/users/migrations/0018_auto_20200705_0829.py b/api/funkwhale_api/users/migrations/0018_auto_20200705_0829.py new file mode 100644 index 000000000..d07f9aa5b --- /dev/null +++ b/api/funkwhale_api/users/migrations/0018_auto_20200705_0829.py @@ -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), + ), + ] diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py index c6da1a115..dfb8a3f55 100644 --- a/api/funkwhale_api/users/models.py +++ b/api/funkwhale_api/users/models.py @@ -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"]: diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py index 8646d3b4a..470972135 100644 --- a/api/funkwhale_api/users/serializers.py +++ b/api/funkwhale_api/users/serializers.py @@ -232,6 +232,7 @@ class MeSerializer(UserReadSerializer): "funkwhale_support_message_display_date", "summary", "tokens", + "settings", ] def get_quota_status(self, o): diff --git a/api/funkwhale_api/users/views.py b/api/funkwhale_api/users/views.py index 644e48cc2..5a8b2b07f 100644 --- a/api/funkwhale_api/users/views.py +++ b/api/funkwhale_api/users/views.py @@ -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", diff --git a/api/tests/users/test_models.py b/api/tests/users/test_models.py index c2994640e..497eedcec 100644 --- a/api/tests/users/test_models.py +++ b/api/tests/users/test_models.py @@ -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", + } diff --git a/api/tests/users/test_serializers.py b/api/tests/users/test_serializers.py index 3966d3944..2cb99a36d 100644 --- a/api/tests/users/test_serializers.py +++ b/api/tests/users/test_serializers.py @@ -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"} diff --git a/api/tests/users/test_views.py b/api/tests/users/test_views.py index 12e331798..691ed5cfe 100644 --- a/api/tests/users/test_views.py +++ b/api/tests/users/test_views.py @@ -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"} diff --git a/changes/changelog.d/996.feature b/changes/changelog.d/996.feature new file mode 100644 index 000000000..6c57c3c60 --- /dev/null +++ b/changes/changelog.d/996.feature @@ -0,0 +1 @@ +Persist theme and language settings accross sessions (#996) \ No newline at end of file diff --git a/front/src/components/Footer.vue b/front/src/components/Footer.vue index 776da6230..efa476c4c 100644 --- a/front/src/components/Footer.vue +++ b/front/src/components/Footer.vue @@ -23,7 +23,7 @@