formencode.htmlfill
Covered: 276 lines
Missed: 203 lines
Skipped 54 lines
Percent: 57 %
  1
"""
  2
Parser for HTML forms, that fills in defaults and errors.  See
  3
``render``.
  4
"""
  6
import HTMLParser
  7
import re
  8
from formencode.rewritingparser import RewritingParser, html_quote
 10
__all__ = ['render', 'htmlliteral', 'default_formatter',
 11
           'none_formatter', 'escape_formatter',
 12
           'FillingParser']
 14
def render(form, defaults=None, errors=None, use_all_keys=False,
 15
           error_formatters=None, add_attributes=None,
 16
           auto_insert_errors=True, auto_error_formatter=None,
 17
           text_as_default=False, listener=None, encoding=None,
 18
           error_class='error', prefix_error=True,
 19
           force_defaults=True):
 20
    """
 21
    Render the ``form`` (which should be a string) given the defaults
 22
    and errors.  Defaults are the values that go in the input fields
 23
    (overwriting any values that are there) and errors are displayed
 24
    inline in the form (and also effect input classes).  Returns the
 25
    rendered string.
 27
    If ``auto_insert_errors`` is true (the default) then any errors
 28
    for which ``<form:error>`` tags can't be found will be put just
 29
    above the associated input field, or at the top of the form if no
 30
    field can be found.
 32
    If ``use_all_keys`` is true, if there are any extra fields from
 33
    defaults or errors that couldn't be used in the form it will be an
 34
    error.
 36
    ``error_formatters`` is a dictionary of formatter names to
 37
    one-argument functions that format an error into HTML.  Some
 38
    default formatters are provided if you don't provide this.
 40
    ``error_class`` is the class added to input fields when there is
 41
    an error for that field.
 43
    ``add_attributes`` is a dictionary of field names to a dictionary
 44
    of attribute name/values.  If the name starts with ``+`` then the
 45
    value will be appended to any existing attribute (e.g.,
 46
    ``{'+class': ' important'}``).
 48
    ``auto_error_formatter`` is used to create the HTML that goes
 49
    above the fields.  By default it wraps the error message in a span
 50
    and adds a ``<br>``.
 52
    If ``text_as_default`` is true (default false) then ``<input
 53
    type=unknown>`` will be treated as text inputs.
 55
    ``listener`` can be an object that watches fields pass; the only
 56
    one currently is in ``htmlfill_schemabuilder.SchemaBuilder``
 58
    ``encoding`` specifies an encoding to assume when mixing str and 
 59
    unicode text in the template.
 61
    ``prefix_error`` specifies if the HTML created by auto_error_formatter is
 62
    put before the input control (default) or after the control.
 64
    ``force_defaults`` specifies if a field default is not given in
 65
    the ``defaults`` dictionary then the control associated with the
 66
    field should be set as an unsuccessful control. So checkboxes will
 67
    be cleared, radio and select controls will have no value selected,
 68
    and textareas will be emptied. This defaults to ``True``, which is
 69
    appropriate the defaults are the result of a form submission.
 70
    """
 71
    if defaults is None:
 72
        defaults = {}
 73
    if auto_insert_errors and auto_error_formatter is None:
 74
        auto_error_formatter = default_formatter
 75
    p = FillingParser(
 76
        defaults=defaults, errors=errors,
 77
        use_all_keys=use_all_keys,
 78
        error_formatters=error_formatters,
 79
        add_attributes=add_attributes,
 80
        auto_error_formatter=auto_error_formatter,
 81
        text_as_default=text_as_default,
 82
        listener=listener, encoding=encoding,
 83
        prefix_error=prefix_error,
 84
        error_class=error_class,
 85
        force_defaults=force_defaults,
 86
        )
 87
    p.feed(form)
 88
    p.close()
 89
    return p.text()
 92
class htmlliteral(object):
 94
    def __init__(self, html, text=None):
 95
        if text is None:
 96
            text = re.sub(r'<.*?>', '', html)
 97
            text = html.replace('&gt;', '>')
 98
            text = html.replace('&lt;', '<')
 99
            text = html.replace('&quot;', '"')
