0001-misc-automatically-resize-profile-image-27644.patch
src/authentic2/app_settings.py | ||
---|---|---|
145 | 145 |
A2_OPENED_SESSION_COOKIE_NAME=Setting(default='A2_OPENED_SESSION', definition='Authentic session open'), |
146 | 146 |
A2_OPENED_SESSION_COOKIE_DOMAIN=Setting(default=None), |
147 | 147 |
A2_ATTRIBUTE_KINDS=Setting(default=(), definition='List of other attribute kinds'), |
148 |
A2_ATTRIBUTE_KIND_PROFILE_IMAGE_MAX_SIZE=Setting(default=200, definition='Max width and height for a profile image'),
|
|
148 |
A2_ATTRIBUTE_KIND_PROFILE_IMAGE_SIZE=Setting(default=200, definition='Width and height for a profile image'),
|
|
149 | 149 |
A2_VALIDATE_EMAIL=Setting(default=False, definition='Validate user email server by doing an RCPT command'), |
150 | 150 |
A2_VALIDATE_EMAIL_DOMAIN=Setting(default=True, definition='Validate user email domain'), |
151 | 151 |
A2_PASSWORD_POLICY_MIN_CLASSES=Setting(default=3, definition='Minimum number of characters classes to be present in passwords'), |
src/authentic2/forms/fields.py | ||
---|---|---|
46 | 46 |
class ProfileImageField(FileField): |
47 | 47 |
widget = ProfileImageInput |
48 | 48 | |
49 |
def __init__(self, *args, **kwargs): |
|
50 |
kwargs.setdefault( |
|
51 |
'help_text', |
|
52 |
_('Image must be JPG or PNG of size less ' |
|
53 |
'than {max_size}x{max_size} pixels').format(max_size=self.max_size)) |
|
54 |
super(ProfileImageField, self).__init__(*args, **kwargs) |
|
55 | ||
56 | 49 |
@property |
57 |
def max_size(self):
|
|
58 |
return app_settings.A2_ATTRIBUTE_KIND_PROFILE_IMAGE_MAX_SIZE
|
|
50 |
def image_size(self):
|
|
51 |
return app_settings.A2_ATTRIBUTE_KIND_PROFILE_IMAGE_SIZE |
|
59 | 52 | |
60 | 53 |
def clean(self, data, initial=None): |
61 | 54 |
if data is FILE_INPUT_CONTRADICTION or data is False or data is None: |
... | ... | |
66 | 59 |
image = PIL.Image.open(io.BytesIO(data.read())) |
67 | 60 |
except (IOError, PIL.Image.DecompressionBombWarning): |
68 | 61 |
raise ValidationError(_('The image is not valid')) |
69 |
width, height = image.size |
|
70 |
max_size = app_settings.A2_ATTRIBUTE_KIND_PROFILE_IMAGE_MAX_SIZE |
|
71 |
if width > max_size or height > max_size: |
|
72 |
raise ValidationError(_('The image is bigger than {max_size}x{max_size} pixels') |
|
73 |
.format(max_size=self.max_size)) |
|
62 |
image = self.normalize_image(image) |
|
74 | 63 |
new_data = self.file_from_image(image, data.name) |
75 | 64 |
return super(ProfileImageField, self).clean(new_data, initial=initial) |
76 | 65 | |
... | ... | |
85 | 74 |
optimize=1) |
86 | 75 |
output.seek(0) |
87 | 76 |
return File(output, name=name) |
77 | ||
78 |
def normalize_image(self, image): |
|
79 |
width = height = self.image_size |
|
80 |
if abs((1.0 * width / height) - (1.0 * image.size[0] / image.size[1])) > 0.1: |
|
81 |
# aspect ratio change, crop the image first |
|
82 |
box = [0, 0, image.size[0], int(image.size[0] * (1.0 * height / width))] |
|
83 | ||
84 |
if box[2] > image.size[0]: |
|
85 |
box = [int(t * (1.0 * image.size[0] / box[2])) for t in box] |
|
86 |
if box[3] > image.size[1]: |
|
87 |
box = [int(t * (1.0 * image.size[1] / box[3])) for t in box] |
|
88 | ||
89 |
if image.size[0] > image.size[1]: # landscape |
|
90 |
box[0] = (image.size[0] - box[2]) / 2 # keep the middle |
|
91 |
box[2] += box[0] |
|
92 |
else: |
|
93 |
box[1] = (image.size[1] - box[3]) / 4 # keep mostly the top |
|
94 |
box[3] += box[1] |
|
95 | ||
96 |
image = image.crop(box) |
|
97 |
return image.resize([width, height], PIL.Image.ANTIALIAS) |
tests/test_attribute_kinds.py | ||
---|---|---|
1 | 1 |
# -*- coding: utf-8 -*- |
2 | 2 |
import datetime |
3 |
import os |
|
4 | ||
5 |
import PIL.Image |
|
6 | ||
7 |
from django.conf import settings |
|
3 | 8 | |
4 | 9 |
from authentic2.custom_user.models import User |
5 | 10 |
from authentic2.models import Attribute |
... | ... | |
398 | 403 |
response = form.submit() |
399 | 404 |
assert response.pyquery.find('.form-field-error #id_cityscape_image') |
400 | 405 | |
401 |
# verify 201x201 image is refused |
|
402 |
form = response.form |
|
403 |
form.set('cityscape_image', Upload('tests/201x201.jpg')) |
|
404 |
form.set('password1', '12345abcdA') |
|
405 |
form.set('password2', '12345abcdA') |
|
406 |
response = form.submit() |
|
407 |
assert response.pyquery.find('.form-field-error #id_cityscape_image') |
|
408 | ||
409 | 406 |
# verify 200x200 image is accepted |
410 | 407 |
form = response.form |
411 | 408 |
form.set('cityscape_image', Upload('tests/200x200.jpg')) |
... | ... | |
413 | 410 |
form.set('password2', '12345abcdA') |
414 | 411 |
response = form.submit() |
415 | 412 |
assert john().attributes.cityscape_image |
413 |
profile_filename = john().attributes.cityscape_image.name |
|
416 | 414 | |
417 | 415 |
# verify API serves absolute URL for profile images |
418 | 416 |
app.authorization = ('Basic', (admin.username, admin.username)) |
... | ... | |
429 | 427 |
response = form.submit() |
430 | 428 |
assert john().attributes.cityscape_image == None |
431 | 429 | |
432 |
# verify API serves absolute URL for profile images
|
|
430 |
# verify API serves None for empty profile images
|
|
433 | 431 |
app.authorization = ('Basic', (admin.username, admin.username)) |
434 | 432 |
response = app.get('/api/users/%s/' % john().uuid) |
435 | 433 |
assert response.json['cityscape_image'] is None |
434 | ||
435 |
# verify 201x201 image is accepted and resized |
|
436 |
response = app.get('/accounts/edit/') |
|
437 |
form = response.form |
|
438 |
form.set('edit-profile-cityscape_image', Upload('tests/201x201.jpg')) |
|
439 |
response = form.submit() |
|
440 |
image = PIL.Image.open(open(os.path.join(settings.MEDIA_ROOT, john().attributes.cityscape_image.name))) |
|
441 |
assert image.width == 200 |
|
442 |
assert image.height == 200 |
|
443 |
assert john().attributes.cityscape_image.name != profile_filename |
|
436 |
- |