Projet

Général

Profil

0001-support-avatar-picture-in-user-profile-26022.patch

Paul Marillonnet, 12 octobre 2018 11:53

Télécharger (20,6 ko)

Voir les différences:

Subject: [PATCH] support avatar picture in user profile (#26022)

 debian-wheezy/control                         |   3 +-
 debian/control                                |   3 +-
 setup.py                                      |   2 +
 src/authentic2/app_settings.py                |   3 +
 src/authentic2/attribute_kinds.py             |  14 +++
 src/authentic2/forms/widgets.py               |  39 +++++++-
 src/authentic2/settings.py                    |   3 +
 .../templates/authentic2/accounts.html        |  13 +--
 .../authentic2/accounts_attribute.html        |  18 ++++
 .../templates/authentic2/accounts_edit.html   |   3 +-
 .../templates/authentic2/accounts_image.html  |   6 ++
 .../registration/registration_form.html       |   3 +-
 src/authentic2/templatetags/__init__.py       |   0
 src/authentic2/templatetags/authentic2.py     |   9 ++
 src/authentic2/urls.py                        |   5 +
 src/authentic2/utils.py                       |  46 +++++++++
 tests/conftest.py                             |   5 +
 tests/test_attribute_kinds.py                 |  88 ++++++++++++++++++
 "tests/une deuxi\303\250me image.png"         | Bin 0 -> 506 bytes
 "tests/une premi\303\250re image.png"         | Bin 0 -> 508 bytes
 tox.ini                                       |   2 +
 21 files changed, 247 insertions(+), 18 deletions(-)
 create mode 100644 src/authentic2/templates/authentic2/accounts_attribute.html
 create mode 100644 src/authentic2/templates/authentic2/accounts_image.html
 create mode 100644 src/authentic2/templatetags/__init__.py
 create mode 100644 src/authentic2/templatetags/authentic2.py
 create mode 100644 "tests/une deuxi\303\250me image.png"
 create mode 100644 "tests/une premi\303\250re image.png"
debian-wheezy/control
25 25
    python-markdown (>= 2.1),
26 26
    python-ldap (>= 2.4),
27 27
    python-six (>= 1.0),
28
    python-django-filters (>= 1)
28
    python-django-filters (>= 1),
29
    python-sorl-thumbnail
29 30
Provides: ${python:Provides}
30 31
Recommends: python-ldap
31 32
Suggests: python-raven
debian/control
27 27
    python-jwcrypto (>= 0.3.1),
28 28
    python-cryptography (>= 1.3.4),
29 29
    python-django-filters (>= 1),
30
    python-django-filters (<< 2)
30
    python-django-filters (<< 2),
31
    python-sorl-thumbnail
31 32
Provides: ${python:Provides}
32 33
Recommends: python-ldap
33 34
Suggests: python-raven
setup.py
131 131
          'XStatic-jQuery',
132 132
          'XStatic-jquery-ui<1.12',
133 133
          'xstatic-select2',
134
          'sorl-thumbnail',
135
          'pillow',
134 136
      ],
135 137
      zip_safe=False,
136 138
      classifiers=[
src/authentic2/app_settings.py
176 176
            'next try after a login failure'),
177 177
    A2_VERIFY_SSL=Setting(default=True, definition='Verify SSL certificate in HTTP requests'),
178 178
    A2_ATTRIBUTE_KIND_TITLE_CHOICES=Setting(default=(), definition='Choices for the title attribute kind'),
179
    A2_ATTRIBUTE_KIND_IMAGE_DIMENSIONS=Setting(default="150x150", definition='Width x Height image dimensions in account management page.'),
180
    A2_ATTRIBUTE_KIND_IMAGE_CROPPING=Setting(default="center", definition='Image cropping in account management page.'),
181
    A2_ATTRIBUTE_KIND_IMAGE_QUALITY=Setting(default=99, definition='Image quality in account management page.'),
179 182
    A2_CORS_WHITELIST=Setting(default=(), definition='List of origin URL to whitelist, must be scheme://netloc[:port]'),
180 183
    A2_EMAIL_CHANGE_TOKEN_LIFETIME=Setting(default=7200, definition='Lifetime in seconds of the '
181 184
                                           'token sent to verify email adresses'),
src/authentic2/attribute_kinds.py
17 17
from .plugins import collect_from_plugins
18 18
from . import app_settings
19 19
from .forms import widgets
20
from .utils import profile_image_serialize, profile_image_deserialize
20 21

  
21 22
capfirst = allow_lazy(capfirst, unicode)
22 23

  
......
160 161
        'field_class': PhoneNumberField,
161 162
        'rest_framework_field_class': PhoneNumberDRFField,
162 163
    },
