wcs/wcs/qommon/form.py

2052 lines
74 KiB
Python

# w.c.s. - web application for online forms
# Copyright (C) 2005-2010 Entr'ouvert
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
import base64
import cStringIO
import fnmatch
import mimetypes
import os
import re
import socket
import tempfile
import time
import random
import datetime
import itertools
import hashlib
import json
from storage import atomic_write
try:
from feedparser import _sanitizeHTML
except ImportError:
_sanitizeHTML = None
try:
import DNS
DNS.ParseResolvConf()
except ImportError:
DNS = None
try:
import magic
except ImportError:
magic = None
import quixote
from quixote import get_publisher, get_request, get_response, get_session
from quixote.http_request import Upload
from quixote.http_response import status_reasons
from quixote.form import *
from quixote.html import htmltext, htmltag, htmlescape, TemplateIO
from quixote.util import randbytes
import misc
from strftime import strftime
from publisher import get_cfg
QuixoteForm = Form
Widget.REQUIRED_ERROR = N_('required field')
get_error_orig = Widget.get_error
def get_i18n_error(self, request=None):
error = get_error_orig(self, request)
if error == Widget.REQUIRED_ERROR:
return _(error)
return error
def set_message(self, message):
self.message = message
def get_message(self):
if hasattr(self, 'message'):
return self.message
else:
return ''
def render_message(self, message):
if message:
return htmltext('<div class="message">%s</div>') % message
else:
return ''
def is_prefilled(self):
if hasattr(self, 'prefilled'):
return self.prefilled
else:
return False
def render_title(self, title):
if title:
if self.required:
title += htmltext('<span class="required">*</span>')
return htmltext('<div class="title"><label for="form_%s">%s</label></div>') % (
self.name, title)
else:
return ''
def render(self):
# quixote/form/widget.py, Widget::render
r = TemplateIO(html=True)
classnames = '%s widget' % self.__class__.__name__
if hasattr(self, 'extra_css_class') and self.extra_css_class:
classnames += ' ' + self.extra_css_class
if self.get_error():
classnames += ' widget-with-error'
if self.get_message():
classnames += ' widget-with-message'
if self.is_required():
classnames += ' widget-required'
if self.is_prefilled():
classnames += ' widget-prefilled'
attributes = {}
if hasattr(self, 'div_id') and self.div_id:
attributes['data-valuecontainerid'] = 'form_%s' % self.name
if hasattr(self, 'prefill_attributes') and self.prefill_attributes:
for k, v in self.prefill_attributes.items():
attributes['data-' + k] = v
if hasattr(self, 'div_id') and self.div_id:
attributes['id'] = self.div_id
attributes['class'] = classnames
r += htmltext('<div %s>' % ' '.join(['%s="%s"' % x for x in attributes.items()]))
r += self.render_title(self.get_title())
classnames = 'content'
if hasattr(self, 'content_extra_css_class') and self.content_extra_css_class:
classnames += ' ' + self.content_extra_css_class
r += htmltext('<div class="%s">' % classnames)
r += self.render_content()
r += self.render_hint(self.get_hint())
r += self.render_error(self.get_error())
r += self.render_message(self.get_message())
r += htmltext('</div>')
r += htmltext('</div>')
if self.render_br:
r += htmltext('<br class="%s" />') % classnames
r += htmltext('\n')
return r.getvalue()
Widget.get_error = get_i18n_error
Widget.render = render
Widget.cleanup = None
Widget.set_message = set_message
Widget.get_message = get_message
Widget.render_message = render_message
Widget.render_title = render_title
Widget.is_prefilled = is_prefilled
def string_render_content(self):
attrs = {'id': 'form_' + self.name}
if self.required:
attrs['aria-required'] = 'true'
if self.attrs:
attrs.update(self.attrs)
return htmltag("input", xml_end=True, type=self.HTML_TYPE, name=self.name,
value=self.value, **attrs)
StringWidget.render_content = string_render_content
def file_render_content(self):
attrs = {'id': 'form_' + str(self.name).split('$')[0]}
if self.required:
attrs['aria-required'] = 'true'
if self.attrs:
attrs.update(self.attrs)
return htmltag("input", xml_end=True, type=self.HTML_TYPE, name=self.name,
value=self.value, **attrs)
FileWidget.render_content = file_render_content
def text_render_content(self):
attrs = {'id': 'form_' + self.name}
if self.required:
attrs['aria-required'] = 'true'
if self.attrs:
attrs.update(self.attrs)
return (htmltag("textarea", name=self.name, **attrs) +
htmlescape(self.value or "") + htmltext("</textarea>"))
TextWidget.render_content = text_render_content
def submit_render_content(self):
if self.name in ('cancel', 'previous', 'save-draft'):
self.attrs['formnovalidate'] = 'formnovalidate'
return super(self.__class__, self).render_content()
SubmitWidget.render_content = submit_render_content
def radiobuttons_render_content(self):
tags = []
for object, description, key in self.options:
if self.is_selected(object):
checked = 'checked'
else:
checked = None
r = htmltag("input", xml_end=True,
type="radio",
name=self.name,
value=key,
checked=checked,
**self.attrs)
tags.append(htmltext('<label>') + r + htmlescape(description) + htmltext('</label>'))
return htmlescape(self.delim).join(tags)
RadiobuttonsWidget.render_content = radiobuttons_render_content
def checkbox_render_content(self):
attrs = {'id': 'form_' + self.name}
if self.required:
attrs['aria-required'] = 'true'
if self.attrs:
attrs.update(self.attrs)
return htmltag("input", xml_end=True, type="checkbox", name=self.name,
value="yes", checked=self.value and "checked" or None,
**attrs)
CheckboxWidget.render_content = checkbox_render_content
def select_render_content(self):
attrs = {'id': 'form_' + self.name}
if self.required:
attrs['aria-required'] = 'true'
if self.attrs:
attrs.update(self.attrs)
tags = [htmltag("select", name=self.name, **attrs)]
for object, description, key in self.options:
if self.is_selected(object):
selected = 'selected'
else:
selected = None
if description is None:
description = ""
r = htmltag("option", value=key, selected=selected)
tags.append(r + htmlescape(description) + htmltext('</option>'))
tags.append(htmltext("</select>"))
return htmltext("\n").join(tags)
SelectWidget.render_content = select_render_content
class Form(QuixoteForm):
TOKEN_NOTICE = N_(
"The form you have submitted is invalid. Most "
"likely it has been successfully submitted once "
"already. Please review the form data "
"and submit the form again.")
ERROR_NOTICE = N_("There were errors processing your form. See below for details.")
info = None
captcha = None
advanced_label = '+'
def __init__(self, *args, **kwargs):
if kwargs.get('advanced_label'):
self.advanced_label = kwargs.pop('advanced_label')
QuixoteForm.__init__(self, *args, **kwargs)
def keep_referer(self):
self.add(HiddenWidget, '__keep_referer',
value = get_request().environ.get('HTTP_REFERER'))
def referer(self):
if self.get_widget('__keep_referer'):
return self.get_widget('__keep_referer').parse()
def add_captcha(self, hint=None):
if not self.captcha and not (get_session().won_captcha or get_session().user):
self.captcha = CaptchaWidget('captcha', hint=hint)
def add(self, widget_class, name, *args, **kwargs):
if kwargs and not kwargs.has_key('render_br'):
kwargs['render_br'] = False
advanced = False
if kwargs and kwargs.get('advanced', False):
advanced = True
del kwargs['advanced']
QuixoteForm.add(self, widget_class, name, *args, **kwargs)
widget = self._names[name]
widget.advanced = advanced
return widget
def remove(self, name):
widget = self._names.get(name)
del self._names[name]
self.widgets.remove(widget)
def get_all_widgets(self):
l = QuixoteForm.get_all_widgets(self)
if self.captcha:
l.append(self.captcha)
return l
def _get_default_action(self):
if get_request().get_header('x-popup') == 'true':
# do not leave action empty for popups, as they get embedded into
# another URI
return get_request().get_path()
return QuixoteForm._get_default_action(self)
def render_button(self, button):
r = TemplateIO(html=True)
classnames = '%s widget %s-button' % (
button.__class__.__name__, button.name)
r += htmltext('<div class="%s">') % classnames
r += htmltext('<div class="content">')
r += button.render_content()
r += htmltext('</div>')
r += htmltext('</div>')
r += htmltext('\n')
return r.getvalue()
def _render_submit_widgets(self):
r = TemplateIO(html=True)
if self.submit_widgets:
r += htmltext('<div class="buttons submit">')
for widget in self.submit_widgets:
if widget.name == 'prefill':
continue
r += self.render_button(widget)
r += htmltext('</div>')
return r.getvalue()
def _render_info_notice(self):
return htmltext('<div class="infonotice">%s</div>' % _(self.info))
def _render_error_notice(self):
return htmltext('<div class="errornotice">%s</div>' % _(
QuixoteForm._render_error_notice(self)))
def _render_body(self):
r = TemplateIO(html=True)
if self.has_errors():
r += self._render_error_notice()
if self.info:
r += self._render_info_notice()
if self._names.has_key('prefill'):
r += self._render_prefill_widgets()
r += self._render_widgets()
if self.captcha:
r += self.captcha.render()
r += self._render_submit_widgets()
if self.has_errors():
# if form has errors, focus and select the first widget on error
r += htmltext("""<script type="text/javascript">
var all_obj = document.getElementsByTagName('DIV');
for (i=0; i<all_obj.length; i++) {
if ( ( ' ' + all_obj[i].getAttribute("class") + ' ').toLowerCase().match(
new RegExp ( '^.* widget-with-error .*$', 'g' ) ) ) {
less_obj = all_obj[i].getElementsByTagName('INPUT');
for (j=0; j<less_obj.length; j++) {
less_obj[j].select();
less_obj[j].focus();
break;
}
break;
}
}
</script>""")
return r.getvalue()
def _render_prefill_widgets(self):
r = TemplateIO(html=True)
if self.submit_widgets:
r += htmltext('<div class="prefill buttons">')
for widget in self.submit_widgets:
if widget.name == 'prefill':
r += widget.render()
r += htmltext('</div>')
return r.getvalue()
def _render_widgets(self):
r = TemplateIO(html=True)
advanced_widgets = []
for widget in self.widgets:
if not hasattr(widget, 'advanced') or not widget.advanced:
r += widget.render()
else:
advanced_widgets.append(widget)
if advanced_widgets:
get_response().add_javascript(['jquery.js', 'qommon.forms.js'])
r += htmltext('<fieldset class="form-plus">')
r += htmltext('<legend>%s</legend>') % self.advanced_label
for widget in advanced_widgets:
r += widget.render()
r += htmltext('</fieldset>')
return r.getvalue()
class HtmlWidget(object):
error = None
def __init__(self, string, title = None, *args, **kwargs):
self.string = string
self.title = title
def render(self):
return self.render_content()
def render_content(self):
if self.title:
return htmltext(self.title)
return htmltext(self.string)
def has_error(self, request):
return False
def parse(self, *args):
pass
def clear_error(self, request=None):
pass
class CompositeWidget(quixote.form.CompositeWidget):
def render_as_thead(self):
r = TemplateIO(html=True)
r += htmltext('<tr>\n')
for widget in self.get_widgets():
r += htmltext('<th>')
r += widget.get_title()
r += htmltext('</th>')
r += htmltext('</tr>\n')
return r.getvalue()
def render_content_as_tr(self):
r = TemplateIO(html=True)
r += htmltext('<tr>\n')
for widget in self.get_widgets():
r += htmltext('<td>')
r += widget.render_content()
r += htmltext('</td>')
r += htmltext('</tr>\n')
return r.getvalue()
class TextWidget(quixote.form.TextWidget):
def _parse(self, request):
quixote.form.TextWidget._parse(self, request)
if self.value is not None:
try:
maxlength = int(self.attrs.get('maxlength', 0))
except (TypeError, ValueError):
maxlength = 0
if maxlength and len(self.value) > maxlength:
self.error = _('too many characters (limit is %d)') % maxlength
class CheckboxWidget(quixote.form.CheckboxWidget):
'''
Widget just like CheckboxWidget but with an effective support for the
required attribute, if required the checkbox will have to be checked.
'''
def _parse(self, request):
self.value = self.name in request.form and not request.form[self.name] in (False, '', 'False')
if self.required and not self.value:
self.set_error(self.REQUIRED_ERROR)
def set_value(self, value):
if value in (None, False, '', 'False'):
self.value = False
else:
self.value = True
class UploadedFile: #pylint: disable=C1001
def __init__(self, directory, filename, upload):
self.directory = directory
self.base_filename = upload.base_filename
self.content_type = upload.content_type
content = upload.fp.read()
self.size = len(content)
# Find a good filename
if filename:
self.filename = filename
elif self.base_filename:
self.filename = self.base_filename
else:
t = datetime.datetime.now().isoformat()
fd, path = tempfile.mkstemp(prefix=t, suffix='.upload',
dir=self.dir_path())
os.close(fd)
self.filename = os.path.basename(filename)
file_path = self.build_file_path()
if not os.path.exists(self.dir_path()):
os.mkdir(self.dir_path())
file(file_path, 'w').write(content)
def dir_path(self):
return os.path.join(get_publisher().app_dir, self.directory)
def build_file_path(self):
return os.path.join(get_publisher().app_dir, self.directory,
self.filename)
def get_file(self):
return file(self.build_file_path())
def build_response(self):
response = get_response()
response.content_type = self.content_type
response.set_header('content-disposition',
'attachment; filename="%s"' % self.base_filename)
return self.get_file().read()
class UploadWidget(CompositeWidget):
def __init__(self, name, value=None, directory=None, filename=None,
validation=None, **kwargs):
CompositeWidget.__init__(self, name, **kwargs)
del kwargs['title']
kwargs.pop('hint')
self.value = value
self.directory = directory or 'uploads'
self.filename = filename
self.validation = validation
if value:
self.add(CheckboxWidget, 'orexisting',
title = _('Use previous file.'), value=True)
self.widgets[-1].render_br = False
self.add(FileWidget, 'file', title = _('Or upload a new one'), **kwargs)
else:
self.add(FileWidget, 'file', **kwargs)
def _parse(self, request):
if self.value and self.get('orexisting'):
pass
elif self.get('file'):
upload = self.get('file')
self.value = UploadedFile(self.directory, self.filename, upload)
if self.validation:
valid, msg = self.validation(upload)
if not valid:
self.error = msg
else:
self.value = None
class FileWithPreviewWidget(CompositeWidget):
"""Widget that proposes a File Upload widget but that stores the file
ondisk so it has a "readonly" mode where the filename is shown."""
extra_css_class = 'file-upload-widget'
max_file_size = None
file_type = None
max_file_size_bytes = None # will be filled automatically
def __init__(self, name, value=None, **kwargs):
CompositeWidget.__init__(self, name, value, **kwargs)
self.value = value
self.preview = kwargs.get('readonly')
self.max_file_size = kwargs.pop('max_file_size', None)
if self.max_file_size:
self.max_file_size_bytes = FileSizeWidget.parse_file_size(self.max_file_size)
self.add(HiddenWidget, 'token')
if not self.preview:
attrs = {'data-url': get_publisher().get_root_url() + 'tmp-upload'}
self.file_type = kwargs.pop('file_type', None)
if self.file_type:
attrs['accept'] = ','.join(self.file_type)
if self.max_file_size_bytes:
# this could be used for client size validation of file size
attrs['data-max-file-size'] = str(self.max_file_size_bytes)
self.add(FileWidget, 'file', render_br=False, attrs=attrs)
if value:
self.set_value(value)
def set_value(self, value):
self.value = value
if hasattr(self.value, 'token'):
self.get_widget('token').set_value(self.value.token)
if not get_session().get_tempfile(self.value.token):
# oops, it has a token but it's not in the session; this is
# probably because it was restored from a draft file created
# from an expired session.
self.value.token = get_session().add_tempfile(self.value)
self.get_widget('token').set_value(self.value.token)
def render_content(self):
get_response().add_javascript(['jquery.js', 'jquery-ui.js',
'jquery.iframe-transport.js', 'jquery.fileupload.js',
'qommon.fileupload.js'])
temp = get_session().get_tempfile(self.get('token')) or {}
r = TemplateIO(html=True)
for widget in self.get_widgets():
r += widget.render()
r += htmltext('<div class="fileprogress" style="display: none;">')
r += htmltext(' <div class="bar">%s</div>' % _('Upload in progress...'))
r += htmltext('</div>')
r += htmltext('<div class="fileinfo"><span class="filename">%s</span>' % temp.get('base_filename', ''))
if not self.preview:
r += htmltext(' <a href="#" class="remove" title="%s">%s</a>' % (
_('Remove this file'),
_('remove')))
elif temp:
filetype = mimetypes.guess_type(temp.get('orig_filename', ''))
if filetype and filetype[0] and filetype[0].startswith('image'):
r += htmltext('<img alt="" src="tempfile?t=%s&thumbnail=1" />' % \
self.get('token'))
r += htmltext('</div>')
return r.getvalue()
def _parse(self, request):
self.value = None
if self.get('token'):
token = self.get('token')
elif self.get('file'):
token = get_session().add_tempfile(self.get('file'))
request.form[self.get_widget('token').get_name()] = token
else:
token = None
session = get_session()
if token and session.tempfiles and session.tempfiles.has_key(token):
temp = session.tempfiles[token]
self.value = session.get_tempfile_content(token)
if self.value is None:
# there's no file, the other checks are irrelevant.
return
if self.max_file_size:
# validate file size
file_size = os.path.getsize(self.value.fp.name)
if file_size > self.max_file_size_bytes:
self.error = _('over file size limit (%s)') % self.max_file_size
if self.file_type:
# validate file type
accepted_file_types = []
for file_type in self.file_type:
accepted_file_types.extend(file_type.split(','))
if magic:
magic_object = magic.open(magic.MIME)
magic_object.load()
filetype = magic_object.file(self.value.fp.name).split(';')[0]
magic_object.close()
else:
filetype, encoding = mimetypes.guess_type(self.value.base_filename)
if not filetype:
filetype = 'application/octet-stream'
valid_file_type = False
for accepted_file_type in accepted_file_types:
# fnmatch is used to handle generic mimetypes, like
# image/*
if fnmatch.fnmatch(filetype, accepted_file_type):
valid_file_type = True
break
if not valid_file_type:
self.error = _('invalid file type')
class PicklableUpload(Upload):
def __getstate__(self):
odict = self.__dict__.copy()
if 'fp' in odict:
del odict['fp']
basedir = os.path.join(get_publisher().app_dir, 'uploads')
if not os.path.exists(basedir):
os.mkdir(basedir)
if 'qfilename' in odict:
filepath = os.path.join(basedir, self.qfilename)
else:
while True:
filename = str(random.random())
filepath = os.path.join(basedir, filename)
if not os.path.exists(filepath):
break
self.qfilename = filename
if 'fp' in self.__dict__:
self.fp.seek(0)
atomic_write(filepath, self.fp, async=False)
odict['qfilename'] = self.qfilename
return odict
def get_file_pointer(self):
if 'fp' in self.__dict__ and self.__dict__.get('fp') is not None:
return self.__dict__.get('fp')
elif hasattr(self, 'qfilename'):
basedir = os.path.join(get_publisher().app_dir, 'uploads')
self.fp = file(os.path.join(basedir, self.qfilename))
return self.fp
return None
def __setstate__(self, dict):
self.__dict__.update(dict)
if hasattr(self, 'data'):
# backward compatibility with older w.c.s. version
self.fp = cStringIO.StringIO(self.data)
del self.data
def get_file(self):
# quack like UploadedFile
return self.get_file_pointer()
def get_content(self):
if hasattr(self, 'qfilename'):
filename = os.path.join(get_publisher().app_dir, 'uploads', self.qfilename)
return file(filename).read()
return None
class EmailWidget(StringWidget):
HTML_TYPE = 'email'
def __init__(self, *args, **kwargs):
StringWidget.__init__(self, *args, **kwargs)
if not 'size' in kwargs:
self.attrs['size'] = '35'
def _parse(self, request):
StringWidget._parse(self, request)
if self.value is not None:
# basic tests first
if not '@' in self.value[1:-1]:
self.error = _('must be a valid email address')
elif self.value[0] != '"' and ' ' in self.value:
self.error = _('must be a valid email address')
elif self.value[0] != '"' and self.value.count('@') != 1:
self.error = _('must be a valid email address')
elif get_cfg('emails', {}).get('check_domain_with_dns', True):
# testing for domain existence
domain = self.value.split('@')[-1]
if [x for x in domain.split('.') if not x]:
# empty parts in domain, ex: @example..net, or
# @.example.net
self.error = _('invalid address domain')
return
if not type(domain) is unicode:
domain = unicode(domain, 'utf-8', 'ignore')
domain = domain.encode('idna')
# simply lookup host name; note it will fail if hostname
# doesn't have an A entry
try:
socket.gethostbyname(domain)
except socket.error:
# will fail on lack of DNS module or MX lookup failure
try:
l = len(DNS.mxlookup(domain))
except:
l = 0
if l == 0:
self.error = _('invalid address domain')
else:
# and then, localpart could be tested
pass
class WcsExtraStringWidget(StringWidget):
field = None
prefill = False
def render_content(self):
if self.field and self.field.validation and not 'pattern' in self.attrs:
self.attrs['pattern'] = self.field.validation
s = StringWidget.render_content(self)
return s
def _parse(self, request):
StringWidget._parse(self, request)
if self.field and self.field.validation and self.value is not None:
if not re.match(self.field.validation, self.value):
self.error = _('wrong format')
class DateWidget(StringWidget):
'''StringWidget which checks the value entered is a correct date'''
minimum_date = None
maximum_date = None
content_extra_css_class = 'date'
def __init__(self, name, value=None, **kwargs):
self.minimum_date = None
if kwargs.get('minimum_date'):
self.minimum_date = time.strptime(kwargs.get('minimum_date'), misc.date_format())[:3]
del kwargs['minimum_date']
if kwargs.get('maximum_date'):
self.maximum_date = time.strptime(kwargs.get('maximum_date'), misc.date_format())[:3]
del kwargs['maximum_date']
if kwargs.get('minimum_is_future'):
if kwargs.get('date_can_be_today'):
self.minimum_date = time.localtime()
else:
self.minimum_date = (datetime.datetime.today() + datetime.timedelta(1)).timetuple()
if kwargs.get('date_in_the_past'):
if kwargs.get('date_can_be_today'):
self.maximum_date = time.localtime()
else:
self.maximum_date = (datetime.datetime.today() - datetime.timedelta(1)).timetuple()
if 'minimum_is_future' in kwargs:
del kwargs['minimum_is_future']
if 'date_in_the_past' in kwargs:
del kwargs['date_in_the_past']
if 'date_can_be_today' in kwargs:
del kwargs['date_can_be_today']
if isinstance(value, datetime.datetime) or isinstance(value, datetime.date):
value = value.strftime(misc.date_format())
StringWidget.__init__(self, name, value=value, **kwargs)
self.attrs['size'] = '10'
self.attrs['maxlength'] = '10'
self.jpicker_args = kwargs.pop('jpicker_args', {})
self.jquery = kwargs.pop('jquery', True)
def parse(self, request=None):
StringWidget.parse(self, request=request)
if type(self.value) is unicode:
self.value = self.value.encode(get_publisher().site_charset)
return self.value
@classmethod
def get_format_string(cls):
return misc.date_format()
def _parse(self, request):
StringWidget._parse(self, request)
if self.value is not None:
for format_string in (self.get_format_string(),
misc.date_format(), '%Y-%m-%d'):
try:
value = time.strptime(self.value, format_string)
except ValueError:
continue
self.value = strftime(self.get_format_string(), value)
break
else:
self.error = _('invalid date')
self.value = None
return
if value[0] < 1800 or value[0] > 2099:
self.error = _('invalid date')
self.value = None
elif self.minimum_date and value[:3] < self.minimum_date[:3]:
self.error = _('invalid date: date must be on or after %s') % strftime(
misc.date_format(), datetime.datetime(*self.minimum_date[:6]))
elif self.maximum_date and value[:3] > self.maximum_date[:3]:
format_string = misc.date_format()
self.error = _('invalid date; date must be on or before %s') % strftime(
misc.date_format(), datetime.datetime(*self.maximum_date[:6]))
def prepare_javascript(cls):
get_response().add_javascript(['jquery.js', 'bootstrap-datetimepicker.js'])
current_language = get_request().language
if current_language != 'en' and current_language in get_publisher().supported_languages or []:
get_response().add_javascript(['bootstrap-datetimepicker.%s.js' % current_language])
get_response().add_css_include('datetimepicker.css')
prepare_javascript = classmethod(prepare_javascript)
def render_content(self):
self.attrs['id'] = 'form_%s' % self.name
self.attrs['class'] = 'date-pick'
if self.attrs.get('readonly') or not self.jquery:
return StringWidget.render_content(self)
self.prepare_javascript()
date_format = self.get_format_string().replace('%Y', 'yyyy').replace(
'%m', 'mm').replace('%d', 'dd').replace('%H', 'hh').replace(
'%M', 'ii').replace('%S', 'ss')
args = []
args.append('format: "%s"' % date_format)
if not 'hh' in date_format:
# if the date format doesn't contain the time, set widget not to go
# into the time pages
args.append('minView: 2')
if not self.value:
# if there's no value we set the initial view to be the view of
# decades, it's more appropriate to select a far away date.
args.append('startView: 4')
args.append('autoclose: true')
args.append('weekStart: 1')
if self.minimum_date:
start_date = date_format.replace(
'yyyy', '%04d' % self.minimum_date[0]).replace(
'mm', '%02d' % self.minimum_date[1]).replace(
'dd', '%02d' % self.minimum_date[2]).replace(
'hh', '00').replace('ii', '00').replace('ss', '00')
args.append('startDate: "%s"' % start_date)
if self.maximum_date:
end_date = date_format.replace(
'yyyy', '%04d' % self.maximum_date[0]).replace(
'mm', '%02d' % self.maximum_date[1]).replace(
'dd', '%02d' % self.maximum_date[2]).replace(
'hh', '00').replace('ii', '00').replace('ss', '00')
args.append('endDate: "%s"' % end_date)
for key, value in self.jpicker_args.iteritems():
args.append("%s: %s" % (key, value))
if args:
args = '{' + ','.join(args) + '}'
else:
args = ''
if get_request().get_header('x-popup') == 'true':
# unfortunately we cannot add a popup calendar inside a popup :/
return StringWidget.render_content(self)
else:
get_response().add_javascript_code('''
$(function() {
$('#%(id)s').datetimepicker(%(args)s)
.on('changeDate', function(ev) {
$(this).data('datetimepicker').startViewMode = 2;
});
});''' % {'id': self.attrs['id'], 'args': args})
t = StringWidget.render_content(self)
return t
class DateTimeWidget(DateWidget):
'''StringWidget which checks the value entered is a correct date/time'''
content_extra_css_class = 'date'
def __init__(self, name, value=None, **kwargs):
DateWidget.__init__(self, name, value=value, **kwargs)
self.attrs['size'] = '16'
self.attrs['maxlength'] = '16'
def get_format_string(self):
return misc.datetime_format()
class RegexStringWidget(StringWidget):
'''StringWidget which checks the value entered is a correct regex'''
def _parse(self, request):
StringWidget._parse(self, request)
if self.value is not None:
try:
re.compile(self.value)
except:
self.error = _('invalid regular expression')
self.value = None
class CheckboxesWidget(CompositeWidget):
readonly = False
def __init__(self, name, value = None, elements = None, **kwargs):
CompositeWidget.__init__(self, name, value, **kwargs)
self.element_names = {}
if kwargs.has_key('title'):
del kwargs['title']
if kwargs.has_key('readonly'):
del kwargs['readonly']
self.readonly = True
self.inline = kwargs.get('inline', True)
self.max_choices = int(kwargs.get('max_choices', 0) or 0)
for v in elements:
if type(v) is tuple:
title = v[1]
key = v[0]
if type(key) is int:
name = 'element%d' % v[0]
elif type(key) in (str, htmltext):
name = str('element%s' % v[0])
key = str(key)
else:
raise NotImplementedError()
else:
title = v
key = v
name = 'element%d' % len(self.element_names.keys())
if value and key in value:
checked = True
else:
checked = False
self.add(CheckboxWidget, name, title = title, value = checked, **kwargs)
self.element_names[name] = key
def _parse(self, request):
if self.readonly:
return
values = []
for name in self.element_names:
value = self.get(name)
if value is True:
values.append(self.element_names[name])
self.value = values
if self.required and not self.value:
self.set_error(self.REQUIRED_ERROR)
if self.value and self.max_choices and len(self.value) > self.max_choices:
self.set_error(_('You must select at most %d answers.') % self.max_choices)
def set_value(self, value):
self.value = value
for name in self.element_names:
widget = self.get_widget(name)
if self.element_names[name] in self.value:
widget.set_value(True)
else:
widget.set_value(False)
def has_error(self, request=None):
return Widget.has_error(self, request=request)
def render_content(self):
r = TemplateIO(html=True)
if self.inline:
r += htmltext('<ul class="inline">')
else:
r += htmltext('<ul>')
for widget in self.get_widgets():
r += htmltext('<li><label>')
if self.readonly:
widget.attrs['disabled'] = 'disabled'
if widget.value:
r += htmltext('<input type="hidden" name="%s" value="yes" >') % widget.name
widget.name = widget.name + 'xx'
r += widget.render_content()
r += widget.title
r += htmltext('</label>')
r += htmltext('</li>')
r += htmltext('</ul>')
return r.getvalue()
class ValidatedStringWidget(StringWidget):
'''StringWidget which checks the value entered is correct according to a regex'''
regex = None
def _parse(self, request):
StringWidget._parse(self, request)
if self.regex and self.value is not None:
match = re.match(self.regex, self.value)
if not match or not match.group() == self.value:
self.error = _('wrong format')
class UrlWidget(ValidatedStringWidget):
'''StringWidget which checks the value entered is a correct url starting with http or https'''
regex = r'^https?://.+'
def _parse(self, request):
ValidatedStringWidget._parse(self, request)
if self.error:
self.error = _('must start with http:// or https:// and have a domain name')
class VarnameWidget(ValidatedStringWidget):
'''StringWidget which checks the value entered is a syntactically correct
variable name.'''
regex = r'^[a-zA-Z][a-zA-Z0-9_]*'
def _parse(self, request):
ValidatedStringWidget._parse(self, request)
if self.error:
self.error = _('must only consist of letters, numbers, or underscore')
class FileSizeWidget(ValidatedStringWidget):
'''StringWidget which checks the value entered is a syntactically correct
file size.'''
regex = r'^\s*([\d]+)\s*([MK]i?)?[oB]?\s*$'
@classmethod
def parse_file_size(cls, value):
try:
value, unit = re.search(cls.regex, value).groups()
except AttributeError: # None has no .groups()
raise ValueError()
coeffs = {
'Mi': 2**20,
'Ki': 2**10,
'M': 10**6,
'K': 10**3,
None: 1,
}
return int(value) * coeffs.get(unit)
def _parse(self, request):
ValidatedStringWidget._parse(self, request)
if self.error:
self.error = _('invalid file size')
class CaptchaWidget(CompositeWidget):
def __init__(self, name, value = None, mode = 'arithmetic-simple', *args, **kwargs):
CompositeWidget.__init__(self, name, value, **kwargs)
self.render_br = False
if value:
token = value
else:
token = get_session().create_captcha_token()
hidden_input = htmltext('<input type="hidden" name="%s$token" value="%s"></input>')
self.add(HtmlWidget, 'token', title = hidden_input % (self.name, token['token']))
# create question, and fill token['answer']
if mode == 'arithmetic' or mode == 'arithmetic-simple':
a, b = random.randint(2, 9), random.randint(2, 9)
while b == a:
# don't get twice the same number
b = random.randint(2, 9)
if mode == 'arithmetic-simple':
operator = random.choice([N_('plus'), N_('minus')])
else:
operator = random.choice([N_('times'), N_('plus'), N_('minus')])
if operator == 'times':
answer = a * b
elif operator == 'plus':
answer = a + b
elif operator == 'minus':
if b > a:
a, b = b, a
answer = a - b
self.question = _('What is the result of %(a)d %(op)s %(b)d?') % {
'a': a, 'b': b, 'op': _(operator)}
self.hint = kwargs.get('hint')
if self.hint is None:
self.hint = _('Please answer this simple mathematical question as proof you are not a bot.')
self.add(StringWidget, 'q', required=True)
token['answer'] = str(answer)
def _parse(self, request):
v = {'answer': self.get('q'),
'token': request.form.get('%s$token' % self.name) }
token = get_session().get_captcha_token(v['token'])
if v['answer'] and token and token['answer'] == v['answer'].strip():
get_session().won_captcha = True
self.value = v
else:
self.error = _('wrong answer')
def get_title(self):
return self.question
def render_content(self):
r = TemplateIO(html=True)
for widget in self.get_widgets():
r += widget.render_content()
return r.getvalue()
class WidgetList(quixote.form.widget.WidgetList):
def render(self):
get_response().add_javascript(['jquery.js', 'widget_list.js'])
r = TemplateIO(html=True)
r += self.render_title(self.get_title())
r += self.render_error(self.get_error())
add_element_widget = self.get_widget('add_element')
for widget in self.get_widgets():
if widget is add_element_widget:
continue
r += widget.render()
r += add_element_widget.render()
r += self.render_hint(self.get_hint())
return r.getvalue()
class WidgetDict(quixote.form.widget.WidgetDict):
# Fix the title and hint setting
# FIXME: to be fixed in Quixote upstream : title and hint parameters should be removed
def __init__(self, name, value=None, title='', hint='',
element_key_type=StringWidget,
element_value_type=StringWidget,
element_key_kwargs={},
element_value_kwargs={},
add_element_label='Add row', **kwargs):
if add_element_label == 'Add row':
add_element_label = _('Add row')
quixote.form.widget.WidgetDict.__init__(self, name, value,
element_key_type=element_key_type,
element_value_type=element_value_type,
element_key_kwargs=element_key_kwargs,
element_value_kwargs=element_value_kwargs,
add_element_label=add_element_label,
**kwargs)
if title:
self.title = title
if hint:
self.hint = hint
class TagsWidget(StringWidget):
def __init__(self, name, value = None, known_tags = None, **kwargs):
StringWidget.__init__(self, name, value, **kwargs)
self.known_tags = known_tags
def _parse(self, request):
StringWidget._parse(self, request)
if self.value is not None:
self.value = [x.strip() for x in self.value.split(',') if x.strip()]
def render_content(self):
r = TemplateIO(html=True)
get_response().add_javascript(['jquery.js', 'jquery.autocomplete.js'])
get_response().add_css_include('../js/jquery.autocomplete.css')
id = 'tags-%s' % randbytes(8)
if self.value:
value = ', '.join(self.value) + ', '
else:
value = ''
r += htmltag('input', xml_end = True,
id = id,
type = self.HTML_TYPE,
name = self.name,
value = value,
**self.attrs)
if self.known_tags:
known_tags = ', '.join([repr(x) for x in sorted(self.known_tags)])
else:
known_tags = ''
r += htmltext('''<script type="text/javascript">
$("#%s").autocompleteArray([
%s
],{mode:'multiple', multipleSeparator:', '});
</script>''' % (id, known_tags))
return r.getvalue()
class WysiwygTextWidget(TextWidget):
def _parse(self, request):
TextWidget._parse(self, request)
if self.value:
if _sanitizeHTML:
self.value = _sanitizeHTML(self.value, get_request().charset, 'text/html')
else:
self.value = str(htmlescape(self.value))
if self.value.startswith('<br />'):
self.value = self.value[6:]
if self.value.endswith('<br />'):
self.value = self.value[:-6]
def render_content(self):
get_response().add_javascript(['jquery.js', 'ckeditor/ckeditor.js',
'qommon.wysiwyg.js', 'ckeditor/adapters/jquery.js'])
attrs = self.attrs.copy()
return (htmltag('textarea', name=self.name, **attrs) +
htmlescape(self.value or '') +
htmltext("</textarea>"))
class TableWidget(CompositeWidget):
readonly = False
def __init__(self, name, value = None, rows = None, columns = None, **kwargs):
CompositeWidget.__init__(self, name, value, **kwargs)
self.rows = rows
self.columns = columns
if kwargs.has_key('title'):
del kwargs['title']
if kwargs.has_key('readonly') and kwargs.get('readonly'):
self.readonly = True
for i, row in enumerate(rows):
for j, column in enumerate(columns):
widget = self.add_widget(kwargs, i, j)
widget = self.get_widget('c-%s-%s' % (i, j))
if value and self.readonly:
if not get_request().form:
get_request().form = {}
get_request().form[widget.name] = value[i][j]
widget.set_value(value[i][j])
def add_widget(self, kwargs, i, j):
widget_kwargs = {}
if kwargs.has_key('readonly') and kwargs.get('readonly'):
widget_kwargs['readonly'] = 'readonly'
return self.add(StringWidget, 'c-%s-%s' % (i, j), **widget_kwargs)
def render_content(self):
r = TemplateIO(html=True)
r += htmltext('<table><thead><tr><td></td>')
for column in self.columns:
r += htmltext('<th><span>%s</span></th>') % column
r += htmltext('</tr></thead><tbody>')
for i, row in enumerate(self.rows):
r += htmltext('<tr><th>%s</th>') % row
for j, column in enumerate(self.columns):
widget = self.get_widget('c-%s-%s' % (i, j))
r += htmltext('<td>')
r += widget.render_content()
r += htmltext('</td>')
r += htmltext('</tr>')
r += htmltext('</tbody></table>')
return r.getvalue()
def parse(self, request=None):
CompositeWidget.parse(self, request=request)
if request is None:
request = get_request()
if (request.form or request.get_method() == 'POST') and self.required:
if not self.value:
self.set_error(self.REQUIRED_ERROR)
else:
for row in self.value:
for column in row:
if column:
break
else:
continue
break
else:
self.set_error(self.REQUIRED_ERROR)
return self.value
def _parse(self, request):
if self.readonly:
return
table = []
for i, row in enumerate(self.rows):
row = []
for j, column in enumerate(self.columns):
widget = self.get_widget('c-%s-%s' % (i, j))
row.append(widget.parse())
table.append(row)
self.value = table
def set_value(self, value):
self.value = value
if not value:
return
for i, row in enumerate(self.rows):
for j, column in enumerate(self.columns):
widget = self.get_widget('c-%s-%s' % (i, j))
widget.set_value(value[i][j])
class SingleSelectTableWidget(TableWidget):
def add_widget(self, kwargs, i, j):
widget_kwargs = {'options': kwargs.get('options')}
if kwargs.has_key('readonly') and kwargs.get('readonly'):
widget_kwargs['readonly'] = 'readonly'
return self.add(SingleSelectWidget, 'c-%s-%s' % (i, j), **widget_kwargs)
class CheckboxesTableWidget(TableWidget):
def add_widget(self, kwargs, i, j):
widget_kwargs = {'options': kwargs.get('options')}
if kwargs.has_key('readonly') and kwargs.get('readonly'):
widget_kwargs['readonly'] = 'readonly'
return self.add(CheckboxWidget, 'c-%s-%s' % (i, j), **widget_kwargs)
class SingleSelectHintWidget(SingleSelectWidget):
def separate_hint(self):
return (self.hint and len(self.hint) > 80)
def render_content(self):
attrs = {'id': 'form_' + self.name}
if self.attrs:
attrs.update(self.attrs)
tags = [htmltag('select', name=self.name, **attrs)]
options = self.options[:]
if not self.separate_hint() and self.hint:
r = htmltag('option', value='', selected=None)
tags.append(r + htmlescape(self.hint) + htmltext('</option>'))
if self.options[0][0] is None:
# hint has been put as first element, skip the default empty
# value.
options = self.options[1:]
for object, description, key in options:
if self.is_selected(object):
selected = 'selected'
else:
selected = None
if description is None:
description = ''
r = htmltag('option', value=key, selected=selected)
tags.append(r + htmlescape(description) + htmltext('</option>'))
tags.append(htmltext('</select>'))
return htmltext('\n').join(tags)
def get_hint(self):
if self.separate_hint():
return SingleSelectWidget.get_hint(self)
return None
class WidgetListAsTable(WidgetList):
def render_content(self):
r = TemplateIO(html=True)
add_element_widget = self.get_widget('add_element')
for widget in self.get_widgets():
if widget is add_element_widget:
continue
if not hasattr(widget, 'render_content_as_tr'):
r += widget.render()
r += htmltext('<table>')
r += self.get_widgets()[1].render_as_thead()
for widget in self.get_widgets():
if widget is add_element_widget:
continue
if hasattr(widget, 'render_content_as_tr'):
r += widget.render_content_as_tr()
r += htmltext('</table>')
if not self.readonly:
r += add_element_widget.render()
return r.getvalue()
def render(self):
r = TemplateIO(html=True)
classnames = '%s widget' % self.__class__.__name__
if hasattr(self, 'extra_css_class') and self.extra_css_class:
classnames += ' ' + self.extra_css_class
if self.get_error():
classnames += ' widget-with-error'
r += htmltext('<div class="%s">') % classnames
r += self.render_title(self.get_title())
r += htmltext('<div class="content">')
r += self.render_error(self.get_error())
r += self.render_content()
r += self.render_hint(self.get_hint())
r += htmltext('</div>')
r += htmltext('</div>')
return r.getvalue()
class TableRowWidget(CompositeWidget):
def __init__(self, name, value=None, **kwargs):
CompositeWidget.__init__(self, name, value, **kwargs)
for i, column in enumerate(self.columns):
self.add(StringWidget, name='col%s'%i, title=column, **kwargs)
class TableListRowsWidget(WidgetListAsTable):
readonly = False
def create_row_class(self, columns):
class klass(TableRowWidget): pass
setattr(klass, 'columns', columns)
return klass
def __init__(self, name, value=None, columns=None, min_rows=5, **kwargs):
table_row_class = self.create_row_class(columns)
widget_kwargs = {}
if kwargs.has_key('readonly'):
del kwargs['readonly']
self.readonly = True
widget_kwargs['readonly'] = 'readonly'
WidgetListAsTable.__init__(self, name, value, element_type=table_row_class,
element_kwargs=widget_kwargs, **kwargs)
self.columns = columns
def add_element(value=None):
name = "element%d" % len(self.element_names)
self.add(table_row_class, name, value=value, **widget_kwargs)
self.element_names.append(name)
while len(self.element_names) < min_rows:
add_element()
self.get_widget('added_elements').value += 1
self.set_value(value)
def parse(self, request=None):
WidgetListAsTable.parse(self, request=request)
if request is None:
request = get_request()
add_element_pushed = self.get_widget('add_element').parse()
if (request.form or request.get_method() == 'POST') and self.required:
if not self.value and not add_element_pushed:
self.set_error(self.REQUIRED_ERROR)
for row in self.value:
for column in row:
if column:
break
else:
continue
break
else:
if not add_element_pushed:
self.set_error(self.REQUIRED_ERROR)
return self.value
def _parse(self, request):
if self.readonly:
return
table = []
for i, row_name in enumerate(self.element_names):
row = []
row_widget = self.get_widget(row_name)
notnull = False
for j, column in enumerate(self.columns):
widget = row_widget.get_widget('col%s'%j)
row.append(widget.parse())
if row[-1]:
notnull = True
if notnull:
table.append(row)
self.value = table
def set_value(self, value):
self.value = value
if not value:
return
for i, row_name in enumerate(self.element_names):
widget_row = self.get_widget(row_name)
for j, column in enumerate(self.columns):
widget = widget_row.get_widget('col%s'%j)
try:
widget.set_value(value[i][j])
if not get_request().form.get(widget.get_name()):
get_request().form[widget.get_name()] = value[i][j]
except IndexError:
pass
class RankedItemsWidget(CompositeWidget):
readonly = False
def __init__(self, name, value=None, elements=None, **kwargs):
CompositeWidget.__init__(self, name, value, **kwargs)
self.element_names = {}
if kwargs.has_key('title'):
del kwargs['title']
if kwargs.has_key('readonly'):
if kwargs['readonly']:
self.readonly = True
del kwargs['readonly']
if kwargs.has_key('required'):
if kwargs['required']:
self.required = True
del kwargs['required']
self.randomize_items = False
if kwargs.has_key('randomize_items'):
if kwargs['randomize_items']:
self.randomize_items = True
del kwargs['randomize_items']
for v in elements:
if type(v) is tuple:
title = v[1]
key = v[0]
if type(key) is int:
name = 'element%d' % v[0]
elif type(key) in (str, htmltext):
name = str('element%s' % v[0])
key = str(key)
else:
raise NotImplementedError()
else:
title = v
key = v
name = 'element%d' % len(self.element_names.keys())
if value:
position = value.get(key)
else:
position = None
self.add(IntWidget, name, title=title, value=position, size=5,
required=False, **kwargs)
self.element_names[name] = key
if self.randomize_items:
random.shuffle(self.widgets)
if self.readonly:
def cmp_w(x, y):
if x.value is None and y.value is None:
return 0
if x.value is None:
return 1
if y.value is None:
return -1
return cmp(x.value, y.value)
self.widgets.sort(cmp_w)
if value:
# in readonly mode, we mark all fields as already parsed, to
# avoid getting them reinitialized on a has_error() call
for name in self.element_names:
self.get_widget(name)._parsed = True
self._parsed = True
def _parse(self, request):
values = {}
for name in self.element_names:
value = self.get(name)
if value is not None:
values[self.element_names[name]] = value
if value is not None and type(value) is not int:
self.get_widget(name).set_error(IntWidget.TYPE_ERROR)
self.value = values or None
def set_value(self, value):
self.value = value
if value:
for name in self.element_names:
self.get_widget(name).set_value(self.value.get(self.element_names[name]))
def render_content(self):
r = TemplateIO(html=True)
r += htmltext('<ul>')
for widget in self.get_widgets():
if widget.has_error():
r += htmltext('<li class="error"><label>')
else:
r += htmltext('<li><label>')
if self.readonly:
widget.attrs['disabled'] = 'disabled'
if widget.value:
r += htmltext('<input type="hidden" name="%s" value="%s" >') % (
widget.name, widget.value)
widget.name = widget.name + 'xx'
r += widget.render_content()
r += widget.title
r += htmltext('</label>')
r += htmltext('</li>')
r += htmltext('</ul>')
return r.getvalue()
class JsonpSingleSelectWidget(Widget):
def __init__(self, name, value=None, url=None, **kwargs):
Widget.__init__(self, name, value=value, **kwargs)
self.url = url
def render_content(self):
if self.value is None:
value = None
else:
value = htmlescape(self.value)
r = TemplateIO(html=True)
attrs = {'id': 'form_' + self.name}
r += htmltag('input', xml_end=True, type="hidden", name=self.name, value=value, **attrs)
attrs = {'id': 'form_' + self.name + '_display'}
if value and get_session().jsonp_display_values:
key = '%s_%s' % (self.url, value)
if key in get_session().jsonp_display_values:
attrs['value'] = get_session().jsonp_display_values.get(key)
r += htmltag('input', xml_end=True, type="hidden", name=self.name + '_display', **attrs)
initial_display_value = attrs.get('value')
get_response().add_javascript(['jquery.js', 'select2/select2.min.js'])
get_response().add_css_include('../js/select2/select2.css')
# init select2 widget
allowclear = ""
if not self.required:
allowclear = "placeholder: '...', allowClear: true,"
r += htmltext("""
<script id="script_%(id)s">
$(document).ready(function() {
$("#form_%(id)s").select2({
minimumInputLength: 1,
width: '20em',
%(allowclear)s
formatNoMatches: function () { return "%(nomatches)s"; },
formatInputTooShort: function (input, min) { return "%(tooshort)s"; },
formatLoadMore: function (pageNumber) { return "%(loadmore)s"; },
formatSearching: function () { return "%(searching)s"; },
ajax: {
""" % {'id': self.name,
'allowclear': allowclear,
'nomatches': _('No matches found'),
'tooshort': _('Please enter more characters'),
'loadmore': _('Loading more results...'),
'searching': _('Searching...')})
if '[' in self.url:
vars = get_publisher().substitutions.get_context_variables()
# skip variables that were not set (None)
vars = dict((x, y) for x, y in vars.items() if y is not None)
url = misc.get_variadic_url(self.url, vars, encode_query=False)
else:
url = self.url
if not '[var_' in url:
# if the url is not parametric, set the url directly
r += htmltext("""url: "%(url)s",""" % {'url': url})
else:
# otherwise keep that blank for now (see below)
r += htmltext("""url: null,""")
# setting up the select2 widget continues here
r += htmltext("""
dataType: 'jsonp',
data: function (term, page) {
return {
q: term,
page_limit: 10
};
},
results: function (data, page) { // parse the results into the format expected by Select2.
var more = (page * 10) < data.total; // whether or not there are more results available
// since we are using custom formatting functions we do not need to alter remote JSON data
return {results: data.data, more: more};
}
},
formatResult: function(result) { return result.text; }
});""")
if '[var_' in url:
# if this is a parametric url, store template url and hook to the
# appropriate onchange event to give the url to select2
r += htmltext("""
$("#form_%(id)s").data('select2').opts.wcs_base_url = '%(url)s';
""" % {'id': self.name, 'url': url})
variables = re.findall(r'\[(var_.+?)\]', url)
r += htmltext("""
function url_replace_%(id)s() {
var url = $("#form_%(id)s").data('select2').opts.wcs_base_url;""" % {'id': self.name})
for variable in variables:
r += htmltext("""
selector = '#' + $('#%(variable)s').data('valuecontainerid');
url = url.replace('[%(variable)s]', $(selector).val());""" % {'variable': variable})
r += htmltext("""
$("#form_%(id)s").data('select2').opts.ajax.url = url;
$("#form_%(id)s").data('select2').clear();
}
""" % {'id': self.name} )
for variable in variables:
r += htmltext("""
$('#%(variable)s').change(url_replace_%(id)s);
$('#%(variable)s').change();
""" % {'id': self.name, 'variable': variable})
# finish setting up select2, update the _display hidden field with the
# selected text
r += htmltext("""
$("#form_%(id)s").change(function() {
var text = $("#form_%(id)s").data('select2').selection.find('span').text();
$('#form_%(id)s_display').val(text);
});
""" % {'id': self.name})
if initial_display_value:
r += htmltext("""
$("#form_%(id)s").select2("data", {id: "%(value)s", text: "%(text)s"});
""" % {'id': self.name, 'value': self.value, 'text': initial_display_value})
r += htmltext("""});</script>""")
return r.getvalue()
def parse(self, request=None):
if request and request.form.get(self.name) and request.form.get(self.name + '_display'):
# store text value associated to the jsonp value
if not get_session().jsonp_display_values:
get_session().jsonp_display_values = {}
value = request.form.get(self.name)
display_value = request.form.get(self.name + '_display')
get_session().jsonp_display_values['%s_%s' % (self.url, value)] = display_value
return Widget.parse(self, request=request)
class AutocompleteStringWidget(WcsExtraStringWidget):
url = None
def __init__(self, *args, **kwargs):
WcsExtraStringWidget.__init__(self, *args, **kwargs)
if kwargs.get('url'):
self.url = kwargs.get('url')
def render_content(self):
get_response().add_javascript(['jquery.js', 'jquery-ui.js'])
get_response().add_css_include('../js/smoothness/jquery-ui-1.10.0.custom.min.css')
if '[' in self.url:
vars = get_publisher().substitutions.get_context_variables()
# skip variables that were not set (None)
vars = dict((x, y) for x, y in vars.items() if y is not None)
url = misc.get_variadic_url(self.url, vars, encode_query=False)
else:
url = self.url
r = TemplateIO(html=True)
r += WcsExtraStringWidget.render_content(self)
r += htmltext("""
<script id="script_%(id)s">
$(function() {
$("#form_%(id)s").autocomplete({
source: function( request, response ) {
$.ajax({
url: $("#form_%(id)s").data('uiAutocomplete').options.url,
dataType: "jsonp",
data: {
q: request.term
},
success: function( data ) {
response( $.map(data.data, function(item) {
return {label: item.text, value: item.label};
}));
}
});
},
minLength: 2,
open: function() {
$(this).removeClass("ui-corner-all").addClass("ui-corner-top");
},
close: function() {
$(this).removeClass("ui-corner-top").addClass("ui-corner-all");
}
});
""" % {'id': self.name})
if not '[var_' in url:
r += htmltext("""
$("#form_%(id)s").data('uiAutocomplete').options.url = '%(url)s';
""" % {'id': self.name, 'url': url})
if '[var_' in url:
# if this is a parametric url, store template url and hook to the
# appropriate onchange event to give the url to autocomplete
r += htmltext("""
$("#form_%(id)s").data('uiAutocomplete').options.wcs_base_url = '%(url)s';
""" % {'id': self.name, 'url': url})
variables = re.findall(r'\[(var_.+?)\]', url)
r += htmltext("""
function url_replace_%(id)s() {
var url = $("#form_%(id)s").data('uiAutocomplete').options.wcs_base_url;""" % {'id': self.name})
for variable in variables:
r += htmltext("""
selector = '#' + $('#%(variable)s').data('valuecontainerid');
url = url.replace('[%(variable)s]', $(selector).val());""" % {'variable': variable})
r += htmltext("""
$("#form_%(id)s").data('uiAutocomplete').options.url = url;
if ($("form_%(id)s").val() != $("form_%(id)s").attr('value'))
$("#form_%(id)s").val('');
}
""" % {'id': self.name} )
for variable in variables:
r += htmltext("""
$('#%(variable)s').change(url_replace_%(id)s);
$('#%(variable)s').change();
""" % {'id': self.name, 'variable': variable})
r += htmltext("""
});
</script>""")
return r.getvalue()
class ColourWidget(SingleSelectWidget):
def __init__(self, *args, **kwargs):
colours = ['%s%s%s' % x for x in itertools.product(('00', '66', '99', 'FF'), repeat=3)]
SelectWidget.__init__(self, options=colours, *args, **kwargs)
self.attrs['class'] = 'colour-picker'
def render_content(self):
get_response().add_javascript(['jquery.js', 'jquery.colourpicker.js'])
return SingleSelectWidget.render_content(self)
def _parse(self, request):
SingleSelectWidget._parse(self, request)
if self.value is not None:
self.value = self.value.upper()
class PasswordEntryWidget(CompositeWidget):
min_length = 0
max_length = 0
count_uppercase = 0
count_lowercase = 0
count_digit = 0
count_special = 0
confirmation = True
def __init__(self, name, value=None, **kwargs):
# hint will be displayed with pwd1 widget
hint = kwargs.pop('hint', None)
CompositeWidget.__init__(self, name, value, **kwargs)
self.min_length = kwargs.get('min_length', 0)
self.max_length = kwargs.get('max_length', 0)
self.count_uppercase = kwargs.get('count_uppercase', 0)
self.count_lowercase = kwargs.get('count_lowercase', 0)
self.count_digit = kwargs.get('count_digit', 0)
self.count_special = kwargs.get('count_special', 0)
self.confirmation = kwargs.get('confirmation', True)
confirmation_title = kwargs.get('confirmation_title') or _('Confirmation')
self.strength_indicator = kwargs.get('strength_indicator', True)
self.formats = kwargs.get('formats', ['sha1'])
if not self.attrs.get('readonly'):
self.add(PasswordWidget, name='pwd1', title='',
value='',
required=kwargs.get('required', False),
autocomplete='off',
hint=hint)
if self.confirmation:
self.add(PasswordWidget, name='pwd2', title=confirmation_title,
required=kwargs.get('required', False),
autocomplete='off')
else:
encoded_value = base64.encodestring(json.dumps(value))
if value:
fake_value = '*' * 8
else:
fake_value = ''
self.add(HtmlWidget, 'hashed', title=htmltext(
'<input value="%s" readonly="readonly">'\
'<input type="hidden" name="%s$encoded" value="%s"></input>' % (
fake_value, self.name, encoded_value)))
def render_content(self):
if self.attrs.get('readonly') or not self.strength_indicator:
return CompositeWidget.render_content(self)
get_response().add_javascript(['jquery.js', 'jquery.passstrength.js'])
r = TemplateIO(html=True)
r += CompositeWidget.render_content(self)
r += htmltext('''<script>
$(function() {
$('input[id="form_%(form_id)s"]').passStrengthify({
levels: ["%(veryweak)s", "%(veryweak)s", "%(weak)s", "%(weak)s", "%(moderate)s", "%(good)s", "%(strong)s", "%(verystrong)s"],
minimum: %(min_length)s,
labels: {
passwordStrength: "%(password_strength)s",
tooShort: "%(tooshort)s"
}
});
});
</script>''') % {'form_id': self.get_widget('pwd1').get_name(),
'min_length': self.min_length,
'veryweak': _('Very weak'),
'weak': _('Weak'),
'moderate': _('Moderate'),
'good': _('Good'),
'strong': _('Strong'),
'verystrong': _('Very strong'),
'password_strength': _('Password strength:'),
'tooshort': _('Too short')}
return r.getvalue()
def _parse(self, request):
CompositeWidget._parse(self, request)
if request.form.get('%s$encoded' % self.name):
self.value = json.loads(base64.decodestring(
request.form.get('%s$encoded' % self.name)))
return
pwd1 = self.get('pwd1') or ''
if not self.get_widget('pwd1'):
# we are in read-only mode, stop here.
return
set_errors = []
min_len = self.min_length
if len(pwd1) < min_len:
set_errors.append(_('Password is too short. It must be at least %d characters.') % min_len)
max_len = self.max_length
if max_len and len(pwd1) > max_len:
set_errors.append(_('Password is too long. It must be at most %d characters.') % max_len)
count = self.count_uppercase
if len(filter(lambda c: c.isupper(), pwd1)) < count:
set_errors.append(
ngettext('Password must contain an uppercase character.',
'Password must contain at least %(count)d uppercase characters.',
count) % {'count': count})
count = self.count_lowercase
if len(filter(lambda c: c.islower(), pwd1)) < count:
set_errors.append(
ngettext('Password must contain a lowercase character.',
'Password must contain at least %(count)d lowercase characters.',
count) % {'count': count})
count = self.count_digit
if len(filter(lambda c: c.isdigit(), pwd1)) < self.count_digit:
set_errors.append(
ngettext('Password must contain a digit.',
'Password must contain at least %(count)d digits.',
count) % {'count': count})
count = self.count_special
if len(filter(lambda c: not c.isalnum(), pwd1)) < count:
set_errors.append(
ngettext('Password must contain a special character.',
'Password must contain at least %(count)d special characters.',
count) % {'count': count})
if self.confirmation:
pwd2 = self.get('pwd2') or ''
if pwd1 != pwd2:
self.get_widget('pwd2').set_error(_('Passwords do not match'))
pwd1 = None
if set_errors:
self.get_widget('pwd1').set_error(' '.join(set_errors))
pwd1 = None
PASSWORD_FORMATS = {
'cleartext': lambda x: x,
'md5': lambda x: hashlib.md5(x).hexdigest(),
'sha1': lambda x: hashlib.sha1(x).hexdigest(),
}
if pwd1:
self.value = {}
for fmt in self.formats:
self.value[fmt] = PASSWORD_FORMATS[fmt](pwd1)
else:
self.value = None
class MapWidget(CompositeWidget):
def __init__(self, name, value=None, **kwargs):
CompositeWidget.__init__(self, name, value, **kwargs)
self.add(HiddenWidget, 'latlng', value=value)
widget = self.get_widget('latlng')
if value and not get_request().form.get(widget.name):
get_request().form[widget.name] = value
self.readonly = kwargs.pop('readonly', False)
self.kwargs = kwargs
def render_content(self):
get_response().add_javascript(['qommon.map.js'])
r = TemplateIO(html=True)
for widget in self.get_widgets():
r += widget.render()
attrs = {
'class': 'qommon-map',
'id': 'map-%s' % self.name,
}
if self.value:
attrs['data-init-lat'], attrs['data-init-lng'] = self.value.split(';')
if self.readonly:
attrs['data-readonly'] = 'true'
for attribute in ('initial_zoom', 'min_zoom', 'max_zoom'):
if attribute in self.kwargs and self.kwargs.get(attribute) is not None:
attrs['data-%s' % attribute] = self.kwargs.get(attribute)
if self.kwargs.get('default_position'):
attrs['data-def-lat'], attrs['data-def-lng'] = self.kwargs.get('default_position').split(';')
if self.kwargs.get('init_with_geoloc'):
attrs['data-init-with-geoloc'] = 1
r += htmltext('<div %s></div>' % ' '.join(['%s="%s"' % x for x in attrs.items()]))
return r.getvalue()
def _parse(self, request):
CompositeWidget._parse(self, request)
self.value = self.get('latlng')