Projet

Général

Profil

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

Paul Marillonnet, 23 octobre 2018 09:36

Télécharger (19 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               |  21 ++++-
 .../migrations/0023_auto_20181022_1914.py     |  19 +++++
 src/authentic2/models.py                      |   2 +-
 src/authentic2/settings.py                    |   3 +
 .../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/urls.py                        |   5 ++
 src/authentic2/utils.py                       |  46 +++++++++++
 src/authentic2/views.py                       |   2 +
 tests/conftest.py                             |   5 ++
 tests/test_attribute_kinds.py                 |  73 ++++++++++++++++++
 "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, 209 insertions(+), 6 deletions(-)
 create mode 100644 src/authentic2/migrations/0023_auto_20181022_1914.py
 create mode 100644 src/authentic2/templates/authentic2/accounts_image.html
 create mode 100644 src/authentic2/templatetags/__init__.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_html_value
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
        'rest_framework_field_kwargs': {
173
            'read_only': True,
174
        },
175
        'html_value': profile_image_html_value,
176
    },
163 177
]
164 178

  
165 179

  
src/authentic2/forms/widgets.py
7 7
# License: BSD
8 8
# Initial Author: Alfredo Saglimbeni
9 9

  
10
import django
10 11
import json
11 12
import re
12 13
import uuid
13 14

  
14
from django.forms.widgets import DateTimeInput, DateInput, TimeInput
15
from django.forms.widgets import DateTimeInput, DateInput, TimeInput, \
16
        ClearableFileInput
15 17
from django.forms.widgets import PasswordInput as BasePasswordInput
16 18
from django.utils.formats import get_language, get_format
17 19
from django.utils.safestring import mark_safe
......
246 248
                    json.dumps(_id),
247 249
                )
248 250
        return output
251

  
252

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

  
260
        def get_template_substitution_values(self, value):
261
            return {'initial': value}
262

  
263
    else:
264
        template_name = "authentic2/accounts_image.html"
265

  
266
    def is_initial(self, value):
267
        return bool(value)
src/authentic2/migrations/0023_auto_20181022_1914.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import migrations, models
5

  
6

  
7
class Migration(migrations.Migration):
8

  
9
    dependencies = [
10
        ('authentic2', '0022_attribute_scopes'),
11
    ]
12

  
13
    operations = [
14
        migrations.AlterField(
15
            model_name='attributevalue',
16
            name='content',
17
            field=models.TextField(null=True, verbose_name='content', db_index=True),
18
        ),
19
    ]
src/authentic2/models.py
270 270
            verbose_name=_('attribute'))
271 271
    multiple = models.BooleanField(default=False)
272 272

  
273
    content = models.TextField(verbose_name=_('content'), db_index=True)
273
    content = models.TextField(verbose_name=_('content'), db_index=True, null=True)
274 274
    verified = models.BooleanField(default=False)
275 275

  
276 276
    objects = managers.AttributeValueManager()
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_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/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 get_image_thumbnail(img):
1092
    from sorl.thumbnail import get_thumbnail
1093
    logger = logging.getLogger(__name__)
1094

  
1095
    dimensions = app_settings.A2_ATTRIBUTE_KIND_IMAGE_DIMENSIONS
1096
    crop = app_settings.A2_ATTRIBUTE_KIND_IMAGE_CROPPING
1097
    quality =  app_settings.A2_ATTRIBUTE_KIND_IMAGE_QUALITY
1098

  
1099
    try:
1100
        local_img = img.split(default_storage.base_url)[-1]
1101
        thumb = get_thumbnail(local_img, dimensions, crop=crop, quality=quality)
1102
    except:
1103
        logger.error("Couldn't generate thumbnail for image %s" % img)
1104
    else:
1105
        return thumb
1106

  
1107

  
1108
def profile_image_html_value(attribute, value):
1109
    fragment = u'<a href="%s"><img class="%s" src="%s"/></a>' % (
1110
        value, attribute.name, value)
1111
    return html.mark_safe(fragment)
1112

  
1113

  
1114
def profile_image_serialize(image):
1115
    from urllib import unquote
1116

  
1117
    if isinstance(image, basestring):
1118
        return unquote(image).decode('utf8')
1119
    elif image:
1120
        img_tmp_path = _store_image(image)
1121
        return get_image_thumbnail(img_tmp_path).url
src/authentic2/views.py
444 444
            if attribute:
445 445
                if not attribute.user_visible:
446 446
                    continue
447
                html_value = attribute.get_kind().get('html_value', lambda a, b: b)
447 448
                qs = models.AttributeValue.objects.with_owner(request.user)
448 449
                qs = qs.filter(attribute=attribute)
449 450
                qs = qs.select_related()
450 451
                value = [at_value.to_python() for at_value in qs]
451 452
                value = filter(None, value)
453
                value = [html_value(attribute, at_value) for at_value in value]
452 454
                if not title:
453 455
                    title = unicode(attribute)
454 456
            else:
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
    Attribute.objects.create(name='cityscape_image', label='cityscape', kind='profile_image',
377
                             asked_on_registration=True, required=False,
378
                             user_visible=True, user_editable=True)
379

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

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

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

  
403
    response = app.get('/accounts/edit/')
404
    form = response.form
405
    form.set('edit-profile-first_name', 'John')
406
    form.set('edit-profile-last_name', 'Doe')
407
    form.set('edit-profile-cityscape_image-clear', True)
408
    response = form.submit()
409

  
410
    qs = User.objects.filter(first_name='John')
411
    assert qs.get().attributes.cityscape_image == None
412
    qs.delete()
413

  
414

  
415
def test_profile_images_account_registration(db, app, admin, mailoutbox, media):
416
    Attribute.objects.create(name='cityscape_image', label='cityscape', kind='profile_image',
417
                             asked_on_registration=True)
418
    Attribute.objects.create(name='garden_image', label='garden', kind='profile_image',
419
                             asked_on_registration=True)
420

  
421
    response = app.get('/accounts/register/')
422
    form = response.form
423
    form.set('email', 'john.doe@example.com')
424
    response = form.submit().follow()
425
    assert 'john.doe@example.com' in response
426
    url = get_link_from_mail(mailoutbox[0])
427
    response = app.get(url)
428

  
429
    form = response.form
430
    assert form.get('cityscape_image')
431
    assert form.get('garden_image')
432
    form.set('first_name', 'John')
433
    form.set('last_name', 'Doe')
434
    form.set('cityscape_image', Upload('tests/une première image.png'))
435
    form.set('garden_image', Upload('tests/une deuxième image.png'))
436
    form.set('password1', '12345abcdA')
437
    form.set('password2', '12345abcdA')
438
    response = form.submit()
439

  
440
    qs = User.objects.filter(first_name='John')
441
    john = qs.get()
442
    assert john.attributes.cityscape_image
443
    assert john.attributes.garden_image
444
    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
-