"""
Widgets are small components that render form fields for inputing data in a
certain format.
Caution: the args argument in the widgets' render methods is a disaster
in python3 -- it maps from bytes when there are errors (it's a t.w
Request.args), from string otherwise (it's a formal-internal thing).
And don't get me started on its values.
"""
# TODO: Fix this -- have a common sort of args for both cases in
# some way.
import itertools
from pkg_resources import resource_filename
from zope.interface import implementer
from twisted.web import template
from twisted.web.template import tags as T
from . import iformal, validation, nevowc
from .util import render_cssid
# Marker object for args that are not supplied
_UNSET = object()
def getStringFromArgs(args, key, default=''):
"""returns args[key][0] suitably decoded.
This is used to insert erroneous inputs into fields from request.args.
Default must already be a string, not bytes.
Unfortunately, depending on the context, formal will pass in args
with either string keys or bytes keys. While we can't rebuild
the thing to be halfway sane, we simply adapt key based on some
key we pick from args. Yeah, madness.
"""
if not args:
return default
if isinstance(next(iter(args)), str):
if isinstance(key, bytes):
key = key.decode("ascii")
else:
if isinstance(key, str):
key = key.encode("ascii")
if key in args and args[key]:
return args[key][0].decode("utf-8", "replace")
else:
return default
def perhapsDecode(val):
"""returns val utf-8-decoded if it's bytes, unchanged otherwise.
"""
if isinstance(val, bytes):
return val.decode("utf-8")
return val
[docs]@implementer( iformal.IWidget )
class TextInput(object):
"""
A text input field.
<input type="text" ... />
"""
inputType = 'text'
showValueOnFailure = True
placeholder = None
def __init__(self, original):
self.original = original
def _renderTag(self, request, key, value, readonly):
tag=T.input(type=self.inputType, name=key, id=render_cssid(key))
if value is not None:
tag(value=str(value))
if readonly:
tag(class_='readonly', readonly='readonly')
if self.placeholder is not None:
tag(placeholder=self.placeholder)
return tag
[docs] def render(self, request, key, args, errors):
if errors:
value = getStringFromArgs(args, key)
else:
value = iformal.IStringConvertible(self.original).fromType(
args.get(key))
if not self.showValueOnFailure:
value = None
return self._renderTag(request, key, value, False)
[docs] def renderImmutable(self, request, key, args, errors):
value = iformal.IStringConvertible(self.original).fromType(
args.get(key))
return self._renderTag(request, key, value, True)
[docs] def processInput(self, request, key, args, default=''):
value = getStringFromArgs(args, key, default)
value = iformal.IStringConvertible(self.original).toType(value)
return self.original.validate(value)
[docs]@implementer( iformal.IWidget )
class Checkbox(object):
"""
A checkbox input field.
<input type="checkbox" ... />
"""
def __init__(self, original):
self.original = original
def _renderTag(self, request, key, value, disabled):
tag = T.input(type='checkbox', name=key, id=render_cssid(key), value='True')
if value == 'True':
tag(checked='checked')
if disabled:
tag(class_='disabled', disabled='disabled')
return tag
[docs] def render(self, request, key, args, errors):
if errors:
value = getStringFromArgs(args, key)
else:
value = iformal.IBooleanConvertible(self.original).fromType(
args.get(key))
return self._renderTag(request, key, value, False)
[docs] def renderImmutable(self, request, key, args, errors):
value = iformal.IBooleanConvertible(self.original).fromType(
args.get(key))
return self._renderTag(request, key, value, True)
[docs]class Password(TextInput):
"""
A text input field that hides the text.
<input type="password" ... />
"""
inputType = 'password'
showValueOnFailure = False
[docs]@implementer( iformal.IWidget )
class TextArea(object):
"""
A large text entry area that accepts newline characters.
<textarea>...</textarea>
"""
cols = 48
rows = 6
def __init__(self, original, cols=None, rows=None):
self.original = original
if cols is not None:
self.cols = cols
if rows is not None:
self.rows = rows
def _renderTag(self, request, key, value, readonly):
tag=T.textarea(name=key,
id=render_cssid(key),
cols=str(self.cols),
rows=str(self.rows))[str(value or '')]
if readonly:
tag(class_='readonly', readonly='readonly')
return tag
[docs] def render(self, request, key, args, errors):
if errors:
value = getStringFromArgs(args, key)
else:
value = iformal.IStringConvertible(self.original).fromType(
args.get(key))
return self._renderTag(request, key, value or "", False)
[docs] def renderImmutable(self, request, key, args, errors):
value = iformal.IStringConvertible(self.original).fromType(
args.get(key))
return self._renderTag(request, key, value, True)
[docs] def processInput(self, request, key, args, default=''):
value = getStringFromArgs(args, key, default)
value = iformal.IStringConvertible(self.original).fromType(value)
return self.original.validate(value)
[docs]@implementer( iformal.IWidget )
class TextAreaList(object):
"""
A text area that allows a list of values to be entered, one per line. Any
empty lines are discarded.
"""
cols = 48
rows = 6
def __init__(self, original, cols=None, rows=None):
self.original = original
if cols is not None:
self.cols = cols
if rows is not None:
self.rows = rows
def _renderTag(self, request, key, values, readonly):
value = '\n'.join(values)
tag=T.textarea(name=key,
id=render_cssid(key),
cols=str(self.cols),
rows=str(self.rows))[str(value or '')]
if readonly:
tag(class_='readonly', readonly='readonly')
return tag
[docs] def render(self, request, key, args, errors):
converter = iformal.IStringConvertible(self.original.type)
if errors:
values = args.get(key, [])
else:
values = args.get(key)
if values is not None:
values = [converter.fromType(v) for v in values]
else:
values = []
return self._renderTag(request, key, values, False)
[docs] def renderImmutable(self, request, key, args, errors):
converter = iformal.IStringConvertible(self.original.type)
values = args.get(key)
if values is not None:
values = [converter.fromType(v) for v in values]
else:
values = []
return self._renderTag(request, key, values, True)
[docs] def processInput(self, request, key, args, default=''):
# Get the whole string
value = getStringFromArgs(args, key, default)
# Split into lines
values = value.splitlines()
# Strip each line
values = [v.strip() for v in values]
# Discard empty lines
values = [v for v in values if v]
# Convert values to correct type
converter = iformal.IStringConvertible(self.original.type)
values = [converter.toType(v) for v in values]
# Validate and return
return self.original.validate(values)
[docs]@implementer( iformal.IWidget )
class CheckedPassword(object):
"""
Two password entry fields that must contain the same value to validate.
"""
def __init__(self, original):
self.original = original
[docs] def render(self, request, key, args, errors):
if errors and not errors.getFieldError(key):
values = args.get(key)
else:
values = ('', '')
return [
T.input(type='password', name=key, id=render_cssid(key),
value=str(values[0])),
T.br,
T.label(for_=render_cssid(key, 'confirm'))[' Confirm '],
T.input(type='password', name=key, id=render_cssid(key, 'confirm'), value=str(values[1])),
]
[docs] def renderImmutable(self, request, key, args, errors):
values = ('', '')
return [
T.input(type='password', name=key, id=render_cssid(key),
value=str(values[0]), class_='readonly', readonly='readonly'),
T.br,
T.label(for_=render_cssid(key, 'confirm'))[' Confirm '],
T.input(type='password', name=key, id=render_cssid(key, 'confirm'),
value=str(values[1]), class_='readonly',
readonly='readonly')
]
class ChoiceBase(object):
"""
A base class for widgets that provide the UI to select one or more items
from a list.
options:
A sequence of objects adaptable to IKey and ILabel. IKey is used as the
<option>'s value attribute; ILabel is used as the <option>'s child.
IKey and ILabel adapters for tuple are provided.
noneOption:
An object adaptable to IKey and ILabel that is used to identify when
nothing has been selected.
"""
options = None
noneOption = None
def __init__(self, original, options=None, noneOption=_UNSET):
self.original = original
if options is not None:
self.options = options
if noneOption is not _UNSET:
self.noneOption = noneOption
def processInput(self, request, key, args, default=''):
value = getStringFromArgs(args, key, default)
value = iformal.IStringConvertible(self.original).toType(value)
if self.noneOption is not None and \
value == iformal.IKey(self.noneOption).key():
value = None
return self.original.validate(value)
[docs]@implementer( iformal.IWidget )
class SelectChoice(ChoiceBase):
"""
A drop-down list of options.
"""
noneOption = ('', '')
def _renderTag(self, request, key, value, converter, disabled):
def renderOptions():
data = self.options
if self.noneOption is not None:
noneVal = iformal.IKey(self.noneOption).key()
option = T.option(value=str(noneVal))[
iformal.ILabel(self.noneOption).label()]
if value is None or value==str(noneVal):
option = option(selected='selected')
yield option
if data is None:
return
for item in data:
optValue = iformal.IKey(item).key()
optLabel = iformal.ILabel(item).label()
optValue = converter.fromType(optValue)
option = T.option(value=stringifyOptionValue(optValue))[
iformal.ILabel(optLabel).label()]
if optValue == value:
option = option(selected='selected')
yield option
tag=T.select(name=key, id=render_cssid(key))[list(renderOptions())]
if disabled:
tag(class_='disabled', disabled='disabled')
return tag
[docs] def render(self, request, key, args, errors):
converter = iformal.IStringConvertible(self.original)
if errors:
value = getStringFromArgs(args, key)
else:
value = converter.fromType(args.get(key))
return self._renderTag(request, key, value, converter, False)
[docs] def renderImmutable(self, request, key, args, errors):
converter = iformal.IStringConvertible(self.original)
value = converter.fromType(args.get(key))
return self._renderTag(request, key, value, converter, True)
[docs]@implementer(iformal.IWidget)
class SelectOtherChoice(object):
"""
A <select> widget that includes an "Other ..." option. When the other
option is selected an <input> field is enabled to allow free text entry.
Unlike SelectChoice, the options items are not a (value,label) tuple
because that makes no sense with the free text entry facility.
TODO:
* Make the Other option configurable in the JS
* Refactor, refactor, refactor
"""
options = None
noneOption = ('', '')
otherOption = ('...', 'Other ...')
template = None
def __init__(self, original, options=None, otherOption=None):
self.original = original
if options is not None:
self.options = options
if otherOption is not None:
self.otherOption = otherOption
if self.template is None:
self.template = nevowc.XMLFile(resource_filename('formal',
'html/SelectOtherChoice.html'))
def _valueFromRequestArgs(self, charset, key, args, default=''):
value = getStringFromArgs(args, key, default)
if value == self.otherOption[0]:
value = getStringFromArgs(args, key+'-other')
return value
[docs] def render(self, request, key, args, errors):
return self._render(request, key, args, errors, False)
[docs] def renderImmutable(self, request, key, args, errors):
return self._render(request, key, args, errors, True)
def _render(self, request, key, args, errors, immutable):
charset = "utf-8"
converter = iformal.IStringConvertible(self.original)
if errors:
value = self._valueFromRequestArgs(
charset, key, args)
else:
value = converter.fromType(args.get(key))
if value is None:
value = iformal.IKey(self.noneOption).key()
if immutable:
template = nevowc.locatePattern(self.template, ['immutable'])
else:
template = nevowc.locatePattern(self.template, ['editable']
)["editable"]
optionGen = nevowc.locatePattern(template, ['option']
)["option"]
selectedOptionGen = nevowc.locatePattern(template,
['selectedOption'])["selectedOption"]
optionTags = []
selectOther = True
if self.noneOption is not None:
noneValue = iformal.IKey(self.noneOption).key()
if value == noneValue:
tag = selectedOptionGen.clone()
selectOther = False
else:
tag = optionGen.clone()
tag.fillSlots('value', noneValue)
tag.fillSlots('label', iformal.ILabel(self.noneOption).label())
optionTags.append(tag)
if self.options is not None:
for item in self.options:
if value == item:
tag = selectedOptionGen.clone()
selectOther = False
else:
tag = optionGen.clone()
tag.fillSlots('value', item)
tag.fillSlots('label', item)
optionTags.append(tag)
if selectOther:
tag = selectedOptionGen.clone()
otherValue = value
else:
tag = optionGen.clone()
otherValue = ''
tag.fillSlots('value', self.otherOption[0])
tag.fillSlots('label', self.otherOption[1])
optionTags.append(tag)
tag = template
tag.fillSlots('key', key)
tag.fillSlots('id', render_cssid(key))
tag.fillSlots('options', optionTags)
tag.fillSlots('otherValue', otherValue)
return tag
[docs]@implementer( iformal.IWidget )
class RadioChoice(ChoiceBase):
"""
A list of options in the form of radio buttons.
<div class="radiobutton"><input type="radio" ... value="..."/><label>...</label></div>
"""
def _renderTag(self, request, key, value, converter, disabled):
def renderOption(request, itemKey, itemLabel, num, selected):
cssid = render_cssid(key, num)
tag = T.input(name=key, type='radio', id=cssid,
value=stringifyOptionValue(itemKey))
if selected:
tag = tag(checked='checked')
if disabled:
tag = tag(disabled='disabled')
return T.div(class_='radiobutton')[ tag, T.label(for_=cssid)[itemLabel] ]
def renderOptions():
# A counter to assign unique ids to each input
data = self.options
idCounter = itertools.count()
if self.noneOption is not None:
itemKey = iformal.IKey(self.noneOption).key()
itemLabel = iformal.ILabel(self.noneOption).label()
yield renderOption(request, itemKey, itemLabel, next(idCounter), value is None)
if not data:
return
for item in data:
itemKey = iformal.IKey(item).key()
itemLabel = iformal.ILabel(item).label()
itemKey = converter.fromType(itemKey)
yield renderOption(request, itemKey, itemLabel, next(idCounter), itemKey==value)
return T.transparent[list(renderOptions())]
[docs] def render(self, request, key, args, errors):
converter = iformal.IStringConvertible(self.original)
if errors:
value = getStringFromArgs(args, key)
else:
value = converter.fromType(args.get(key))
return self._renderTag(request, key, value, converter, False)
[docs] def renderImmutable(self, request, key, args, errors):
converter = iformal.IStringConvertible(self.original)
value = converter.fromType(args.get(key))
return self._renderTag(request, key, value, converter, True)
[docs]@implementer( iformal.IWidget )
class DatePartsSelect(object):
"""
A date entry widget that uses three <input> elements for the day, month and
year parts.
The default entry format is the US (month, day, year) but can be switched to
the more common (day, month, year) by setting the dayFirst attribute to
True.
The start and end year can be passed through but default to 1970 and 2070.
The months default to non-zero prefixed numerics but can be passed as a list
of label, value pairs
default can be whitespace-separated parts here.
"""
dayFirst = False
days = [ (d,d) for d in range(1,32) ]
months = [ (m,m) for m in range(1,13) ]
yearFrom = 1970
yearTo = 2070
noneOption = ('', '')
def __init__(self, original, dayFirst=None, yearFrom=None, yearTo=None, months=None, noneOption=_UNSET):
self.original = original
if dayFirst is not None:
self.dayFirst = dayFirst
if yearFrom is not None:
self.yearFrom = yearFrom
if yearTo is not None:
self.yearTo = yearTo
if months is not None:
self.months = months
if noneOption is not _UNSET:
self.noneOption = noneOption
def _namer(self, prefix):
def _(part):
return '%s__%s' % (prefix,part)
return _
def _renderTag(self, request, year, month, day, namer, readonly):
years = [(v,v) for v in range(self.yearFrom,self.yearTo)]
months = self.months
days = self.days
options = []
if self.noneOption is not None:
options.append( T.option(value=str(self.noneOption[0]))[
iformal.ILabel(self.noneOption[1]).label()] )
for value in years:
if str(value[0]) == str(year):
options.append( T.option(value=str(value[0]),
selected='selected')[
iformal.ILabel(value[1]).label()] )
else:
options.append( T.option(value=str(value)[0])[
iformal.ILabel(value[1]).label()] )
yearTag = T.select(name=namer('year'))[ options ]
options = []
if self.noneOption is not None:
options.append( T.option(value=str(self.noneOption[0]))[
iformal.ILabel(self.noneOption[1]).label()] )
for value in months:
if str(value[0]) == str(month):
options.append( T.option(value=str(value[0]),
selected='selected')[
iformal.ILabel(value[1]).label()] )
else:
options.append( T.option(value=str(value[0]))[
iformal.ILabel(value[1]).label()] )
monthTag = T.select(name=namer('month'))[ options ]
options = []
if self.noneOption is not None:
options.append( T.option(value=str(self.noneOption[0]))[
iformal.ILabel(self.noneOption[1]).label()] )
for value in days:
if str(value[0]) == str(day):
options.append( T.option(value=str(value[0]),
selected='selected')[
iformal.ILabel(value[1]).label()] )
else:
options.append( T.option(value=str(value[0]))[
iformal.ILabel(value[1]).label()] )
dayTag = T.select(name=namer('day'))[ options ]
if readonly:
tags = (yearTag, monthTag, dayTag)
for tag in tags:
tag(class_='readonly', readonly='readonly')
if self.dayFirst:
return dayTag, ' / ', monthTag, ' / ', yearTag, ' ', ('(day/month/year)')
else:
return monthTag, ' / ', dayTag, ' / ', yearTag, ' ', ('(month/day/year)')
[docs] def render(self, request, key, args, errors):
converter = iformal.IDateTupleConvertible(self.original)
namer = self._namer(key)
if errors:
year = getStringFromArgs(args, namer('year').encode("ascii"))
month = getStringFromArgs(args, namer('month').encode("ascii"))
day = getStringFromArgs(args, namer('day').encode("ascii"))
else:
year, month, day = converter.fromType(args.get(key))
return self._renderTag(request, year, month, day, namer, False)
[docs] def renderImmutable(self, request, key, args, errors):
converter = iformal.IDateTupleConvertible(self.original)
namer = self._namer(key)
year, month, day = converter.fromType(args.get(key))
return self._renderTag(request, year, month, day, namer, True)
def stringifyOptionValue(s):
"""returns s safe for inclusion into selection boxes.
That's a str with + replaced by %2b; I introduced this during a time
of confusion before I understood that plus-quoting was for urlencoded
values only. But doing this helps in case there are broken http
components in the way, so I'll keep replacing where I easily can.
"""
return str(s).replace("+", "%2b")
[docs]@implementer( iformal.IWidget )
class CheckboxMultiChoice(template.Element):
"""
Multiple choice list, rendered as a list of checkbox fields.
lots enters blanks between the items (which allows a bit more
flexibility in styling the stuff).
emphasized can be a set of item titles that should additionally
get an emphasized css class.
Default is a whitespace-spearated enumeration for now.
"""
options = None
def __init__(self, original, options=None, lots=False,
emphasized=frozenset()):
self.original = original
self.lots = lots
self.emphasized = emphasized
if options is not None:
self.options = options
def _renderTag(self, request, key, values, converter, disabled):
def _():
options = self.options
# loops through checkbox options and renders
for n,item in enumerate(options):
optValue = iformal.IKey(item).key()
optLabel = iformal.ILabel(item).label()
optValue = converter.fromType(optValue)
optid = render_cssid(key, n)
checkbox = T.input(type='checkbox', name=key,
value=stringifyOptionValue(optValue),
id=optid+["-box"], class_="multichoice",
onchange="Forms.Util.updateLabel(this)")
if disabled:
checkbox = checkbox(class_='disabled', disabled='disabled')
# Label for is abominable on most browsers for this purpose.
# So, let's use a span (sigh)
if optValue in self.emphasized:
cssCls = "multichoice emphasized"
else:
cssCls = "multichoice"
label = T.span(class_=cssCls, id=optid+["-label"],
onclick="Forms.Util.labelClick(this)")[optLabel]
if optValue in values:
checkbox = checkbox(checked='checked')
label = label(class_=cssCls+" selected")
yield T.span(class_="nobreak")[checkbox, label]
if self.lots:
yield " "
else:
yield T.br()
return T.transparent[list(_())]
[docs] def render(self, request, key, args, errors):
converter = iformal.IStringConvertible(self.original)
if errors:
values = args.get(key, [])
else:
values = args.get(key)
if values is not None:
values = [converter.fromType(v) for v in values]
else:
values = []
return self._renderTag(request, key, values, converter, False)
[docs] def renderImmutable(self, request, key, args, errors):
converter = iformal.IStringConvertible(self.original)
values = args.get(key)
if values is not None:
values = [converter.fromType(v) for v in values]
else:
values = []
return self._renderTag(request, key, values, converter, True)
[docs]@implementer( iformal.IWidget )
class FileUpload(object):
"""NOTE: You have use something like twistedpatch.MPRequest as
your site's requestFactory to make this work.
"""
def __init__(self, original):
self.original = original
def _renderTag(self, request, key, disabled):
tag=T.input(name=key, id=render_cssid(key),type='file')
if disabled:
tag(class_='disabled', disabled='disabled')
return tag
[docs] def render(self, request, key, args, errors):
return self._renderTag(request, key, False)
[docs] def renderImmutable(self, request, key, args, errors):
iformal.IFileConvertible(self.original).fromType(args.get(key))
return self._renderTag(request, key, True)
[docs]class Hidden(object):
"""
A hidden form field.
"""
__implements__ = iformal.IWidget,
inputType = 'hidden'
def __init__(self, original):
self.original = original
[docs] def render(self, request, key, args, errors):
if errors:
value = getStringFromArgs(args, key)
else:
value = iformal.IStringConvertible(self.original).fromType(
args.get(key))
if value is None:
value = ""
return T.input(type=self.inputType, name=key, id=render_cssid(key),
value=stringifyOptionValue(value))
[docs] def renderImmutable(self, request, key, args, errors):
return self.render(request, key, args, errors)
__all__ = [
'Checkbox', 'CheckboxMultiChoice', 'CheckedPassword',
'Password', 'SelectChoice', 'TextArea', 'TextInput', 'DatePartsInput',
'DatePartsSelect', 'MMYYDatePartsInput', 'Hidden', 'RadioChoice',
'SelectOtherChoice', 'FileUpload', 'TextAreaList',
]