101
        self.html = html
102
        self.text = text
104
    def __str__(self):
105
        return self.text
107
    def __repr__(self):
108
        return '<%s html=%r text=%r>' % (
109
            self.__class__.__name__, self.html, self.text)
111
    def __html__(self):
112
        return self.html
114
def default_formatter(error):
115
    """
116
    Formatter that escapes the error, wraps the error in a span with
117
    class ``error-message``, and adds a ``<br>``
118
    """
119
    return '<span class="error-message">%s</span><br />\n' % html_quote(error)
121
def none_formatter(error):
122
    """
123
    Formatter that does nothing, no escaping HTML, nothin'
124
    """
125
    return error
127
def escape_formatter(error):
128
    """
129
    Formatter that escapes HTML, no more.
130
    """
131
    return html_quote(error)
133
def escapenl_formatter(error):
134
    """
135
    Formatter that escapes HTML, and translates newlines to ``<br>``
136
    """
137
    error = html_quote(error)
138
    error = error.replace('\n', '<br>\n')
139
    return error
141
def ignore_formatter(error):
142
    """
143
    Formatter that emits nothing, regardless of the error.
144
    """
145
    return ''
147
class FillingParser(RewritingParser):
148
    r"""
149
    Fills HTML with default values, as in a form.
151
    Examples::
153
        >>> defaults = {'name': 'Bob Jones',
154
        ...             'occupation': 'Crazy Cultist',
155
        ...             'address': '14 W. Canal\nNew Guinea',
156
        ...             'living': 'no',
157
        ...             'nice_guy': 0}
158
        >>> parser = FillingParser(defaults)
159
        >>> parser.feed('<input type="text" name="name" value="fill">\
160
        ... <select name="occupation"><option value="">Default</option>\
161
        ... <option value="Crazy Cultist">Crazy cultist</option>\
162
        ... </select> <textarea cols="20" style="width: 100%" name="address">An address\
163
        ... </textarea> <input type="radio" name="living" value="yes">\
164
        ... <input type="radio" name="living" value="no">\
165
        ... <input type="checkbox" name="nice_guy" checked="checked">')
166
        >>> parser.close()
167
        >>> print parser.text() # doctest: +NORMALIZE_WHITESPACE
168
        <input type="text" name="name" value="Bob Jones">
169
        <select name="occupation">
170
        <option value="">Default</option>
171
        <option value="Crazy Cultist" selected="selected">Crazy cultist</option>
172
        </select>
173
        <textarea cols="20" style="width: 100%" name="address">14 W. Canal
174
        New Guinea</textarea>
175
        <input type="radio" name="living" value="yes">
176
        <input type="radio" name="living" value="no" checked="checked">
177
        <input type="checkbox" name="nice_guy">
179
    """
181
    default_encoding = 'utf8'
183
    def __init__(self, defaults, errors=None, use_all_keys=False,
184
                 error_formatters=None, error_class='error',
185
                 add_attributes=None, listener=None,
186
                 auto_error_formatter=None,
187
                 text_as_default=False, encoding=None, prefix_error=True,
188
                 force_defaults=True):
189
        RewritingParser.__init__(self)
190
        self.source = None
191
        self.lines = None
192
        self.source_pos = None
193
        self.defaults = defaults
194
        self.in_textarea = None
195
        self.skip_textarea = False
196
        self.last_textarea_name = None
197
        self.in_select = None
198
        self.skip_next = False        
199
        self.errors = errors or {}
200
        if isinstance(self.errors, (str, unicode)):
201
            self.errors = {None: self.errors}
202
        self.in_error = None
203
        self.skip_error = False
204
        self.use_all_keys = use_all_keys
205
        self.used_keys = {}
206
        self.used_errors = {}
207
        if error_formatters is None:
208
            self.error_formatters = default_formatter_dict
209
        else:
