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

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

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

View File

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

View File

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

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