# -*- coding: utf-8 -*-
from datetime import datetime
import enum
import itertools
from watson.common.imports import get_qualified_name
from watson.html.elements import TagMixin, flatten_attributes
from watson import validators, filters as filters_
from watson.form.validators import SuppliedValues
[docs]class Definition(object):
"""Placeholder form element which allows for the creation of new form
elements when the form is instantiated.
"""
_counter = itertools.count()
[docs] def __init__(self, class_, *args, **kwargs):
self.count = next(Definition._counter)
self.class_ = class_
self.args = args
self.kwargs = kwargs
def generate_instance(self, form):
cls = self.class_
self.kwargs['form_'] = form
return cls(definition=False, *self.args, **self.kwargs)
[docs]class Label(TagMixin):
"""A <label> tag which can be automatically included with fields.
Attributes:
html (string): the html used to render the label
text (string): the text associated with the label
"""
html = '<label {0}>{1}</label>'
text = None
[docs] def __init__(self, text, **kwargs):
self.text = text
super(Label, self).__init__(**kwargs)
def render(self, field=None, **kwargs):
attrs = self.attributes.copy()
if 'text' in kwargs:
self.text = kwargs['text']
del kwargs['text']
if 'for_' in kwargs:
attrs['for'] = kwargs['for_']
del kwargs['for_']
attrs.update(kwargs)
if field and 'id' not in field.attributes and field.name:
# inject id based on field name
id = field.name
field.attributes['id'] = id
attrs['for'] = id
return self.html.format(flatten_attributes(attrs), self.text)
__call__ = render
[docs]class FieldMixin(TagMixin):
"""A mixin that can be used as a base to simplify the creation of fields.
When defining a field, a fully instantiated field must be created with
definition=False as an argument in it's __init__ method. This is to
facilitate the way fields are defined in Form objects in 2.0.0.
Attributes:
label (watson.form.fields.Label): the label associated with the field
html (string): the html used to render the field
validators (list): the validators that will be used to validate the
value.
filters (list): the filters that will be used prior to validation
"""
_counter = itertools.count()
label = None
html = '{0}'
validators = None
filters = None
_errors = None
_value = None
_default_value = None
_original_value = None
form = None
def __new__(cls, definition=True, *args, **kwargs):
if definition:
return Definition(cls, *args, **kwargs)
return super(FieldMixin, cls).__new__(cls)
[docs] def __init__(self, name=None, value=None, default_value=None,
label=None, label_attrs=None, **kwargs):
"""Initializes the field with a specific name.
"""
self.count = next(FieldMixin._counter)
if not name:
name = ''
self.label = Label(label or name)
if label_attrs and isinstance(label_attrs, dict):
self.label.attributes.update(label_attrs)
kwargs['name'] = name
self.value = value
self.default_value = default_value
self.filters = self.__process_filters(kwargs)
self.validators = self.__process_validators(kwargs)
if '_class' in kwargs:
kwargs['class'] = kwargs.get('_class')
del kwargs['_class']
if 'form_' in kwargs:
self.form = kwargs['form_']
del kwargs['form_']
self.clear_errors()
super(FieldMixin, self).__init__(**kwargs)
def __process_filters(self, kwargs):
filters = [filters_.Trim()] + kwargs.get('filters', [])
if 'filters' in kwargs:
del kwargs['filters']
return filters
def __process_validators(self, kwargs):
default_validators = []
if 'required' in kwargs:
default_validators = [validators.Required()]
kwargs['required'] = 'required'
_validators = default_validators + kwargs.get('validators', [])
if 'validators' in kwargs:
del kwargs['validators']
return _validators
@property
def value(self):
"""Return the value for the field.
If the field has been cleaned, the original value can be retrieved
with FieldMixin.original_value.
"""
return self._value
@value.setter
def value(self, value):
"""Convenience method to set the value on the field.
"""
if value is None and self.default_value and self.default_value is not None:
value = self.default_value
self._value = value
@property
def default_value(self):
return self._default_value
@default_value.setter
def default_value(self, value):
self._default_value = value
@property
def original_value(self):
"""Return the original value for the field.
"""
return self._original_value if self._original_value else self.value
[docs] def filter(self):
"""Filter the value on the field based on the associated filters.
Set the original_value of the field to the first value stored. Note, if
this is called a second time, then the original value will be
overridden.
"""
for _filter in self.filters:
self._original_value = self.value
self.value = _filter(self.value)
[docs] def validate(self, form):
"""Validate the value of the field against the associated validators.
Args:
form (watson.form.types.Form): The parent form of the field.
Returns:
A list of errors that have occurred when the field has been
validated.
"""
self._errors = []
for validator in self.validators:
try:
validator(self.value, form=form, field=self)
except ValueError as exc:
self._errors.append(str(exc))
return self._errors
@property
def errors(self):
return self._errors
def clear_errors(self):
self._errors = []
@property
def name(self):
"""Convenience method to retrieve the name of the field.
"""
return self.attributes['name']
@name.setter
def name(self, name):
"""Override the name attribute on the field.
"""
self.attributes['name'] = name
if not self.label.text:
self.label.text = name
[docs] def render_with_label(self):
"""Render the field with the label attached.
"""
raise NotImplementedError('The render method has not been implemented')
def __str__(self):
return self.render()
def __call__(self, **kwargs):
return self.render(**kwargs)
def __repr__(self):
return '<{0} name:{1}>'.format(get_qualified_name(self), self.name)
[docs]class Radio(GroupInputMixin):
"""Creates a radio input.
Example:
.. code-block:: python
field = Radio(name='test', label='My Radio Options', values=(('Test', 1), ('Testing', 2)))
str(field)
.. code-block:: html
<fieldset>
<legend>My Radio Options</legend>
<label for="test_0">Test<input id="test_0" name="test" type="radio" value="1" /></label>
<label for="test_1">Testing<input id="test_1" name="test" type="radio" value="2" /></label>
</fieldset>
.. code-block:: python
field = Radio(name='test', label='My Radio', values=1)
str(field)
.. code-block:: html
<label for="test"><input type="radio" name="test" values="1" />My Radio</label>
"""
uselist = False
[docs] def __init__(self, name=None, values=None, value=None, **kwargs):
"""Initializes the radio.
If a value is specified, then that value out of the available values will
be checked.
If multiple values are specified, then a radio group will be created.
Args:
string name: the name of the field
tuple|list values: the values to be used
mixed value: the value for the field
"""
super(
Radio,
self).__init__(
name,
values,
value,
type='radio',
**kwargs)
[docs]class Checkbox(GroupInputMixin):
"""Creates a checkbox input.
Example:
.. code-block:: python
field = Checkbox(name='test', label='My Radio Options', values=(('Test', 1), ('Testing', 2)))
str(field)
.. code-block:: html
<fieldset>
<legend>My Checkbox Options</legend>
<label for="test_0">Test<input id="test_0" name="test" type="checkbox" /></label>
<label for="test_1">Testing<input id="test_1" name="test" type="checkbox" /></label>
</fieldset>
.. code-block:: python
field = Checkbox(name='test', label='My Checkbox', values=1)
str(field)=None
.. code-block:: html
<label for="test"><input type="checkbox" name="test" value="1" />My Checkbox</label>
"""
[docs] def __init__(self, name=None, values=None, value=None, **kwargs):
"""Initializes the checkbox.
If a value is specified, then that value out of the available values will
be checked.
If multiple values are specified, then a checkbox group will be created.
Args:
name (string): the name of the field
values (tuple|list): the values to be used
value (mixed): the value for the field
"""
super(
Checkbox,
self).__init__(
name,
values,
value,
type='checkbox',
**kwargs)
[docs]class Submit(Input):
"""Creates a submit input.
Attributes:
button_mode (bool): whether or not to render as <button> or <input>
"""
button_mode = False
[docs] def __init__(self, name=None, value=None, button_mode=False, **kwargs):
real_value = value or kwargs.get('label', name)
if button_mode:
self.html = '<button {0}>{1}</button>'
self.button_mode = True
super(Submit, self).__init__(name, real_value, type='submit', **kwargs)
def render(self, **kwargs):
label = kwargs.pop('label') if 'label' in kwargs else self.label.text
if self.button_mode:
attributes = self.attributes.copy()
attributes.update(kwargs)
return (
self.html.format(
flatten_attributes(attributes),
label)
)
return super(Submit, self).render()
def render_with_label(self, **kwargs):
return self.render(**kwargs)
[docs]class Textarea(Input):
"""Creates a textarea field.
"""
html = '<textarea {0}>{1}</textarea>'
def render(self, **kwargs):
attributes = self.attributes.copy()
attributes.update(kwargs)
value = self.value if self.value else ''
return self.html.format(flatten_attributes(attributes), value)
[docs]class Select(FieldMixin):
"""Creates a select field.
Attributes:
html (string): the html for the outer select element
option_html (string): the individual option html element
optgroup_html (string): the optgroup html element
options (list|dict): the options available
"""
html = '<select {0}>{1}</select>'
option_html = '<option value="{0}"{2}>{1}</option>'
optgroup_html = '<optgroup label="{0}">{1}</optgroup>'
_options = None
[docs] def __init__(self, name=None, options=None,
value=None, multiple=False, **kwargs):
"""Initializes the select field.
If the options passed through are a dict, and the value of each key is
a list or tuple, then an optgroup will be rendered, using the key
as the label for the optgroup.
Args:
name (string): the name of the field
options (list|dict): the options available
value (string): the selected value
multiple (bool): whether or not to allow multiple selections
Example:
.. code-block:: python
field = Select(name='test', options=collections.OrderedDict([('Group One', [1, 2]), ('Group Two', [1, 2])]))
str(field)
.. code-block:: html
<select name="test">
<opgroup label="Group One">
<option value="1">1</option>
</optgroup>
<opgroup label="Group Two">
<option value="2">2</option>
</optgroup>
</select>
"""
values = []
if 'values' in kwargs:
values = kwargs.pop('values')
if multiple or isinstance(value, (tuple, list)):
kwargs['multiple'] = 'multiple'
super(Select, self).__init__(name, value, **kwargs)
self.options = options or values
def render(self, **kwargs):
attributes = self.attributes.copy()
attributes.update(kwargs)
return (
self.html.format(
flatten_attributes(attributes),
self._options_render())
)
def render_with_label(self, **kwargs):
return ''.join((self.label.render(self), self.render(**kwargs)))
@property
def options(self):
if isinstance(self._options, enum.EnumMeta):
return [(e.name, e.value) for e in self._options]
return self._options
@options.setter
def options(self, options):
should_include_values_validator = True
for validator in self.validators:
if isinstance(validator, SuppliedValues):
should_include_values_validator = False
if should_include_values_validator:
self.validators.append(SuppliedValues())
self._options = options
@property
def actual_values(self):
if isinstance(self.values, dict):
return [value for key, value in self.values.items()]
return [value[0] if isinstance(value, (list, tuple)) else value for value in self.values]
# Options can also be referenced as values
@property
def values(self):
return self.options
@values.setter
def values(self, values):
self.options = values
def _options_render(self):
# internal method the render the options
if isinstance(self.options, dict):
options = []
for label, value in self.options.items():
if isinstance(value, (tuple, list)):
options.append(
self.optgroup_html.format(label,
self.__render_options(value)))
else:
options.append(self.__render_option(label, value))
return ''.join(options)
else:
return self.__render_options(self.options)
def __render_options(self, options):
options = [self.__render_option(value, value) for value in options]
return (''.join(options))
def __render_option(self, label, value):
# internal method to render an individual option
if isinstance(value, (tuple, list)):
value, label = value
match = False
str_value = str(value)
str_self_value = str(self.value)
if str_value == str_self_value:
match = True
elif isinstance(self.value, (list, tuple)):
str_values = (str(val) for val in self.value)
match = str_value in str_values
elif isinstance(self.value, enum.Enum):
match = self.value.name == value
selected = ' selected="selected"' if match else ''
return self.option_html.format(value, label, selected)
# Convenience classes for input types. Can use Input(type='something') instead
# if required to create a different input field.
# Some of the input types add additional validators and filters to simplify the
# process.
[docs]class Text(Input):
"""Creates an <input type="text" /> element.
"""
[docs] def __init__(self, name=None, value=None, **kwargs):
super(Text, self).__init__(name, value, type='text', **kwargs)
[docs]class Date(Input):
"""Creates an <input type="date" /> element.
"""
format = None
[docs] def __init__(self, name=None, value=None, format='%Y-%m-%d', **kwargs):
self.format = format
date_filter = filters_.Date(format)
if 'filters' in kwargs:
kwargs['filters'].append(date_filter)
else:
kwargs['filters'] = [date_filter]
if format:
self.format = format
super(Date, self).__init__(name, value, type='date', **kwargs)
[docs] def render(self, **kwargs):
"""Format the date in the format the HTML5 spec requires.
"""
if self.value:
if not isinstance(self.value, str):
self.value = self.value.strftime(self.format)
else:
date = datetime.strptime(self.value, self.format)
self.value = date.strftime(self.format)
return super(Date, self).render(**kwargs)
[docs]class Email(Input):
"""Creates an <input type="email" /> element.
"""
[docs] def __init__(self, name=None, value=None, **kwargs):
super(Email, self).__init__(name, value, type='email', **kwargs)
[docs]class Hidden(Input):
"""Creates an <input type="hidden" /> element.
"""
[docs] def __init__(self, name=None, value=None, **kwargs):
super(Hidden, self).__init__(name, value, type='hidden', **kwargs)
[docs]class Csrf(Input):
"""Creates an <input type="hidden" /> element for use in csrf protection.
"""
[docs] def __init__(self, name='csrf_token', value=None, **kwargs):
super(
Csrf,
self).__init__(name,
value,
type='hidden',
label='Cross-Site Request Forgery',
required=True,
**kwargs)
self.validators.append(validators.Csrf())
[docs]class Password(Input):
"""Creates an <input type="password" /> element.
"""
[docs] def __init__(self, name=None, value=None, **kwargs):
super(Password, self).__init__(name, value, type='password', **kwargs)
class File(Input):
def __init__(self, name=None, value=None, **kwargs):
super(File, self).__init__(name, value, type='file', **kwargs)
def render(self):
"""Overridden to prevent value from being put back into the field.
"""
attributes = self.attributes.copy()
return self.html.format(flatten_attributes(attributes))