summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFrédéric Péters <fpeters@entrouvert.com>2018-08-10 15:11:26 (GMT)
committerFrédéric Péters <fpeters@entrouvert.com>2018-08-30 09:01:24 (GMT)
commite3353682a5f8439a1d7f4b2e43d8c9b7fef33f22 (patch)
treee5c45e09024c424d281ec4ab2de27c717e956db6
parente40e8b83ae6fbac0ef5d57a968a51626371d3a82 (diff)
downloadcombo-wip/guichet.zip
combo-wip/guichet.tar.gz
combo-wip/guichet.tar.bz2
profile: add a new "user profile" cell (#25633)wip/guichet
-rw-r--r--combo/data/models.py7
-rw-r--r--combo/profile/__init__.py26
-rw-r--r--combo/profile/migrations/0002_profilecell.py35
-rw-r--r--combo/profile/models.py37
-rw-r--r--combo/profile/templates/combo/profile.html14
-rw-r--r--combo/public/templatetags/combo.py14
-rw-r--r--data/themes/gadjo/static/css/agent-portal.scss7
-rw-r--r--tests/settings.py17
-rw-r--r--tests/test_profile.py51
9 files changed, 206 insertions, 2 deletions
diff --git a/combo/data/models.py b/combo/data/models.py
index dbc3e19..9fba00a 100644
--- a/combo/data/models.py
+++ b/combo/data/models.py
@@ -1043,6 +1043,7 @@ class JsonCellBase(CellBase):
# },
# ...
# ]
+ first_data_key = 'json'
_json_content = None
@@ -1063,7 +1064,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 [])
@@ -1128,7 +1131,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 <http://www.gnu.org/licenses/>.
+
+
+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/migrations/0002_profilecell.py b/combo/profile/migrations/0002_profilecell.py
new file mode 100644
index 0000000..1c5d27a
--- /dev/null
+++ b/combo/profile/migrations/0002_profilecell.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.12 on 2018-08-10 13:52
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('data', '0035_page_related_cells'),
+ ('profile', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ProfileCell',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('placeholder', models.CharField(max_length=20)),
+ ('order', models.PositiveIntegerField()),
+ ('slug', models.SlugField(blank=True, verbose_name='Slug')),
+ ('extra_css_class', models.CharField(blank=True, max_length=100, verbose_name='Extra classes for CSS styling')),
+ ('public', models.BooleanField(default=True, verbose_name='Public')),
+ ('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
+ ('last_update_timestamp', models.DateTimeField(auto_now=True)),
+ ('groups', models.ManyToManyField(blank=True, to='auth.Group', verbose_name='Groups')),
+ ('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='data.Page')),
+ ],
+ options={
+ 'verbose_name': 'Profile',
+ },
+ ),
+ ]
diff --git a/combo/profile/models.py b/combo/profile/models.py
index 1e7824e..6659e33 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 <http://www.gnu.org/licenses/>.
+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..ccc482f
--- /dev/null
+++ b/combo/profile/templates/combo/profile.html
@@ -0,0 +1,14 @@
+{% load i18n %}
+{% block cell-content %}
+<div class="profile">
+<h2>{% trans "Profile" %}</h2>
+{% for key, details in profile_fields.items %}
+ {% if details.value and details.user_visible %}
+ <p><span class="label">{{ details.label }}</span> <span class="value">{{ details.value }}</span></p>
+ {% endif %}
+{% endfor %}
+{% if error == 'unknown user' %}
+<p>{% trans 'Unknown User' %}</p>
+{% endif %}
+</div>
+{% 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/data/themes/gadjo/static/css/agent-portal.scss b/data/themes/gadjo/static/css/agent-portal.scss
index bedeaf7..814aadd 100644
--- a/data/themes/gadjo/static/css/agent-portal.scss
+++ b/data/themes/gadjo/static/css/agent-portal.scss
@@ -184,6 +184,13 @@ div.cell {
}
}
+div.profile {
+ span.value {
+ display: block;
+ margin-left: 1rem;
+ }
+}
+
@media screen and (min-width: 1586px) {
div#page-content div.cubesbarchart {
width: 49.5%;
diff --git a/tests/settings.py b/tests/settings.py
index ccfe998..b7ec4d1 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -62,3 +62,20 @@ 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..57681e1
--- /dev/null
+++ b/tests/test_profile.py
@@ -0,0 +1,51 @@
+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
+
+
+@override_settings(
+ KNOWN_SERVICES={'authentic': {'idp': {'title': 'IdP', 'url': 'http://example.org/'}}})
+@mock.patch('combo.public.templatetags.combo.UserSAMLIdentifier')
+@mock.patch('combo.utils.requests.get')
+def test_profile_cell(requests_get, user_saml, app, admin_user):
+ page = Page()
+ page.save()
+
+ cell = ProfileCell(page=page, order=0)
+ cell.save()
+
+ 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