Projet

Général

Profil

0001-profile-add-a-new-user-profile-cell-25633.patch

Frédéric Péters, 31 août 2018 14:53

Télécharger (12,2 ko)

Voir les différences:

Subject: [PATCH] profile: add a new "user profile" cell (#25633)

 combo/data/models.py                          |  7 ++-
 combo/profile/__init__.py                     | 26 ++++++++++
 combo/profile/migrations/0002_profilecell.py  | 35 +++++++++++++
 combo/profile/models.py                       | 42 +++++++++++++++
 combo/profile/templates/combo/profile.html    | 14 +++++
 combo/public/templatetags/combo.py            | 14 +++++
 .../themes/gadjo/static/css/agent-portal.scss |  7 +++
 tests/settings.py                             | 17 +++++++
 tests/test_profile.py                         | 51 +++++++++++++++++++
 9 files changed, 211 insertions(+), 2 deletions(-)
 create mode 100644 combo/profile/migrations/0002_profilecell.py
 create mode 100644 combo/profile/templates/combo/profile.html
 create mode 100644 tests/test_profile.py
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
-