Files
iRedAdmin-Pro-SQL/web/form.py
2023-04-10 07:22:09 +02:00

691 lines
21 KiB
Python

"""
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'<table>\n <tr><th><label for="x">x</label></th><td><input id="x" name="x" type="text"/></td></tr>\n</table>'
>>> f.fill(x="42")
True
>>> f.render()
u'<table>\n <tr><th><label for="x">x</label></th><td><input id="x" name="x" type="text" value="42"/></td></tr>\n</table>'
"""
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 += "<table>\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 += ' <tr style="display: none;"><th></th><td>%s</td></tr>\n' % (
html
)
else:
out += (
' <tr><th><label for="%s">%s</label></th><td>%s</td></tr>\n'
% (net.websafe(i.id), net.websafe(i.description), html)
)
out += "</table>"
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(
'<label for="%s">%s</label>'
% (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 '<strong class="wrong">%s</strong>' % 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: <https://www.w3.org/TR/html52/sec-forms.html#the-input-element>
Currently only types which can be written inside one `<input />` 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 id="foo" name="foo" type="email" value="user@domain.com"/>'
>>> Input(name='foo', type='number', value="bar").render()
u'<input id="foo" name="foo" type="number" value="bar"/>'
>>> Input(name='num', type="number", min='0', max='10', step='2', value='5').render()
u'<input id="num" max="10" min="0" name="num" step="2" type="number" value="5"/>'
>>> Input(name='foo', type="tel", value='55512345').render()
u'<input id="foo" name="foo" type="tel" value="55512345"/>'
>>> Input(name='search', type="search", value='Search').render()
u'<input id="search" name="search" type="search" value="Search"/>'
>>> Input(name='search', type="search", value='Search', required='required', pattern='[a-z0-9]{2,30}', placeholder='Search...').render()
u'<input id="search" name="search" pattern="[a-z0-9]{2,30}" placeholder="Search..." required="required" type="search" value="Search"/>'
>>> Input(name='url', type="url", value='url').render()
u'<input id="url" name="url" type="url" value="url"/>'
>>> Input(name='range', type="range", min='0', max='10', step='2', value='5').render()
u'<input id="range" max="10" min="0" name="range" step="2" type="range" value="5"/>'
>>> Input(name='color', type="color").render()
u'<input id="color" name="color" type="color"/>'
>>> Input(name='f', type="file", accept=".doc,.docx,.xml").render()
u'<input accept=".doc,.docx,.xml" id="f" name="f" type="file"/>'
"""
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 "<input %s/>" % attrs
def rendernote(self, note):
if note:
return '<strong class="wrong">%s</strong>' % 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
<attrs: 'name="x" type="text" value="20"'>
"""
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 "<attrs: %s>" % repr(str(self))
class Textbox(Input):
"""Textbox input.
>>> Textbox(name='foo', value='bar').render()
u'<input id="foo" name="foo" type="text" value="bar"/>'
>>> Textbox(name='foo', value=0).render()
u'<input id="foo" name="foo" type="text" value="0"/>'
"""
def get_type(self):
return "text"
class Password(Input):
"""Password input.
>>> Password(name='password', value='secret').render()
u'<input id="password" name="password" type="password" value="secret"/>'
"""
def get_type(self):
return "password"
class Textarea(Input):
"""Textarea input.
>>> Textarea(name='foo', value='bar').render()
u'<textarea id="foo" name="foo">bar</textarea>'
"""
def render(self):
attrs = self.attrs.copy()
attrs["name"] = self.name
value = net.websafe(self.value or "")
return f"<textarea {attrs}>{value}</textarea>"
class Dropdown(Input):
r"""Dropdown/select input.
>>> Dropdown(name='foo', args=['a', 'b', 'c'], value='b').render()
u'<select id="foo" name="foo">\n <option value="a">a</option>\n <option selected="selected" value="b">b</option>\n <option value="c">c</option>\n</select>\n'
>>> Dropdown(name='foo', args=[('a', 'aa'), ('b', 'bb'), ('c', 'cc')], value='b').render()
u'<select id="foo" name="foo">\n <option value="a">aa</option>\n <option selected="selected" value="b">bb</option>\n <option value="c">cc</option>\n</select>\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 = "<select %s>\n" % attrs
for arg in self.args:
x += self._render_option(arg)
x += "</select>\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 + '<option{} value="{}">{}</option>\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'<select id="car_type" name="car_type">\n <optgroup label="Swedish Cars">\n <option value="Volvo">Volvo</option>\n <option value="Saab">Saab</option>\n </optgroup>\n <optgroup label="German Cars">\n <option value="Mercedes">Mercedes</option>\n <option selected="selected" value="Audi">Audi</option>\n </optgroup>\n</select>\n'
>>> GroupedDropdown(name='car_type', args=(('Swedish Cars', (('v', 'Volvo'), ('s', 'Saab'))), ('German Cars', (('m', 'Mercedes'), ('a', 'Audi')))), value='a').render()
u'<select id="car_type" name="car_type">\n <optgroup label="Swedish Cars">\n <option value="v">Volvo</option>\n <option value="s">Saab</option>\n </optgroup>\n <optgroup label="German Cars">\n <option value="m">Mercedes</option>\n <option selected="selected" value="a">Audi</option>\n </optgroup>\n</select>\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 = "<select %s>\n" % attrs
for label, options in self.args:
x += ' <optgroup label="%s">\n' % net.websafe(label)
for arg in options:
x += self._render_option(arg, indent=" ")
x += " </optgroup>\n"
x += "</select>\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 = "<span>"
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"<input {attrs}/> {net.websafe(desc)}"
x += "</span>"
return x
class Checkbox(Input):
"""Checkbox input.
>>> Checkbox('foo', value='bar', checked=True).render()
u'<input checked="checked" id="foo_bar" name="foo" type="checkbox" value="bar"/>'
>>> Checkbox('foo', value='bar').render()
u'<input id="foo_bar" name="foo" type="checkbox" value="bar"/>'
>>> c = Checkbox('foo', value='bar')
>>> c.validate('on')
True
>>> c.render()
u'<input checked="checked" id="foo_bar" name="foo" type="checkbox" value="bar"/>'
"""
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 "<input %s/>" % 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 id="save" name="save">save</button>'
>>> Button("action", value="save", html="<b>Save Changes</b>").render()
u'<button id="action" name="action" value="save"><b>Save Changes</b></button>'
"""
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"<button {attrs}>{html}</button>"
class Hidden(Input):
"""Hidden Input.
>>> Hidden(name='foo', value='bar').render()
u'<input id="foo" name="foo" type="hidden" value="bar"/>'
"""
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'<input accept=".doc,.docx,.xml" id="f" name="f" type="file"/>'
"""
def get_type(self):
return "file"
class Telephone(Input):
"""Telephone input.
See: <https://html.spec.whatwg.org/#telephone-state-(type=tel)>
>>> Telephone(name='tel', value='55512345').render()
u'<input id="tel" name="tel" type="tel" value="55512345"/>'
"""
def get_type(self):
return "tel"
class Email(Input):
"""Email input.
See: <https://html.spec.whatwg.org/#e-mail-state-(type=email)>
>>> Email(name='email', value='me@example.org').render()
u'<input id="email" name="email" type="email" value="me@example.org"/>'
"""
def get_type(self):
return "email"
class Date(Input):
"""Date input.
Note: Not supported by desktop Safari, Internet Explorer, or Opera Mini
See: <https://html.spec.whatwg.org/#date-state-(type=date)>
>>> Date(name='date', value='2020-04-01').render()
u'<input id="date" name="date" type="date" value="2020-04-01"/>'
"""
def get_type(self):
return "date"
class Time(Input):
"""Time input.
Note: Not supported by desktop Safari, Internet Explorer, or Opera Mini
See: <https://html.spec.whatwg.org/#time-state-(type=time)>
>>> Time(name='time', value='07:00').render()
u'<input id="time" name="time" type="time" value="07:00"/>'
"""
def get_type(self):
return "time"
class Search(Input):
"""Search input.
See: <https://html.spec.whatwg.org/#text-(type=text)-state-and-search-state-(type=search)>
>> Search(name='search', value='Search').render()
u'<input id="search" name="search" type="search" value="Search"/>'
>>> Search(name='search', value='Search', required='required', pattern='[a-z0-9]{2,30}', placeholder='Search...').render()
u'<input id="search" name="search" pattern="[a-z0-9]{2,30}" placeholder="Search..." required="required" type="search" value="Search"/>'
"""
def get_type(self):
return "search"
class Url(Input):
"""URL input.
See: <https://html.spec.whatwg.org/#url-state-(type=url)>
>>> Url(name='url', value='url').render()
u'<input id="url" name="url" type="url" value="url"/>'
"""
def get_type(self):
return "url"
class Number(Input):
"""Number input.
See: <https://html.spec.whatwg.org/#number-state-(type=number)>
>>> Number(name='num', min='0', max='10', step='2', value='5').render()
u'<input id="num" max="10" min="0" name="num" step="2" type="number" value="5"/>'
"""
def get_type(self):
return "number"
class Range(Input):
"""Range input.
See: <https://html.spec.whatwg.org/#range-state-(type=range)>
>>> Range(name='range', min='0', max='10', step='2', value='5').render()
u'<input id="range" max="10" min="0" name="range" step="2" type="range" value="5"/>'
"""
def get_type(self):
return "range"
class Color(Input):
"""Color input.
Note: Not supported by Internet Explorer or Opera Mini
See: <https://html.spec.whatwg.org/#color-stat://html.spec.whatwg.org/#color-state-(type=color)>
>>> Color(name='color').render()
u'<input id="color" name="color" type="color"/>'
"""
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.
<https://html.spec.whatwg.org/#the-datalist-element>
>>> Datalist(name='list', args=[('a', 'b'), ('c', 'd')]).render()
u'<datalist id="list" name="list"><option label="a" value="b"/><option label="c" value="d"/></datalist>'
>>> Datalist(name='list', args=[['a', 'b'], ['c', 'd']]).render()
u'<datalist id="list" name="list"><option label="a" value="b"/><option label="c" value="d"/></datalist>'
>>> Datalist(name='list', args=['a', 'b', 'c', 'd']).render()
u'<datalist id="list" name="list"><option value="a"/><option value="b"/><option value="c"/><option value="d"/></datalist>'
"""
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 = "<datalist %s>" % 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'<option{label_p} value="{label}"/>'
x += "</datalist>"
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()