""" HTML forms (part of web.py) """ import copy import re from . import net, utils from . import webapi as web def attrget(obj, attr, value=None): try: if hasattr(obj, "has_key") and attr in obj: return obj[attr] except TypeError: # Handle the case where has_key takes different number of arguments. # This is the case with Model objects on appengine. See #134 pass if ( hasattr(obj, "keys") and attr in obj ): # needed for Py3, has_key doesn't exist anymore return obj[attr] elif hasattr(obj, attr): return getattr(obj, attr) return value class Form: r""" HTML form. >>> f = Form(Textbox("x")) >>> f.render() u'\n \n
' >>> f.fill(x="42") True >>> f.render() u'\n \n
' """ def __init__(self, *inputs, **kw): self.inputs = inputs self.valid = True self.note = None self.validators = kw.pop("validators", []) def __call__(self, x=None): o = copy.deepcopy(self) if x: o.validates(x) return o def render(self): out = "" out += self.rendernote(self.note) out += "\n" for i in self.inputs: html = ( utils.safeunicode(i.pre) + i.render() + self.rendernote(i.note) + utils.safeunicode(i.post) ) if i.is_hidden(): out += ' \n' % ( html ) else: out += ( ' \n' % (net.websafe(i.id), net.websafe(i.description), html) ) out += "
%s
%s
" return out def render_css(self): out = [] out.append(self.rendernote(self.note)) for i in self.inputs: if not i.is_hidden(): out.append( '' % (net.websafe(i.id), net.websafe(i.description)) ) out.append(i.pre) out.append(i.render()) out.append(self.rendernote(i.note)) out.append(i.post) out.append("\n") return "".join(out) def rendernote(self, note): if note: return '%s' % net.websafe(note) else: return "" def validates(self, source=None, _validate=True, **kw): source = source or kw or web.input() out = True for i in self.inputs: v = attrget(source, i.name) if _validate: out = i.validate(v) and out else: i.set_value(v) if _validate: out = out and self._validate(source) self.valid = out return out def _validate(self, value): self.value = value for v in self.validators: if not v.valid(value): self.note = v.msg return False return True def fill(self, source=None, **kw): return self.validates(source, _validate=False, **kw) def __getitem__(self, i): for x in self.inputs: if x.name == i: return x raise KeyError(i) def __getattr__(self, name): # don't interfere with deepcopy inputs = self.__dict__.get("inputs") or [] for x in inputs: if x.name == name: return x raise AttributeError(name) def get(self, i, default=None): try: return self[i] except KeyError: return default def _get_d(self): # @@ should really be form.attr, no? return utils.storage([(i.name, i.get_value()) for i in self.inputs]) d = property(_get_d) class Input: """Generic input. Type attribute must be specified when called directly. See also: Currently only types which can be written inside one `` tag are supported. - For checkbox, please use `Checkbox` class for better control. - For radiobox, please use `Radio` class for better control. >>> Input(name='foo', type='email', value="user@domain.com").render() u'' >>> Input(name='foo', type='number', value="bar").render() u'' >>> Input(name='num', type="number", min='0', max='10', step='2', value='5').render() u'' >>> Input(name='foo', type="tel", value='55512345').render() u'' >>> Input(name='search', type="search", value='Search').render() u'' >>> Input(name='search', type="search", value='Search', required='required', pattern='[a-z0-9]{2,30}', placeholder='Search...').render() u'' >>> Input(name='url', type="url", value='url').render() u'' >>> Input(name='range', type="range", min='0', max='10', step='2', value='5').render() u'' >>> Input(name='color', type="color").render() u'' >>> Input(name='f', type="file", accept=".doc,.docx,.xml").render() u'' """ def __init__(self, name, *validators, **attrs): self.name = name self.validators = validators self.attrs = attrs = AttributeList(attrs) self.type = attrs.pop("type", None) self.description = attrs.pop("description", name) self.value = attrs.pop("value", None) self.pre = attrs.pop("pre", "") self.post = attrs.pop("post", "") self.note = None self.id = attrs.setdefault("id", self.get_default_id()) if "class_" in attrs: attrs["class"] = attrs["class_"] del attrs["class_"] def is_hidden(self): return False def get_type(self): if self.type is not None: return self.type else: raise AttributeError("missing attribute 'type'") def get_default_id(self): return self.name def validate(self, value): self.set_value(value) for v in self.validators: if not v.valid(value): self.note = v.msg return False return True def set_value(self, value): self.value = value def get_value(self): return self.value def render(self): attrs = self.attrs.copy() attrs["type"] = self.get_type() if self.value is not None: attrs["value"] = self.value attrs["name"] = self.name attrs["id"] = self.id return "" % attrs def rendernote(self, note): if note: return '%s' % net.websafe(note) else: return "" def addatts(self): # add leading space for backward-compatibility return " " + str(self.attrs) class AttributeList(dict): """List of attributes of input. >>> a = AttributeList(type='text', name='x', value=20) >>> a """ def copy(self): return AttributeList(self) def __str__(self): return " ".join([f'{k}="{net.websafe(v)}"' for k, v in sorted(self.items())]) def __repr__(self): return "" % repr(str(self)) class Textbox(Input): """Textbox input. >>> Textbox(name='foo', value='bar').render() u'' >>> Textbox(name='foo', value=0).render() u'' """ def get_type(self): return "text" class Password(Input): """Password input. >>> Password(name='password', value='secret').render() u'' """ def get_type(self): return "password" class Textarea(Input): """Textarea input. >>> Textarea(name='foo', value='bar').render() u'' """ def render(self): attrs = self.attrs.copy() attrs["name"] = self.name value = net.websafe(self.value or "") return f"" class Dropdown(Input): r"""Dropdown/select input. >>> Dropdown(name='foo', args=['a', 'b', 'c'], value='b').render() u'\n' >>> Dropdown(name='foo', args=[('a', 'aa'), ('b', 'bb'), ('c', 'cc')], value='b').render() u'\n' """ def __init__(self, name, args, *validators, **attrs): self.args = args super().__init__(name, *validators, **attrs) def render(self): attrs = self.attrs.copy() attrs["name"] = self.name x = "\n" return x def _render_option(self, arg, indent=" "): if isinstance(arg, (tuple, list)): value, desc = arg else: value, desc = arg, arg value = utils.safestr(value) if isinstance(self.value, (tuple, list)): s_value = [utils.safestr(x) for x in self.value] else: s_value = utils.safestr(self.value) if s_value == value or (isinstance(s_value, list) and value in s_value): select_p = ' selected="selected"' else: select_p = "" return indent + '{}\n'.format( select_p, net.websafe(value), net.websafe(desc), ) class GroupedDropdown(Dropdown): r"""Grouped Dropdown/select input. >>> GroupedDropdown(name='car_type', args=(('Swedish Cars', ('Volvo', 'Saab')), ('German Cars', ('Mercedes', 'Audi'))), value='Audi').render() u'\n' >>> GroupedDropdown(name='car_type', args=(('Swedish Cars', (('v', 'Volvo'), ('s', 'Saab'))), ('German Cars', (('m', 'Mercedes'), ('a', 'Audi')))), value='a').render() u'\n' """ def __init__(self, name, args, *validators, **attrs): self.args = args super().__init__(name, *validators, **attrs) def render(self): attrs = self.attrs.copy() attrs["name"] = self.name x = "\n" return x class Radio(Input): def __init__(self, name, args, *validators, **attrs): self.args = args super().__init__(name, *validators, **attrs) def render(self): x = "" for idx, arg in enumerate(self.args, start=1): if isinstance(arg, (tuple, list)): value, desc = arg else: value, desc = arg, arg attrs = self.attrs.copy() attrs["name"] = self.name attrs["type"] = "radio" attrs["value"] = value attrs["id"] = self.name + str(idx) if self.value == value: attrs["checked"] = "checked" x += f" {net.websafe(desc)}" x += "" return x class Checkbox(Input): """Checkbox input. >>> Checkbox('foo', value='bar', checked=True).render() u'' >>> Checkbox('foo', value='bar').render() u'' >>> c = Checkbox('foo', value='bar') >>> c.validate('on') True >>> c.render() u'' """ def __init__(self, name, *validators, **attrs): self.checked = attrs.pop("checked", False) Input.__init__(self, name, *validators, **attrs) def get_default_id(self): value = utils.safestr(self.value or "") return self.name + "_" + value.replace(" ", "_") def render(self): attrs = self.attrs.copy() attrs["type"] = "checkbox" attrs["name"] = self.name attrs["value"] = self.value if self.checked: attrs["checked"] = "checked" return "" % attrs def set_value(self, value): self.checked = bool(value) def get_value(self): return self.checked class Button(Input): """HTML Button. >>> Button("save").render() u'' >>> Button("action", value="save", html="Save Changes").render() u'' """ def __init__(self, name, *validators, **attrs): super().__init__(name, *validators, **attrs) self.description = "" def render(self): attrs = self.attrs.copy() attrs["name"] = self.name if self.value is not None: attrs["value"] = self.value html = attrs.pop("html", None) or net.websafe(self.name) return f"" class Hidden(Input): """Hidden Input. >>> Hidden(name='foo', value='bar').render() u'' """ def is_hidden(self): return True def get_type(self): return "hidden" class File(Input): """File input. >>> File(name='f', accept=".doc,.docx,.xml").render() u'' """ def get_type(self): return "file" class Telephone(Input): """Telephone input. See: >>> Telephone(name='tel', value='55512345').render() u'' """ def get_type(self): return "tel" class Email(Input): """Email input. See: >>> Email(name='email', value='me@example.org').render() u'' """ def get_type(self): return "email" class Date(Input): """Date input. Note: Not supported by desktop Safari, Internet Explorer, or Opera Mini See: >>> Date(name='date', value='2020-04-01').render() u'' """ def get_type(self): return "date" class Time(Input): """Time input. Note: Not supported by desktop Safari, Internet Explorer, or Opera Mini See: >>> Time(name='time', value='07:00').render() u'' """ def get_type(self): return "time" class Search(Input): """Search input. See: >> Search(name='search', value='Search').render() u'' >>> Search(name='search', value='Search', required='required', pattern='[a-z0-9]{2,30}', placeholder='Search...').render() u'' """ def get_type(self): return "search" class Url(Input): """URL input. See: >>> Url(name='url', value='url').render() u'' """ def get_type(self): return "url" class Number(Input): """Number input. See: >>> Number(name='num', min='0', max='10', step='2', value='5').render() u'' """ def get_type(self): return "number" class Range(Input): """Range input. See: >>> Range(name='range', min='0', max='10', step='2', value='5').render() u'' """ def get_type(self): return "range" class Color(Input): """Color input. Note: Not supported by Internet Explorer or Opera Mini See: >>> Color(name='color').render() u'' """ def get_type(self): return "color" class Datalist(Input): """Datalist input. This is currently supported by Chrome, Firefox, Edge, and Opera. It is not supported on Safari or Internet Explorer. Use it with caution. Datalist cannot be used separately. It must be bound to an input. >>> Datalist(name='list', args=[('a', 'b'), ('c', 'd')]).render() u'' >>> Datalist(name='list', args=[['a', 'b'], ['c', 'd']]).render() u'' >>> Datalist(name='list', args=['a', 'b', 'c', 'd']).render() u'' """ def __init__(self, name, args, *validators, **kwargs): self.args = args super().__init__(name, *validators, **kwargs) def render(self): attrs = self.attrs.copy() attrs["name"] = self.name label_p = "" x = "" % attrs for arg in self.args: if isinstance(arg, (tuple, list)): label_p = ' label="%s"' % net.websafe(arg[0]) label = net.websafe(arg[1]) else: label = net.websafe(arg) x += f'' x += "" return x class Validator: def __deepcopy__(self, memo): return copy.copy(self) def __init__(self, msg, test, jstest=None): utils.autoassign(self, locals()) def valid(self, value): try: return self.test(value) except: return False notnull = Validator("Required", bool) class regexp(Validator): def __init__(self, rexp, msg): self.rexp = re.compile(rexp) self.msg = msg def valid(self, value): return bool(self.rexp.match(value)) if __name__ == "__main__": import doctest doctest.testmod()