210
            self.error_formatters = error_formatters
211
        self.error_class = error_class
212
        self.add_attributes = add_attributes or {}
213
        self.listener = listener
214
        self.auto_error_formatter = auto_error_formatter
215
        self.text_as_default = text_as_default
216
        self.encoding = encoding
217
        self.prefix_error = prefix_error
218
        self.force_defaults = force_defaults
220
    def str_compare(self, str1, str2):
221
        """
222
        Compare the two objects as strings (coercing to strings if necessary).
223
        Also uses encoding to compare the strings.
224
        """
225
        if not isinstance(str1, basestring):
226
            if hasattr(str1, '__unicode__'):
227
                str1 = unicode(str1)
228
            else:
229
                str1 = str(str1)
230
        if type(str1) == type(str2):
231
            return str1 == str2
232
        if isinstance(str1, unicode):
233
            str1 = str1.encode(self.encoding or self.default_encoding)
234
        else:
235
            str2 = str2.encode(self.encoding or self.default_encoding)
236
        return str1 == str2
238
    def close(self):
239
        self.handle_misc(None)
240
        RewritingParser.close(self)
241
        unused_errors = self.errors.copy()
242
        for key in self.used_errors.keys():
243
            if unused_errors.has_key(key):
244
                del unused_errors[key]
245
        if self.auto_error_formatter:
246
            for key, value in unused_errors.items():
247
                error_message = self.auto_error_formatter(value)
248
                error_message = '<!-- for: %s -->\n%s' % (key, error_message)
249
                self.insert_at_marker(
250
                    key, error_message)
251
            unused_errors = {}
252
        if self.use_all_keys:
253
            unused = self.defaults.copy()
254
            for key in self.used_keys.keys():
255
                if unused.has_key(key):
256
                    del unused[key]
257
            assert not unused, (
258
                "These keys from defaults were not used in the form: %s"
259
                % unused.keys())
260
            if unused_errors:
261
                error_text = []
262
                for key in unused_errors.keys():
263
                    error_text.append("%s: %s" % (key, self.errors[key]))
264
                assert False, (
265
                    "These errors were not used in the form: %s" % 
266
                    ', '.join(error_text))
267
        if self.encoding is not None:
268
            new_content = []
269
            for item in self._content:
270
                if isinstance(item, str):
271
                    item = item.decode(self.encoding)
272
                new_content.append(item)
273
            self._content = new_content
274
        self._text = self._get_text()
276
    def skip_output(self):
277
        return (self.in_textarea and self.skip_textarea) or self.skip_error
279
    def add_key(self, key):
280
        self.used_keys[key] = 1
282
    def handle_starttag(self, tag, attrs, startend=False):
283
        self.write_pos()
284
        if tag == 'input':
285
            self.handle_input(attrs, startend)
286
        elif tag == 'textarea':
287
            self.handle_textarea(attrs)
288
        elif tag == 'select':
289
            self.handle_select(attrs)
290
        elif tag == 'option':
291
            self.handle_option(attrs)
292
            return
293
        elif tag == 'form:error':
294
            self.handle_error(attrs)
295
            return
296
        elif tag == 'form:iferror':
297
            self.handle_iferror(attrs)
298
            return
299
        else:
300
            return
301
        if self.listener:
302
            self.listener.listen_input(self, tag, attrs)
304
    def handle_endtag(self, tag):
305
        self.write_pos()
306
        if tag == 'textarea':
307
            self.handle_end_textarea()
308
        elif tag == 'select':
309
            self.handle_end_select()
310
        elif tag == 'form:iferror':
311
            self.handle_end_iferror()
313
    def handle_startendtag(self, tag, attrs):
314
        return self.handle_starttag(tag, attrs, True)
316
    def handle_iferror(self, attrs):
317
        name = self.get_attr(attrs, 'name')
318
        notted = False
319
        if name.startswith('not '):
320
            notted = True
321
            name = name.split(None, 1)[1]
322
        assert name, "Name attribute in <iferror> required (%s)" % self.getpos()
