authentic/src/authentic2/forms/fields.py

122 lines
4.3 KiB
Python

# authentic2 - versatile identity manager
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import io
import warnings
import PIL.Image
from django.core.files import File
from django.forms import CharField, EmailField, FileField, ValidationError
from django.forms.fields import FILE_INPUT_CONTRADICTION
from django.utils.translation import ugettext_lazy as _
from authentic2 import app_settings
from authentic2.forms.widgets import (
CheckPasswordInput,
EmailInput,
NewPasswordInput,
PasswordInput,
ProfileImageInput,
)
from authentic2.passwords import password_help_text, validate_password
from authentic2.validators import email_validator
class PasswordField(CharField):
widget = PasswordInput
class NewPasswordField(CharField):
widget = NewPasswordInput
default_validators = [validate_password]
def __init__(self, *args, **kwargs):
kwargs['help_text'] = password_help_text()
super(NewPasswordField, self).__init__(*args, **kwargs)
class CheckPasswordField(CharField):
widget = CheckPasswordInput
def __init__(self, *args, **kwargs):
kwargs[
'help_text'
] = u'''
<span class="a2-password-check-equality-default">%(default)s</span>
<span class="a2-password-check-equality-matched">%(match)s</span>
<span class="a2-password-check-equality-unmatched">%(nomatch)s</span>
''' % {
'default': _('Both passwords must match.'),
'match': _('Passwords match.'),
'nomatch': _('Passwords do not match.'),
}
super(CheckPasswordField, self).__init__(*args, **kwargs)
class ProfileImageField(FileField):
widget = ProfileImageInput
@property
def image_size(self):
return app_settings.A2_ATTRIBUTE_KIND_PROFILE_IMAGE_SIZE
def clean(self, data, initial=None):
if data is FILE_INPUT_CONTRADICTION or data is False or data is None:
return super(ProfileImageField, self).clean(data, initial=initial)
# we have a file
try:
with warnings.catch_warnings():
image = PIL.Image.open(io.BytesIO(data.read()))
except (IOError, PIL.Image.DecompressionBombWarning):
raise ValidationError(_('The image is not valid'))
image = self.normalize_image(image)
new_data = self.file_from_image(image, data.name)
return super(ProfileImageField, self).clean(new_data, initial=initial)
def file_from_image(self, image, name=None):
output = io.BytesIO()
if image.mode != 'RGB':
image = image.convert('RGB')
image.save(output, format='JPEG', quality=99, optimize=1)
output.seek(0)
return File(output, name=name)
def normalize_image(self, image):
width = height = self.image_size
if abs((1.0 * width / height) - (1.0 * image.size[0] / image.size[1])) > 0.1:
# aspect ratio change, crop the image first
box = [0, 0, image.size[0], int(image.size[0] * (1.0 * height / width))]
if box[2] > image.size[0]:
box = [int(t * (1.0 * image.size[0] / box[2])) for t in box]
if box[3] > image.size[1]:
box = [int(t * (1.0 * image.size[1] / box[3])) for t in box]
if image.size[0] > image.size[1]: # landscape
box[0] = (image.size[0] - box[2]) / 2 # keep the middle
box[2] += box[0]
else:
box[1] = (image.size[1] - box[3]) / 4 # keep mostly the top
box[3] += box[1]
image = image.crop(box)
return image.resize([width, height], PIL.Image.ANTIALIAS)
class ValidatedEmailField(EmailField):
default_validators = [email_validator]
widget = EmailInput