164
    {
165
        'label': _('profile image'),
166
        'name': 'profile_image',
167
        'field_class': forms.ImageField,
168
        'kwargs': {
169
            'widget': widgets.ProfileImageInput,
170
        },
171
        'serialize': profile_image_serialize,
172
        'deserialize': profile_image_deserialize,
173
        'rest_framework_field_kwargs': {
174
            'read_only': True,
175
            },
176
    },
163 177
]
164 178

  
165 179

  
src/authentic2/forms/widgets.py
11 11
import re
12 12
import uuid
13 13

  
14
from django.forms.widgets import DateTimeInput, DateInput, TimeInput
14
from django.forms.widgets import DateTimeInput, DateInput, TimeInput, \
15
        ClearableFileInput
15 16
from django.forms.widgets import PasswordInput as BasePasswordInput
16 17
from django.utils.formats import get_language, get_format
17 18
from django.utils.safestring import mark_safe
......
19 20

  
20 21
from gadjo.templatetags.gadjo import xstatic
21 22

  
22
from authentic2 import app_settings
23
from authentic2 import app_settings, utils
23 24

  
24 25
DATE_FORMAT_JS_PY_MAPPING = {
25 26
    'P': '%p',
......
246 247
                    json.dumps(_id),
247 248
                )
248 249
        return output
250

  
251

  
252
class ProfileImageInput(ClearableFileInput):
253
    import django
254

  
255
    if django.VERSION < (1, 9):
256
        template_with_initial = (
257
            '%(initial_text)s: <br /> <img src="%(initial)s"/> <br />'
258
            '%(clear_template)s %(input_text)s: %(input)s'
259
        )
260

  
261
        def get_template_substitution_values(self, value):
262
            from authentic2.middleware import StoreRequestMiddleware
263

  
264
            request = StoreRequestMiddleware.get_request()
265
            thumb = utils.get_image_thumbnail(request, value)
266
            if hasattr(thumb, 'url'):
267
                return {'initial': thumb.url}
268
            return {'initial': ''}
269

  
270
    else:
271
        template_name = "authentic2/accounts_image.html"
272

  
273
        def format_value(self, value):
274
            from authentic2.middleware import StoreRequestMiddleware
275

  
276
            request = StoreRequestMiddleware.get_request()
277
            thumb = utils.get_image_thumbnail(request, value)
278
            if hasattr(thumb, 'url'):
279
                return thumb.url
280
            return ''
281

  
282
    def is_initial(self, value):
283
        return bool(value)
src/authentic2/settings.py
22 22
DEBUG = False
23 23
DEBUG_DB = False
24 24
MEDIA = 'media'
25
MEDIA_ROOT = 'media'
26
MEDIA_URL = '/media/'
25 27

  
26 28
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
27 29
ALLOWED_HOSTS = []
......
132 134
    'xstatic.pkg.jquery',
133 135
    'xstatic.pkg.jquery_ui',
134 136
    'xstatic.pkg.select2',
137
    'sorl.thumbnail',
135 138
)
136 139

  
137 140
INSTALLED_APPS = tuple(plugins.register_plugins_installed_apps(INSTALLED_APPS))
src/authentic2/templates/authentic2/accounts.html
16 16
      {% if attributes %}
17 17
        <dl>
18 18
          {% for attribute in attributes %}
19
            <dt>{{ attribute.attribute.label|capfirst }}&nbsp;:</dt>
20
            <dd>
21
              {% if attribute.values|length == 1 %}
22
                {{ attribute.values.0 }}
23
              {% else %}
24
                <ul>
25
                  {% for value in attribute.values %}
26
                    <li>{{ value }}</li>
27
                  {% endfor %}
28
                </ul>
29
              {% endif %}
30
            </dd>
19
            {% include "authentic2/accounts_attribute.html" %}
31 20
          {% endfor %}
32 21
        </dl>
33 22
      {% endif %}
src/authentic2/templates/authentic2/accounts_attribute.html
1
{% load authentic2 %}
2
<dt>{{ attribute.attribute.label|capfirst }}&nbsp;:</dt>
3
<dd>
4
  {% if attribute.attribute.kind == 'profile_image' %}
5
      {% thumbnail attribute.values.0 as thumb_im %}
6
      <img class="{{ attribute.attribute.name }}" src="{{ thumb_im.url }}">
7
  {% else %}
8
    {% if attribute.values|length == 1 %}
9
      {{ attribute.values.0 }}
10
    {% else %}
11
      <ul>
12
        {% for value in attribute.values %}
13
          <li>{{ value }}</li>
14
        {% endfor %}
15
      </ul>
16
    {% endif %}
17
  {% endif %}
