Source code for publiforge.lib.form
"""Form validation and rendering library."""
from re import sub
import colander
from webhelpers2.html import tags, HTML, literal
from pyramid.httpexceptions import HTTPNotAcceptable
from .i18n import _
# =============================================================================
[docs]class SameAs(object):
# pylint: disable = too-few-public-methods
"""This class implements a ``colander`` validator to check if to fields are
identical."""
# -------------------------------------------------------------------------
def __init__(self, request, reference):
"""Constructor method."""
self.request = request
self.reference = reference
# -------------------------------------------------------------------------
def __call__(self, node, value):
"""This method raises a :class:`colander.Invalid` instance as an
exception value is not same as ``self.reference``.
:type node: colander.SchemaNode
:type value: cstruct
"""
if self.request.POST.get(self.reference) != value:
raise colander.Invalid(node, _('The two fields are not identical'))
# =============================================================================
[docs]def button(url, label='', src=None, title=None, class_='button'):
"""Output a link on a label and an image with a button aspect.
:param str url:
Target URL.
:param str label: (optional)
Label for roll over and ``alt``.
:param str src: (optional)
Image path.
:param str title: (optional)
Label for roll over.
:param str class_: (default='button')
The class attribute.
:rtype: str
HTML tag.
"""
if class_ == 'button' and not label and src:
class_ = None
return literal(u'<a href="{0}"{1}{2}>{3}{4}</a> '.format(
literal.escape(url), title and ' title="%s"' % title or '',
class_ and ' class="%s"' % class_ or '',
src and '<img src="%s" alt="%s"/>' % (src, label or title) or '',
label))
# =============================================================================
[docs]def grid_item(name, label, content, required=False, hint=None, error=None,
class_=None, tag='div', clear=False):
"""Display an item with label, hint and error message.
:param str name:
Input ID.
:param str label:
Label.
:param str content:
HTML content.
:param bool required: (default=False)
Indicate if this field is required.
:param str hint: (optional)
Help message.
:param str error: (optional)
Error message.
:param str class_: (optional)
The class attribute.
:param str tag: (default='div')
Tag which contains content, hint and error message.
:param bool clear: (default=False)
If ``True``, add a ``<div class="clear"/>`` at the end.
:rtype: str
This ouputs a structure such as:
.. code-block:: html
<div class="[class_]">
<label for="[name]"><em>[label]<strong>*</strong></em></label>
<tag>
[content]
<em>[hint]</em>
<strong class="error">[form.error(name)]</strong>
</tag>
<div class="clear"></div>
</div>
If ``class_`` is an empty string, ``'formItem'`` is used.
"""
# pylint: disable = too-many-arguments
if not content:
return ''
if class_ == '':
class_ = 'formItem'
class_ = class_ or ''
if error:
class_ += ' error'
return literal(
u'<div{class_}><label{name}><em>{label}{required}</em></label>'
u'<{tag}>{content}{hint}{error}</{tag}>{clear}</div>'.format(
class_=class_ and ' class="%s"' % class_ or '',
name=name and ' for="%s"' % name or '',
label=label or '',
required=HTML.strong('*') if required else '',
tag=tag,
content=content,
hint=HTML.em(' %s' % hint) if hint else '',
error=HTML.strong(' %s' % error) if error else '',
clear=clear and '<div class="clear"></div>' or ''))
# =============================================================================
[docs]class Form(object):
"""Form validation class."""
# pylint: disable = too-many-public-methods
# -------------------------------------------------------------------------
def __init__(self, request, schema=None, defaults=None, secure=True,
obj=None, force_defaults=False):
"""Constructor method."""
# pylint: disable = too-many-arguments
self.values = defaults \
if defaults and (not request.POST or force_defaults) else {}
self._request = request
self._schema = schema
self._secure = secure
self._errors = {}
self._special = [[], None]
self._validated = False
if obj is not None and schema is not None and not request.POST:
for field in [k.name for k in schema]:
if hasattr(obj, field):
self.values[field] = getattr(obj, field)
# -------------------------------------------------------------------------
[docs] def validate(self, obj=None):
"""Check if the form is validated.
:param object obj: (optional)
Object to fill.
:rtype: bool
:return:
``True`` if validated.
"""
# Something to do?
if not self._request.POST:
return False
if self._validated:
return not self._errors
# Cross-site request forgery protection
if self._secure and self._request.POST.get('_csrf') \
!= self._request.session.get_csrf_token():
raise HTTPNotAcceptable()
# Schema validation
params = dict(self._request.POST.items())
if self._schema:
try:
self.values = self._schema.deserialize(params)
except colander.Invalid as err:
self._errors = {}
for child in err.children:
self._errors[child.node.name] = child.messages()
else:
self.values.update(params)
# Fill object
if obj is not None and not self._errors:
for field in self.values:
if hasattr(obj, field):
setattr(obj, field, self.values[field])
self._validated = True
return len(self._errors) == 0
# -------------------------------------------------------------------------
[docs] def has_error(self, name=None):
"""Return ``True`` if field ``name`` has an error.
:param str name: (optional)
Input ID.
:rtype: bool
"""
return bool(name is None and self._errors) or name in self._errors
# -------------------------------------------------------------------------
[docs] def set_error(self, name, message):
"""Set an error message for field ``name``.
:param str name:
Input ID.
:param str message:
Error message.
"""
if name in self._errors:
self._errors[name].append(message)
else:
self._errors[name] = [message]
# -------------------------------------------------------------------------
[docs] def error(self, name):
"""Return error message for field ``name``.
:param str name:
Input ID.
:rtype: str
:return:
Translated error message.
"""
if name not in self._errors:
return ''
return ' ; '.join([self._request.localizer.translate(error)
for error in self._errors[name]])
# -------------------------------------------------------------------------
[docs] def static(self, name):
"""The field ``name`` will not be updated by the form.
:param str name:
Name of field to set static.
"""
if name not in self._special[0]:
self._special[0].append(name)
# -------------------------------------------------------------------------
[docs] def forget(self, prefix):
"""Fields beginning by ``prefix`` are forgotten when the page is
refreshed.
:param str prefix:
Prefix to select fields.
"""
self._special[1] = prefix
# -------------------------------------------------------------------------
[docs] @classmethod
def make_safe_id(cls, name):
"""Make a string safe for including in an id attribute
:param str name:
String to transform.
:rtype: str
"""
return sub(r'(?!-)\W', '', sub(r'\s', '_', name)).lower()
# -------------------------------------------------------------------------
[docs] def begin(self, url=None, multipart=False, **attrs):
"""Ouput the ``<form>`` tag.
:param str url: (optional)
URL to submit form, by default, the current URL.
:param bool multipart: (default=False)
If set to ``True``, the enctype is set to ``multipart/form-data``.
:param dict attrs:
Keyworded arguments for ``webhelpers2.html.tags`` object.
:rtype: str
:return:
HTML tag.
"""
html = tags.form(
url or self._request.path_qs, 'post', multipart, **attrs)
if self._secure:
token = self._request.session.get_csrf_token() \
or self._request.session.new_csrf_token()
html += HTML.div(self.hidden('_csrf', token), class_='hidden')
return html
# -------------------------------------------------------------------------
# -------------------------------------------------------------------------
[docs] @classmethod
def submit(cls, name, label=None, class_='button', **attrs):
"""Output a submit button with the label as the caption.
:param str name:
Input ID.
:param str label: (optional)
Button caption.
:param str class_: (default='button')
The class attribute.
:param dict attrs:
Keyworded arguments for ``webhelpers2.html.tags`` object.
:rtype: str
:return:
HTML tag.
"""
return tags.submit(name, label, class_=class_, **attrs)
# -------------------------------------------------------------------------
[docs] @classmethod
def submit_image(cls, name, label, src, class_=None):
"""Output an image submit button.
:param str name:
Input ID.
:param str label:
Label for roll over and ``alt``.
:param str src:
Image path.
:param str class_:
The class attribute.
:rtype: str
:return:
HTML tag.
"""
label = label.replace('"', "'")
return literal(
u'<input type="image" name="{0}" src="{1}" title="{2}"'
' alt="{2}"{3}/>'.format(
name, src, label or name,
class_ and ' class="%s"' % class_ or ''))
# -------------------------------------------------------------------------
[docs] @classmethod
def submit_cancel(cls, label):
"""Output a cancel submit button.
:param str label:
Label for roll over and ``alt``.
:rtype: str
:return:
HTML tag.
"""
label = label.replace('"', "'")
return literal(
u'<input type="image" name="ccl!" '
'src="/Static/Images/action_cancel.png" title="{0}" '
'alt="{0}"/>'.format(label))
# -------------------------------------------------------------------------
[docs] @classmethod
def button(cls, url, label='', src=None, title=None, class_='button'):
"""Output a link on a label and an image with a button aspect.
See :func:`button`.
"""
return button(url, label, src, title, class_)
# -------------------------------------------------------------------------
[docs] @classmethod
def grid_item(cls, label, content, required=False, hint=None,
error=None, class_=None, tag='div', clear=False):
"""Output an item with label, hint and error message.
See :func:`grid_item`.
"""
# pylint: disable = too-many-arguments
return grid_item(
None, label, content, required, hint, error, class_, tag, clear)
# -------------------------------------------------------------------------
# -------------------------------------------------------------------------
[docs] def text(self, name, value=None, **attrs):
"""Output a standard text field.
:param str name:
Input ID.
:param str value: (optional)
Default value.
:param dict attrs:
Keyworded arguments for ``webhelpers2.html.tags`` object.
:param dict attrs:
Keyworded arguments for ``webhelpers2.html.tags`` object.
:rtype: str
:return:
HTML tag.
"""
return tags.text(name, self._value(name, value), **attrs)
# -------------------------------------------------------------------------
[docs] def password(self, name, value=None, **attrs):
"""Output a password field.
This method takes the same options as text().
"""
return tags.password(name, self._value(name, value), **attrs)
# -------------------------------------------------------------------------
[docs] def checkbox(self, name, value=u'1', checked=False, **attrs):
"""Output a check box.
:param str name:
Input ID.
:param str value: (default=u'1')
The value to return to the application if the box is checked.
:param bool checked: (default=False)
``True`` if the box should be initially checked.
:param dict attrs:
Keyworded arguments for ``webhelpers2.html.tags`` object.
:rtype: str
:return:
HTML tag.
"""
return tags.checkbox(
name, value, checked or self._value(name), **attrs)
# -------------------------------------------------------------------------
[docs] def custom_checkbox(
self, name, value=u'1', checked=False, class_=None, **attrs):
"""Output a check box followed by an empty label to customize the
aspect of the box.
:param str name:
Input ID.
:param str value: (default=u'1')
The value to return to the application if the box is checked.
:param bool checked: (default=False)
``True`` if the box should be initially checked.
:param str class_: (default='wbCustomCheckbox')
The class attribute.
:param dict attrs:
Keyworded arguments for ``webhelpers2.html.tags`` object.
:rtype: str
:return:
HTML tag.
"""
if class_ is None:
class_ = 'wbCustomCheckbox'
return literal('{0}{1}'.format(
tags.checkbox(
name, value, checked or self._value(name), class_=class_,
**attrs),
'<label for="{0}" class="{1}"> </label>'.format(
self.make_safe_id(name), class_)))
# -------------------------------------------------------------------------
[docs] def radio(self, name, value, checked=False, **attrs):
"""Output a radio button.
:param str name:
Input ID.
:param str value: (default='1')
The value to return to the application if the radio is checked.
:param bool checked: (default=False)
``True`` if the radio should be initially checked.
:param str label: (optional)
A text label to display to the right of the radio.
:param dict attrs:
Keyworded arguments for ``webhelpers2.html.tags`` object.
:rtype: str
:return:
HTML tag.
"""
return tags.radio(
name, value, checked or value == self._value(name), **attrs)
# -------------------------------------------------------------------------
[docs] def select(self, name, selected_values, options, autosubmit=False,
**attrs):
"""Output a dropdown selection box.
:param str name:
Input ID.
:type selected_value: :class:`str` or :class:`list`
:param selected_value:
A string or list of strings or integers giving the value(s) that
should be preselected.
:type options:
(list of :class:`str`, :class:`int` or ``(value, label)`` pairs)
:param options:
The label will be shown on the form; the option will be returned to
the application if that option is chosen. If you pass a ``string``
or ``int`` instead of a ``2-tuple``, it will be used for both the
value and the label.
:param bool autosubmit: (default=False)
If ``True``, it adds ``onchange="submit()"`` attribute.
:param dict attrs:
Keyworded arguments for ``webhelpers2.html.tags`` object.
:rtype: str
:return:
HTML tag.
"""
if not options:
return ''
opts = []
translate = self._request.localizer.translate
for opt in options:
if isinstance(opt, tuple):
opts.append(
tags.Option(translate(opt[1]), u'{0}'.format(opt[0])))
else:
opts.append(tags.Option(u'{0}'.format(opt)))
return tags.select(
name, u'{0}'.format(self._value(name, selected_values)),
tags.Options(opts),
onchange='submit()' if autosubmit else None, **attrs)
# -------------------------------------------------------------------------
[docs] def upload(self, name, value=None, **attrs):
"""Output a file upload field.
:param str name:
Input ID.
:param str value: (optional)
Default value.
:param dict attrs:
Keyworded arguments for ``webhelpers2.html.tags`` object.
:rtype: str
:return:
HTML tag.
"""
return tags.file(name, self._value(name, value), **attrs)
# -------------------------------------------------------------------------
[docs] def textarea(self, name, content='', **attrs):
"""Output a text input area.
:param str name:
Input ID.
:param str content: (optional)
Default value.
:param dict attrs:
Keyworded arguments for ``webhelpers2.html.tags`` object.
:rtype: str
:return:
HTML tag.
"""
return tags.textarea(name, self._value(name, content), **attrs)
# -------------------------------------------------------------------------
[docs] def grid_text(self, name, label, required=False, hint=None, class_='',
clear=False, **attrs):
"""Output a standard text field in a CSS grid layout.
:param str name:
Input ID.
:param str label:
Label.
:param bool required: (default=False)
Indicate if this field is required.
:param str hint: (optional)
Help message.
:param str class_: (optional)
The class attribute.
:param bool clear: (default=False)
If ``True``, add a ``<div class="clear"/>`` at the end.
:param dict attrs:
Keyworded arguments for ``webhelpers2.html.tags`` object.
:rtype: str
:return:
Output a grid layout.
"""
# pylint: disable = too-many-arguments
return grid_item(
name, label, self.text(name, **attrs), required, hint,
self.error(name), class_, clear=clear)
# -------------------------------------------------------------------------
[docs] def grid_password(self, name, label, required=False, hint=None, class_='',
clear=False, **attrs):
"""Output a password field in a CSS grid layout.
This method takes the same options as grid_text().
"""
# pylint: disable = too-many-arguments
return grid_item(
name, label, self.password(name, **attrs), required, hint,
self.error(name), class_, clear=clear)
# -------------------------------------------------------------------------
[docs] def grid_checkbox(self, name, label, required=False, hint=None, class_='',
clear=False, **attrs):
"""Output a check box in a CSS grid layout.
:param str name:
Input ID.
:param str label:
Label.
:param bool required: (default=False)
Indicate if this field is required.
:param str hint: (optional)
Help message.
:param str class_: (optional)
The class attribute.
:param bool clear: (default=False)
If ``True``, add a ``<div class="clear"/>`` at the end.
:param dict attrs:
Keyworded arguments for ``webhelpers2.html.tags`` object.
:rtype: str
:return:
Output a grid layout.
"""
# pylint: disable = too-many-arguments
return grid_item(
name, label, self.checkbox(name, **attrs), required, hint,
self.error(name), class_, tag='span', clear=clear)
# -------------------------------------------------------------------------
[docs] def grid_custom_checkbox(self, name, label, required=False, hint=None,
clear=False, class_='formItem', **attrs):
"""Output a custom check box in a CSS grid layout.
:param str name:
Input ID.
:param str label:
Label.
:param bool required: (default=False)
Indicate if this field is required.
:param str hint: (optional)
Help message.
:param bool clear: (default=False)
If ``True``, add a ``<div class="wbClear"/>`` at the end.
:param str class_: (optional)
The class attribute.
:param dict attrs:
Keyworded arguments for ``webhelpers2.html.tags`` object.
:rtype: str
:return:
Output a grid layout.
"""
# pylint: disable = too-many-arguments
return grid_item(
name, label, self.custom_checkbox(name, **attrs), required, hint,
self.error(name), class_, tag='span', clear=clear)
# -------------------------------------------------------------------------
[docs] def grid_select(self, name, label, options, autosubmit=False,
required=False, hint=None, class_='', clear=False,
**attrs):
"""Output a dropdown selection box in a CSS grid layout.
:param str name:
Input ID.
:param str label:
Label.
:type options:
(list of :class:`str`, :class:`int` or ``(value, label)`` pairs)
:param options:
Values in the dropdown list.
:param bool autosubmit: (default=False)
If ``True``, it adds ``onchange="submit()"`` attribute.
:param bool required: (default=False)
Indicate if this field is required.
:param str hint: (optional)
Help message.
:param str class_: (optional)
The class attribute.
:param bool clear: (default=False)
If ``True``, add a ``<div class="clear"/>`` at the end.
:param dict attrs:
Keyworded arguments for ``webhelpers2.html.tags`` object.
:rtype: str
:return:
Output a grid layout.
"""
# pylint: disable = too-many-arguments
return grid_item(
name, label, self.select(name, None, options, autosubmit, **attrs),
required, hint, self.error(name), class_, clear=clear)
# -------------------------------------------------------------------------
[docs] def grid_upload(self, name, label, required=False, hint=None, class_='',
clear=False, **attrs):
"""Output a file upload field in a CSS grid layout.
:param str name:
Input ID.
:param str label:
Label.
:param bool required: (default=False)
Indicate if this field is required.
:param str hint: (optional)
Help message.
:param str class_: (optional)
The class attribute.
:param bool clear: (default=False)
If ``True``, add a ``<div class="clear"/>`` at the end.
:param dict attrs:
Keyworded arguments for ``webhelpers2.html.tags`` object.
:rtype: str
:return:
Output a grid layout.
"""
# pylint: disable = too-many-arguments
return grid_item(
name, label, self.upload(name, **attrs), required, hint,
self.error(name), class_, clear=clear)
# -------------------------------------------------------------------------
[docs] def grid_textarea(self, name, label, required=False, hint=None, class_='',
clear=False, **attrs):
"""Output a text input area in a CSS grid layout.
:param str name:
Input ID.
:param str label:
Label.
:param bool required: (default=False)
Indicate if this field is required.
:param str hint: (optional)
Help message.
:param str class_: (optional)
The class attribute.
:param bool clear: (default=False)
If ``True``, add a ``<div class="clear"/>`` at the end.
:param dict attrs:
Keyworded arguments for ``webhelpers2.html.tags`` object.
:rtype: str
:return:
Output a grid layout.
"""
# pylint: disable = too-many-arguments
return grid_item(
name, label, self.textarea(name, **attrs), required, hint,
self.error(name), class_, clear=clear)
# -------------------------------------------------------------------------
def _value(self, name, default=None):
"""Return the best value for the field ``name``.
:param str name:
Input ID.
:param str default: (optional)
Default value.
:rtype: str
"""
if name not in self._special[0] and \
(not self._special[1] or not name.startswith(self._special[1])) and\
name in self._request.POST:
return self._request.POST[name]
elif name in self.values:
return self.values[name]
return default