323
        self.in_error = name
324
        ok = self.errors.get(name)
325
        if notted:
326
            ok = not ok
327
        if not ok:
328
            self.skip_error = True
329
        self.skip_next = True
331
    def handle_end_iferror(self):
332
        self.in_error = None
333
        self.skip_error = False
334
        self.skip_next = True
336
    def handle_error(self, attrs):
337
        name = self.get_attr(attrs, 'name')
338
        formatter = self.get_attr(attrs, 'format') or 'default'
339
        if name is None:
340
            name = self.in_error
341
        assert name is not None, (
342
            "Name attribute in <form:error> required if not contained in "
343
            "<form:iferror> (%i:%i)" % self.getpos())
344
        error = self.errors.get(name, '')
345
        if error:
346
            error = self.error_formatters[formatter](error)
347
            self.write_text(error)
348
        self.skip_next = True
349
        self.used_errors[name] = 1
351
    def handle_input(self, attrs, startend):
352
        t = (self.get_attr(attrs, 'type') or 'text').lower()
353
        name = self.get_attr(attrs, 'name')
354
        if self.prefix_error:
355
            self.write_marker(name)
356
        value = self.defaults.get(name)
357
        if self.add_attributes.has_key(name):
358
            for attr_name, attr_value in self.add_attributes[name].items():
359
                if attr_name.startswith('+'):
360
                    attr_name = attr_name[1:]
361
                    self.set_attr(attrs, attr_name,
362
                                  self.get_attr(attrs, attr_name, '')
363
                                  + attr_value)
364
                else:
365
                    self.set_attr(attrs, attr_name, attr_value)
366
        if (self.error_class
367
            and self.errors.get(self.get_attr(attrs, 'name'))):
368
            self.add_class(attrs, self.error_class)
369
        if t in ('text', 'hidden'):
370
            if value is None and not self.force_defaults:
371
                value = self.get_attr(attrs, 'value', '')
372
            self.set_attr(attrs, 'value', value)
373
            self.write_tag('input', attrs, startend)
374
            self.skip_next = True
375
            self.add_key(name)
376
        elif t == 'checkbox':
377
            if self.force_defaults:
378
                selected = False
379
            else:
380
                selected = self.get_attr(attrs, 'checked')
381
            if not self.get_attr(attrs, 'value'):
382
                selected = value
383
            elif self.selected_multiple(value,
384
                                        self.get_attr(attrs, 'value', '')):
385
                selected = True
386
            if selected:
387
                self.set_attr(attrs, 'checked', 'checked')
388
            else:
389
                self.del_attr(attrs, 'checked')
390
            self.write_tag('input', attrs, startend)
391
            self.skip_next = True
392
            self.add_key(name)
393
        elif t == 'radio':
394
            if self.str_compare(value, self.get_attr(attrs, 'value', '')):
395
                self.set_attr(attrs, 'checked', 'checked')
396
            elif self.force_defaults or name in self.defaults:
397
                self.del_attr(attrs, 'checked')
398
            self.write_tag('input', attrs, startend)
399
            self.skip_next = True
400
            self.add_key(name)
401
        elif t == 'file':
402
            pass # don't skip next
403
        elif t == 'password':
404
            if value is None and not self.force_defaults:
405
                value = value or self.get_attr(attrs, 'value', '')
406
            self.set_attr(attrs, 'value', value)
407
            self.write_tag('input', attrs, startend)
408
            self.skip_next = True
409
            self.add_key(name)
410
        elif t == 'image':
411
            self.write_tag('input', attrs, startend)
412
            self.skip_next = True
413
            self.add_key(name)
414
        elif t == 'submit' or t == 'reset' or t == 'button':
415
            self.set_attr(attrs, 'value', value or
416
                          self.get_attr(attrs, 'value', ''))
417
            self.write_tag('input', attrs, startend)
418
            self.skip_next = True
419
            self.add_key(name)
420
        elif self.text_as_default:
