om@3
|
1 |
"""
|
om@3
|
2 |
HTML forms
|
om@3
|
3 |
(part of web.py)
|
om@3
|
4 |
"""
|
om@3
|
5 |
|
om@3
|
6 |
import copy, re
|
om@3
|
7 |
import webapi as web
|
om@3
|
8 |
import utils, net
|
om@3
|
9 |
|
om@3
|
10 |
def attrget(obj, attr, value=None):
|
om@3
|
11 |
try:
|
om@3
|
12 |
if hasattr(obj, 'has_key') and obj.has_key(attr):
|
om@3
|
13 |
return obj[attr]
|
om@3
|
14 |
except TypeError:
|
om@3
|
15 |
# Handle the case where has_key takes different number of arguments.
|
om@3
|
16 |
# This is the case with Model objects on appengine. See #134
|
om@3
|
17 |
pass
|
om@3
|
18 |
if hasattr(obj, attr):
|
om@3
|
19 |
return getattr(obj, attr)
|
om@3
|
20 |
return value
|
om@3
|
21 |
|
om@3
|
22 |
class Form(object):
|
om@3
|
23 |
r"""
|
om@3
|
24 |
HTML form.
|
om@3
|
25 |
|
om@3
|
26 |
>>> f = Form(Textbox("x"))
|
om@3
|
27 |
>>> f.render()
|
om@3
|
28 |
u'<table>\n <tr><th><label for="x">x</label></th><td><input type="text" id="x" name="x"/></td></tr>\n</table>'
|
om@3
|
29 |
"""
|
om@3
|
30 |
def __init__(self, *inputs, **kw):
|
om@3
|
31 |
self.inputs = inputs
|
om@3
|
32 |
self.valid = True
|
om@3
|
33 |
self.note = None
|
om@3
|
34 |
self.validators = kw.pop('validators', [])
|
om@3
|
35 |
|
om@3
|
36 |
def __call__(self, x=None):
|
om@3
|
37 |
o = copy.deepcopy(self)
|
om@3
|
38 |
if x: o.validates(x)
|
om@3
|
39 |
return o
|
om@3
|
40 |
|
om@3
|
41 |
def render(self):
|
om@3
|
42 |
out = ''
|
om@3
|
43 |
out += self.rendernote(self.note)
|
om@3
|
44 |
out += '<table>\n'
|
om@3
|
45 |
|
om@3
|
46 |
for i in self.inputs:
|
om@3
|
47 |
html = utils.safeunicode(i.pre) + i.render() + self.rendernote(i.note) + utils.safeunicode(i.post)
|
om@3
|
48 |
if i.is_hidden():
|
om@3
|
49 |
out += ' <tr style="display: none;"><th></th><td>%s</td></tr>\n' % (html)
|
om@3
|
50 |
else:
|
om@3
|
51 |
out += ' <tr><th><label for="%s">%s</label></th><td>%s</td></tr>\n' % (i.id, net.websafe(i.description), html)
|
om@3
|
52 |
out += "</table>"
|
om@3
|
53 |
return out
|
om@3
|
54 |
|
om@3
|
55 |
def render_css(self):
|
om@3
|
56 |
out = []
|
om@3
|
57 |
out.append(self.rendernote(self.note))
|
om@3
|
58 |
for i in self.inputs:
|
om@3
|
59 |
if not i.is_hidden():
|
om@3
|
60 |
out.append('<label for="%s">%s</label>' % (i.id, net.websafe(i.description)))
|
om@3
|
61 |
out.append(i.pre)
|
om@3
|
62 |
out.append(i.render())
|
om@3
|
63 |
out.append(self.rendernote(i.note))
|
om@3
|
64 |
out.append(i.post)
|
om@3
|
65 |
out.append('\n')
|
om@3
|
66 |
return ''.join(out)
|
om@3
|
67 |
|
om@3
|
68 |
def rendernote(self, note):
|
om@3
|
69 |
if note: return '<strong class="wrong">%s</strong>' % net.websafe(note)
|
om@3
|
70 |
else: return ""
|
om@3
|
71 |
|
om@3
|
72 |
def validates(self, source=None, _validate=True, **kw):
|
om@3
|
73 |
source = source or kw or web.input()
|
om@3
|
74 |
out = True
|
om@3
|
75 |
for i in self.inputs:
|
om@3
|
76 |
v = attrget(source, i.name)
|
om@3
|
77 |
if _validate:
|
om@3
|
78 |
out = i.validate(v) and out
|
om@3
|
79 |
else:
|
om@3
|
80 |
i.set_value(v)
|
om@3
|
81 |
if _validate:
|
om@3
|
82 |
out = out and self._validate(source)
|
om@3
|
83 |
self.valid = out
|
om@3
|
84 |
return out
|
om@3
|
85 |
|
om@3
|
86 |
def _validate(self, value):
|
om@3
|
87 |
self.value = value
|
om@3
|
88 |
for v in self.validators:
|
om@3
|
89 |
if not v.valid(value):
|
om@3
|
90 |
self.note = v.msg
|
om@3
|
91 |
return False
|
om@3
|
92 |
return True
|
om@3
|
93 |
|
om@3
|
94 |
def fill(self, source=None, **kw):
|
om@3
|
95 |
return self.validates(source, _validate=False, **kw)
|
om@3
|
96 |
|
om@3
|
97 |
def __getitem__(self, i):
|
om@3
|
98 |
for x in self.inputs:
|
om@3
|
99 |
if x.name == i: return x
|
om@3
|
100 |
raise KeyError, i
|
om@3
|
101 |
|
om@3
|
102 |
def __getattr__(self, name):
|
om@3
|
103 |
# don't interfere with deepcopy
|
om@3
|
104 |
inputs = self.__dict__.get('inputs') or []
|
om@3
|
105 |
for x in inputs:
|
om@3
|
106 |
if x.name == name: return x
|
om@3
|
107 |
raise AttributeError, name
|
om@3
|
108 |
|
om@3
|
109 |
def get(self, i, default=None):
|
om@3
|
110 |
try:
|
om@3
|
111 |
return self[i]
|
om@3
|
112 |
except KeyError:
|
om@3
|
113 |
return default
|
om@3
|
114 |
|
om@3
|
115 |
def _get_d(self): #@@ should really be form.attr, no?
|
om@3
|
116 |
return utils.storage([(i.name, i.get_value()) for i in self.inputs])
|
om@3
|
117 |
d = property(_get_d)
|
om@3
|
118 |
|
om@3
|
119 |
class Input(object):
|
om@3
|
120 |
def __init__(self, name, *validators, **attrs):
|
om@3
|
121 |
self.name = name
|
om@3
|
122 |
self.validators = validators
|
om@3
|
123 |
self.attrs = attrs = AttributeList(attrs)
|
om@3
|
124 |
|
om@3
|
125 |
self.description = attrs.pop('description', name)
|
om@3
|
126 |
self.value = attrs.pop('value', None)
|
om@3
|
127 |
self.pre = attrs.pop('pre', "")
|
om@3
|
128 |
self.post = attrs.pop('post', "")
|
om@3
|
129 |
self.note = None
|
om@3
|
130 |
|
om@3
|
131 |
self.id = attrs.setdefault('id', self.get_default_id())
|
om@3
|
132 |
|
om@3
|
133 |
if 'class_' in attrs:
|
om@3
|
134 |
attrs['class'] = attrs['class_']
|
om@3
|
135 |
del attrs['class_']
|
om@3
|
136 |
|
om@3
|
137 |
def is_hidden(self):
|
om@3
|
138 |
return False
|
om@3
|
139 |
|
om@3
|
140 |
def get_type(self):
|
om@3
|
141 |
raise NotImplementedError
|
om@3
|
142 |
|
om@3
|
143 |
def get_default_id(self):
|
om@3
|
144 |
return self.name
|
om@3
|
145 |
|
om@3
|
146 |
def validate(self, value):
|
om@3
|
147 |
self.set_value(value)
|
om@3
|
148 |
|
om@3
|
149 |
for v in self.validators:
|
om@3
|
150 |
if not v.valid(value):
|
om@3
|
151 |
self.note = v.msg
|
om@3
|
152 |
return False
|
om@3
|
153 |
return True
|
om@3
|
154 |
|
om@3
|
155 |
def set_value(self, value):
|
om@3
|
156 |
self.value = value
|
om@3
|
157 |
|
om@3
|
158 |
def get_value(self):
|
om@3
|
159 |
return self.value
|
om@3
|
160 |
|
om@3
|
161 |
def render(self):
|
om@3
|
162 |
attrs = self.attrs.copy()
|
om@3
|
163 |
attrs['type'] = self.get_type()
|
om@3
|
164 |
if self.value is not None:
|
om@3
|
165 |
attrs['value'] = self.value
|
om@3
|
166 |
attrs['name'] = self.name
|
om@3
|
167 |
return '<input %s/>' % attrs
|
om@3
|
168 |
|
om@3
|
169 |
def rendernote(self, note):
|
om@3
|
170 |
if note: return '<strong class="wrong">%s</strong>' % net.websafe(note)
|
om@3
|
171 |
else: return ""
|
om@3
|
172 |
|
om@3
|
173 |
def addatts(self):
|
om@3
|
174 |
# add leading space for backward-compatibility
|
om@3
|
175 |
return " " + str(self.attrs)
|
om@3
|
176 |
|
om@3
|
177 |
class AttributeList(dict):
|
om@3
|
178 |
"""List of atributes of input.
|
om@3
|
179 |
|
om@3
|
180 |
>>> a = AttributeList(type='text', name='x', value=20)
|
om@3
|
181 |
>>> a
|
om@3
|
182 |
<attrs: 'type="text" name="x" value="20"'>
|
om@3
|
183 |
"""
|
om@3
|
184 |
def copy(self):
|
om@3
|
185 |
return AttributeList(self)
|
om@3
|
186 |
|
om@3
|
187 |
def __str__(self):
|
om@3
|
188 |
return " ".join(['%s="%s"' % (k, net.websafe(v)) for k, v in self.items()])
|
om@3
|
189 |
|
om@3
|
190 |
def __repr__(self):
|
om@3
|
191 |
return '<attrs: %s>' % repr(str(self))
|
om@3
|
192 |
|
om@3
|
193 |
class Textbox(Input):
|
om@3
|
194 |
"""Textbox input.
|
om@3
|
195 |
|
om@3
|
196 |
>>> Textbox(name='foo', value='bar').render()
|
om@3
|
197 |
u'<input type="text" id="foo" value="bar" name="foo"/>'
|
om@3
|
198 |
>>> Textbox(name='foo', value=0).render()
|
om@3
|
199 |
u'<input type="text" id="foo" value="0" name="foo"/>'
|
om@3
|
200 |
"""
|
om@3
|
201 |
def get_type(self):
|
om@3
|
202 |
return 'text'
|
om@3
|
203 |
|
om@3
|
204 |
class Password(Input):
|
om@3
|
205 |
"""Password input.
|
om@3
|
206 |
|
om@3
|
207 |
>>> Password(name='password', value='secret').render()
|
om@3
|
208 |
u'<input type="password" id="password" value="secret" name="password"/>'
|
om@3
|
209 |
"""
|
om@3
|
210 |
|
om@3
|
211 |
def get_type(self):
|
om@3
|
212 |
return 'password'
|
om@3
|
213 |
|
om@3
|
214 |
class Textarea(Input):
|
om@3
|
215 |
"""Textarea input.
|
om@3
|
216 |
|
om@3
|
217 |
>>> Textarea(name='foo', value='bar').render()
|
om@3
|
218 |
u'<textarea id="foo" name="foo">bar</textarea>'
|
om@3
|
219 |
"""
|
om@3
|
220 |
def render(self):
|
om@3
|
221 |
attrs = self.attrs.copy()
|
om@3
|
222 |
attrs['name'] = self.name
|
om@3
|
223 |
value = net.websafe(self.value or '')
|
om@3
|
224 |
return '<textarea %s>%s</textarea>' % (attrs, value)
|
om@3
|
225 |
|
om@3
|
226 |
class Dropdown(Input):
|
om@3
|
227 |
r"""Dropdown/select input.
|
om@3
|
228 |
|
om@3
|
229 |
>>> Dropdown(name='foo', args=['a', 'b', 'c'], value='b').render()
|
om@3
|
230 |
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'
|
om@3
|
231 |
>>> Dropdown(name='foo', args=[('a', 'aa'), ('b', 'bb'), ('c', 'cc')], value='b').render()
|
om@3
|
232 |
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'
|
om@3
|
233 |
"""
|
om@3
|
234 |
def __init__(self, name, args, *validators, **attrs):
|
om@3
|
235 |
self.args = args
|
om@3
|
236 |
super(Dropdown, self).__init__(name, *validators, **attrs)
|
om@3
|
237 |
|
om@3
|
238 |
def render(self):
|
om@3
|
239 |
attrs = self.attrs.copy()
|
om@3
|
240 |
attrs['name'] = self.name
|
om@3
|
241 |
|
om@3
|
242 |
x = '<select %s>\n' % attrs
|
om@3
|
243 |
|
om@3
|
244 |
for arg in self.args:
|
om@3
|
245 |
x += self._render_option(arg)
|
om@3
|
246 |
|
om@3
|
247 |
x += '</select>\n'
|
om@3
|
248 |
return x
|
om@3
|
249 |
|
om@3
|
250 |
def _render_option(self, arg, indent=' '):
|
om@3
|
251 |
if isinstance(arg, (tuple, list)):
|
om@3
|
252 |
value, desc= arg
|
om@3
|
253 |
else:
|
om@3
|
254 |
value, desc = arg, arg
|
om@3
|
255 |
|
om@3
|
256 |
if self.value == value or (isinstance(self.value, list) and value in self.value):
|
om@3
|
257 |
select_p = ' selected="selected"'
|
om@3
|
258 |
else:
|
om@3
|
259 |
select_p = ''
|
om@3
|
260 |
return indent + '<option%s value="%s">%s</option>\n' % (select_p, net.websafe(value), net.websafe(desc))
|
om@3
|
261 |
|
om@3
|
262 |
|
om@3
|
263 |
class GroupedDropdown(Dropdown):
|
om@3
|
264 |
r"""Grouped Dropdown/select input.
|
om@3
|
265 |
|
om@3
|
266 |
>>> GroupedDropdown(name='car_type', args=(('Swedish Cars', ('Volvo', 'Saab')), ('German Cars', ('Mercedes', 'Audi'))), value='Audi').render()
|
om@3
|
267 |
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'
|
om@3
|
268 |
>>> GroupedDropdown(name='car_type', args=(('Swedish Cars', (('v', 'Volvo'), ('s', 'Saab'))), ('German Cars', (('m', 'Mercedes'), ('a', 'Audi')))), value='a').render()
|
om@3
|
269 |
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'
|
om@3
|
270 |
|
om@3
|
271 |
"""
|
om@3
|
272 |
def __init__(self, name, args, *validators, **attrs):
|
om@3
|
273 |
self.args = args
|
om@3
|
274 |
super(Dropdown, self).__init__(name, *validators, **attrs)
|
om@3
|
275 |
|
om@3
|
276 |
def render(self):
|
om@3
|
277 |
attrs = self.attrs.copy()
|
om@3
|
278 |
attrs['name'] = self.name
|
om@3
|
279 |
|
om@3
|
280 |
x = '<select %s>\n' % attrs
|
om@3
|
281 |
|
om@3
|
282 |
for label, options in self.args:
|
om@3
|
283 |
x += ' <optgroup label="%s">\n' % net.websafe(label)
|
om@3
|
284 |
for arg in options:
|
om@3
|
285 |
x += self._render_option(arg, indent = ' ')
|
om@3
|
286 |
x += ' </optgroup>\n'
|
om@3
|
287 |
|
om@3
|
288 |
x += '</select>\n'
|
om@3
|
289 |
return x
|
om@3
|
290 |
|
om@3
|
291 |
class Radio(Input):
|
om@3
|
292 |
def __init__(self, name, args, *validators, **attrs):
|
om@3
|
293 |
self.args = args
|
om@3
|
294 |
super(Radio, self).__init__(name, *validators, **attrs)
|
om@3
|
295 |
|
om@3
|
296 |
def render(self):
|
om@3
|
297 |
x = '<span>'
|
om@3
|
298 |
for arg in self.args:
|
om@3
|
299 |
if isinstance(arg, (tuple, list)):
|
om@3
|
300 |
value, desc= arg
|
om@3
|
301 |
else:
|
om@3
|
302 |
value, desc = arg, arg
|
om@3
|
303 |
attrs = self.attrs.copy()
|
om@3
|
304 |
attrs['name'] = self.name
|
om@3
|
305 |
attrs['type'] = 'radio'
|
om@3
|
306 |
attrs['value'] = value
|
om@3
|
307 |
if self.value == value:
|
om@3
|
308 |
attrs['checked'] = 'checked'
|
om@3
|
309 |
x += '<input %s/> %s' % (attrs, net.websafe(desc))
|
om@3
|
310 |
x += '</span>'
|
om@3
|
311 |
return x
|
om@3
|
312 |
|
om@3
|
313 |
class Checkbox(Input):
|
om@3
|
314 |
"""Checkbox input.
|
om@3
|
315 |
|
om@3
|
316 |
>>> Checkbox('foo', value='bar', checked=True).render()
|
om@3
|
317 |
u'<input checked="checked" type="checkbox" id="foo_bar" value="bar" name="foo"/>'
|
om@3
|
318 |
>>> Checkbox('foo', value='bar').render()
|
om@3
|
319 |
u'<input type="checkbox" id="foo_bar" value="bar" name="foo"/>'
|
om@3
|
320 |
>>> c = Checkbox('foo', value='bar')
|
om@3
|
321 |
>>> c.validate('on')
|
om@3
|
322 |
True
|
om@3
|
323 |
>>> c.render()
|
om@3
|
324 |
u'<input checked="checked" type="checkbox" id="foo_bar" value="bar" name="foo"/>'
|
om@3
|
325 |
"""
|
om@3
|
326 |
def __init__(self, name, *validators, **attrs):
|
om@3
|
327 |
self.checked = attrs.pop('checked', False)
|
om@3
|
328 |
Input.__init__(self, name, *validators, **attrs)
|
om@3
|
329 |
|
om@3
|
330 |
def get_default_id(self):
|
om@3
|
331 |
value = utils.safestr(self.value or "")
|
om@3
|
332 |
return self.name + '_' + value.replace(' ', '_')
|
om@3
|
333 |
|
om@3
|
334 |
def render(self):
|
om@3
|
335 |
attrs = self.attrs.copy()
|
om@3
|
336 |
attrs['type'] = 'checkbox'
|
om@3
|
337 |
attrs['name'] = self.name
|
om@3
|
338 |
attrs['value'] = self.value
|
om@3
|
339 |
|
om@3
|
340 |
if self.checked:
|
om@3
|
341 |
attrs['checked'] = 'checked'
|
om@3
|
342 |
return '<input %s/>' % attrs
|
om@3
|
343 |
|
om@3
|
344 |
def set_value(self, value):
|
om@3
|
345 |
self.checked = bool(value)
|
om@3
|
346 |
|
om@3
|
347 |
def get_value(self):
|
om@3
|
348 |
return self.checked
|
om@3
|
349 |
|
om@3
|
350 |
class Button(Input):
|
om@3
|
351 |
"""HTML Button.
|
om@3
|
352 |
|
om@3
|
353 |
>>> Button("save").render()
|
om@3
|
354 |
u'<button id="save" name="save">save</button>'
|
om@3
|
355 |
>>> Button("action", value="save", html="<b>Save Changes</b>").render()
|
om@3
|
356 |
u'<button id="action" value="save" name="action"><b>Save Changes</b></button>'
|
om@3
|
357 |
"""
|
om@3
|
358 |
def __init__(self, name, *validators, **attrs):
|
om@3
|
359 |
super(Button, self).__init__(name, *validators, **attrs)
|
om@3
|
360 |
self.description = ""
|
om@3
|
361 |
|
om@3
|
362 |
def render(self):
|
om@3
|
363 |
attrs = self.attrs.copy()
|
om@3
|
364 |
attrs['name'] = self.name
|
om@3
|
365 |
if self.value is not None:
|
om@3
|
366 |
attrs['value'] = self.value
|
om@3
|
367 |
html = attrs.pop('html', None) or net.websafe(self.name)
|
om@3
|
368 |
return '<button %s>%s</button>' % (attrs, html)
|
om@3
|
369 |
|
om@3
|
370 |
class Hidden(Input):
|
om@3
|
371 |
"""Hidden Input.
|
om@3
|
372 |
|
om@3
|
373 |
>>> Hidden(name='foo', value='bar').render()
|
om@3
|
374 |
u'<input type="hidden" id="foo" value="bar" name="foo"/>'
|
om@3
|
375 |
"""
|
om@3
|
376 |
def is_hidden(self):
|
om@3
|
377 |
return True
|
om@3
|
378 |
|
om@3
|
379 |
def get_type(self):
|
om@3
|
380 |
return 'hidden'
|
om@3
|
381 |
|
om@3
|
382 |
class File(Input):
|
om@3
|
383 |
"""File input.
|
om@3
|
384 |
|
om@3
|
385 |
>>> File(name='f').render()
|
om@3
|
386 |
u'<input type="file" id="f" name="f"/>'
|
om@3
|
387 |
"""
|
om@3
|
388 |
def get_type(self):
|
om@3
|
389 |
return 'file'
|
om@3
|
390 |
|
om@3
|
391 |
class Validator:
|
om@3
|
392 |
def __deepcopy__(self, memo): return copy.copy(self)
|
om@3
|
393 |
def __init__(self, msg, test, jstest=None): utils.autoassign(self, locals())
|
om@3
|
394 |
def valid(self, value):
|
om@3
|
395 |
try: return self.test(value)
|
om@3
|
396 |
except: return False
|
om@3
|
397 |
|
om@3
|
398 |
notnull = Validator("Required", bool)
|
om@3
|
399 |
|
om@3
|
400 |
class regexp(Validator):
|
om@3
|
401 |
def __init__(self, rexp, msg):
|
om@3
|
402 |
self.rexp = re.compile(rexp)
|
om@3
|
403 |
self.msg = msg
|
om@3
|
404 |
|
om@3
|
405 |
def valid(self, value):
|
om@3
|
406 |
return bool(self.rexp.match(value))
|
om@3
|
407 |
|
om@3
|
408 |
if __name__ == "__main__":
|
om@3
|
409 |
import doctest
|
om@3
|
410 |
doctest.testmod()
|