Skip to content

Commit

Permalink
Merge pull request #3310 from unicef/30107-preferences-json-update-pr…
Browse files Browse the repository at this point in the history
…ofile

[ch30107] Add User.preferences field and PATCH support
  • Loading branch information
robertavram authored Jun 17, 2022
2 parents 1360dad + 766a395 commit f3d9e9a
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 4 deletions.
17 changes: 17 additions & 0 deletions src/etools/applications/core/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from django.http.response import HttpResponseRedirect
from django.template.response import SimpleTemplateResponse
from django.urls import reverse
from django.utils import translation
from django.utils.deprecation import MiddlewareMixin
from django.utils.translation.trans_real import get_languages

from django_tenants.middleware import TenantMainMiddleware
from django_tenants.utils import get_public_schema_name
Expand Down Expand Up @@ -101,3 +103,18 @@ def process_request(self, request):
# Do we have a public-specific urlconf?
if hasattr(settings, 'PUBLIC_SCHEMA_URLCONF') and request.tenant.schema_name == get_public_schema_name():
request.urlconf = settings.PUBLIC_SCHEMA_URLCONF


class EToolsLocaleMiddleware(MiddlewareMixin):
"""
Activates translations for the language persisted in user preferences.
"""

def process_request(self, request):
if request.user.is_anonymous:
return
preferences = request.user.preferences
if preferences and 'language' in preferences:
language_code = preferences['language']
if language_code in get_languages() or language_code == settings.LANGUAGE_CODE:
translation.activate(language_code)
35 changes: 34 additions & 1 deletion src/etools/applications/core/tests/test_middleware.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from unittest import skip
from unittest.mock import patch

from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.test import override_settings, RequestFactory, TestCase
from django.urls import reverse

from etools.applications.core.middleware import ANONYMOUS_ALLOWED_URL_FRAGMENTS, EToolsTenantMiddleware
from etools.applications.core.middleware import (
ANONYMOUS_ALLOWED_URL_FRAGMENTS,
EToolsLocaleMiddleware,
EToolsTenantMiddleware,
)
from etools.applications.users.tests.factories import CountryFactory, ProfileLightFactory, UserFactory


Expand Down Expand Up @@ -89,3 +94,31 @@ def test_public_schema_urlconf(self):
self.request.user = superuser
EToolsTenantMiddleware().process_request(self.request)
self.assertEqual(self.request.urlconf, 'foo')


@override_settings(LANGUAGE_CODE="fr")
class EToolsLocaleMiddlewareTest(TestCase):
request_factory = RequestFactory()

@classmethod
def setUpTestData(cls):
cls.user = ProfileLightFactory().user
cls.request = cls.request_factory.get('/')
cls.request.user = cls.user

def test_translation_activated(self):
self.assertEqual(
self.user.preferences,
{"language": "fr"}
)
with patch('etools.applications.core.middleware.translation.activate') as mock_method:
EToolsLocaleMiddleware().process_request(self.request)
mock_method.assert_called_once_with("fr")

def test_translation_not_activated(self):
self.user.preferences = {"language": "nonsense"}
self.user.save(update_fields=['preferences'])

with patch('etools.applications.core.middleware.translation.activate') as mock_method:
EToolsLocaleMiddleware().process_request(self.request)
mock_method.not_called()
9 changes: 9 additions & 0 deletions src/etools/applications/users/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.http.response import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext_lazy as _

from admin_extra_urls.decorators import button
from admin_extra_urls.mixins import ExtraUrlMixin
Expand Down Expand Up @@ -176,6 +177,14 @@ def save_model(self, request, obj, form, change):


class UserAdminPlus(ExtraUrlMixin, UserAdmin):
fieldsets = UserAdmin.fieldsets + (
(_('User Preferences'), {
'fields':
(
'preferences',
)
}),
)
inlines = [ProfileInline]
readonly_fields = ('date_joined',)

Expand Down
19 changes: 19 additions & 0 deletions src/etools/applications/users/migrations/0018_user_preferences.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 3.2.6 on 2022-05-30 09:01

from django.db import migrations, models
import etools.applications.users.models


class Migration(migrations.Migration):

dependencies = [
('users', '0017_auto_20220408_1558'),
]

operations = [
migrations.AddField(
model_name='user',
name='preferences',
field=models.JSONField(default=etools.applications.users.models.preferences_default_dict),
),
]
6 changes: 6 additions & 0 deletions src/etools/applications/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
logger = logging.getLogger(__name__)


def preferences_default_dict():
return {'language': settings.LANGUAGE_CODE}


class User(TimeStampedModel, AbstractBaseUser, PermissionsMixin):
USERNAME_FIELD = "username"
REQUIRED_FIELDS = ['email']
Expand All @@ -39,6 +43,8 @@ class User(TimeStampedModel, AbstractBaseUser, PermissionsMixin):
is_staff = models.BooleanField(_('staff'), default=False)
is_superuser = models.BooleanField(_('superuser'), default=False)

