authentic/src/authentic2/forms/widgets.py

364 lines
12 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/>.
# Bootstrap django-datetime-widget is a simple and clean widget for DateField,
# Timefiled and DateTimeField in Django framework. It is based on Bootstrap
# datetime picker, supports Bootstrap 2
#
# https://github.com/asaglimbeni/django-datetime-widget
#
# License: BSD
# Initial Author: Alfredo Saglimbeni
import datetime
import json
import re
import uuid
import django
from django import forms
from django.forms.widgets import ClearableFileInput, DateInput, DateTimeInput
from django.forms.widgets import EmailInput as BaseEmailInput
from django.forms.widgets import PasswordInput as BasePasswordInput
from django.forms.widgets import TextInput, TimeInput
from django.utils.encoding import force_text
from django.utils.formats import get_format, get_language
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from gadjo.templatetags.gadjo import xstatic
from authentic2 import app_settings
DATE_FORMAT_JS_PY_MAPPING = {
'P': '%p',
'ss': '%S',
'ii': '%M',
'hh': '%H',
'HH': '%I',
'dd': '%d',
'mm': '%m',
'yy': '%y',
'yyyy': '%Y',
}
DATE_FORMAT_TO_PYTHON_REGEX = re.compile(r'\b(' + '|'.join(DATE_FORMAT_JS_PY_MAPPING.keys()) + r')\b')
DATE_FORMAT_PY_JS_MAPPING = {
'%M': 'ii',
'%m': 'mm',
'%I': 'HH',
'%H': 'hh',
'%d': 'dd',
'%Y': 'yyyy',
'%y': 'yy',
'%p': 'P',
'%S': 'ss',
}
DATE_FORMAT_TO_JS_REGEX = re.compile(r'(?<!\w)(' + '|'.join(DATE_FORMAT_PY_JS_MAPPING.keys()) + r')\b')
BOOTSTRAP_INPUT_TEMPLATE = """
%(rendered_widget)s
%(clear_button)s
<span class="add-on"><i class="icon-th"></i></span>
<span class="helptext">%(help_text)s</span>
<script type="text/javascript">
$("#%(id)s").datetimepicker({%(options)s});
</script>
"""
BOOTSTRAP_DATE_INPUT_TEMPLATE = """
%(rendered_widget)s
%(clear_button)s
<span class="add-on"><i class="icon-th"></i></span>
<span class="%(id)s helptext">%(help_text)s</span>
<script type="text/javascript">
if ($("#%(id)s").attr('type') != "date") {
$("#%(id)s").datetimepicker({%(options)s});
var date = new Date($("#%(id)s").val());
$("#%(id)s").val(date.toLocaleDateString('%(language)s'));
} else {
$(".%(id)s.helptext").hide();
}
</script>
"""
CLEAR_BTN_TEMPLATE = """<span class="add-on"><i class="icon-remove"></i></span>"""
class PickerWidgetMixin(object):
class Media:
css = {
'all': ('css/datetimepicker.css',),
}
js = (
xstatic('jquery', 'jquery.min.js'),
xstatic('jquery_ui', 'jquery-ui.min.js'),
'js/bootstrap-datetimepicker.js',
'js/locales/bootstrap-datetimepicker.fr.js',
)
format_name = None
glyphicon = None
help_text = None
render_template = BOOTSTRAP_INPUT_TEMPLATE
def __init__(self, attrs=None, options=None, usel10n=None):
if attrs is None:
attrs = {}
self.options = options
self.options['language'] = get_language().split('-')[0]
# We're not doing localisation, get the Javascript date format provided by the user,
# with a default, and convert it to a Python data format for later string parsing
date_format = self.options['format']
self.format = DATE_FORMAT_TO_PYTHON_REGEX.sub(
lambda x: DATE_FORMAT_JS_PY_MAPPING[x.group()], date_format
)
super(PickerWidgetMixin, self).__init__(attrs, format=self.format)
def get_format(self):
format = get_format(self.format_name)[0]
for py, js in DATE_FORMAT_PY_JS_MAPPING.items():
format = format.replace(py, js)
return format
def render(self, name, value, attrs=None, renderer=None):
attrs = attrs or {}
final_attrs = self.build_attrs(attrs)
final_attrs['class'] = "controls input-append date"
rendered_widget = super(PickerWidgetMixin, self).render(
name, value, attrs=final_attrs, renderer=renderer
)
# if not set, autoclose have to be true.
self.options.setdefault('autoclose', True)
# Build javascript options out of python dictionary
options_list = []
for key, value in iter(self.options.items()):
options_list.append("%s: %s" % (key, json.dumps(value)))
js_options = ",\n".join(options_list)
# Use provided id or generate hex to avoid collisions in document
id = final_attrs.get('id', uuid.uuid4().hex)
help_text = self.help_text
if not help_text:
help_text = '%s %s' % (_('Format:'), self.options['format'])
return mark_safe(
self.render_template
% dict(
id=id,
rendered_widget=rendered_widget,
clear_button=CLEAR_BTN_TEMPLATE if self.options.get('clearBtn') else '',
glyphicon=self.glyphicon,
language=get_language(),
options=js_options,
help_text=help_text,
)
)
class DateTimeWidget(PickerWidgetMixin, DateTimeInput):
"""
DateTimeWidget is the corresponding widget for Datetime field, it renders both the date and time
sections of the datetime picker.
"""
format_name = 'DATETIME_INPUT_FORMATS'
glyphicon = 'glyphicon-th'
def __init__(self, attrs=None, options=None, usel10n=None):
if options is None:
options = {}
# Set the default options to show only the datepicker object
options['format'] = options.get('format', self.get_format())
super(DateTimeWidget, self).__init__(attrs, options, usel10n)
class DateWidget(PickerWidgetMixin, DateInput):
"""
DateWidget is the corresponding widget for Date field, it renders only the date section of
datetime picker.
"""
format_name = 'DATE_INPUT_FORMATS'
glyphicon = 'glyphicon-calendar'
input_type = 'date'
render_template = BOOTSTRAP_DATE_INPUT_TEMPLATE
def __init__(self, attrs=None, options=None, usel10n=None):
if options is None:
options = {}
# Set the default options to show only the datepicker object
options['startView'] = options.get('startView', 2)
options['minView'] = options.get('minView', 2)
options['format'] = options.get('format', self.get_format())
super(DateWidget, self).__init__(attrs, options, usel10n)
def format_value(self, value):
if value is not None:
if isinstance(value, datetime.datetime):
return force_text(value.isoformat())
return value
class TimeWidget(PickerWidgetMixin, TimeInput):
"""
TimeWidget is the corresponding widget for Time field, it renders only the time section of
datetime picker.
"""
format_name = 'TIME_INPUT_FORMATS'
glyphicon = 'glyphicon-time'
def __init__(self, attrs=None, options=None, usel10n=None):
if options is None:
options = {}
# Set the default options to show only the timepicker object
options['startView'] = options.get('startView', 1)
options['minView'] = options.get('minView', 0)
options['maxView'] = options.get('maxView', 1)
options['format'] = options.get('format', self.get_format())
super(TimeWidget, self).__init__(attrs, options, usel10n)
class PasswordInput(BasePasswordInput):
class Media:
js = (
xstatic('jquery', 'jquery.min.js'),
'authentic2/js/password.js',
)
css = {'all': ('authentic2/css/password.css',)}
def render(self, name, value, attrs=None, renderer=None):
output = super(PasswordInput, self).render(name, value, attrs=attrs, renderer=renderer)
if attrs and app_settings.A2_PASSWORD_POLICY_SHOW_LAST_CHAR:
_id = attrs.get('id')
if _id:
output += '''\n<script>a2_password_show_last_char(%s);</script>''' % json.dumps(_id)
return output
class NewPasswordInput(PasswordInput):
def render(self, name, value, attrs=None, renderer=None):
if attrs is None:
attrs = {}
attrs['autocomplete'] = 'new-password'
output = super(NewPasswordInput, self).render(name, value, attrs=attrs, renderer=renderer)
if attrs:
_id = attrs.get('id')
if _id:
output += '''\n<script>a2_password_validate(%s);</script>''' % json.dumps(_id)
return output
class CheckPasswordInput(PasswordInput):
# this widget must be named xxx2 and the other widget xxx1, it's a
# convention, js code expect it.
def render(self, name, value, attrs=None, renderer=None):
if attrs is None:
attrs = {}
attrs['autocomplete'] = 'new-password'
output = super(CheckPasswordInput, self).render(name, value, attrs=attrs, renderer=renderer)
if attrs:
_id = attrs.get('id')
if _id and _id.endswith('2'):
other_id = _id[:-1] + '1'
output += '''\n<script>$(a2_password_check_equality(%s, %s))</script>''' % (
json.dumps(other_id),
json.dumps(_id),
)
return output
class ProfileImageInput(ClearableFileInput):
if django.VERSION < (1, 9):
template_with_initial = (
'%(initial_text)s: <a href="%(initial_url)s"><img src="%(initial_url)s"/></a> '
'%(clear_template)s<br />%(input_text)s: %(input)s'
)
else:
template_name = "authentic2/profile_image_input.html"
def __init__(self, *args, **kwargs):
attrs = kwargs.pop('attrs', {})
attrs['accept'] = 'image/*'
super(ProfileImageInput, self).__init__(*args, attrs=attrs, **kwargs)
class DatalistTextInput(TextInput):
def __init__(self, name='', data=(), attrs=None):
super(DatalistTextInput, self).__init__(attrs)
self.data = data
self.name = 'list__%s' % name
self.attrs.update({'list': self.name})
def render(self, name, value, attrs=None, renderer=None):
output = super(DatalistTextInput, self).render(name, value, attrs=attrs, renderer=renderer)
datalist = '<datalist id="%s">' % self.name
for element in self.data:
datalist += '<option value="%s">' % element
datalist += '</datalist>'
output += datalist
return output
class PhoneNumberInput(TextInput):
input_type = 'tel'
class EmailInput(BaseEmailInput):
template_name = 'authentic2/widgets/email.html'
@property
def media(self):
if app_settings.A2_SUGGESTED_EMAIL_DOMAINS:
return forms.Media(
js=(
xstatic('jquery', 'jquery.min.js'),
'authentic2/js/email_domains_suggestions.js',
)
)
def get_context(self, *args, **kwargs):
context = super(EmailInput, self).get_context(*args, **kwargs)
if app_settings.A2_SUGGESTED_EMAIL_DOMAINS:
context['widget']['attrs']['data-suggested-domains'] = ':'.join(
app_settings.A2_SUGGESTED_EMAIL_DOMAINS
)
context['domains_suggested'] = True
return context