0001-support-avatar-picture-in-user-profile-26022.patch
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 |
- |