18
</dd>
src/authentic2/templates/authentic2/accounts_edit.html
12 12
{% endblock %}
13 13

  
14 14
{% block content %}
15
  <form method="post">
15
  <form enctype="multipart/form-data" method="post">
16

  
16 17
    {% csrf_token %}
17 18
    {{ form.as_p }}
18 19
    {% if form.instance and form.instance.id %}
src/authentic2/templates/authentic2/accounts_image.html
1
{% if widget.is_initial %}{{ widget.initial_text }}: <br/><img src="{{ widget.value }}"/><br/>
2
{% if not widget.required %}
3
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}" />
4
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}
5
{{ widget.input_text }}:{% endif %}
6
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %} />
src/authentic2/templates/registration/registration_form.html
15 15

  
16 16
<h2>{{ view.title }}</h2>
17 17

  
18
<form method="post">
18
<form enctype="multipart/form-data" method="post">
19

  
19 20
  {% csrf_token %}
20 21
  {{ form.as_p }}
21 22
  <button class="submit-button">{% trans 'Submit' %}</button>
src/authentic2/templatetags/authentic2.py
1
from django import template
2

  
3
register = template.Library()
4

  
5

  
6
@register.assignment_tag(takes_context=True)
7
def thumbnail(context, img_value):
8
    from ..utils import get_image_thumbnail
9
    return get_image_thumbnail(context['request'], img_value)
src/authentic2/urls.py
2 2
from django.conf import settings
3 3
from django.contrib import admin
4 4
from django.contrib.staticfiles.views import serve
5
from django.views.static import serve as media_serve
5 6

  
6 7
from . import app_settings, plugins, views
7 8

  
......
44 45
    urlpatterns += [
45 46
        url(r'^static/(?P<path>.*)$', serve)
46 47
    ]
48
    urlpatterns += [
49
        url(r'^media/(?P<path>.*)$', media_serve, {
50
        'document_root': settings.MEDIA_ROOT})
51
    ]
47 52

  
48 53
if settings.DEBUG and 'debug_toolbar' in settings.INSTALLED_APPS:
49 54
    import debug_toolbar
src/authentic2/utils.py
8 8
import uuid
9 9
import datetime
10 10
import copy
11
import os
11 12

  
12 13
from functools import wraps
13 14
from itertools import islice, chain, count
......
30 31
from django.template.loader import render_to_string, TemplateDoesNotExist
31 32
from django.core.mail import send_mail
32 33
from django.core import signing
34
from django.core.files.storage import default_storage
33 35
from django.core.urlresolvers import reverse, NoReverseMatch
34 36
from django.utils.formats import localize
35 37
from django.contrib import messages
......
1073 1075
        if ou_value is not None:
1074 1076
            return ou_value
1075 1077
    return default
1078

  
1079

  
1080
def _store_image(in_memory_image):
1081
    from hashlib import sha1
1082

  
1083
    h = sha1(in_memory_image.read()).hexdigest()
1084
    extension = in_memory_image.image.format.lower()
1085
    img_tmp_path = u'images/%s/%s.%s' % (h[:3], h[3:], extension)
1086
    img_media_path = default_storage.save(img_tmp_path, in_memory_image)
1087

  
1088
    return img_media_path
1089

  
1090

  
1091
def profile_image_serialize(image):
1092
    from urllib import unquote
1093

  
1094
    img_media_path = ''
1095
    if isinstance(image, basestring):
1096
        img_url = unquote(image).decode('utf8')
1097
        img_media_path = img_url.split(default_storage.base_url)[-1]
1098
    elif image:
1099
        img_media_path = _store_image(image)
1100
    return img_media_path
1101

  
1102

  
1103
def profile_image_deserialize(img_media_path):
1104
    if img_media_path:
1105
        return os.path.join(settings.MEDIA_URL, img_media_path)
1106

  
1107

  
1108
def get_image_thumbnail(request, img):
1109
    from sorl.thumbnail import get_thumbnail
1110
    logger = logging.getLogger(__name__)
1111

  
1112
    dimensions = app_settings.A2_ATTRIBUTE_KIND_IMAGE_DIMENSIONS
1113
    crop = app_settings.A2_ATTRIBUTE_KIND_IMAGE_CROPPING
1114
    quality =  app_settings.A2_ATTRIBUTE_KIND_IMAGE_QUALITY
1115

  
1116
    try:
1117
        thumb = get_thumbnail(request.build_absolute_uri(img), dimensions, crop=crop, quality=quality)
1118
    except:
1119
        logger.error("Couldn't generate thumbnail for image %s" % img)
1120
    else:
1121
        return thumb
tests/conftest.py
339 339
    activate('fr')
340 340
    yield
341 341
    deactivate()
