import tw2.core as twc
import itertools
import webob
import cgi
import math
import six
#--
# Basic Fields
#--
[docs]class TextFieldMixin(twc.Widget):
'''Misc mixin class with attributes for textual input fields'''
maxlength = twc.Param('Maximum length of field',
attribute=True, default=None) #: Maximum length of the field
placeholder = twc.Param('Placeholder text (HTML5 only)',
attribute=True, default=None) #: Placeholder text, until user writes something.
[docs]class PostlabeledInputField(InputField):
""" Inherits InputField, but with a text
label that follows the input field """
text = twc.Param('Text to display after the field.') #: Text to display in the label after the field.
text_attrs = twc.Param('Dict of attributes to inject into the label.',
default={}) #: Attributes of the label displayed after to the field.
template = "tw2.forms.templates.postlabeled_input_field"
[docs]class TextField(TextFieldMixin, InputField):
"""A simple text field where to input a single line of text"""
size = twc.Param('Size of the field', default=None, attribute=True) #: Add size attribute to the HTML field.
type = 'text'
[docs]class TextArea(TextFieldMixin, FormField):
"""A multiline text area"""
rows = twc.Param('Number of rows', default=None, attribute=True) #: Add a rows= attribute to the HTML textarea
cols = twc.Param('Number of columns', default=None, attribute=True) #: Add a cols= attribute to the HTML textarea
template = "tw2.forms.templates.textarea"
[docs]class CheckBox(InputField):
"""A single checkbox.
Its value will be True or Folse if selected or not."""
type = "checkbox"
validator = twc.BoolValidator
def _validate(self, value, state=None):
# Since twc.BoolValidator returns None if no value is present
# (which is the common case if a HTML checkbox is not checked)
# we explicitly convert to bool again here
self.value = bool(super(CheckBox, self)._validate(value, state))
return self.value
[docs] def prepare(self):
super(CheckBox, self).prepare()
try:
checked = self.validator.to_python(self.value)
except twc.validation.catch:
# If if fails conversion/validation it is considered to be false
checked = False
self.safe_modify('attrs')
self.attrs['checked'] = checked and 'checked' or None
self.attrs['value'] = None
[docs]class PasswordField(TextFieldMixin, InputField):
"""
A password field. This never displays a value passed into the widget,
although it does redisplay entered values on validation failure. If no
password is entered, this validates as EmptyField.
"""
type = 'password'
[docs] def prepare(self):
super(PasswordField, self).prepare()
self.safe_modify('attrs')
self.attrs['value'] = None
def _validate(self, value, state=None):
value = super(PasswordField, self)._validate(value, state)
return value or twc.EmptyField
[docs]class FileValidator(twc.Validator):
"""Validate a file upload field
`extension`
Allowed extension for the file
"""
extension = None
msgs = {
'required': ('file_required', 'Select a file'),
'badext': "File name must have '$extension' extension",
}
def _validate_python(self, value, outer_call=None):
if isinstance(value, cgi.FieldStorage):
if self.required and not getattr(value, 'filename', None):
raise twc.ValidationError('required', self)
if (self.extension is not None
and not value.filename.endswith(self.extension)):
raise twc.ValidationError('badext', self)
elif value:
raise twc.ValidationError('corrupt', self)
elif self.required:
raise twc.ValidationError('required', self)
[docs]class FileField(InputField):
"""
A field for uploading files. The returned object has (at least) two
properties of note:
* filename:
the name of the uploaded file
* value:
a bytestring of the contents of the uploaded file, suitable for being
written to disk
"""
type = "file"
validator = FileValidator
[docs] def prepare(self):
self.value = None
super(FileField, self).prepare()
def _validate(self, value, state=None):
try:
return super(FileField, self)._validate(value, state)
except twc.ValidationError:
self.value = None
raise
[docs]class HiddenField(InputField):
"""
A hidden field.
Typically this is used to bring around in the form
values that the user should not be able to modify
or see. Like the ID of the entity edited by the form.
"""
type = 'hidden'
[docs]class IgnoredField(HiddenField):
"""
A hidden field. The value is never included in validated data.
"""
def _validate(self, value):
super(IgnoredField, self)._validate(value)
return twc.EmptyField
[docs]class LabelField(InputField):
"""
A read-only label showing the value of a field. The value is stored in a
hidden field, so it remains through validation failures. However, the
value is never included in validated data.
"""
type = 'hidden'
template = "tw2.forms.templates.label_field"
escape = twc.Param('Whether text shall be html-escaped or not', default=True)
validator = twc.BlankValidator
[docs]class LinkField(twc.Widget):
"""
A dynamic link based on the value of a field. If either *link* or *text*
contain a $, it is replaced with the field value. If the value is None,
and there is no default, the entire link is hidden.
"""
template = "tw2.forms.templates.link_field"
link = twc.Param('Link target', default='')
text = twc.Param('Link text', default='')
value = twc.Param("Value to replace $ with in the link/text")
escape = twc.Param('Whether text shall be html-escaped or not', default=True)
validator = twc.BlankValidator
[docs] def prepare(self):
super(LinkField, self).prepare()
self.safe_modify('attrs')
self.attrs['href'] = self.link.replace('$', six.text_type(self.value or ''))
if '$' in self.text:
self.text = \
self.value and \
self.text.replace('$', six.text_type(self.value)) or \
''
#--
# HTML5 Mixins
#--
[docs]class HTML5PatternMixin(twc.Widget):
'''HTML5 mixin for input field regex pattern matching
See http://html5pattern.com/ for common patterns.
TODO: Configure server-side validator
'''
pattern = twc.Param('JavaScript regex to match field with',
attribute=True, default=None)
title = twc.Param('Tooltip and message shown on invalid value',
attribute=True, default=None)
[docs]class HTML5MinMaxMixin(twc.Widget):
'''HTML5 mixin for input field value limits
TODO: Configure server-side validator
'''
min = twc.Param('Minimum value for field',
attribute=True, default=None)
max = twc.Param('Maximum value for field',
attribute=True, default=None)
[docs]class HTML5StepMixin(twc.Widget):
'''HTML5 mixin for input field step size'''
step = twc.Param('The step size between numbers',
attribute=True, default=None)
[docs]class HTML5NumberMixin(HTML5MinMaxMixin, HTML5StepMixin):
'''HTML5 mixin for number input fields'''
pass
#--
# HTML5 Fields
#--
[docs]class EmailField(TextField):
'''An email input field (HTML5 only).
Will fallback to a normal text input field on browser not supporting HTML5.
'''
type = 'email'
validator = twc.EmailValidator
[docs]class UrlField(TextField):
'''An url input field (HTML5 only).
Will fallback to a normal text input field on browser not supporting HTML5.
'''
type = 'url'
validator = twc.UrlValidator
[docs]class NumberField(HTML5NumberMixin, TextField):
'''A number spinbox (HTML5 only).
Will fallback to a normal text input field on browser not supporting HTML5.
'''
type = 'number'
[docs]class RangeField(HTML5NumberMixin, TextField):
'''A number slider (HTML5 only).
Will fallback to a normal text input field on browser not supporting HTML5.
'''
type = 'range'
[docs]class SearchField(TextField):
'''A search box (HTML5 only).
Will fallback to a normal text input field on browser not supporting HTML5.
'''
type = 'search'
[docs]class ColorField(TextField):
'''A color picker field (HTML5 only).
Will fallback to a normal text input field on browser not supporting HTML5.
'''
type = 'color'
#--
# Selection fields
#--
[docs]class SelectionField(FormField):
"""
Base class for single and multiple selection fields.
The `options` parameter must be a list; it can take several formats:
* A list of values, e.g.
``['', 'Red', 'Blue']``
* A list of (code, value) tuples, e.g.
``[(0, ''), (1, 'Red'), (2, 'Blue')]``
* A mixed list of values and tuples. If the code is not specified, it
defaults to the value. e.g.
``['', (1, 'Red'), (2, 'Blue')]``
* Attributes can be specified for individual items, e.g.
``[(1, 'Red', {'style':'background-color:red'})]``
* A list of groups, e.g.
``[('group1', [(1, 'Red')]), ('group2', ['Pink', 'Yellow'])]``
Setting ``value`` before rendering will set the default displayed value on
the page. In ToscaWidgets1, this was accomplished by setting ``default``.
That is no longer the case.
"""
options = twc.Param('Options to be displayed') #: List of options to pick from in the form ``[(id, text), (id, text), ...]``
prompt_text = twc.Param('Text to prompt user to select an option.',
default=None) #: Prompt to display when no option is selected. Set to ``None`` to disable this.
selected_verb = twc.Variable(default='selected')
field_type = twc.Variable(default=False)
grouped_options = twc.Variable()
[docs] def prepare(self):
super(SelectionField, self).prepare()
options = self.options
self.options = []
self.grouped_options = []
counter = itertools.count(0)
for optgroup in self._iterate_options(options):
opts = []
group = isinstance(optgroup[1], (list, tuple))
for option in self._iterate_options(
group and optgroup[1] or [optgroup]):
if len(option) is 2:
option_attrs = {}
elif len(option) is 3:
option_attrs = dict(option[2])
option_attrs['value'] = option[0]
if self.field_type:
option_attrs['type'] = self.field_type
option_attrs['name'] = self.compound_id
option_attrs['id'] = ':'.join([
self.compound_id, str(six.advance_iterator(counter))
])
if self._opt_matches_value(option[0]):
option_attrs[self.selected_verb] = self.selected_verb
opts.append((option_attrs, option[1]))
self.options.extend(opts)
if group:
self.grouped_options.append((six.text_type(optgroup[0]), opts))
if not self.grouped_options:
self.grouped_options = [(None, self.options)]
if self.prompt_text is not None:
self.grouped_options.insert(0, (None, [({'value': ''}, self.prompt_text)]))
def _opt_matches_value(self, opt):
return six.text_type(opt) == six.text_type(self.value)
def _iterate_options(self, optlist):
for option in optlist:
if not isinstance(option, (tuple, list)):
yield (option, option)
else:
yield option
[docs]class MultipleSelectionField(SelectionField):
item_validator = twc.Param('Validator that applies to each item',
default=None) #: Validator that has to be applied to each item.
[docs] def prepare(self):
if not self.value:
self.value = []
if not isinstance(self.value, (list, tuple)):
self.value = [self.value]
if not hasattr(self, '_validated') and self.item_validator:
self.value = [
self.item_validator.from_python(v) for v in self.value
]
super(MultipleSelectionField, self).prepare()
def _opt_matches_value(self, opt):
return six.text_type(opt) in [six.text_type(v) for v in self.value]
def _validate(self, value, state=None):
value = value or []
if not isinstance(value, (list, tuple)):
value = [value]
if self.validator:
self.validator.to_python(value, state)
if self.item_validator:
value = [twc.safe_validate(self.item_validator, v) for v in value]
self.value = [v for v in value if v is not twc.Invalid]
return self.value
[docs]class SingleSelectField(SelectionField):
"""Specialised :class:`SelectionField` to pick one element from a list of options."""
template = "tw2.forms.templates.select_field"
prompt_text = ''
[docs]class MultipleSelectField(MultipleSelectionField):
"""Specialised :class:`SelectionField` to pick multiple elements from a list of options."""
size = twc.Param('Number of visible options', default=None, attribute=True) #: Number of options to show
multiple = twc.Variable(attribute=True, default='multiple')
template = "tw2.forms.templates.select_field"
[docs]class SelectionList(SelectionField):
selected_verb = "checked"
template = "tw2.forms.templates.selection_list"
name = None
[docs]class SeparatedSelectionTable(SelectionList):
template = "tw2.forms.templates.separated_selection_table"
[docs]class CheckBoxList(SelectionList, MultipleSelectionField):
field_type = "checkbox"
[docs]class SelectionTable(SelectionField):
selected_verb = "checked"
template = "tw2.forms.templates.selection_table"
cols = twc.Param('Number of columns', default=1)
options_rows = twc.Variable()
grouped_options_rows = twc.Variable()
name = None
def _group_rows(self, seq, size):
if not hasattr(seq, 'next'):
seq = iter(seq)
while True:
chunk = []
try:
for i in range(size):
chunk.append(six.advance_iterator(seq))
yield chunk
except StopIteration:
if chunk:
yield chunk
break
[docs] def prepare(self):
super(SelectionTable, self).prepare()
self.options_rows = self._group_rows(self.options, self.cols)
self.grouped_options_rows = [
(g, self._group_rows(o, self.cols))
for g, o in self.grouped_options
]
[docs]class VerticalSelectionTable(SelectionField):
field_type = twc.Variable(default=True)
selected_verb = "checked"
template = "tw2.forms.templates.vertical_selection_table"
cols = twc.Param(
'Number of columns. If the options are grouped, this is overidden.',
default=1)
options_rows = twc.Variable()
def _gen_row_single(self, single, cols):
row_count = int(math.ceil(float(len(single)) / float(cols)))
# This shouldn't really need spacers. It's hackish.
# (Problem: 4 items in a 3 column table)
spacer_count = (row_count * cols) - len(single)
single.extend([(None, None)] * spacer_count)
col_iters = []
for i in range(cols):
start = i * row_count
col_iters.append(iter(single[start:start + row_count]))
while True:
row = []
try:
for col_iter in col_iters:
row.append(six.advance_iterator(col_iter))
yield row
except StopIteration:
if row:
yield row
break
def _gen_row_grouped(self, grouped_options):
row_count = max([len(o) for g, o in grouped_options])
col_iters = []
for g, o in grouped_options:
spacer_count = row_count - len(o)
o.extend([(None, None)] * spacer_count)
col_iters.append(hasattr(o, 'next') and o or iter(o))
while True:
row = []
try:
for col_iter in col_iters:
row.append(six.advance_iterator(col_iter))
yield row
except StopIteration:
if row:
yield row
break
[docs] def prepare(self):
super(VerticalSelectionTable, self).prepare()
if self.grouped_options[0][0]:
self.options_rows = self._gen_row_grouped(self.grouped_options)
else:
self.options_rows = self._gen_row_single(self.options, self.cols)
[docs]class CheckBoxTable(SelectionTable,
MultipleSelectionField):
field_type = 'checkbox'
[docs]class SeparatedCheckBoxTable(SeparatedSelectionTable,
MultipleSelectionField):
field_type = 'checkbox'
[docs]class VerticalCheckBoxTable(VerticalSelectionTable):
field_type = 'checkbox'
multiple = True
#--
# Layout widgets
#--
[docs]class BaseLayout(twc.CompoundWidget):
"""
The following CSS classes are used, on the element containing
both a child widget and its label.
`odd` / `even`
On alternating rows. The first row is odd.
`required`
If the field is a required field.
`error`
If the field contains a validation error.
"""
label = twc.ChildParam(
'Label for the field. Auto generates this from the ' +
'id; None supresses the label.',
default=twc.Auto)
help_text = twc.ChildParam('A longer description of the field',
default=None)
hover_help = twc.Param('Whether to display help text as hover tips',
default=False)
container_attrs = twc.ChildParam(
'Extra attributes to include in the element containing ' +
'the widget and its label.',
default={})
resources = [twc.CSSLink(modname='tw2.forms', filename='static/forms.css')]
@property
def children_hidden(self):
return [c for c in self.children if isinstance(c, HiddenField)]
@property
def children_non_hidden(self):
return [c for c in self.children if not isinstance(c, HiddenField)]
@property
def rollup_errors(self):
errors = [
c.error_msg for c in self.children
if isinstance(c, HiddenField) and c.error_msg
]
if self.error_msg:
errors.insert(0, self.error_msg)
return errors
[docs] def prepare(self):
super(BaseLayout, self).prepare()
for c in self.children:
if c.label is twc.Auto:
c.label = c.id and twc.util.name2label(c.id) or ''
[docs]class TableLayout(BaseLayout):
__doc__ = """
Arrange widgets and labels in a table.
""" + BaseLayout.__doc__
template = "tw2.forms.templates.table_layout"
[docs]class ListLayout(BaseLayout):
__doc__ = """
Arrange widgets and labels in a list.
""" + BaseLayout.__doc__
template = "tw2.forms.templates.list_layout"
[docs]class RowLayout(BaseLayout):
"""
Arrange widgets in a table row. This is normally only useful as a child to
GridLayout.
"""
resources = [twc.Link(id='error', modname='tw2.forms',
filename='static/dialog-warning.png'),
]
template = "tw2.forms.templates.row_layout"
[docs] def prepare(self):
row_class = (self.repetition % 2 and 'even') or 'odd'
if not self.css_class or row_class not in self.css_class:
self.css_class = ' '.join((
self.css_class or '', row_class
)).strip()
super(RowLayout, self).prepare()
[docs]class StripBlanks(twc.Validator):
def any_content(self, val):
if isinstance(val, list):
for v in val:
if self.any_content(v):
return True
return False
elif isinstance(val, dict):
for k in val:
if k == 'id':
continue
if self.any_content(val[k]):
return True
return False
elif isinstance(val, cgi.FieldStorage):
return bool(val.filename)
else:
return bool(val)
[docs] def to_python(self, value, state=None):
value = value or []
if not isinstance(value, list):
raise twc.ValidationError('corrupt', self)
return [v for v in value if self.any_content(v)]
[docs]class GridLayout(twc.RepeatingWidget):
""" Arrange labels and multiple rows of widgets in a grid. """
child = RowLayout
children = twc.Required
template = "tw2.forms.templates.grid_layout"
def _validate(self, value, state=None):
return super(GridLayout, self)._validate(
StripBlanks().to_python(value, state), state
)
[docs]class Spacer(FormField):
""" A blank widget, used to insert a blank row in a layout. """
template = "tw2.forms.templates.spacer"
id = None
label = None
def _validate(self, value, state=None):
return twc.EmptyField
[docs]class Label(twc.Widget):
"""
A textual label. This disables any label that would be displayed by
a parent layout.
"""
template = 'tw2.forms.templates.label'
text = twc.Param('Text to appear in label')
escape = twc.Param('Whether text shall be html-escaped or not', default=True)
label = None
id = None
def _validate(self, value, state=None):
return twc.EmptyField
[docs]class FieldSet(twc.DisplayOnlyWidget):
"""
A field set. It's common to pass a TableLayout or ListLayout
widget as the child.
"""
template = "tw2.forms.templates.fieldset"
legend = twc.Param('Text for the legend', default=None)
id_suffix = 'fieldset'
[docs]class TableFieldSet(FieldSet):
"""Equivalent to a FieldSet containing a TableLayout."""
child = twc.Variable(default=TableLayout)
children = twc.Required
[docs]class ListFieldSet(FieldSet):
"""Equivalent to a FieldSet containing a ListLayout."""
child = twc.Variable(default=ListLayout)
children = twc.Required
[docs]class FormPage(twc.Page):
"""
A page that contains a form. The request method performs
validation, redisplaying the form on errors.
On success, it calls validated_request.
"""
_no_autoid = True
@classmethod
def request(cls, req):
if req.method == 'GET':
return super(FormPage, cls).request(req)
elif req.method == 'POST':
try:
data = cls.validate(req.POST)
except twc.ValidationError as e:
resp = webob.Response(
request=req,
content_type="text/html; charset=UTF8",
)
if six.PY3:
resp.text = e.widget.display().encode('utf-8')
else:
resp.body = e.widget.display().encode('utf-8')
else:
resp = cls.validated_request(req, data)
return resp
@classmethod
def validated_request(cls, req, data):
resp = webob.Response(
request=req,
content_type="text/html; charset=UTF8",
)
if six.PY3:
resp.text = 'Form posted successfully'
else:
resp.body = 'Form posted successfully'
if twc.core.request_local()['middleware'].config.debug:
if six.PY3:
resp.text += ' ' + repr(data)
else:
resp.body += ' ' + repr(data)
return resp