preferences = models.JSONField(default=preferences_default_dict)

objects = UserManager()

class Meta:
Expand Down
15 changes: 15 additions & 0 deletions src/etools/applications/users/serializers_v3.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.conf.global_settings import LANGUAGES
from django.contrib.auth import get_user_model
from django.db import connection

Expand Down Expand Up @@ -85,8 +86,13 @@ class Meta:
)


class UserPreferencesSerializer(serializers.Serializer):
language = serializers.ChoiceField(choices=dict(LANGUAGES))


class ProfileRetrieveUpdateSerializer(serializers.ModelSerializer):
countries_available = SimpleCountrySerializer(many=True, read_only=True)

supervisor = serializers.CharField(read_only=True)
groups = GroupSerializer(source="user.groups", read_only=True, many=True)
supervisees = serializers.PrimaryKeyRelatedField(source='user.supervisee', many=True, read_only=True)
Expand All @@ -104,6 +110,8 @@ class ProfileRetrieveUpdateSerializer(serializers.ModelSerializer):
show_ap = serializers.SerializerMethodField()
is_unicef_user = serializers.SerializerMethodField()

preferences = UserPreferencesSerializer(source="user.preferences", allow_null=False)

class Meta:
model = UserProfile
exclude = ('id',)
Expand All @@ -120,6 +128,13 @@ def get_show_ap(self, obj):
def get_is_unicef_user(self, obj):
return obj.user.is_unicef_user()

def update(self, instance, validated_data):
user = validated_data.pop('user', None)
if user and user.get('preferences'):
instance.user.preferences = user.get('preferences')
instance.user.save(update_fields=['preferences'])
return super().update(instance, validated_data)


class SimpleUserSerializer(serializers.ModelSerializer):
country = serializers.CharField(source='profile.country', read_only=True)
Expand Down
75 changes: 75 additions & 0 deletions src/etools/applications/users/tests/test_views_v3.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json

from django.conf import settings
from django.contrib.auth import get_user_model
from django.urls import reverse

Expand Down Expand Up @@ -257,6 +258,80 @@ def test_patch(self):
self.assertEqual(response.data["oic"], self.unicef_superuser.id)
self.assertEqual(response.data["is_superuser"], "False")

def test_patch_preferences(self):
self.assertEqual(
self.unicef_staff.preferences,
{"language": settings.LANGUAGE_CODE}
)
data = {
"preferences": {
"language": "fr"
}
}
response = self.forced_auth_req(
'patch',
self.url,
user=self.unicef_staff,
data=data
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["preferences"], self.unicef_staff.preferences)
self.assertEqual(self.unicef_staff.preferences, data['preferences'])

response = self.forced_auth_req(
'get',
self.url,
user=self.unicef_staff,
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["preferences"], self.unicef_staff.preferences)

def test_patch_preferences_unregistered_language(self):
self.assertEqual(
self.unicef_staff.preferences,
{"language": settings.LANGUAGE_CODE}
)
data = {
"preferences": {
"language": "nonsense"
}
}
response = self.forced_auth_req(
'patch',
self.url,
user=self.unicef_staff,
data=data
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertDictEqual(
response.data,
{
'preferences': {'language': ['"nonsense" is not a valid choice.']}
}
)

def test_patch_nonexistent_preference(self):
self.assertEqual(
self.unicef_staff.preferences,
{"language": settings.LANGUAGE_CODE}
)
data = {
"preferences": {
"nonexistent": "fr"
}
}
response = self.forced_auth_req(
'patch',
self.url,
user=self.unicef_staff,
data=data
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
self.unicef_staff.preferences,
{"language": settings.LANGUAGE_CODE}
)


class TestExternalUserAPIView(BaseTenantTestCase):
def setUp(self):
Expand Down
6 changes: 3 additions & 3 deletions src/etools/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ def get_from_secrets_or_env(var_name, default=None):
# DJANGO: GLOBALIZATION (I18N/L10N)
LANGUAGE_CODE = 'en-us'
LANGUAGES = [
('en', _('English US')),
('fr', _('French')),
('en', _('English')),
('fr', _('Français')),
]

TIME_ZONE = 'UTC'
Expand All @@ -126,14 +126,14 @@ def get_from_secrets_or_env(var_name, default=None):
'django.contrib.sessions.middleware.SessionMiddleware',
'etools.applications.core.auth.CustomSocialAuthExceptionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'corsheaders.middleware.CorsMiddleware',
'etools.applications.core.middleware.EToolsTenantMiddleware',
'waffle.middleware.WaffleMiddleware', # needs request.tenant from EToolsTenantMiddleware
'etools.applications.core.middleware.EToolsLocaleMiddleware',
)
WSGI_APPLICATION = 'etools.config.wsgi.application'

Expand Down

0 comments on commit f3d9e9a

Please sign in to comment.