From 1282779c1dc200a99c52c24efec542d8e9a0945b Mon Sep 17 00:00:00 2001 From: Agate Berriot Date: Wed, 4 Jan 2023 12:05:56 +0100 Subject: [PATCH] user: allow customization of User.get_full_name() through templates (#72945) --- debian/debian_config_common.py | 2 + hobo/logger.py | 5 +- hobo/user_name/__init__.py | 0 hobo/user_name/apps.py | 61 ++++++++++++++++++++ tests/settings.py | 2 +- tests/test_user_name.py | 44 +++++++++++++++ tests_authentic/test_user_name.py | 92 +++++++++++++++++++++++++++++++ 7 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 hobo/user_name/__init__.py create mode 100644 hobo/user_name/apps.py create mode 100644 tests/test_user_name.py create mode 100644 tests_authentic/test_user_name.py diff --git a/debian/debian_config_common.py b/debian/debian_config_common.py index 758045b..821bed9 100644 --- a/debian/debian_config_common.py +++ b/debian/debian_config_common.py @@ -288,6 +288,8 @@ if PROJECT_NAME != 'wcs' and 'authentic2' not in INSTALLED_APPS: 'mellon.middleware.PassiveAuthenticationMiddleware', 'hobo.provisionning.middleware.ProvisionningMiddleware', ) +if PROJECT_NAME != 'wcs': + INSTALLED_APPS += ('hobo.user_name.apps.UserNameConfig',) if 'authentic2' in INSTALLED_APPS: MIDDLEWARE = MIDDLEWARE + ('hobo.agent.authentic2.middleware.ProvisionningMiddleware',) diff --git a/hobo/logger.py b/hobo/logger.py index 2c7924f..b2dc3f1 100644 --- a/hobo/logger.py +++ b/hobo/logger.py @@ -106,8 +106,9 @@ class RequestContextFilter(logging.Filter): if saml_identifier: record.user_uuid = saml_identifier.name_id record.user = record.user_uuid[:6] - if hasattr(user, 'get_full_name') and user.get_full_name(): - record.user = record.user_display_name = user.get_full_name() + if hasattr(user, 'original_get_full_name') and user.original_get_full_name(): + # record original full name, not templated version from user_name app + record.user = record.user_display_name = user.original_get_full_name() if getattr(user, 'email', None): record.user = record.user_email = user.email if getattr(user, 'username', None): diff --git a/hobo/user_name/__init__.py b/hobo/user_name/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hobo/user_name/apps.py b/hobo/user_name/apps.py new file mode 100644 index 0000000..016287e --- /dev/null +++ b/hobo/user_name/apps.py @@ -0,0 +1,61 @@ +import functools +import logging + +import django.db +import django.template.exceptions +from django.apps import AppConfig +from django.conf import settings +from django.contrib.auth import get_user_model +from django.template import engines + +logger = logging.getLogger(__name__) + + +def get_full_name(user): + from hobo.agent.common.models import UserExtraAttributes + + context = {} + context['user'] = user + template_vars = getattr(settings, 'TEMPLATE_VARS', {}) + if 'user_full_name_template' in template_vars: + try: + template = engines['django'].from_string(template_vars['user_full_name_template']) + return template.render(context) + except django.template.exceptions.TemplateSyntaxError: + logger.exception('hobo.user_name: syntax error in inline user name template var') + except UserExtraAttributes.DoesNotExist: + logger.exception('hobo.user_name: inline user name template refers to nonexistent attribute') + return user.original_get_full_name() + + +def cached_extra_attributes(user): + try: + return user.extra_attributes.data + except django.db.models.ObjectDoesNotExist: + return {} + + +class UserNameConfig(AppConfig): + name = 'hobo.user_name' + label = 'hobo_user_name' + verbose_name = 'Hobo User Name' + + def ready(self): + """ + We monkey-patch AUTH_USER_MODEL() + to ensure consistency in the rendering of user name + in the front-office, backoffice, emails, etc. + """ + logger.info('hobo.user_name: installing User.get_full_name customization…') + User = get_user_model() + + # to have a fallback when necessary if the new method crashes during render + User.original_get_full_name = User.get_full_name + # to replace the rendering everywhere in a consistent manner + User.get_full_name = get_full_name + # to avoid performance/recursion issues + User.__str__ = User.original_get_full_name + + if 'attributes' not in User.__dict__: + User.attributes = functools.cached_property(cached_extra_attributes) + User.attributes.__set_name__(User, 'attributes') diff --git a/tests/settings.py b/tests/settings.py index 9b10d98..5f3aa37 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -5,7 +5,7 @@ import hobo.test_utils LANGUAGE_CODE = 'en-us' BROKER_URL = 'memory://' -INSTALLED_APPS += ('hobo.agent.common',) +INSTALLED_APPS += ('hobo.agent.common', 'hobo.user_name.apps.UserNameConfig') ALLOWED_HOSTS.append('localhost') diff --git a/tests/test_user_name.py b/tests/test_user_name.py new file mode 100644 index 0000000..af7c409 --- /dev/null +++ b/tests/test_user_name.py @@ -0,0 +1,44 @@ +import pytest +from django.contrib.auth.models import User +from django.test import override_settings + +from hobo.agent.common.models import UserExtraAttributes +from hobo.user_name.apps import get_full_name + + +@pytest.fixture +def user(db): + u = User.objects.create( + first_name='Jane', + last_name='Doe', + ) + UserExtraAttributes.objects.create(user=u, data={'foo': 'bar'}) + return u + + +def test_user_original_get_full_name(user): + assert user.original_get_full_name() == 'Jane Doe' + + +def test_user_cached_extra_attributes(user): + assert user.attributes == {'foo': 'bar'} + + +def test_user_cached_extra_attributes_missing_fallbacks_to_empty_dict(user): + user.extra_attributes.delete() + user.refresh_from_db() + assert user.attributes == {} + + +def test_user_get_full_name_from_template(user): + with override_settings( + TEMPLATE_VARS={'user_full_name_template': '{{ user.first_name }} {{ user.attributes.foo }}'} + ): + assert get_full_name(user) == 'Jane bar' + + +def test_user_get_full_name(user): + with override_settings( + TEMPLATE_VARS={'user_full_name_template': '{{ user.first_name }} {{ user.attributes.foo }}'} + ): + assert user.get_full_name() == 'Jane bar' diff --git a/tests_authentic/test_user_name.py b/tests_authentic/test_user_name.py new file mode 100644 index 0000000..9089231 --- /dev/null +++ b/tests_authentic/test_user_name.py @@ -0,0 +1,92 @@ +from authentic2.models import Attribute +from django.contrib.auth import get_user_model +from django.test import override_settings +from tenant_schemas.utils import tenant_context + +from hobo.user_name.apps import get_full_name + +User = get_user_model() + + +def test_get_full_name_from_template_utils_from_multiple_attrs(db, tenant, settings): + with tenant_context(tenant): + user = User.objects.create( + first_name='Jane', + last_name='Doe', + ) + Attribute.objects.create( + name='foo', + label='Foo', + kind='string', + required=False, + multiple=False, + user_visible=True, + user_editable=True, + ) + Attribute.objects.create( + name='nicknames', + label='Nicknames', + kind='string', + required=False, + multiple=True, + user_visible=True, + user_editable=True, + ) + user.attributes.nicknames = ['Milly', 'Molly', 'Minnie'] + user.attributes.foo = 'bar' + user.save() + user.refresh_from_db() + + with override_settings( + TEMPLATE_VARS={'user_full_name_template': '{{ user.first_name }} {{ user.attributes.foo }}'} + ): + assert get_full_name(user) == 'Jane bar' + + with override_settings( + TEMPLATE_VARS={ + 'user_full_name_template': '{{ user.first_name }} {{ user.attributes.nicknames.0 }} {{ user.attributes.nicknames.2 }}' + } + ): + assert get_full_name(user) == 'Jane Milly Minnie' + + +def test_get_full_name_from_template_accessor_from_multiple_attrs(db, tenant, settings): + with tenant_context(tenant): + user = User.objects.create( + first_name='Jane', + last_name='Doe', + ) + Attribute.objects.create( + name='foo', + label='Foo', + kind='string', + required=False, + multiple=False, + user_visible=True, + user_editable=True, + ) + Attribute.objects.create( + name='nicknames', + label='Nicknames', + kind='string', + required=False, + multiple=True, + user_visible=True, + user_editable=True, + ) + user.attributes.nicknames = ['Milly', 'Molly', 'Minnie'] + user.attributes.foo = 'bar' + user.save() + user.refresh_from_db() + + with override_settings( + TEMPLATE_VARS={'user_full_name_template': '{{ user.first_name }} {{ user.attributes.foo }}'} + ): + assert user.get_full_name() == 'Jane bar' + + with override_settings( + TEMPLATE_VARS={ + 'user_full_name_template': '{{ user.first_name }} {{ user.attributes.nicknames.0 }} {{ user.attributes.nicknames.2 }}' + } + ): + assert user.get_full_name() == 'Jane Milly Minnie' -- 2.39.2