Code

Ticket #6630: 00-forms-fieldsets.diff

File 00-forms-fieldsets.diff, 63.6 KB (added by Petr Marhoun <petr.marhoun@…>, 6 years ago)
Line 
1=== added file 'django/forms/metaclassing.py'
2--- django/forms/metaclassing.py        1970-01-01 00:00:00 +0000
3+++ django/forms/metaclassing.py        2008-08-01 08:58:33 +0000
4@@ -0,0 +1,71 @@
5+"""
6+Functions for form metaclasses. Their name describes their purpose.
7+"""
8+
9+from django.core.exceptions import ImproperlyConfigured
10+from django.utils.datastructures import SortedDict
11+
12+from fields import Field
13+from widgets import media_property
14+
15+def create_meta(cls, attrs):
16+    cls._meta = cls._options(getattr(cls, 'Meta', None))
17+
18+def create_declared_fields(cls, attrs):
19+    fields = []
20+    for name, possible_field in attrs.items():
21+        if isinstance(possible_field, Field):
22+            fields.append((name, possible_field))
23+            delattr(cls, name)
24+    fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
25+    cls.declared_fields = SortedDict(fields)
26+
27+def create_model_fields(cls, attrs):
28+    formfield_callback = attrs.pop('formfield_callback', lambda f: f.formfield())
29+    fields = []
30+    if cls._meta.model:
31+        for dbfield in cls._meta.model._meta.fields + cls._meta.model._meta.many_to_many:
32+            if dbfield.editable:
33+                formfield = formfield_callback(dbfield)
34+                if formfield:
35+                    fields.append((dbfield.name, formfield))
36+    cls.model_fields = SortedDict(fields)
37+
38+def create_base_fields_pool_from_declared_fields(cls, attrs):
39+    fields = []
40+    for base in cls.__mro__[::-1]:
41+        try:
42+            fields += base.declared_fields.items()
43+        except AttributeError:
44+            pass
45+    cls.base_fields_pool = SortedDict(fields)
46+
47+def create_base_fields_pool_from_model_fields_and_declared_fields(cls, attrs):
48+    model_fields, declared_fields = [], []
49+    for base in cls.__mro__[::-1]:
50+        try:
51+            declared_fields += base.declared_fields.items()
52+            if base._meta.model:
53+                model_fields = base.model_fields.items()
54+        except AttributeError:
55+            pass
56+    cls.base_fields_pool = SortedDict(model_fields + declared_fields)
57+
58+def create_base_fields_from_base_fields_pool(cls, attrs):
59+    if (cls._meta.fieldsets is None) + (cls._meta.fields is None) + (cls._meta.exclude is None) < 2:
60+        raise ImproperlyConfigured("%s cannot have more than one option from fieldsets, fields and exclude." % cls.__name__)
61+    if cls._meta.fieldsets:
62+        names = []
63+        for fieldset in cls._meta.fieldsets:
64+            names.extend(fieldset['fields'])
65+    elif cls._meta.fields:
66+        names = cls._meta.fields
67+    elif cls._meta.exclude:
68+        names = [name for name in cls.base_fields_pool if name not in cls._meta.exclude]
69+    else:
70+        names = cls.base_fields_pool.keys()
71+    cls.base_fields = SortedDict([(name, cls.base_fields_pool[name]) for name in names])
72+
73+def create_media(cls, attrs):
74+    if not 'media' in attrs:
75+        cls.media = media_property(cls)
76
77=== modified file 'django/contrib/admin/validation.py'
78--- django/contrib/admin/validation.py  2008-08-01 21:11:46 +0000
79+++ django/contrib/admin/validation.py  2008-08-01 21:12:28 +0000
80@@ -263,7 +263,7 @@
81                 % (cls.__name__, label, field, model.__name__))
82 
83 def _check_form_field_exists(cls, model, opts, label, field):
84-    if hasattr(cls.form, 'base_fields'):
85+    if hasattr(cls.form, 'base_fields') and cls.form.base_fields:
86         try:
87             cls.form.base_fields[field]
88         except KeyError:
89
90=== modified file 'django/contrib/auth/forms.py'
91--- django/contrib/auth/forms.py        2008-08-04 13:48:30 +0000
92+++ django/contrib/auth/forms.py        2008-08-04 13:49:02 +0000
93@@ -19,7 +19,7 @@
94     
95     class Meta:
96         model = User
97-        fields = ("username",)
98+        fields = ("username", "password1", "password2")
99     
100     def clean_username(self):
101         username = self.cleaned_data["username"]
102
103=== modified file 'django/contrib/auth/tests/forms.py'
104--- django/contrib/auth/tests/forms.py  2008-07-31 21:19:05 +0000
105+++ django/contrib/auth/tests/forms.py  2008-07-31 21:27:17 +0000
106@@ -42,7 +42,7 @@
107 >>> form.is_valid()
108 False
109 >>> form["password2"].errors
110-[u"The two password fields didn't match."]
111+[u'The two password fields didn&#39;t match.']
112 
113 The success case.
114 
115@@ -107,7 +107,7 @@
116 >>> form.is_valid()
117 False
118 >>> form["new_password2"].errors
119-[u"The two password fields didn't match."]
120+[u'The two password fields didn&#39;t match.']
121 
122 The success case.
123 
124@@ -145,7 +145,7 @@
125 >>> form.is_valid()
126 False
127 >>> form["new_password2"].errors
128-[u"The two password fields didn't match."]
129+[u'The two password fields didn&#39;t match.']
130 
131 The success case.
132 
133
134=== modified file 'django/contrib/auth/tests/views.py'
135--- django/contrib/auth/tests/views.py  2008-07-31 21:19:05 +0000
136+++ django/contrib/auth/tests/views.py  2008-07-31 21:27:17 +0000
137@@ -13,7 +13,7 @@
138         response = self.client.get('/password_reset/')
139         self.assertEquals(response.status_code, 200)
140         response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'})
141-        self.assertContains(response, "That e-mail address doesn't have an associated user account")
142+        self.assertContains(response, "That e-mail address doesn&#39;t have an associated user account")
143         self.assertEquals(len(mail.outbox), 0)
144     
145     def test_email_found(self):
146@@ -84,5 +84,5 @@
147         response = self.client.post(path, {'new_password1': 'anewpassword',
148                                            'new_password2':' x'})
149         self.assertEquals(response.status_code, 200)
150-        self.assert_("The two password fields didn't match" in response.content)
151+        self.assert_("The two password fields didn&#39;t match" in response.content)
152 
153
154=== modified file 'django/forms/extras/widgets.py'
155--- django/forms/extras/widgets.py      2008-07-20 00:01:26 +0000
156+++ django/forms/extras/widgets.py      2008-07-30 18:37:15 +0000
157@@ -24,9 +24,9 @@
158     day_field = '%s_day'
159     year_field = '%s_year'
160 
161-    def __init__(self, attrs=None, years=None):
162+    def __init__(self, attrs=None, years=None, row_attrs=None):
163         # years is an optional list/tuple of years to use in the "year" select box.
164-        self.attrs = attrs or {}
165+        super(SelectDateWidget, self).__init__(attrs, row_attrs)
166         if years:
167             self.years = years
168         else:
169
170=== modified file 'django/forms/forms.py'
171--- django/forms/forms.py       2008-07-20 00:01:26 +0000
172+++ django/forms/forms.py       2008-08-01 08:58:33 +0000
173@@ -4,14 +4,14 @@
174 
175 from copy import deepcopy
176 
177-from django.utils.datastructures import SortedDict
178-from django.utils.html import escape
179+from django.utils.html import conditional_escape
180 from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode
181 from django.utils.safestring import mark_safe
182 
183-from fields import Field, FileField
184-from widgets import Media, media_property, TextInput, Textarea
185+from fields import FileField
186+from widgets import Media, TextInput, Textarea
187 from util import flatatt, ErrorDict, ErrorList, ValidationError
188+import metaclassing
189 
190 __all__ = ('BaseForm', 'Form')
191 
192@@ -22,45 +22,20 @@
193     name = name[0].upper() + name[1:]
194     return name.replace('_', ' ')
195 
196-def get_declared_fields(bases, attrs, with_base_fields=True):
197-    """
198-    Create a list of form field instances from the passed in 'attrs', plus any
199-    similar fields on the base classes (in 'bases'). This is used by both the
200-    Form and ModelForm metclasses.
201-
202-    If 'with_base_fields' is True, all fields from the bases are used.
203-    Otherwise, only fields in the 'declared_fields' attribute on the bases are
204-    used. The distinction is useful in ModelForm subclassing.
205-    Also integrates any additional media definitions
206-    """
207-    fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
208-    fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
209-
210-    # If this class is subclassing another Form, add that Form's fields.
211-    # Note that we loop over the bases in *reverse*. This is necessary in
212-    # order to preserve the correct order of fields.
213-    if with_base_fields:
214-        for base in bases[::-1]:
215-            if hasattr(base, 'base_fields'):
216-                fields = base.base_fields.items() + fields
217-    else:
218-        for base in bases[::-1]:
219-            if hasattr(base, 'declared_fields'):
220-                fields = base.declared_fields.items() + fields
221-
222-    return SortedDict(fields)
223-
224-class DeclarativeFieldsMetaclass(type):
225-    """
226-    Metaclass that converts Field attributes to a dictionary called
227-    'base_fields', taking into account parent class 'base_fields' as well.
228-    """
229+class FormOptions(object):
230+    def __init__(self, options=None):
231+        self.fieldsets = getattr(options, 'fieldsets', None)
232+        self.fields = getattr(options, 'fields', None)
233+        self.exclude = getattr(options, 'exclude', None)
234+
235+class FormMetaclass(type):
236     def __new__(cls, name, bases, attrs):
237-        attrs['base_fields'] = get_declared_fields(bases, attrs)
238-        new_class = super(DeclarativeFieldsMetaclass,
239-                     cls).__new__(cls, name, bases, attrs)
240-        if 'media' not in attrs:
241-            new_class.media = media_property(new_class)
242+        new_class = type.__new__(cls, name, bases, attrs)
243+        metaclassing.create_meta(new_class, attrs)
244+        metaclassing.create_declared_fields(new_class, attrs)
245+        metaclassing.create_base_fields_pool_from_declared_fields(new_class, attrs)
246+        metaclassing.create_base_fields_from_base_fields_pool(new_class, attrs)
247+        metaclassing.create_media(new_class, attrs)
248         return new_class
249 
250 class BaseForm(StrAndUnicode):
251@@ -106,7 +81,7 @@
252         return BoundField(self, field, name)
253 
254     def _get_errors(self):
255-        "Returns an ErrorDict for the data provided for the form"
256+        "Returns an ErrorDict for the data provided for the form."
257         if self._errors is None:
258             self.full_clean()
259         return self._errors
260@@ -119,70 +94,169 @@
261         """
262         return self.is_bound and not bool(self.errors)
263 
264-    def add_prefix(self, field_name):
265+    def add_prefix(self, name):
266         """
267         Returns the field name with a prefix appended, if this Form has a
268         prefix set.
269 
270         Subclasses may wish to override.
271         """
272-        return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name
273-
274-    def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
275+        return self.prefix and ('%s-%s' % (self.prefix, name)) or name
276+
277+    def has_fieldsets(self):
278+        "Returns True if this form has fieldsets."
279+        return bool(self._meta.fieldsets)
280+
281+    def first_fieldset_attrs(self):
282+        "Returns attributes for first fieldset as HTML code."
283+        if self.has_fieldsets() and 'attrs' in self._meta.fieldsets[0]:
284+            return flatatt(self._meta.fieldsets[0]['attrs'])
285+        else:
286+            return u''
287+
288+    def first_fieldset_legend_tag(self):
289+        "Returns legend tag for first fieldset as HTML code."
290+        if self.has_fieldsets() and 'legend' in self._meta.fieldsets[0]:
291+            return mark_safe(u'<legend>%s</legend>' % conditional_escape(force_unicode(self._meta.fieldsets[0]['legend'])))
292+        else:
293+            return u''
294+
295+    def _label_tag_html_output(self, bf, label_tag_html):
296+        "Helper function for outputting HTML from a label. Used by _row_html_output."
297+        label, label_id = bf.label, bf.label_id
298+        if self.label_suffix and label and label[-1] not in ':?.!':
299+            label += self.label_suffix
300+        if label and label_id:
301+            return label_tag_html % {
302+                'label': label,
303+                'id': label_id,
304+            }
305+        else:
306+            return label
307+
308+    def _help_text_html_output(self, bf, help_text_html):
309+        "Helper function for outputting HTML from a help text. Used by _row_html_output."
310+        if bf.help_text:
311+            return help_text_html % {
312+                'help_text': bf.help_text,
313+            }
314+        else:
315+            return u''
316+
317+    def _row_html_output(self, bf, row_html, label_tag_html, help_text_html):
318+        "Helper function for outputting HTML from a widget. Used by _html_output."
319+        return row_html % {
320+            'rendered_widget': unicode(bf),
321+            'rendered_errors': unicode(bf.errors),
322+            'label_tag': self._label_tag_html_output(bf, label_tag_html),
323+            'help_text': self._help_text_html_output(bf, help_text_html),
324+            'attrs': bf.row_attrs,
325+        }
326+
327+    def _top_errors_html_output(self, top_errors, top_errors_html):
328+        "Helper function for outputting HTML from a top errors. Used by _html_output."
329+        return top_errors_html % {
330+            'top_errors': unicode(top_errors),
331+        }
332+
333+    def _fieldset_html_output(self, fields, fieldset, is_first, is_last, fieldset_start_html, fieldset_end_html, legend_tag_html):
334+        "Helper function for outputting HTML from a fieldset. Used by _html_output."
335+        output = []
336+        if not is_first:
337+            legend_tag = attrs = u''
338+            if 'legend' in fieldset:
339+                legend_tag = legend_tag_html % {
340+                    'legend': conditional_escape(force_unicode(fieldset['legend'])),
341+                }
342+            if 'attrs' in fieldset:
343+                attrs = flatatt(fieldset.get('attrs'))
344+            output.append(fieldset_start_html % {
345+                'legend_tag': legend_tag,
346+                'attrs': attrs,
347+            })
348+        for name in fieldset['fields']:
349+            output.append(fields[name])
350+        if not is_last:
351+            output.append(fieldset_end_html)
352+        return u'\n'.join(output)
353+
354+    def _hidden_fields_html_output(self, hidden_fields, hidden_fields_html):
355+        "Helper function for outputting HTML from a hidden fields. Used by _html_output."
356+        return hidden_fields_html % {
357+            'hidden_fields': u''.join(hidden_fields),
358+        }
359+
360+    def _html_output(self, row_html, label_tag_html, help_text_html, top_errors_html,
361+            fieldset_start_html, legend_tag_html, fieldset_end_html, hidden_fields_html):
362         "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()."
363-        top_errors = self.non_field_errors() # Errors that should be displayed above all fields.
364-        output, hidden_fields = [], []
365+        output = []
366+        top_errors, hidden_fields, visible_fields = self.non_field_errors(), [], {}
367         for name, field in self.fields.items():
368             bf = BoundField(self, field, name)
369-            bf_errors = self.error_class([escape(error) for error in bf.errors]) # Escape and cache in local variable.
370             if bf.is_hidden:
371-                if bf_errors:
372-                    top_errors.extend([u'(Hidden field %s) %s' % (name, force_unicode(e)) for e in bf_errors])
373+                if bf.errors:
374+                    top_errors.extend([u'(Hidden field %s) %s' % (name, conditional_escape(force_unicode(e))) for e in bf.errors])
375                 hidden_fields.append(unicode(bf))
376             else:
377-                if errors_on_separate_row and bf_errors:
378-                    output.append(error_row % force_unicode(bf_errors))
379-                if bf.label:
380-                    label = escape(force_unicode(bf.label))
381-                    # Only add the suffix if the label does not end in
382-                    # punctuation.
383-                    if self.label_suffix:
384-                        if label[-1] not in ':?.!':
385-                            label += self.label_suffix
386-                    label = bf.label_tag(label) or ''
387-                else:
388-                    label = ''
389-                if field.help_text:
390-                    help_text = help_text_html % force_unicode(field.help_text)
391-                else:
392-                    help_text = u''
393-                output.append(normal_row % {'errors': force_unicode(bf_errors), 'label': force_unicode(label), 'field': unicode(bf), 'help_text': help_text})
394+                visible_fields[name] = self._row_html_output(bf, row_html, label_tag_html, help_text_html)
395         if top_errors:
396-            output.insert(0, error_row % force_unicode(top_errors))
397-        if hidden_fields: # Insert any hidden fields in the last row.
398-            str_hidden = u''.join(hidden_fields)
399-            if output:
400-                last_row = output[-1]
401-                # Chop off the trailing row_ender (e.g. '</td></tr>') and
402-                # insert the hidden fields.
403-                output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender
404-            else:
405-                # If there aren't any rows in the output, just append the
406-                # hidden fields.
407-                output.append(str_hidden)
408+            output.append(self._top_errors_html_output(top_errors, top_errors_html))
409+        if self.has_fieldsets():
410+            for i, fieldset in enumerate(self._meta.fieldsets):
411+                fields = dict((name, visible_fields[name]) for name in fieldset['fields'] if name in visible_fields)
412+                is_first = (i == 0)
413+                is_last = (i + 1 == len(self._meta.fieldsets))
414+                output.append(self._fieldset_html_output(fields, fieldset, is_first, is_last,
415+                    fieldset_start_html, fieldset_end_html, legend_tag_html))
416+        else:
417+            for name in self.fields:
418+                if name in visible_fields:
419+                    output.append(visible_fields[name])
420+        if hidden_fields:
421+            output.append(self._hidden_fields_html_output(hidden_fields, hidden_fields_html))
422         return mark_safe(u'\n'.join(output))
423 
424     def as_table(self):
425         "Returns this form rendered as HTML <tr>s -- excluding the <table></table>."
426-        return self._html_output(u'<tr><th>%(label)s</th><td>%(errors)s%(field)s%(help_text)s</td></tr>', u'<tr><td colspan="2">%s</td></tr>', '</td></tr>', u'<br />%s', False)
427+        kwargs = {
428+            'row_html': u'<tr%(attrs)s><th>%(label_tag)s</th><td>%(rendered_errors)s%(rendered_widget)s%(help_text)s</td></tr>',
429+            'label_tag_html': u'<label for="%(id)s">%(label)s</label>',
430+            'help_text_html': u'<br />%(help_text)s',
431+            'top_errors_html': u'<tr><td colspan="2">%(top_errors)s</td></tr>',
432+            'fieldset_start_html': u'<fieldset%(attrs)s>\n%(legend_tag)s<table>',
433+            'fieldset_end_html': u'</table>\n</fieldset>',
434+            'legend_tag_html': u'<legend>%(legend)s</legend>\n',
435+            'hidden_fields_html': u'<tr class="hidden"><td colspan="2">%(hidden_fields)s</td></tr>',
436+        }
437+        return self._html_output(**kwargs)
438 
439     def as_ul(self):
440         "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."
441-        return self._html_output(u'<li>%(errors)s%(label)s %(field)s%(help_text)s</li>', u'<li>%s</li>', '</li>', u' %s', False)
442+        kwargs = {
443+            'row_html': u'<li%(attrs)s>%(rendered_errors)s%(label_tag)s %(rendered_widget)s%(help_text)s</li>',
444+            'label_tag_html': u'<label for="%(id)s">%(label)s</label>',
445+            'help_text_html': u' %(help_text)s',
446+            'top_errors_html': u'<li>%(top_errors)s</li>',
447+            'fieldset_start_html': u'<fieldset%(attrs)s>\n%(legend_tag)s<ul>',
448+            'fieldset_end_html': u'</ul>\n</fieldset>',
449+            'legend_tag_html': u'<legend>%(legend)s</legend>\n',
450+            'hidden_fields_html': u'<li class="hidden">%(hidden_fields)s</li>',
451+        }
452+        return self._html_output(**kwargs)
453 
454     def as_p(self):
455         "Returns this form rendered as HTML <p>s."
456-        return self._html_output(u'<p>%(label)s %(field)s%(help_text)s</p>', u'%s', '</p>', u' %s', True)
457+        kwargs = {
458+            'row_html': u'%(rendered_errors)s<p%(attrs)s>%(label_tag)s %(rendered_widget)s%(help_text)s</p>',
459+            'label_tag_html': u'<label for="%(id)s">%(label)s</label>',
460+            'help_text_html': u' %(help_text)s',
461+            'top_errors_html': u'%(top_errors)s',
462+            'fieldset_start_html': u'<fieldset%(attrs)s>\n%(legend_tag)s',
463+            'fieldset_end_html': u'</fieldset>',
464+            'legend_tag_html': u'<legend>%(legend)s</legend>\n',
465+            'hidden_fields_html': u'<p class="hidden">%(hidden_fields)s</p>',
466+        }
467+        return self._html_output(**kwargs)
468 
469     def non_field_errors(self):
470         """
471@@ -221,13 +295,13 @@
472                     value = getattr(self, 'clean_%s' % name)()
473                     self.cleaned_data[name] = value
474             except ValidationError, e:
475-                self._errors[name] = e.messages
476+                self._errors[name] = self.error_class(e.messages)
477                 if name in self.cleaned_data:
478                     del self.cleaned_data[name]
479         try:
480             self.cleaned_data = self.clean()
481         except ValidationError, e:
482-            self._errors[NON_FIELD_ERRORS] = e.messages
483+            self._errors[NON_FIELD_ERRORS] = self.error_class(e.messages)
484         if self._errors:
485             delattr(self, 'cleaned_data')
486 
487@@ -291,20 +365,19 @@
488     # fancy metaclass stuff purely for the semantic sugar -- it allows one
489     # to define a form using declarative syntax.
490     # BaseForm itself has no way of designating self.fields.
491-    __metaclass__ = DeclarativeFieldsMetaclass
492+    __metaclass__ = FormMetaclass
493+    _options = FormOptions
494 
495 class BoundField(StrAndUnicode):
496     "A Field plus data"
497     def __init__(self, form, field, name):
498         self.form = form
499         self.field = field
500+        self.widget = field.widget
501+        self.required = self.field.required
502+        self.is_hidden = self.widget.is_hidden
503         self.name = name
504         self.html_name = form.add_prefix(name)
505-        if self.field.label is None:
506-            self.label = pretty_name(name)
507-        else:
508-            self.label = self.field.label
509-        self.help_text = field.help_text or ''
510 
511     def __unicode__(self):
512         """Renders this field as an HTML widget."""
513@@ -325,7 +398,7 @@
514         field's default widget will be used.
515         """
516         if not widget:
517-            widget = self.field.widget
518+            widget = self.widget
519         attrs = attrs or {}
520         auto_id = self.auto_id
521         if auto_id and 'id' not in attrs and 'id' not in widget.attrs:
522@@ -358,9 +431,37 @@
523         """
524         Returns the data for this BoundField, or None if it wasn't given.
525         """
526-        return self.field.widget.value_from_datadict(self.form.data, self.form.files, self.html_name)
527+        return self.widget.value_from_datadict(self.form.data, self.form.files, self.html_name)
528     data = property(_data)
529 
530+    def _label(self):
531+        "Returns label for this field as safe HTML."
532+        if self.field.label is None:
533+            return pretty_name(self.name)
534+        else:
535+            return conditional_escape(force_unicode(self.field.label))
536+    label = property(_label)
537+
538+    def _label_id(self):
539+        "Returns label id for this field as safe HTML."
540+        id_ = self.widget.attrs.get('id') or self.auto_id
541+        if id_:
542+            return self.widget.id_for_label(id_)
543+    label_id = property(_label_id)
544+
545+    def _help_text(self):
546+        "Returns help text for this field as safe HTML."
547+        if self.field.help_text is None:
548+            return u''
549+        else:
550+            return force_unicode(self.field.help_text)
551+    help_text = property(_help_text)
552+
553+    def _row_attrs(self):
554+        "Returns row attributes for this field as safe HTML."
555+        return flatatt(self.widget.row_attrs)
556+    row_attrs = property(_row_attrs)
557+
558     def label_tag(self, contents=None, attrs=None):
559         """
560         Wraps the given contents in a <label>, if the field has an ID attribute.
561@@ -369,19 +470,12 @@
562 
563         If attrs are given, they're used as HTML attributes on the <label> tag.
564         """
565-        contents = contents or escape(self.label)
566-        widget = self.field.widget
567-        id_ = widget.attrs.get('id') or self.auto_id
568-        if id_:
569+        contents = contents or self.label
570+        if self.label_id:
571             attrs = attrs and flatatt(attrs) or ''
572-            contents = '<label for="%s"%s>%s</label>' % (widget.id_for_label(id_), attrs, contents)
573+            contents = '<label for="%s"%s>%s</label>' % (self.label_id, attrs, contents)
574         return mark_safe(contents)
575 
576-    def _is_hidden(self):
577-        "Returns True if this BoundField's widget is hidden."
578-        return self.field.widget.is_hidden
579-    is_hidden = property(_is_hidden)
580-
581     def _auto_id(self):
582         """
583         Calculates and returns the ID attribute for this BoundField, if the
584
585=== modified file 'django/forms/models.py'
586--- django/forms/models.py      2008-08-01 21:11:46 +0000
587+++ django/forms/models.py      2008-08-01 21:12:28 +0000
588@@ -10,11 +10,11 @@
589 from django.utils.datastructures import SortedDict
590 
591 from util import ValidationError, ErrorList
592-from forms import BaseForm, get_declared_fields
593+from forms import FormOptions, FormMetaclass, BaseForm
594 from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
595 from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
596-from widgets import media_property
597 from formsets import BaseFormSet, formset_factory, DELETION_FIELD_NAME
598+import metaclassing
599 
600 __all__ = (
601     'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
602@@ -206,42 +206,20 @@
603             field_list.append((f.name, formfield))
604     return SortedDict(field_list)
605 
606-class ModelFormOptions(object):
607+class ModelFormOptions(FormOptions):
608     def __init__(self, options=None):
609+        super(ModelFormOptions, self).__init__(options)
610         self.model = getattr(options, 'model', None)
611-        self.fields = getattr(options, 'fields', None)
612-        self.exclude = getattr(options, 'exclude', None)
613-
614-
615-class ModelFormMetaclass(type):
616+
617+class ModelFormMetaclass(FormMetaclass):
618     def __new__(cls, name, bases, attrs):
619-        formfield_callback = attrs.pop('formfield_callback',
620-                lambda f: f.formfield())
621-        try:
622-            parents = [b for b in bases if issubclass(b, ModelForm)]
623-        except NameError:
624-            # We are defining ModelForm itself.
625-            parents = None
626-        new_class = super(ModelFormMetaclass, cls).__new__(cls, name, bases,
627-                attrs)
628-        if not parents:
629-            return new_class
630-
631-        if 'media' not in attrs:
632-            new_class.media = media_property(new_class)
633-        declared_fields = get_declared_fields(bases, attrs, False)
634-        opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
635-        if opts.model:
636-            # If a model is defined, extract form fields from it.
637-            fields = fields_for_model(opts.model, opts.fields,
638-                                      opts.exclude, formfield_callback)
639-            # Override default model fields with any custom declared ones
640-            # (plus, include all the other declared fields).
641-            fields.update(declared_fields)
642-        else:
643-            fields = declared_fields
644-        new_class.declared_fields = declared_fields
645-        new_class.base_fields = fields
646+        new_class = type.__new__(cls, name, bases, attrs)
647+        metaclassing.create_meta(new_class, attrs)
648+        metaclassing.create_model_fields(new_class, attrs)
649+        metaclassing.create_declared_fields(new_class, attrs)
650+        metaclassing.create_base_fields_pool_from_model_fields_and_declared_fields(new_class, attrs)
651+        metaclassing.create_base_fields_from_base_fields_pool(new_class, attrs)
652+        metaclassing.create_media(new_class, attrs)
653         return new_class
654 
655 class BaseModelForm(BaseForm):
656@@ -255,7 +233,7 @@
657             object_data = {}
658         else:
659             self.instance = instance
660-            object_data = model_to_dict(instance, opts.fields, opts.exclude)
661+            object_data = model_to_dict(instance, self.base_fields.keys())
662         # if initial was provided, it should override the values from instance
663         if initial is not None:
664             object_data.update(initial)
665@@ -274,10 +252,11 @@
666             fail_message = 'created'
667         else:
668             fail_message = 'changed'
669-        return save_instance(self, self.instance, self._meta.fields, fail_message, commit)
670+        return save_instance(self, self.instance, self.base_fields.keys(), fail_message, commit)
671 
672 class ModelForm(BaseModelForm):
673     __metaclass__ = ModelFormMetaclass
674+    _options = ModelFormOptions
675 
676 def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
677                        formfield_callback=lambda f: f.formfield()):
678
679=== modified file 'django/forms/util.py'
680--- django/forms/util.py        2008-07-24 12:03:58 +0000
681+++ django/forms/util.py        2008-07-30 18:37:15 +0000
682@@ -1,4 +1,4 @@
683-from django.utils.html import escape
684+from django.utils.html import conditional_escape
685 from django.utils.encoding import smart_unicode, StrAndUnicode, force_unicode
686 from django.utils.safestring import mark_safe
687 
688@@ -9,7 +9,7 @@
689     XML-style pairs.  It is assumed that the keys do not need to be XML-escaped.
690     If the passed dictionary is empty, then return an empty string.
691     """
692-    return u''.join([u' %s="%s"' % (k, escape(v)) for k, v in attrs.items()])
693+    return mark_safe(u''.join([u' %s="%s"' % (k, conditional_escape(v)) for k, v in attrs.items()]))
694 
695 class ErrorDict(dict, StrAndUnicode):
696     """
697@@ -55,9 +55,9 @@
698         a string) or a list of objects.
699         """
700         if isinstance(message, list):
701-            self.messages = ErrorList([smart_unicode(msg) for msg in message])
702+            self.messages = ErrorList([conditional_escape(smart_unicode(msg)) for msg in message])
703         else:
704-            message = smart_unicode(message)
705+            message = conditional_escape(smart_unicode(message))
706             self.messages = ErrorList([message])
707 
708     def __str__(self):
709
710=== modified file 'django/forms/widgets.py'
711--- django/forms/widgets.py     2008-07-20 00:01:25 +0000
712+++ django/forms/widgets.py     2008-07-30 18:37:15 +0000
713@@ -132,15 +132,20 @@
714     is_hidden = False          # Determines whether this corresponds to an <input type="hidden">.
715     needs_multipart_form = False # Determines does this widget need multipart-encrypted form
716 
717-    def __init__(self, attrs=None):
718+    def __init__(self, attrs=None, row_attrs=None):
719         if attrs is not None:
720             self.attrs = attrs.copy()
721         else:
722             self.attrs = {}
723+        if row_attrs is not None:
724+            self.row_attrs = row_attrs.copy()
725+        else:
726+            self.row_attrs = {}
727 
728     def __deepcopy__(self, memo):
729         obj = copy.copy(self)
730         obj.attrs = self.attrs.copy()
731+        obj.row_attrs = self.row_attrs.copy()
732         memo[id(self)] = obj
733         return obj
734 
735@@ -220,8 +225,8 @@
736 class PasswordInput(Input):
737     input_type = 'password'
738 
739-    def __init__(self, attrs=None, render_value=True):
740-        super(PasswordInput, self).__init__(attrs)
741+    def __init__(self, attrs=None, render_value=True, row_attrs=None):
742+        super(PasswordInput, self).__init__(attrs, row_attrs)
743         self.render_value = render_value
744 
745     def render(self, name, value, attrs=None):
746@@ -237,8 +242,8 @@
747     A widget that handles <input type="hidden"> for fields that have a list
748     of values.
749     """
750-    def __init__(self, attrs=None, choices=()):
751-        super(MultipleHiddenInput, self).__init__(attrs)
752+    def __init__(self, attrs=None, choices=(), row_attrs=None):
753+        super(MultipleHiddenInput, self).__init__(attrs, row_attrs)
754         # choices can be any iterable
755         self.choices = choices
756 
757@@ -271,11 +276,12 @@
758         return True
759 
760 class Textarea(Widget):
761-    def __init__(self, attrs=None):
762+    def __init__(self, attrs=None, row_attrs=None):
763         # The 'rows' and 'cols' attributes are required for HTML correctness.
764-        self.attrs = {'cols': '40', 'rows': '10'}
765+        default_attrs = {'cols': '40', 'rows': '10'}
766         if attrs:
767-            self.attrs.update(attrs)
768+            default_attrs.update(attrs)
769+        super(Textarea, self).__init__(default_attrs, row_attrs)
770 
771     def render(self, name, value, attrs=None):
772         if value is None: value = ''
773@@ -288,8 +294,8 @@
774     input_type = 'text'
775     format = '%Y-%m-%d %H:%M:%S'     # '2006-10-25 14:30:59'
776 
777-    def __init__(self, attrs=None, format=None):
778-        super(DateTimeInput, self).__init__(attrs)
779+    def __init__(self, attrs=None, format=None, row_attrs=None):
780+        super(DateTimeInput, self).__init__(attrs, row_attrs)
781         if format:
782             self.format = format
783 
784@@ -302,8 +308,8 @@
785         return super(DateTimeInput, self).render(name, value, attrs)
786 
787 class CheckboxInput(Widget):
788-    def __init__(self, attrs=None, check_test=bool):
789-        super(CheckboxInput, self).__init__(attrs)
790+    def __init__(self, attrs=None, check_test=bool, row_attrs=None):
791+        super(CheckboxInput, self).__init__(attrs, row_attrs)
792         # check_test is a callable that takes a value and returns True
793         # if the checkbox should be checked for that value.
794         self.check_test = check_test
795@@ -334,8 +340,8 @@
796         return bool(initial) != bool(data)
797 
798 class Select(Widget):
799-    def __init__(self, attrs=None, choices=()):
800-        super(Select, self).__init__(attrs)
801+    def __init__(self, attrs=None, choices=(), row_attrs=None):
802+        super(Select, self).__init__(attrs, row_attrs)
803         # choices can be any iterable, but we may need to render this widget
804         # multiple times. Thus, collapse it into a list so it can be consumed
805         # more than once.
806@@ -375,9 +381,9 @@
807     """
808     A Select Widget intended to be used with NullBooleanField.
809     """
810-    def __init__(self, attrs=None):
811+    def __init__(self, attrs=None, row_attrs=None):
812         choices = ((u'1', ugettext('Unknown')), (u'2', ugettext('Yes')), (u'3', ugettext('No')))
813-        super(NullBooleanSelect, self).__init__(attrs, choices)
814+        super(NullBooleanSelect, self).__init__(attrs, choices, row_attrs)
815 
816     def render(self, name, value, attrs=None, choices=()):
817         try:
818@@ -570,9 +576,9 @@
819 
820     You'll probably want to use this class with MultiValueField.
821     """
822-    def __init__(self, widgets, attrs=None):
823+    def __init__(self, widgets, attrs=None, row_attrs=None):
824         self.widgets = [isinstance(w, type) and w() or w for w in widgets]
825-        super(MultiWidget, self).__init__(attrs)
826+        super(MultiWidget, self).__init__(attrs, row_attrs)
827 
828     def render(self, name, value, attrs=None):
829         # value is a list of values, each corresponding to a widget
830@@ -642,9 +648,9 @@
831     """
832     A Widget that splits datetime input into two <input type="text"> boxes.
833     """
834-    def __init__(self, attrs=None):
835+    def __init__(self, attrs=None, row_attrs=None):
836         widgets = (TextInput(attrs=attrs), TextInput(attrs=attrs))
837-        super(SplitDateTimeWidget, self).__init__(widgets, attrs)
838+        super(SplitDateTimeWidget, self).__init__(widgets, attrs, row_attrs)
839 
840     def decompress(self, value):
841         if value:
842
843=== modified file 'tests/modeltests/model_forms/models.py'
844--- tests/modeltests/model_forms/models.py      2008-07-21 12:42:18 +0000
845+++ tests/modeltests/model_forms/models.py      2008-07-30 18:37:15 +0000
846@@ -151,9 +151,16 @@
847 ...         model = Category
848 ...         fields = ['name', 'url']
849 ...         exclude = ['url']
850-
851->>> CategoryForm.base_fields.keys()
852-['name']
853+Traceback (most recent call last):
854+  File "/home/petr/django/local2/00-forms-fieldsets/django/test/_doctest.py", line 1267, in __run
855+    compileflags, 1) in test.globs
856+  File "<doctest modeltests.model_forms.models.__test__.API_TESTS[12]>", line 1, in ?
857+    class CategoryForm(ModelForm):
858+  File "/home/petr/django/local2/00-forms-fieldsets/django/forms/models.py", line 220, in __new__
859+    metaclassing.create_base_fields_from_base_fields_pool(new_class)
860+  File "/home/petr/django/local2/00-forms-fieldsets/django/forms/metaclassing.py", line 50, in create_base_fields_from_base_fields_pool
861+    raise ImproperlyConfigured("%s cannot have more than one option from fieldsets, fields and exclude." % cls.__name__)
862+ImproperlyConfigured: CategoryForm cannot have more than one option from fieldsets, fields and exclude.
863 
864 Don't allow more than one 'model' definition in the inheritance hierarchy.
865 Technically, it would generate a valid form, but the fact that the resulting
866
867=== modified file 'tests/modeltests/model_formsets/models.py'
868--- tests/modeltests/model_formsets/models.py   2008-08-01 21:11:46 +0000
869+++ tests/modeltests/model_formsets/models.py   2008-08-01 21:12:28 +0000
870@@ -46,9 +46,12 @@
871 >>> formset = AuthorFormSet(queryset=qs)
872 >>> for form in formset.forms:
873 ...     print form.as_p()
874-<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /><input type="hidden" name="form-0-id" id="id_form-0-id" /></p>
875-<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" maxlength="100" /><input type="hidden" name="form-1-id" id="id_form-1-id" /></p>
876-<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" maxlength="100" /><input type="hidden" name="form-2-id" id="id_form-2-id" /></p>
877+<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /></p>
878+<p class ="hidden"><input type="hidden" name="form-0-id" id="id_form-0-id" /></p>
879+<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" maxlength="100" /></p>
880+<p class ="hidden"><input type="hidden" name="form-1-id" id="id_form-1-id" /></p>
881+<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" maxlength="100" /></p>
882+<p class ="hidden"><input type="hidden" name="form-2-id" id="id_form-2-id" /></p>
883 
884 >>> data = {
885 ...     'form-TOTAL_FORMS': '3', # the number of forms rendered
886@@ -82,9 +85,12 @@
887 >>> formset = AuthorFormSet(queryset=qs)
888 >>> for form in formset.forms:
889 ...     print form.as_p()
890-<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /><input type="hidden" name="form-0-id" value="2" id="id_form-0-id" /></p>
891-<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" value="Charles Baudelaire" maxlength="100" /><input type="hidden" name="form-1-id" value="1" id="id_form-1-id" /></p>
892-<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" maxlength="100" /><input type="hidden" name="form-2-id" id="id_form-2-id" /></p>
893+<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /></p>
894+<p class ="hidden"><input type="hidden" name="form-0-id" value="2" id="id_form-0-id" /></p>
895+<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" value="Charles Baudelaire" maxlength="100" /></p>
896+<p class ="hidden"><input type="hidden" name="form-1-id" value="1" id="id_form-1-id" /></p>
897+<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" maxlength="100" /></p>
898+<p class ="hidden"><input type="hidden" name="form-2-id" id="id_form-2-id" /></p>
899 
900 
901 >>> data = {
902@@ -122,13 +128,17 @@
903 >>> for form in formset.forms:
904 ...     print form.as_p()
905 <p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /></p>
906-<p><label for="id_form-0-DELETE">Delete:</label> <input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE" /><input type="hidden" name="form-0-id" value="2" id="id_form-0-id" /></p>
907+<p><label for="id_form-0-DELETE">Delete:</label> <input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE" /></p>
908+<p class ="hidden"><input type="hidden" name="form-0-id" value="2" id="id_form-0-id" /></p>
909 <p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" value="Charles Baudelaire" maxlength="100" /></p>
910-<p><label for="id_form-1-DELETE">Delete:</label> <input type="checkbox" name="form-1-DELETE" id="id_form-1-DELETE" /><input type="hidden" name="form-1-id" value="1" id="id_form-1-id" /></p>
911+<p><label for="id_form-1-DELETE">Delete:</label> <input type="checkbox" name="form-1-DELETE" id="id_form-1-DELETE" /></p>
912+<p class ="hidden"><input type="hidden" name="form-1-id" value="1" id="id_form-1-id" /></p>
913 <p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" value="Paul Verlaine" maxlength="100" /></p>
914-<p><label for="id_form-2-DELETE">Delete:</label> <input type="checkbox" name="form-2-DELETE" id="id_form-2-DELETE" /><input type="hidden" name="form-2-id" value="3" id="id_form-2-id" /></p>
915+<p><label for="id_form-2-DELETE">Delete:</label> <input type="checkbox" name="form-2-DELETE" id="id_form-2-DELETE" /></p>
916+<p class ="hidden"><input type="hidden" name="form-2-id" value="3" id="id_form-2-id" /></p>
917 <p><label for="id_form-3-name">Name:</label> <input id="id_form-3-name" type="text" name="form-3-name" maxlength="100" /></p>
918-<p><label for="id_form-3-DELETE">Delete:</label> <input type="checkbox" name="form-3-DELETE" id="id_form-3-DELETE" /><input type="hidden" name="form-3-id" id="id_form-3-id" /></p>
919+<p><label for="id_form-3-DELETE">Delete:</label> <input type="checkbox" name="form-3-DELETE" id="id_form-3-DELETE" /></p>
920+<p class ="hidden"><input type="hidden" name="form-3-id" id="id_form-3-id" /></p>
921 
922 >>> data = {
923 ...     'form-TOTAL_FORMS': '4', # the number of forms rendered
924@@ -243,9 +253,12 @@
925 >>> formset = AuthorBooksFormSet(instance=author)
926 >>> for form in formset.forms:
927 ...     print form.as_p()
928-<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" maxlength="100" /><input type="hidden" name="book_set-0-id" id="id_book_set-0-id" /></p>
929-<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>
930-<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>
931+<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" maxlength="100" /></p>
932+<p class ="hidden"><input type="hidden" name="book_set-0-id" id="id_book_set-0-id" /></p>
933+<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /></p>
934+<p class ="hidden"><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>
935+<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /></p>
936+<p class ="hidden"><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>
937 
938 >>> data = {
939 ...     'book_set-TOTAL_FORMS': '3', # the number of forms rendered
940@@ -277,9 +290,12 @@
941 >>> formset = AuthorBooksFormSet(instance=author)
942 >>> for form in formset.forms:
943 ...     print form.as_p()
944-<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" value="Les Fleurs du Mal" maxlength="100" /><input type="hidden" name="book_set-0-id" value="1" id="id_book_set-0-id" /></p>
945-<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>
946-<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>
947+<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" value="Les Fleurs du Mal" maxlength="100" /></p>
948+<p class ="hidden"><input type="hidden" name="book_set-0-id" value="1" id="id_book_set-0-id" /></p>
949+<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /></p>
950+<p class ="hidden"><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>
951+<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /></p>
952+<p class ="hidden"><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>
953 
954 >>> data = {
955 ...     'book_set-TOTAL_FORMS': '3', # the number of forms rendered
956@@ -331,8 +347,10 @@
957 >>> formset = AuthorBooksFormSet(prefix="test")
958 >>> for form in formset.forms:
959 ...     print form.as_p()
960-<p><label for="id_test-0-title">Title:</label> <input id="id_test-0-title" type="text" name="test-0-title" maxlength="100" /><input type="hidden" name="test-0-id" id="id_test-0-id" /></p>
961-<p><label for="id_test-1-title">Title:</label> <input id="id_test-1-title" type="text" name="test-1-title" maxlength="100" /><input type="hidden" name="test-1-id" id="id_test-1-id" /></p>
962+<p><label for="id_test-0-title">Title:</label> <input id="id_test-0-title" type="text" name="test-0-title" maxlength="100" /></p>
963+<p class ="hidden"><input type="hidden" name="test-0-id" id="id_test-0-id" /></p>
964+<p><label for="id_test-1-title">Title:</label> <input id="id_test-1-title" type="text" name="test-1-title" maxlength="100" /></p>
965+<p class ="hidden"><input type="hidden" name="test-1-id" id="id_test-1-id" /></p>
966 
967 # Test a custom primary key ###################################################
968 
969
970=== modified file 'tests/regressiontests/forms/extra.py'
971--- tests/regressiontests/forms/extra.py        2008-07-20 00:01:26 +0000
972+++ tests/regressiontests/forms/extra.py        2008-07-20 20:54:46 +0000
973@@ -439,10 +439,8 @@
974 >>> f = CommentForm(data, auto_id=False, error_class=DivErrorList)
975 >>> print f.as_p()
976 <p>Name: <input type="text" name="name" maxlength="50" /></p>
977-<div class="errorlist"><div class="error">Enter a valid e-mail address.</div></div>
978-<p>Email: <input type="text" name="email" value="invalid" /></p>
979-<div class="errorlist"><div class="error">This field is required.</div></div>
980-<p>Comment: <input type="text" name="comment" /></p>
981+<div class="errorlist"><div class="error">Enter a valid e-mail address.</div></div><p>Email: <input type="text" name="email" value="invalid" /></p>
982+<div class="errorlist"><div class="error">This field is required.</div></div><p>Comment: <input type="text" name="comment" /></p>
983 
984 #################################
985 # Test multipart-encoded form #
986
987=== modified file 'tests/regressiontests/forms/forms.py'
988--- tests/regressiontests/forms/forms.py        2008-07-20 00:01:26 +0000
989+++ tests/regressiontests/forms/forms.py        2008-07-30 18:37:15 +0000
990@@ -94,12 +94,9 @@
991 <li><ul class="errorlist"><li>This field is required.</li></ul><label for="id_last_name">Last name:</label> <input type="text" name="last_name" id="id_last_name" /></li>
992 <li><ul class="errorlist"><li>This field is required.</li></ul><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /></li>
993 >>> print p.as_p()
994-<ul class="errorlist"><li>This field is required.</li></ul>
995-<p><label for="id_first_name">First name:</label> <input type="text" name="first_name" id="id_first_name" /></p>
996-<ul class="errorlist"><li>This field is required.</li></ul>
997-<p><label for="id_last_name">Last name:</label> <input type="text" name="last_name" id="id_last_name" /></p>
998-<ul class="errorlist"><li>This field is required.</li></ul>
999-<p><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /></p>
1000+<ul class="errorlist"><li>This field is required.</li></ul><p><label for="id_first_name">First name:</label> <input type="text" name="first_name" id="id_first_name" /></p>
1001+<ul class="errorlist"><li>This field is required.</li></ul><p><label for="id_last_name">Last name:</label> <input type="text" name="last_name" id="id_last_name" /></p>
1002+<ul class="errorlist"><li>This field is required.</li></ul><p><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /></p>
1003 
1004 If you don't pass any values to the Form's __init__(), or if you pass None,
1005 the Form will be considered unbound and won't do any validation. Form.errors
1006@@ -563,7 +560,8 @@
1007 ...     composers = MultipleChoiceField(choices=[('J', 'John Lennon'), ('P', 'Paul McCartney')], widget=MultipleHiddenInput)
1008 >>> f = SongFormHidden(MultiValueDict(dict(name=['Yesterday'], composers=['J', 'P'])), auto_id=False)
1009 >>> print f.as_ul()
1010-<li>Name: <input type="text" name="name" value="Yesterday" /><input type="hidden" name="composers" value="J" />
1011+<li>Name: <input type="text" name="name" value="Yesterday" /></li>
1012+<li class ="hidden"><input type="hidden" name="composers" value="J" />
1013 <input type="hidden" name="composers" value="P" /></li>
1014 
1015 When using CheckboxSelectMultiple, the framework expects a list of input and
1016@@ -808,30 +806,36 @@
1017 >>> print p
1018 <tr><th>First name:</th><td><input type="text" name="first_name" /></td></tr>
1019 <tr><th>Last name:</th><td><input type="text" name="last_name" /></td></tr>
1020-<tr><th>Birthday:</th><td><input type="text" name="birthday" /><input type="hidden" name="hidden_text" /></td></tr>
1021+<tr><th>Birthday:</th><td><input type="text" name="birthday" /></td></tr>
1022+<tr class="hidden"><td colspan="2"><input type="hidden" name="hidden_text" /></td></tr>
1023 >>> print p.as_ul()
1024 <li>First name: <input type="text" name="first_name" /></li>
1025 <li>Last name: <input type="text" name="last_name" /></li>
1026-<li>Birthday: <input type="text" name="birthday" /><input type="hidden" name="hidden_text" /></li>
1027+<li>Birthday: <input type="text" name="birthday" /></li>
1028+<li class ="hidden"><input type="hidden" name="hidden_text" /></li>
1029 >>> print p.as_p()
1030 <p>First name: <input type="text" name="first_name" /></p>
1031 <p>Last name: <input type="text" name="last_name" /></p>
1032-<p>Birthday: <input type="text" name="birthday" /><input type="hidden" name="hidden_text" /></p>
1033+<p>Birthday: <input type="text" name="birthday" /></p>
1034+<p class ="hidden"><input type="hidden" name="hidden_text" /></p>
1035 
1036 With auto_id set, a HiddenInput still gets an ID, but it doesn't get a label.
1037 >>> p = Person(auto_id='id_%s')
1038 >>> print p
1039 <tr><th><label for="id_first_name">First name:</label></th><td><input type="text" name="first_name" id="id_first_name" /></td></tr>
1040 <tr><th><label for="id_last_name">Last name:</label></th><td><input type="text" name="last_name" id="id_last_name" /></td></tr>
1041-<tr><th><label for="id_birthday">Birthday:</label></th><td><input type="text" name="birthday" id="id_birthday" /><input type="hidden" name="hidden_text" id="id_hidden_text" /></td></tr>
1042+<tr><th><label for="id_birthday">Birthday:</label></th><td><input type="text" name="birthday" id="id_birthday" /></td></tr>
1043+<tr class="hidden"><td colspan="2"><input type="hidden" name="hidden_text" id="id_hidden_text" /></td></tr>
1044 >>> print p.as_ul()
1045 <li><label for="id_first_name">First name:</label> <input type="text" name="first_name" id="id_first_name" /></li>
1046 <li><label for="id_last_name">Last name:</label> <input type="text" name="last_name" id="id_last_name" /></li>
1047-<li><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /><input type="hidden" name="hidden_text" id="id_hidden_text" /></li>
1048+<li><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /></li>
1049+<li class ="hidden"><input type="hidden" name="hidden_text" id="id_hidden_text" /></li>
1050 >>> print p.as_p()
1051 <p><label for="id_first_name">First name:</label> <input type="text" name="first_name" id="id_first_name" /></p>
1052 <p><label for="id_last_name">Last name:</label> <input type="text" name="last_name" id="id_last_name" /></p>
1053-<p><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /><input type="hidden" name="hidden_text" id="id_hidden_text" /></p>
1054+<p><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /></p>
1055+<p class ="hidden"><input type="hidden" name="hidden_text" id="id_hidden_text" /></p>
1056 
1057 If a field with a HiddenInput has errors, the as_table() and as_ul() output
1058 will include the error message(s) with the text "(Hidden field [fieldname]) "
1059@@ -842,17 +846,20 @@
1060 <tr><td colspan="2"><ul class="errorlist"><li>(Hidden field hidden_text) This field is required.</li></ul></td></tr>
1061 <tr><th>First name:</th><td><input type="text" name="first_name" value="John" /></td></tr>
1062 <tr><th>Last name:</th><td><input type="text" name="last_name" value="Lennon" /></td></tr>
1063-<tr><th>Birthday:</th><td><input type="text" name="birthday" value="1940-10-9" /><input type="hidden" name="hidden_text" /></td></tr>
1064+<tr><th>Birthday:</th><td><input type="text" name="birthday" value="1940-10-9" /></td></tr>
1065+<tr class="hidden"><td colspan="2"><input type="hidden" name="hidden_text" /></td></tr>
1066 >>> print p.as_ul()
1067 <li><ul class="errorlist"><li>(Hidden field hidden_text) This field is required.</li></ul></li>
1068 <li>First name: <input type="text" name="first_name" value="John" /></li>
1069 <li>Last name: <input type="text" name="last_name" value="Lennon" /></li>
1070-<li>Birthday: <input type="text" name="birthday" value="1940-10-9" /><input type="hidden" name="hidden_text" /></li>
1071+<li>Birthday: <input type="text" name="birthday" value="1940-10-9" /></li>
1072+<li class ="hidden"><input type="hidden" name="hidden_text" /></li>
1073 >>> print p.as_p()
1074 <ul class="errorlist"><li>(Hidden field hidden_text) This field is required.</li></ul>
1075 <p>First name: <input type="text" name="first_name" value="John" /></p>
1076 <p>Last name: <input type="text" name="last_name" value="Lennon" /></p>
1077-<p>Birthday: <input type="text" name="birthday" value="1940-10-9" /><input type="hidden" name="hidden_text" /></p>
1078+<p>Birthday: <input type="text" name="birthday" value="1940-10-9" /></p>
1079+<p class ="hidden"><input type="hidden" name="hidden_text" /></p>
1080 
1081 A corner case: It's possible for a form to have only HiddenInputs.
1082 >>> class TestForm(Form):
1083@@ -860,11 +867,11 @@
1084 ...     bar = CharField(widget=HiddenInput)
1085 >>> p = TestForm(auto_id=False)
1086 >>> print p.as_table()
1087-<input type="hidden" name="foo" /><input type="hidden" name="bar" />
1088+<tr class="hidden"><td colspan="2"><input type="hidden" name="foo" /><input type="hidden" name="bar" /></td></tr>
1089 >>> print p.as_ul()
1090-<input type="hidden" name="foo" /><input type="hidden" name="bar" />
1091+<li class ="hidden"><input type="hidden" name="foo" /><input type="hidden" name="bar" /></li>
1092 >>> print p.as_p()
1093-<input type="hidden" name="foo" /><input type="hidden" name="bar" />
1094+<p class ="hidden"><input type="hidden" name="foo" /><input type="hidden" name="bar" /></p>
1095 
1096 A Form's fields are displayed in the same order in which they were defined.
1097 >>> class TestForm(Form):
1098@@ -1215,7 +1222,8 @@
1099 >>> p = UserRegistration(auto_id=False)
1100 >>> print p.as_ul()
1101 <li>Username: <input type="text" name="username" maxlength="10" /> e.g., user@example.com</li>
1102-<li>Password: <input type="password" name="password" /><input type="hidden" name="next" value="/" /></li>
1103+<li>Password: <input type="password" name="password" /></li>
1104+<li class ="hidden"><input type="hidden" name="next" value="/" /></li>
1105 
1106 Help text can include arbitrary Unicode characters.
1107 >>> class UserRegistration(Form):
1108@@ -1259,10 +1267,10 @@
1109 ...     haircut_type = CharField()
1110 >>> b = Beatle(auto_id=False)
1111 >>> print b.as_ul()
1112+<li>Instrument: <input type="text" name="instrument" /></li>
1113 <li>First name: <input type="text" name="first_name" /></li>
1114 <li>Last name: <input type="text" name="last_name" /></li>
1115 <li>Birthday: <input type="text" name="birthday" /></li>
1116-<li>Instrument: <input type="text" name="instrument" /></li>
1117 <li>Haircut type: <input type="text" name="haircut_type" /></li>
1118 
1119 # Forms with prefixes #########################################################
1120
1121=== modified file 'tests/regressiontests/forms/formsets.py'
1122--- tests/regressiontests/forms/formsets.py     2008-07-24 12:03:58 +0000
1123+++ tests/regressiontests/forms/formsets.py     2008-07-30 18:37:15 +0000
1124@@ -20,7 +20,7 @@
1125 
1126 >>> formset = ChoiceFormSet(auto_id=False, prefix='choices')
1127 >>> print formset
1128-<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" />
1129+<tr class="hidden"><td colspan="2"><input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /></td></tr>
1130 <tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr>
1131 <tr><th>Votes:</th><td><input type="text" name="choices-0-votes" /></td></tr>
1132 
1133
1134=== modified file 'tests/regressiontests/forms/localflavor/ch.py'
1135--- tests/regressiontests/forms/localflavor/ch.py       2007-11-13 02:22:36 +0000
1136+++ tests/regressiontests/forms/localflavor/ch.py       2008-07-20 21:27:55 +0000
1137@@ -41,13 +41,13 @@
1138 >>> f.clean('C1234567<1')
1139 Traceback (most recent call last):
1140 ...
1141-ValidationError: [u'Enter a valid Swiss identity or passport card number in X1234567<0 or 1234567890 format.']
1142+ValidationError: [u'Enter a valid Swiss identity or passport card number in X1234567&lt;0 or 1234567890 format.']
1143 >>> f.clean('2123456700')
1144 u'2123456700'
1145 >>> f.clean('2123456701')
1146 Traceback (most recent call last):
1147 ...
1148-ValidationError: [u'Enter a valid Swiss identity or passport card number in X1234567<0 or 1234567890 format.']
1149+ValidationError: [u'Enter a valid Swiss identity or passport card number in X1234567&lt;0 or 1234567890 format.']
1150 
1151 # CHStateSelect #############################################################
1152 
1153
1154=== modified file 'tests/regressiontests/forms/regressions.py'
1155--- tests/regressiontests/forms/regressions.py  2008-07-20 00:01:26 +0000
1156+++ tests/regressiontests/forms/regressions.py  2008-07-20 21:29:49 +0000
1157@@ -56,7 +56,7 @@
1158 >>> activate('ru')
1159 >>> f = SomeForm({})
1160 >>> f.as_p()
1161-u'<ul class="errorlist"><li>\u041e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u043f\u043e\u043b\u0435.</li></ul>\n<p><label for="id_somechoice_0">\xc5\xf8\xdf:</label> <ul>\n<li><label for="id_somechoice_0"><input type="radio" id="id_somechoice_0" value="\xc5" name="somechoice" /> En tied\xe4</label></li>\n<li><label for="id_somechoice_1"><input type="radio" id="id_somechoice_1" value="\xf8" name="somechoice" /> Mies</label></li>\n<li><label for="id_somechoice_2"><input type="radio" id="id_somechoice_2" value="\xdf" name="somechoice" /> Nainen</label></li>\n</ul></p>'
1162+u'<ul class="errorlist"><li>\u041e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u043f\u043e\u043b\u0435.</li></ul><p><label for="id_somechoice_0">\xc5\xf8\xdf:</label> <ul>\n<li><label for="id_somechoice_0"><input type="radio" id="id_somechoice_0" value="\xc5" name="somechoice" /> En tied\xe4</label></li>\n<li><label for="id_somechoice_1"><input type="radio" id="id_somechoice_1" value="\xf8" name="somechoice" /> Mies</label></li>\n<li><label for="id_somechoice_2"><input type="radio" id="id_somechoice_2" value="\xdf" name="somechoice" /> Nainen</label></li>\n</ul></p>'
1163 >>> deactivate()
1164 
1165 Deep copying translated text shouldn't raise an error
1166
1167=== modified file 'tests/regressiontests/forms/util.py'
1168--- tests/regressiontests/forms/util.py 2008-07-20 00:01:26 +0000
1169+++ tests/regressiontests/forms/util.py 2008-07-31 21:27:18 +0000
1170@@ -7,6 +7,11 @@
1171 >>> from django.forms.util import *
1172 >>> from django.utils.translation import ugettext_lazy
1173 
1174+# Escaping.
1175+>>> from django.utils.html import escape
1176+>>> from django.utils.html import conditional_escape
1177+>>> script = "$('#example').html('<a href=\"http://www.example.com/\">example</a>');"
1178+
1179 ###########
1180 # flatatt #
1181 ###########
1182@@ -19,6 +24,22 @@
1183 >>> flatatt({})
1184 u''
1185 
1186+# Escaping.
1187+
1188+>>> flatatt({'onclick': script})
1189+u' onclick="$(&#39;#example&#39;).html(&#39;&lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;&#39;);"'
1190+>>> flatatt({'onclick': escape(script)})
1191+u' onclick="$(&#39;#example&#39;).html(&#39;&lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;&#39;);"'
1192+>>> flatatt({'onclick': conditional_escape(script)})
1193+u' onclick="$(&#39;#example&#39;).html(&#39;&lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;&#39;);"'
1194+
1195+>>> conditional_escape(flatatt({'onclick': script}))
1196+u' onclick="$(&#39;#example&#39;).html(&#39;&lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;&#39;);"'
1197+>>> conditional_escape(flatatt({'onclick': escape(script)}))
1198+u' onclick="$(&#39;#example&#39;).html(&#39;&lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;&#39;);"'
1199+>>> conditional_escape(flatatt({'onclick': conditional_escape(script)}))
1200+u' onclick="$(&#39;#example&#39;).html(&#39;&lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;&#39;);"'
1201+
1202 ###################
1203 # ValidationError #
1204 ###################
1205@@ -49,4 +70,33 @@
1206 # Can take a non-string.
1207 >>> print ValidationError(VeryBadError()).messages
1208 <ul class="errorlist"><li>A very bad error.</li></ul>
1209+
1210+# Escaping.
1211+
1212+>>> print ValidationError(script).messages
1213+<ul class="errorlist"><li>$(&#39;#example&#39;).html(&#39;&lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;&#39;);</li></ul>
1214+>>> print ValidationError(escape(script)).messages
1215+<ul class="errorlist"><li>$(&#39;#example&#39;).html(&#39;&lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;&#39;);</li></ul>
1216+>>> print ValidationError(conditional_escape(script)).messages
1217+<ul class="errorlist"><li>$(&#39;#example&#39;).html(&#39;&lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;&#39;);</li></ul>
1218+>>> print ErrorDict({'example': ValidationError(script).messages})
1219+<ul class="errorlist"><li>example<ul class="errorlist"><li>$(&#39;#example&#39;).html(&#39;&lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;&#39;);</li></ul></li></ul>
1220+>>> print ErrorDict({'example': ValidationError(escape(script)).messages})
1221+<ul class="errorlist"><li>example<ul class="errorlist"><li>$(&#39;#example&#39;).html(&#39;&lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;&#39;);</li></ul></li></ul>
1222+>>> print ErrorDict({'example': ValidationError(conditional_escape(script)).messages})
1223+<ul class="errorlist"><li>example<ul class="errorlist"><li>$(&#39;#example&#39;).html(&#39;&lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;&#39;);</li></ul></li></ul>
1224+
1225+>>> print conditional_escape(unicode(ValidationError(script).messages))
1226+<ul class="errorlist"><li>$(&#39;#example&#39;).html(&#39;&lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;&#39;);</li></ul>
1227+>>> print conditional_escape(unicode(ValidationError(escape(script)).messages))
1228+<ul class="errorlist"><li>$(&#39;#example&#39;).html(&#39;&lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;&#39;);</li></ul>
1229+>>> print conditional_escape(unicode(ValidationError(conditional_escape(script)).messages))
1230+<ul class="errorlist"><li>$(&#39;#example&#39;).html(&#39;&lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;&#39;);</li></ul>
1231+>>> print conditional_escape(unicode(ErrorDict({'example': ValidationError(script).messages})))
1232+<ul class="errorlist"><li>example<ul class="errorlist"><li>$(&#39;#example&#39;).html(&#39;&lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;&#39;);</li></ul></li></ul>
1233+>>> print conditional_escape(unicode(ErrorDict({'example': ValidationError(escape(script)).messages})))
1234+<ul class="errorlist"><li>example<ul class="errorlist"><li>$(&#39;#example&#39;).html(&#39;&lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;&#39;);</li></ul></li></ul>
1235+>>> print conditional_escape(unicode(ErrorDict({'example': ValidationError(conditional_escape(script)).messages})))
1236+<ul class="errorlist"><li>example<ul class="errorlist"><li>$(&#39;#example&#39;).html(&#39;&lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;&#39;);</li></ul></li></ul>
1237+
1238 """
1239