421
            if value is None:
422
                value = self.get_attr(attrs, 'value', '')
423
            self.set_attr(attrs, 'value', value)
424
            self.write_tag('input', attrs, startend)
425
            self.skip_next = True
426
            self.add_key(name)
427
        else:
428
            assert 0, "I don't know about this kind of <input>: %s (pos: %s)" \
429
                   % (t, self.getpos())
430
        if not self.prefix_error:
431
            self.write_marker(name)
433
    def handle_textarea(self, attrs):
434
        name = self.get_attr(attrs, 'name')
435
        if self.prefix_error:
436
            self.write_marker(name)
437
        if (self.error_class
438
            and self.errors.get(name)):
439
            self.add_class(attrs, self.error_class)
440
        value = self.defaults.get(name, '')
441
        if value or self.force_defaults:
442
            self.write_tag('textarea', attrs)
443
            self.write_text(html_quote(value))
444
            self.write_text('</textarea>')
445
            self.skip_textarea = True
446
        self.in_textarea = True
447
        self.last_textarea_name = name
448
        self.add_key(name)
450
    def handle_end_textarea(self):
451
        if self.skip_textarea:
452
            self.skip_textarea = False
453
        else:
454
            self.write_text('</textarea>')            
455
        self.in_textarea = False
456
        self.skip_next = True
457
        if not self.prefix_error:
458
            self.write_marker(self.last_textarea_name)
459
        self.last_textarea_name = None
461
    def handle_select(self, attrs):
462
        name = self.get_attr(attrs, 'name', False)
463
        if name and self.prefix_error:
464
            self.write_marker(name)
465
        if (self.error_class
466
            and self.errors.get(name)):
467
            self.add_class(attrs, self.error_class)
468
        self.in_select = self.get_attr(attrs, 'name', False)
469
        self.write_tag('select', attrs)
470
        self.skip_next = True
471
        self.add_key(self.in_select)
473
    def handle_end_select(self):
474
        self.write_text('</select>')
475
        self.skip_next = True
476
        if not self.prefix_error and self.in_select:
477
            self.write_marker(self.in_select)
478
        self.in_select = None
480
    def handle_option(self, attrs):
481
        assert self.in_select is not None, (
482
            "<option> outside of <select>: line %i, column %i"
483
            % self.getpos())
484
        if self.in_select != False:
485
            default = self.defaults.get(self.in_select, '')
486
            if self.force_defaults or (self.in_select in self.defaults):
487
                if self.selected_multiple(self.defaults.get(self.in_select, ''),
488
                                          self.get_attr(attrs, 'value', '')):
489
                    self.set_attr(attrs, 'selected', 'selected')
490
                    self.add_key(self.in_select)
491
                else:
492
                    self.del_attr(attrs, 'selected')
493
        self.write_tag('option', attrs)
494
        self.skip_next = True
496
    def selected_multiple(self, obj, value):
497
        """
498
        Returns true/false if obj indicates that value should be
499
        selected.  If obj has a __contains__ method it is used, otherwise
500
        identity is used.
501
        """
502
        if obj is None:
503
            return value == ""
504
        if isinstance(obj, (str, unicode)):
505
            return obj == value
506
        if hasattr(obj, '__contains__'):
507
            if value in obj:
508
                return True
509
        if hasattr(obj, '__iter__'):
510
            for inner in obj:
511
                if self.str_compare(inner, value):
512
                    return True
513
        return self.str_compare(obj, value)
515
    def write_marker(self, marker):
516
        self._content.append((marker,))
518
    def insert_at_marker(self, marker, text):
519
        for i, item in enumerate(self._content):
520
            if item == (marker,):
521
                self._content.insert(i, text)
522
                break
523
        else:
524
            self._content.insert(0, text)
527
default_formatter_dict = {'default': default_formatter,
528
                          'none': none_formatter,
529
                          'escape': escape_formatter,
530
                          'escapenl': escapenl_formatter,
531
                          'ignore': ignore_formatter,
532
                          }