profile: add a new "user profile" cell (#25633)
This commit is contained in:
parent
80fcdb2f67
commit
cb4ccd0579
|
@ -1043,6 +1043,7 @@ class JsonCellBase(CellBase):
|
||||||
# },
|
# },
|
||||||
# ...
|
# ...
|
||||||
# ]
|
# ]
|
||||||
|
first_data_key = 'json'
|
||||||
|
|
||||||
_json_content = None
|
_json_content = None
|
||||||
|
|
||||||
|
@ -1063,7 +1064,9 @@ class JsonCellBase(CellBase):
|
||||||
context[varname] = context['request'].GET[varname]
|
context[varname] = context['request'].GET[varname]
|
||||||
self._json_content = None
|
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}]
|
'log_errors': self.log_errors, 'timeout': self.timeout}]
|
||||||
data_urls.extend(self.additional_data or [])
|
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
|
# keep cache of first response as it may be used to find the
|
||||||
# appropriate template.
|
# appropriate template.
|
||||||
self._json_content = extra_context['json']
|
self._json_content = extra_context[self.first_data_key]
|
||||||
|
|
||||||
return extra_context
|
return extra_context
|
||||||
|
|
||||||
|
|
|
@ -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'
|
|
@ -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',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -14,10 +14,47 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# 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.conf import settings
|
||||||
from django.db import models
|
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):
|
class Profile(models.Model):
|
||||||
user = models.OneToOneField(settings.AUTH_USER_MODEL)
|
user = models.OneToOneField(settings.AUTH_USER_MODEL)
|
||||||
initial_login_view_timestamp = models.DateTimeField(null=True)
|
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
|
||||||
|
|
|
@ -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 %}
|
|
@ -19,8 +19,10 @@ from __future__ import absolute_import
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
|
from django.conf import settings
|
||||||
from django.core import signing
|
from django.core import signing
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.template import VariableDoesNotExist
|
||||||
from django.template.base import TOKEN_BLOCK, TOKEN_VAR
|
from django.template.base import TOKEN_BLOCK, TOKEN_VAR
|
||||||
from django.template.defaultfilters import stringfilter
|
from django.template.defaultfilters import stringfilter
|
||||||
from django.utils import dateparse
|
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.utils import NothingInCacheException, flatten_context
|
||||||
from combo.apps.dashboard.models import DashboardCell, Tile
|
from combo.apps.dashboard.models import DashboardCell, Tile
|
||||||
|
|
||||||
|
if 'mellon' in settings.INSTALLED_APPS:
|
||||||
|
from mellon.models import UserSAMLIdentifier
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
def skeleton_text(context, placeholder_name, content=''):
|
def skeleton_text(context, placeholder_name, content=''):
|
||||||
|
@ -229,3 +234,12 @@ def as_list(obj):
|
||||||
@register.filter
|
@register.filter
|
||||||
def signed(obj):
|
def signed(obj):
|
||||||
return signing.dumps(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')
|
||||||
|
|
|
@ -184,6 +184,13 @@ div.cell {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.profile {
|
||||||
|
span.value {
|
||||||
|
display: block;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1586px) {
|
@media screen and (min-width: 1586px) {
|
||||||
div#page-content div.cubesbarchart {
|
div#page-content div.cubesbarchart {
|
||||||
width: 49.5%;
|
width: 49.5%;
|
||||||
|
|
|
@ -62,3 +62,20 @@ FAMILY_SERVICE = {'root': '/'}
|
||||||
|
|
||||||
BOOKING_CALENDAR_CELL_ENABLED = True
|
BOOKING_CALENDAR_CELL_ENABLED = True
|
||||||
NEWSLETTERS_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,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue