From 6ce87d0569b32ad61ede89358189f3a0d622245c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Fri, 10 Aug 2018 17:11:26 +0200 Subject: [PATCH] profile: add a new "user profile" cell (#25633) --- combo/data/models.py | 7 ++- combo/profile/__init__.py | 26 +++++++++++ combo/profile/models.py | 37 +++++++++++++++ combo/profile/templates/combo/profile.html | 13 ++++++ combo/public/templatetags/combo.py | 14 ++++++ tests/settings.py | 18 ++++++++ tests/test_profile.py | 52 ++++++++++++++++++++++ 7 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 combo/profile/templates/combo/profile.html create mode 100644 tests/test_profile.py diff --git a/combo/data/models.py b/combo/data/models.py index 0d570ef..a346135 100644 --- a/combo/data/models.py +++ b/combo/data/models.py @@ -1040,6 +1040,7 @@ class JsonCellBase(CellBase): # }, # ... # ] + first_data_key = 'json' _json_content = None @@ -1060,7 +1061,9 @@ class JsonCellBase(CellBase): context[varname] = context['request'].GET[varname] self._json_content = None - data_urls = [{'key': 'json', 'url': self.url, 'cache_duration': self.cache_duration, + context['concerned_user'] = self.get_concerned_user(context) + + data_urls = [{'key': self.first_data_key, 'url': self.url, 'cache_duration': self.cache_duration, 'log_errors': self.log_errors, 'timeout': self.timeout}] data_urls.extend(self.additional_data or []) @@ -1127,7 +1130,7 @@ class JsonCellBase(CellBase): # keep cache of first response as it may be used to find the # appropriate template. - self._json_content = extra_context['json'] + self._json_content = extra_context[self.first_data_key] return extra_context diff --git a/combo/profile/__init__.py b/combo/profile/__init__.py index e69de29..ac17029 100644 --- a/combo/profile/__init__.py +++ b/combo/profile/__init__.py @@ -0,0 +1,26 @@ +# combo - content management system +# Copyright (C) 2014-2018 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import django.apps +from django.utils.translation import ugettext_lazy as _ + + +class AppConfig(django.apps.AppConfig): + name = 'combo.profile' + verbose_name = _('Profile') + +default_app_config = 'combo.profile.AppConfig' diff --git a/combo/profile/models.py b/combo/profile/models.py index 1e7824e..42483cd 100644 --- a/combo/profile/models.py +++ b/combo/profile/models.py @@ -14,10 +14,47 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from collections import OrderedDict +import copy + from django.conf import settings from django.db import models +from django.utils.dateparse import parse_date +from django.utils.translation import ugettext_lazy as _ + +from combo.data.models import JsonCellBase +from combo.data.library import register_cell_class class Profile(models.Model): user = models.OneToOneField(settings.AUTH_USER_MODEL) initial_login_view_timestamp = models.DateTimeField(null=True) + + +@register_cell_class +class ProfileCell(JsonCellBase): + template_name = 'combo/profile.html' + first_data_key = 'profile' + + class Meta: + verbose_name = _('Profile') + + @property + def url(self): + idp = settings.KNOWN_SERVICES.get('authentic').values()[0] + return '{%% load combo %%}%sapi/users/{{ concerned_user|name_id }}' % idp.get('url') + + def get_cell_extra_context(self, context): + extra_context = super(ProfileCell, self).get_cell_extra_context(context) + extra_context['profile_fields'] = OrderedDict() + if extra_context.get('profile') is not None: + for attribute in settings.USER_PROFILE_CONFIG.get('fields'): + extra_context['profile_fields'][attribute['name']] = copy.copy(attribute) + value = extra_context['profile'].get(attribute['name']) + if value: + if attribute['kind'] in ('birthdate', 'date'): + value = parse_date(value) + extra_context['profile_fields'][attribute['name']]['value'] = value + else: + extra_context['error'] = 'unknown user' + return extra_context diff --git a/combo/profile/templates/combo/profile.html b/combo/profile/templates/combo/profile.html new file mode 100644 index 0000000..5b7f98a --- /dev/null +++ b/combo/profile/templates/combo/profile.html @@ -0,0 +1,13 @@ +{% load i18n %} +{% block cell-content %} +
+

{% trans "Profile" %}

+{% for key, details in profile_fields.items %} + {% if details.value and details.user_visible %} +

{{ details.label }} {{ details.value }}

+ {% endif %} +{% endfor %} +{% if error == 'unknown user' %} +

{% trans 'Unknown User' %}

+{% endif %} +{% endblock %} diff --git a/combo/public/templatetags/combo.py b/combo/public/templatetags/combo.py index 72e2ac9..5719157 100644 --- a/combo/public/templatetags/combo.py +++ b/combo/public/templatetags/combo.py @@ -19,8 +19,10 @@ from __future__ import absolute_import import datetime from django import template +from django.conf import settings from django.core import signing from django.core.exceptions import PermissionDenied +from django.template import VariableDoesNotExist from django.template.base import TOKEN_BLOCK, TOKEN_VAR from django.template.defaultfilters import stringfilter from django.utils import dateparse @@ -30,6 +32,9 @@ from combo.public.menu import get_menu_context from combo.utils import NothingInCacheException, flatten_context from combo.apps.dashboard.models import DashboardCell, Tile +if 'mellon' in settings.INSTALLED_APPS: + from mellon.models import UserSAMLIdentifier + register = template.Library() def skeleton_text(context, placeholder_name, content=''): @@ -229,3 +234,12 @@ def as_list(obj): @register.filter def signed(obj): return signing.dumps(obj) + +@register.filter +def name_id(user): + saml_id = UserSAMLIdentifier.objects.filter(user=user).last() + if saml_id: + return saml_id.name_id + # it is important to raise this so get_templated_url is aborted and no call + # is tried with a missing user argument. + raise VariableDoesNotExist('name_id') diff --git a/tests/settings.py b/tests/settings.py index ccfe998..80076eb 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -62,3 +62,21 @@ FAMILY_SERVICE = {'root': '/'} BOOKING_CALENDAR_CELL_ENABLED = True NEWSLETTERS_CELL_ENABLED = True + +USER_PROFILE_CONFIG = { + 'fields': [ + { + 'name': 'first_name', + 'kind': 'string', + 'label': 'First Name', + 'user_visible': True, + }, + { + 'name': 'birthdate', + 'kind': 'birthdate', + 'label': 'Birth Date', + 'user_visible': True, + } + ] +} + diff --git a/tests/test_profile.py b/tests/test_profile.py new file mode 100644 index 0000000..784f57a --- /dev/null +++ b/tests/test_profile.py @@ -0,0 +1,52 @@ +import datetime +import json +import mock +import pytest + +from django.test import override_settings + +from combo.data.models import Page +from combo.profile.models import ProfileCell + +pytestmark = pytest.mark.django_db + +def test_profile_cell(app, admin_user): + page = Page() + page.save() + + cell = ProfileCell(page=page, order=0) + cell.save() + + with override_settings( + KNOWN_SERVICES={'authentic': {'idp': { + 'title': 'IdP', 'url': 'http://example.org/'}}}), \ + mock.patch('combo.utils.requests.get') as requests_get, \ + mock.patch('combo.public.templatetags.combo.UserSAMLIdentifier') as user_saml: + + data = {'first_name': 'Foo', 'birthdate': '2018-08-10'} + requests_get.return_value = mock.Mock( + content=json.dumps(data), + json=lambda: data, + status_code=200) + + def filter_mock(user=None): + assert user is admin_user + return mock.Mock(last=lambda: mock.Mock(name_id='123456')) + + mocked_objects = mock.Mock() + mocked_objects.filter = mock.Mock(side_effect=filter_mock) + user_saml.objects = mocked_objects + + context = cell.get_cell_extra_context({'synchronous': True, 'selected_user': admin_user}) + assert context['profile_fields']['first_name']['value'] == 'Foo' + assert context['profile_fields']['birthdate']['value'] == datetime.date(2018, 8, 10) + assert requests_get.call_args[0][0] == 'http://example.org/api/users/123456' + + def filter_mock_missing(user=None): + return mock.Mock(last=lambda: None) + + mocked_objects.filter = mock.Mock(side_effect=filter_mock_missing) + + context = cell.get_cell_extra_context({'synchronous': True, 'selected_user': admin_user}) + assert context['error'] == 'unknown user' + assert requests_get.call_count == 1 # no new call was made -- 2.18.0