profile: add a new "user profile" cell (#25633)
gitea-wip/combo/pipeline/head Build queued... Details
gitea/combo/pipeline/head Build started... Details

This commit is contained in:
Frédéric Péters 2018-08-10 17:11:26 +02:00 committed by Thomas NOEL
parent 80fcdb2f67
commit cb4ccd0579
9 changed files with 206 additions and 2 deletions

View File

@ -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

View File

@ -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'

View File

@ -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',
},
),
]

View File

@ -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

View File

@ -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 %}

View File

@ -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')

View File

@ -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%;

View File

@ -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,
}
]
}

51
tests/test_profile.py Normal file
View File

@ -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