Source code for gavo.formal.widget

"""
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] def processInput(self, request, key, args, default=''): value = getStringFromArgs(args, key, default) if not value: value = 'False' value = iformal.IBooleanConvertible(self.original).toType(value) return self.original.validate(value)
[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') ]
[docs] def processInput(self, request, key, args, default=''): pwds = [perhapsDecode(pwd) for pwd in args.get(key, [])] if len(pwds) == 0: pwds = ["", ""] elif len(pwds) == 1: raise validation.FieldValidationError('Please enter the password twice for confirmation.') else: if pwds[0] != pwds[1]: raise validation.FieldValidationError('Passwords do not match.') return self.original.validate(pwds[0])
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] def processInput(self, request, key, args, default=''): charset = "utf-8" value = self._valueFromRequestArgs(charset, key, args, 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 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)
[docs] def processInput(self, request, key, args, default=None): namer = self._namer(key) # Get the form field values as a (y,m,d) tuple ymd = [getStringFromArgs(args, namer(part)).strip() for part in ('year', 'month', 'day')] # Remove parts that were not entered. ymd = [p for p in ymd if p] # Nothing entered means None otherwise we need all three. if not ymd: ymd = default and default.split() elif len(ymd) != 3: raise validation.FieldValidationError("Invalid date") # So, we have what looks like a good attempt to enter a date. if ymd is not None: # Map to integers try: ymd = [int(p) for p in ymd] except ValueError: raise validation.FieldValidationError("Invalid date") ymd = iformal.IDateTupleConvertible(self.original).toType(ymd) return self.original.validate(ymd)
[docs]@implementer( iformal.IWidget ) class DatePartsInput(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. By default the widget is designed to only accept unambiguous years, i.e. the user must enter 4 character dates. Many people find it convenient or even necessary to allow a 2 character year. This can be allowed by setting the twoCharCutoffYear attribute to an integer value between 0 and 99. Anything greater than or equal to the cutoff year will be considered part of the 20th century (1900 + year); anything less the cutoff year is a 21st century (2000 + year) date. A typical twoCharCutoffYear value is 70 (i.e. 1970). However, that value is somewhat arbitrary. It's the year that time began according to the PC, but it doesn't mean much to your non-techie user. dayFirst: Make the day the first input field, i.e. day, month, year twoCharCutoffYear: Allow 2 char years and set the year where the century flips between 20th and 21st century. default can be whitespace-separated parts. """ dayFirst = False twoCharCutoffYear = None def __init__(self, original, dayFirst=None, twoCharCutoffYear=None): self.original = original if dayFirst is not None: self.dayFirst = dayFirst if twoCharCutoffYear is not None: self.twoCharCutoffYear = twoCharCutoffYear def _namer(self, prefix): def _(part): return '%s__%s' % (prefix,part) return _ def _renderTag(self, request, year, month, day, namer, readonly): yearTag = T.input(type="text", name=namer('year'), value=str(year or ""), size="4") monthTag = T.input(type="text", name=namer('month'), value=str(month or ""), size="2") dayTag = T.input(type="text", name=namer('day'), value=str(day or ""), size="2") 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)
[docs] def processInput(self, request, key, args, default=None): namer = self._namer(key) # Get the form field values as a (y,m,d) tuple ymd = [getStringFromArgs(args, namer(part)).strip() for part in ('year', 'month', 'day')] # Remove parts that were not entered. ymd = [p for p in ymd if p] # Nothing entered means None otherwise we need all three. if not ymd: ymd = default and default.split() elif len(ymd) != 3: raise validation.FieldValidationError("Invalid date") # So, we have what looks like a good attempt to enter a date. if ymd is not None: # If a 2-char year is allowed then prepend the century. if self.twoCharCutoffYear is not None and len(ymd[0]) == 2: try: if int(ymd[0]) >= self.twoCharCutoffYear: century = '19' else: century = '20' ymd[0] = century + ymd[0] except ValueError: pass # By now, we should have a year of at least 4 characters. if len(ymd[0]) < 4: if self.twoCharCutoffYear is not None: msg = "Please enter a 2 or 4 digit year" else: msg = "Please enter a 4 digit year" raise validation.FieldValidationError(msg) # Map to integers try: ymd = [int(p) for p in ymd] except ValueError: raise validation.FieldValidationError("Invalid date") ymd = iformal.IDateTupleConvertible(self.original).toType(ymd) return self.original.validate(ymd)
[docs]@implementer( iformal.IWidget ) class MMYYDatePartsInput(object): """ Two input fields for entering the month and year. default can be a year and month separated by whitespace. """ cutoffYear = 70 def __init__(self, original, cutoffYear=None): self.original = original if cutoffYear is not None: self.cutoffYear = cutoffYear def _namer(self, prefix): def _(part): return '%s__%s' % (prefix,part) return _ def _renderTag(self, request, year, month, namer, readonly): yearTag = T.input(type="text", name=namer('year'), value=str(year), size="2") monthTag = T.input(type="text", name=namer('month'), value=str(month), size="2") if readonly: tags=(yearTag, monthTag) for tag in tags: tag(class_='readonly', readonly='readonly') return monthTag, ' / ', yearTag, ' (mm/yy)'
[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")) # return a blank for the day day = '' else: year, month, day = converter.fromType(args.get(key)) # if we have a year as default data, stringify it and only use last two digits if year is not None: year = str(year)[2:] return self._renderTag(request, year, month, namer, False)
[docs] def renderImmutable(self, request, key, args, errors): converter = iformal.IDateTupleConvertible(self.original) year, month, day = converter.fromType(args.get(key)) namer = self._namer(key) # if we have a year as default data, stringify it and only use last two digits if year is not None: year = str(year)[2:] return self._renderTag(request, year, month, namer, True)
[docs] def processInput(self, request, key, args, default=None): namer = self._namer(key) value = [getStringFromArgs(args, namer(part)).strip() for part in ('year', 'month')] value = [p for p in value if p] if not value: value = default and default.split() elif len(value) != 2: raise validation.FieldValidationError("Invalid date") if value is not None: try: value = [int(p) for p in value] except ValueError: raise validation.FieldValidationError("Invalid date") if value[1] < 0 or value[1] > 99: raise validation.FieldValidationError("Invalid year. Please enter a two-digit year.") if value[0] > self.cutoffYear: value[0] = 1900 + value[0] else: value[0] = 2000 + value[0] value.append(1) value = iformal.IDateTupleConvertible(self.original).toType( value ) return self.original.validate(value)
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] def processInput(self, request, key, args, default=''): values = [perhapsDecode(v) for v in args.get(key, default.split())] converter = iformal.IStringConvertible(self.original) values = [converter.toType(v) for v in values] return self.original.validate(values)
[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] def processInput(self, request, key, args, default=None): # default is ignored here. if key not in request.files: return self.original.validate(None) fileitem = request.files[key][0] name = fileitem.file_name if isinstance(name, bytes): name = name.decode("utf-8", "replace") value = (name, fileitem.file_object) value = iformal.IFileConvertible(self.original).fromType(value) return self.original.validate(value)
[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)
[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)
__all__ = [ 'Checkbox', 'CheckboxMultiChoice', 'CheckedPassword', 'Password', 'SelectChoice', 'TextArea', 'TextInput', 'DatePartsInput', 'DatePartsSelect', 'MMYYDatePartsInput', 'Hidden', 'RadioChoice', 'SelectOtherChoice', 'FileUpload', 'TextAreaList', ]