0001-profile-add-a-new-user-profile-cell-25633.patch
combo/data/models.py | ||
---|---|---|
1043 | 1043 |
# }, |
1044 | 1044 |
# ... |
1045 | 1045 |
# ] |
1046 |
first_data_key = 'json' |
|
1046 | 1047 | |
1047 | 1048 |
_json_content = None |
1048 | 1049 | |
... | ... | |
1063 | 1064 |
context[varname] = context['request'].GET[varname] |
1064 | 1065 |
self._json_content = None |
1065 | 1066 | |
1066 |
data_urls = [{'key': 'json', 'url': self.url, 'cache_duration': self.cache_duration, |
|
1067 |
context['concerned_user'] = self.get_concerned_user(context) |
|
1068 | ||
1069 |
data_urls = [{'key': self.first_data_key, 'url': self.url, 'cache_duration': self.cache_duration, |
|
1067 | 1070 |
'log_errors': self.log_errors, 'timeout': self.timeout}] |
1068 | 1071 |
data_urls.extend(self.additional_data or []) |
1069 | 1072 | |
... | ... | |
1128 | 1131 | |
1129 | 1132 |
# keep cache of first response as it may be used to find the |
1130 | 1133 |
# appropriate template. |
1131 |
self._json_content = extra_context['json']
|
|
1134 |
self._json_content = extra_context[self.first_data_key]
|
|
1132 | 1135 | |
1133 | 1136 |
return extra_context |
1134 | 1137 |
combo/profile/__init__.py | ||
---|---|---|
1 |
# combo - content management system |
|
2 |
# Copyright (C) 2014-2018 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 | ||
18 |
import django.apps |
|
19 |
from django.utils.translation import ugettext_lazy as _ |
|
20 | ||
21 | ||
22 |
class AppConfig(django.apps.AppConfig): |
|
23 |
name = 'combo.profile' |
|
24 |
verbose_name = _('Profile') |
|
25 | ||
26 |
default_app_config = 'combo.profile.AppConfig' |
combo/profile/migrations/0002_profilecell.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.12 on 2018-08-10 13:52 |
|
3 |
from __future__ import unicode_literals |
|
4 | ||
5 |
from django.db import migrations, models |
|
6 |
import django.db.models.deletion |
|
7 | ||
8 | ||
9 |
class Migration(migrations.Migration): |
|
10 | ||
11 |
dependencies = [ |
|
12 |
('data', '0035_page_related_cells'), |
|
13 |
('profile', '0001_initial'), |
|
14 |
] |
|
15 | ||
16 |
operations = [ |
|
17 |
migrations.CreateModel( |
|
18 |
name='ProfileCell', |
|
19 |
fields=[ |
|
20 |
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|
21 |
('placeholder', models.CharField(max_length=20)), |
|
22 |
('order', models.PositiveIntegerField()), |
|
23 |
('slug', models.SlugField(blank=True, verbose_name='Slug')), |
|
24 |
('extra_css_class', models.CharField(blank=True, max_length=100, verbose_name='Extra classes for CSS styling')), |
|
25 |
('public', models.BooleanField(default=True, verbose_name='Public')), |
|
26 |
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')), |
|
27 |
('last_update_timestamp', models.DateTimeField(auto_now=True)), |
|
28 |
('groups', models.ManyToManyField(blank=True, to='auth.Group', verbose_name='Groups')), |
|
29 |
('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='data.Page')), |
|
30 |
], |
|
31 |
options={ |
|
32 |
'verbose_name': 'Profile', |
|
33 |
}, |
|
34 |
), |
|
35 |
] |
combo/profile/models.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
from collections import OrderedDict |
|
18 |
import copy |
|
19 | ||
17 | 20 |
from django.conf import settings |
18 | 21 |
from django.db import models |
22 |
from django.utils.dateparse import parse_date |
|
23 |
from django.utils.translation import ugettext_lazy as _ |
|
24 | ||
25 |
from combo.data.models import JsonCellBase |
|
26 |
from combo.data.library import register_cell_class |
|
19 | 27 | |
20 | 28 | |
21 | 29 |
class Profile(models.Model): |
22 | 30 |
user = models.OneToOneField(settings.AUTH_USER_MODEL) |
23 | 31 |
initial_login_view_timestamp = models.DateTimeField(null=True) |
32 | ||
33 | ||
34 |
@register_cell_class |
|
35 |
class ProfileCell(JsonCellBase): |
|
36 |
template_name = 'combo/profile.html' |
|
37 |
first_data_key = 'profile' |
|
38 | ||
39 |
class Meta: |
|
40 |
verbose_name = _('Profile') |
|
41 | ||
42 |
@property |
|
43 |
def url(self): |
|
44 |
idp = settings.KNOWN_SERVICES.get('authentic').values()[0] |
|
45 |
return '{%% load combo %%}%sapi/users/{{ concerned_user|name_id }}/' % idp.get('url') |
|
46 | ||
47 |
def is_visible(self, user=None): |
|
48 |
if not user or user.is_anonymous(): |
|
49 |
return False |
|
50 |
return super(ProfileCell, self).is_visible(user) |
|
51 | ||
52 |
def get_cell_extra_context(self, context): |
|
53 |
extra_context = super(ProfileCell, self).get_cell_extra_context(context) |
|
54 |
extra_context['profile_fields'] = OrderedDict() |
|
55 |
if extra_context.get('profile') is not None: |
|
56 |
for attribute in settings.USER_PROFILE_CONFIG.get('fields'): |
|
57 |
extra_context['profile_fields'][attribute['name']] = copy.copy(attribute) |
|
58 |
value = extra_context['profile'].get(attribute['name']) |
|
59 |
if value: |
|
60 |
if attribute['kind'] in ('birthdate', 'date'): |
|
61 |
value = parse_date(value) |
|
62 |
extra_context['profile_fields'][attribute['name']]['value'] = value |
|
63 |
else: |
|
64 |
extra_context['error'] = 'unknown user' |
|
65 |
return extra_context |
combo/profile/templates/combo/profile.html | ||
---|---|---|
1 |
{% load i18n %} |
|
2 |
{% block cell-content %} |
|
3 |
<div class="profile"> |
|
4 |
<h2>{% trans "Profile" %}</h2> |
|
5 |
{% for key, details in profile_fields.items %} |
|
6 |
{% if details.value and details.user_visible %} |
|
7 |
<p><span class="label">{{ details.label }}</span> <span class="value">{{ details.value }}</span></p> |
|
8 |
{% endif %} |
|
9 |
{% endfor %} |
|
10 |
{% if error == 'unknown user' %} |
|
11 |
<p>{% trans 'Unknown User' %}</p> |
|
12 |
{% endif %} |
|
13 |
</div> |
|
14 |
{% endblock %} |
combo/public/templatetags/combo.py | ||
---|---|---|
19 | 19 |
import datetime |
20 | 20 | |
21 | 21 |
from django import template |
22 |
from django.conf import settings |
|
22 | 23 |
from django.core import signing |
23 | 24 |
from django.core.exceptions import PermissionDenied |
25 |
from django.template import VariableDoesNotExist |
|
24 | 26 |
from django.template.base import TOKEN_BLOCK, TOKEN_VAR |
25 | 27 |
from django.template.defaultfilters import stringfilter |
26 | 28 |
from django.utils import dateparse |
... | ... | |
30 | 32 |
from combo.utils import NothingInCacheException, flatten_context |
31 | 33 |
from combo.apps.dashboard.models import DashboardCell, Tile |
32 | 34 | |
35 |
if 'mellon' in settings.INSTALLED_APPS: |
|
36 |
from mellon.models import UserSAMLIdentifier |
|
37 | ||
33 | 38 |
register = template.Library() |
34 | 39 | |
35 | 40 |
def skeleton_text(context, placeholder_name, content=''): |
... | ... | |
229 | 234 |
@register.filter |
230 | 235 |
def signed(obj): |
231 | 236 |
return signing.dumps(obj) |
237 | ||
238 |
@register.filter |
|
239 |
def name_id(user): |
|
240 |
saml_id = UserSAMLIdentifier.objects.filter(user=user).last() |
|
241 |
if saml_id: |
|
242 |
return saml_id.name_id |
|
243 |
# it is important to raise this so get_templated_url is aborted and no call |
|
244 |
# is tried with a missing user argument. |
|
245 |
raise VariableDoesNotExist('name_id') |
data/themes/gadjo/static/css/agent-portal.scss | ||
---|---|---|
184 | 184 |
} |
185 | 185 |
} |
186 | 186 | |
187 |
div.profile { |
|
188 |
span.value { |
|
189 |
display: block; |
|
190 |
margin-left: 1rem; |
|
191 |
} |
|
192 |
} |
|
193 | ||
187 | 194 |
@media screen and (min-width: 1586px) { |
188 | 195 |
div#page-content div.cubesbarchart { |
189 | 196 |
width: 49.5%; |
tests/settings.py | ||
---|---|---|
62 | 62 | |
63 | 63 |
BOOKING_CALENDAR_CELL_ENABLED = True |
64 | 64 |
NEWSLETTERS_CELL_ENABLED = True |
65 | ||
66 |
USER_PROFILE_CONFIG = { |
|
67 |
'fields': [ |
|
68 |
{ |
|
69 |
'name': 'first_name', |
|
70 |
'kind': 'string', |
|
71 |
'label': 'First Name', |
|
72 |
'user_visible': True, |
|
73 |
}, |
|
74 |
{ |
|
75 |
'name': 'birthdate', |
|
76 |
'kind': 'birthdate', |
|
77 |
'label': 'Birth Date', |
|
78 |
'user_visible': True, |
|
79 |
} |
|
80 |
] |
|
81 |
} |
tests/test_profile.py | ||
---|---|---|
1 |
import datetime |
|
2 |
import json |
|
3 |
import mock |
|
4 |
import pytest |
|
5 | ||
6 |
from django.test import override_settings |
|
7 | ||
8 |
from combo.data.models import Page |
|
9 |
from combo.profile.models import ProfileCell |
|
10 | ||
11 |
pytestmark = pytest.mark.django_db |
|
12 | ||
13 | ||
14 |
@override_settings( |
|
15 |
KNOWN_SERVICES={'authentic': {'idp': {'title': 'IdP', 'url': 'http://example.org/'}}}) |
|
16 |
@mock.patch('combo.public.templatetags.combo.UserSAMLIdentifier') |
|
17 |
@mock.patch('combo.utils.requests.get') |
|
18 |
def test_profile_cell(requests_get, user_saml, app, admin_user): |
|
19 |
page = Page() |
|
20 |
page.save() |
|
21 | ||
22 |
cell = ProfileCell(page=page, order=0) |
|
23 |
cell.save() |
|
24 | ||
25 |
data = {'first_name': 'Foo', 'birthdate': '2018-08-10'} |
|
26 |
requests_get.return_value = mock.Mock( |
|
27 |
content=json.dumps(data), |
|
28 |
json=lambda: data, |
|
29 |
status_code=200) |
|
30 | ||
31 |
def filter_mock(user=None): |
|
32 |
assert user is admin_user |
|
33 |
return mock.Mock(last=lambda: mock.Mock(name_id='123456')) |
|
34 | ||
35 |
mocked_objects = mock.Mock() |
|
36 |
mocked_objects.filter = mock.Mock(side_effect=filter_mock) |
|
37 |
user_saml.objects = mocked_objects |
|
38 | ||
39 |
context = cell.get_cell_extra_context({'synchronous': True, 'selected_user': admin_user}) |
|
40 |
assert context['profile_fields']['first_name']['value'] == 'Foo' |
|
41 |
assert context['profile_fields']['birthdate']['value'] == datetime.date(2018, 8, 10) |
|
42 |
assert requests_get.call_args[0][0] == 'http://example.org/api/users/123456/' |
|
43 | ||
44 |
def filter_mock_missing(user=None): |
|
45 |
return mock.Mock(last=lambda: None) |
|
46 | ||
47 |
mocked_objects.filter = mock.Mock(side_effect=filter_mock_missing) |
|
48 | ||
49 |
context = cell.get_cell_extra_context({'synchronous': True, 'selected_user': admin_user}) |
|
50 |
assert context['error'] == 'unknown user' |
|
51 |
assert requests_get.call_count == 1 # no new call was made |
|
0 |
- |