342

  
343

  
344
@pytest.fixture
345
def media(settings, tmpdir):
346
    settings.MEDIA_ROOT = str(tmpdir.mkdir('media'))
tests/test_attribute_kinds.py
5 5
from authentic2.models import Attribute
6 6

  
7 7
from utils import get_link_from_mail
8
from webtest import Upload
8 9

  
9 10

  
10 11
def test_string(db, app, admin, mailoutbox):
......
369 370
    app.post_json('/api/users/', params=payload)
370 371
    assert qs.get().attributes.birthdate == datetime.date(1900, 1, 1)
371 372
    qs.delete()
373

  
374

  
375
def test_profile_image(db, app, admin, mailoutbox, media):
376
    from hashlib import sha1
377

  
378
    Attribute.objects.create(name='cityscape_image', label='cityscape', kind='profile_image',
379
                             asked_on_registration=True, required=False,
380
                             user_visible=True, user_editable=True)
381
    qs = User.objects.filter(first_name='John')
382

  
383
    response = app.get('/accounts/register/')
384
    form = response.form
385
    form.set('email', 'john.doe@example.com')
386
    response = form.submit().follow()
387
    assert 'john.doe@example.com' in response
388
    url = get_link_from_mail(mailoutbox[0])
389
    response = app.get(url)
390

  
391
    form = response.form
392
    form.set('first_name', 'John')
393
    form.set('last_name', 'Doe')
394
    form.set('cityscape_image', Upload('/dev/null'))
395
    form.set('password1', '12345abcdA')
396
    form.set('password2', '12345abcdA')
397
    response = form.submit()
398
    assert response.pyquery.find('.form-field-error #id_cityscape_image')
399

  
400
    form = response.form
401
    form.set('cityscape_image', Upload('tests/une première image.png'))
402
    form.set('password1', '12345abcdA')
403
    form.set('password2', '12345abcdA')
404
    response = form.submit()
405

  
406
    with open('tests/une première image.png') as f:
407
        john = qs.get()
408
        xdigest = sha1(f.read()).hexdigest()
409
        assert xdigest[:3] in john.attributes.cityscape_image
410
        assert xdigest[3:] in john.attributes.cityscape_image
411

  
412
    app.authorization = ('Basic', (admin.username, admin.username))
413
    resp = app.get('/api/users/?first_name=John&last_name=Doe')
414

  
415
    with open('tests/une première image.png') as f:
416
        xdigest = sha1(f.read()).hexdigest()
417
        assert xdigest[:3] in resp.json_body['results'][0]['cityscape_image']
418
        assert xdigest[3:] in resp.json_body['results'][0]['cityscape_image']
419

  
420
    response = app.get('/accounts/edit/')
421
    form = response.form
422
    form.set('edit-profile-first_name', 'John')
423
    form.set('edit-profile-last_name', 'Doe')
424
    form.set('edit-profile-cityscape_image-clear', True)
425
    response = form.submit()
426
    assert qs.get().attributes.cityscape_image == None
427
    qs.delete()
428

  
429

  
430
def test_profile_images_account_registration(db, app, admin, mailoutbox, media):
431
    Attribute.objects.create(name='cityscape_image', label='cityscape', kind='profile_image',
432
                             asked_on_registration=True)
433
    Attribute.objects.create(name='garden_image', label='garden', kind='profile_image',
434
                             asked_on_registration=True)
435
    qs = User.objects.filter(first_name='John')
436

  
437
    response = app.get('/accounts/register/')
438
    form = response.form
439
    form.set('email', 'john.doe@example.com')
440
    response = form.submit().follow()
441
    assert 'john.doe@example.com' in response
442
    url = get_link_from_mail(mailoutbox[0])
443
    response = app.get(url)
444

  
445
    form = response.form
446
    assert form.get('cityscape_image')
447
    assert form.get('garden_image')
448
    form.set('first_name', 'John')
449
    form.set('last_name', 'Doe')
450
    form.set('cityscape_image', Upload('tests/une première image.png'))
451
    form.set('garden_image', Upload('tests/une deuxième image.png'))
452
    form.set('password1', '12345abcdA')
453
    form.set('password2', '12345abcdA')
454
    response = form.submit()
455

  
456
    john = qs.get()
457
    assert john.attributes.cityscape_image
458
    assert john.attributes.garden_image
459
    john.delete()
tox.ini
48 48
  httmock
49 49
  pytz
50 50
  pytest-freezegun
51
  pillow
52
  sorl-thumbnail
51 53
commands =
52 54
  ./getlasso.sh
53 55
  authentic: py.test {env:FAST:} {env:REUSEDB:} {env:COVERAGE:} {posargs:tests/ --random}
54
-