Code

Ticket #6632: 02-forms-inlines.diff

File 02-forms-inlines.diff, 36.6 KB (added by Petr Marhoun <petr.marhoun@…>, 6 years ago)
Line 
1=== modified file 'django/contrib/admin/options.py'
2--- django/contrib/admin/options.py     2008-08-02 08:47:58 +0000
3+++ django/contrib/admin/options.py     2008-08-02 08:48:05 +0000
4@@ -1,7 +1,7 @@
5 from django import forms, template
6 from django.forms.formsets import all_valid
7 from django.forms.models import modelform_factory, inlineformset_factory
8-from django.forms.models import BaseInlineFormset
9+from django.forms.models import BaseInlineFormSet
10 from django.contrib.contenttypes.models import ContentType
11 from django.contrib.admin import widgets
12 from django.contrib.admin.util import quote, unquote, get_deleted_objects
13@@ -714,7 +714,7 @@
14     """
15     model = None
16     fk_name = None
17-    formset = BaseInlineFormset
18+    formset = BaseInlineFormSet
19     extra = 3
20     max_num = 0
21     template = None
22
23=== modified file 'django/forms/__init__.py'
24--- django/forms/__init__.py    2008-07-20 21:58:25 +0000
25+++ django/forms/__init__.py    2008-07-31 10:28:22 +0000
26@@ -14,4 +14,5 @@
27 from widgets import *
28 from fields import *
29 from forms import *
30+from formsets import *
31 from models import *
32
33=== modified file 'django/forms/forms.py'
34--- django/forms/forms.py       2008-08-01 09:07:13 +0000
35+++ django/forms/forms.py       2008-08-01 09:12:00 +0000
36@@ -30,6 +30,7 @@
37         self.fieldsets = getattr(options, 'fieldsets', None)
38         self.fields = getattr(options, 'fields', None)
39         self.exclude = getattr(options, 'exclude', None)
40+        self.inlines = getattr(options, 'inlines', None)
41         # other options
42         self.error_class = getattr(options, 'error_class', ErrorList)
43         self.error_row_class = getattr(options, 'error_row_class', 'error')
44@@ -38,6 +39,7 @@
45         self.label_capfirst = getattr(options, 'label_capfirst', True)
46         # self.label_capfirst = getattr(options, 'label_capfirst', False) # backward-compatible
47         self.label_suffix = getattr(options, 'label_suffix', ':')
48+        self.output_type = getattr(options, 'output_type', 'table')
49         self.required_row_class = getattr(options, 'required_row_class', 'required')
50         # self.required_row_class = getattr(options, 'required_row_class', None) # backward-compatible
51         self.use_field_row_class = getattr(options, 'use_field_row_class', True)
52@@ -50,6 +52,7 @@
53         metaclassing.create_declared_fields(new_class, attrs)
54         metaclassing.create_base_fields_pool_from_declared_fields(new_class, attrs)
55         metaclassing.create_base_fields_from_base_fields_pool(new_class, attrs)
56+        metaclassing.create_fieldsets_if_inlines_exist(new_class, attrs)
57         metaclassing.create_media(new_class, attrs)
58         return new_class
59 
60@@ -75,18 +78,26 @@
61         if label_suffix is not None:
62             self.label_suffix = label_suffix
63         self.empty_permitted = empty_permitted
64-        self._errors = None # Stores the errors after clean() has been called.
65+        self._is_valid = None # Stores validation state after full_clean() has been called.
66         self._changed_data = None
67-
68         # The base_fields class attribute is the *class-wide* definition of
69         # fields. Because a particular *instance* of the class might want to
70         # alter self.fields, we create self.fields here by copying base_fields.
71         # Instances should always modify self.fields; they should not modify
72         # self.base_fields.
73         self.fields = deepcopy(self.base_fields)
74+        self._construct_inlines()
75+
76+    def _construct_inlines(self):
77+        # this class cannot create any inlines
78+        self.inlines = []
79+        if self.has_fieldsets():
80+            for fieldset in self._meta.fieldsets:
81+                if not isinstance(fieldset, dict):
82+                    raise ValueError('%s cannot create instance of %s.' % (self.__class__.__name__, fieldset.__name__))
83 
84     def __unicode__(self):
85-        return self.as_table()
86+        return getattr(self, 'as_%s' % self.output_type)()
87 
88     def __iter__(self):
89         for name, field in self.fields.items():
90@@ -102,7 +113,7 @@
91 
92     def _get_errors(self):
93         "Returns an ErrorDict for the data provided for the form."
94-        if self._errors is None:
95+        if self._is_valid is None:
96             self.full_clean()
97         return self._errors
98     errors = property(_get_errors)
99@@ -112,7 +123,9 @@
100         Returns True if the form has no errors. Otherwise, False. If errors are
101         being ignored, returns False.
102         """
103-        return self.is_bound and not bool(self.errors)
104+        if self._is_valid is None:
105+            self.full_clean()
106+        return self._is_valid
107 
108     def add_prefix(self, name):
109         """
110@@ -129,15 +142,29 @@
111 
112     def first_fieldset_attrs(self):
113         "Returns attributes for first fieldset as HTML code."
114-        if self.has_fieldsets() and 'attrs' in self._meta.fieldsets[0]:
115-            return flatatt(self._meta.fieldsets[0]['attrs'])
116+        if self.has_fieldsets():
117+            if isinstance(self._meta.fieldsets[0], dict):
118+                attrs = self._meta.fieldsets[0].get('attrs')
119+            else:
120+                attrs = self.inlines[0].fieldset_attrs
121+        else:
122+            attrs = None
123+        if attrs:
124+            return flatatt(attrs)
125         else:
126             return u''
127 
128     def first_fieldset_legend_tag(self):
129         "Returns legend tag for first fieldset as HTML code."
130-        if self.has_fieldsets() and 'legend' in self._meta.fieldsets[0]:
131-            return mark_safe(u'<legend>%s</legend>' % conditional_escape(force_unicode(self._meta.fieldsets[0]['legend'])))
132+        if self.has_fieldsets():
133+            if isinstance(self._meta.fieldsets[0], dict):
134+                legend = self._meta.fieldsets[0].get('legend')
135+            else:
136+                legend = self.inlines[0].fieldset_legend
137+        else:
138+            legend = None
139+        if legend:
140+            return mark_safe(u'<legend>%s</legend>' % conditional_escape(force_unicode(legend)))
141         else:
142             return u''
143 
144@@ -205,6 +232,26 @@
145             output.append(fieldset_end_html)
146         return u'\n'.join(output)
147 
148+    def _inline_html_output(self, inline, is_first, is_last, fieldset_start_html, fieldset_end_html, legend_tag_html):
149+        "Helper function for outputting HTML from a inline. Used by _html_output."
150+        output = []
151+        if not is_first:
152+            legend_tag = attrs = u''
153+            if inline.fieldset_legend:
154+                legend_tag = legend_tag_html % {
155+                    'legend': conditional_escape(force_unicode(inline.fieldset_legend)),
156+                }
157+            if inline.fieldset_attrs:
158+                attrs = flatatt(inline.fieldset_attrs)
159+            output.append(fieldset_start_html % {
160+                'legend_tag': legend_tag,
161+                'attrs': attrs,
162+            })
163+        output.append(unicode(inline))
164+        if not is_last:
165+            output.append(fieldset_end_html)
166+        return u'\n'.join(output)
167+
168     def _hidden_fields_html_output(self, hidden_fields, hidden_fields_html):
169         "Helper function for outputting HTML from a hidden fields. Used by _html_output."
170         if self.hidden_row_class:
171@@ -232,12 +279,17 @@
172         if top_errors:
173             output.append(self._top_errors_html_output(top_errors, top_errors_html))
174         if self.has_fieldsets():
175+            inlines = list(self.inlines) # Copy it - method pop should not changed self.inlines.
176             for i, fieldset in enumerate(self._meta.fieldsets):
177-                fields = dict((name, visible_fields[name]) for name in fieldset['fields'] if name in visible_fields)
178                 is_first = (i == 0)
179                 is_last = (i + 1 == len(self._meta.fieldsets))
180-                output.append(self._fieldset_html_output(fields, fieldset, is_first, is_last,
181-                    fieldset_start_html, fieldset_end_html, legend_tag_html))
182+                if isinstance(fieldset, dict):
183+                    fields = dict((name, visible_fields[name]) for name in fieldset['fields'] if name in visible_fields)
184+                    output.append(self._fieldset_html_output(fields, fieldset, is_first, is_last,
185+                        fieldset_start_html, fieldset_end_html, legend_tag_html))
186+                else:
187+                    output.append(self._inline_html_output(inlines.pop(0), is_first, is_last,
188+                        fieldset_start_html, fieldset_end_html, legend_tag_html))
189         else:
190             for name in self.fields:
191                 if name in visible_fields:
192@@ -288,21 +340,47 @@
193         }
194         return self._html_output(**kwargs)
195 
196+    def as_tr(self):
197+        "Returns this form rendered as HTML <td>s."
198+        if self.has_fieldsets():
199+            raise ValueError("%s has fieldsets or inlines so its method as_tr cannot be used." % self.__class__.__name__)
200+        colspan = len([bf for bf in self if not bf.is_hidden])
201+        kwargs = {
202+            'row_html': u'<td%(attrs)s>%(rendered_errors)s%(rendered_widget)s%(help_text)s</td>',
203+            'label_tag_html': u'',
204+            'help_text_html': u' %(help_text)s',
205+            'top_errors_html': u'<tr><td colspan="%s">%%(top_errors)s</td></tr>\n<tr>' % colspan,
206+            'fieldset_start_html': u'',
207+            'fieldset_end_html': u'',
208+            'legend_tag_html': u'',
209+            'hidden_fields_html': u'</tr>\n<tr%%(attrs)s><td colspan="%s">%%(hidden_fields)s</td></tr>' % colspan,
210+        }
211+        html_output = self._html_output(**kwargs)
212+        if not html_output.startswith('<tr>'):
213+            html_output = u'<tr>\n%s' % html_output
214+        if not html_output.endswith('</tr>'):
215+            html_output = u'%s\n</tr>' % html_output
216+        return html_output
217+
218     def non_field_errors(self):
219         """
220         Returns an ErrorList of errors that aren't associated with a particular
221         field -- i.e., from Form.clean(). Returns an empty ErrorList if there
222         are none.
223         """
224+        if self._is_valid is None:
225+            self.full_clean()
226         return self.errors.get(NON_FIELD_ERRORS, self.error_class())
227 
228     def full_clean(self):
229         """
230-        Cleans all of self.data and populates self._errors and
231+        Cleans all of self.data and populates self._is_valid, self._errors and
232         self.cleaned_data.
233         """
234+        self._is_valid = True # Assume the form is valid until proven otherwise.
235         self._errors = ErrorDict()
236         if not self.is_bound: # Stop further processing.
237+            self._is_valid = False
238             return
239         self.cleaned_data = {}
240         # If the form is permitted to be empty, and none of the form data has
241@@ -328,12 +406,20 @@
242                 self._errors[name] = self.error_class(e.messages)
243                 if name in self.cleaned_data:
244                     del self.cleaned_data[name]
245+                self._is_valid = False
246         try:
247             self.cleaned_data = self.clean()
248         except ValidationError, e:
249             self._errors[NON_FIELD_ERRORS] = self.error_class(e.messages)
250-        if self._errors:
251+            self._is_valid = False
252+        for inline in self.inlines:
253+            inline.full_clean()
254+            if not inline.is_valid():
255+                self._is_valid = False
256+        if not self._is_valid:
257             delattr(self, 'cleaned_data')
258+            for inline in self.inlines:
259+                inline._is_valid = False
260 
261     def clean(self):
262         """
263@@ -375,17 +461,23 @@
264         media = Media()
265         for field in self.fields.values():
266             media = media + field.widget.media
267+        for inline in self.inlines:
268+            media = media + inline.media
269         return media
270     media = property(_get_media)
271 
272     def is_multipart(self):
273-        """
274-        Returns True if the form needs to be multipart-encrypted, i.e. it has
275-        FileInput. Otherwise, False.
276-        """
277+       
278+        """
279+        Returns True if the form (including inlines) needs to be
280+        multipart-encrypted, i.e. it has FileInput. Otherwise, False.
281+         """
282         for field in self.fields.values():
283             if field.widget.needs_multipart_form:
284                 return True
285+        for inline in self.inlines:
286+            if inline.is_multipart():
287+                return True
288         return False
289 
290 class Form(BaseForm):
291
292=== modified file 'django/forms/formsets.py'
293--- django/forms/formsets.py    2008-07-24 12:10:12 +0000
294+++ django/forms/formsets.py    2008-08-01 09:03:20 +0000
295@@ -1,11 +1,12 @@
296-from forms import Form
297+from forms import Form, FormOptions
298 from django.utils.encoding import StrAndUnicode
299 from django.utils.safestring import mark_safe
300 from fields import IntegerField, BooleanField
301 from widgets import Media, HiddenInput
302-from util import ErrorList, ValidationError
303+from util import ValidationError
304+import metaclassing
305 
306-__all__ = ('BaseFormSet', 'all_valid')
307+__all__ = ('BaseFormSet', 'FormSet', 'formset_factory', 'all_valid')
308 
309 # special field names
310 TOTAL_FORM_COUNT = 'TOTAL_FORMS'
311@@ -24,21 +25,45 @@
312         self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
313         super(ManagementForm, self).__init__(*args, **kwargs)
314 
315+class FormSetOptions(FormOptions):
316+    def __init__(self, options=None):
317+        super(FormSetOptions, self).__init__(options)
318+        # form
319+        self.form = getattr(options, 'form', None)
320+        self.base_form = getattr(options, 'base_form', Form)
321+        # other options
322+        self.can_delete = getattr(options, 'can_delete', False)
323+        self.can_order = getattr(options, 'can_order', False)
324+        self.extra = getattr(options, 'extra', 1)
325+        self.fieldset_attrs = getattr(options, 'fieldset_attrs', None)
326+        self.fieldset_legend = getattr(options, 'fieldset_legend', None)
327+        self.max_num = getattr(options, 'max_num', 0)
328+        self.output_type = getattr(options, 'output_type', 'tr')
329+        # self.output_type = getattr(options, 'output_type', 'original_table') # backward-compatible
330+
331+class FormSetMetaclass(type):
332+    def __new__(cls, name, bases, attrs):
333+        new_class = type.__new__(cls, name, bases, attrs)
334+        metaclassing.create_meta(new_class, attrs)
335+        metaclassing.create_form_if_not_exists(new_class, attrs)
336+        metaclassing.check_no_fieldsets_in_inner_form(new_class, attrs)
337+        return new_class
338+
339 class BaseFormSet(StrAndUnicode):
340     """
341     A collection of instances of the same Form class.
342     """
343     def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
344-                 initial=None, error_class=ErrorList):
345+                 initial=None, error_class=None):
346         self.is_bound = data is not None or files is not None
347         self.prefix = prefix or 'form'
348         self.auto_id = auto_id
349         self.data = data
350         self.files = files
351         self.initial = initial
352-        self.error_class = error_class
353-        self._errors = None
354-        self._non_form_errors = None
355+        if error_class is not None:
356+            self.error_class = error_class
357+        self._is_valid = None # Stores validation state after full_clean() has been called.
358         # initialization is different depending on whether we recieved data, initial, or nothing
359         if data or files:
360             self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix)
361@@ -66,7 +91,7 @@
362         self._construct_forms()
363 
364     def __unicode__(self):
365-        return self.as_table()
366+        return getattr(self, 'as_%s' % self.output_type)()
367 
368     def _construct_forms(self):
369         # instantiate all the forms and put them in self.forms
370@@ -181,15 +206,15 @@
371         form -- i.e., from formset.clean(). Returns an empty ErrorList if there
372         are none.
373         """
374-        if self._non_form_errors is not None:
375-            return self._non_form_errors
376-        return self.error_class()
377+        if self._is_valid is None:
378+            self.full_clean()
379+        return self._non_form_errors
380 
381     def _get_errors(self):
382         """
383         Returns a list of form.errors for every form in self.forms.
384         """
385-        if self._errors is None:
386+        if self._is_valid is None:
387             self.full_clean()
388         return self._errors
389     errors = property(_get_errors)
390@@ -198,31 +223,31 @@
391         """
392         Returns True if form.errors is empty for every form in self.forms.
393         """
394-        if not self.is_bound:
395-            return False
396-        # We loop over every form.errors here rather than short circuiting on the
397-        # first failure to make sure validation gets triggered for every form.
398-        forms_valid = True
399-        for errors in self.errors:
400-            if bool(errors):
401-                forms_valid = False
402-        return forms_valid and not bool(self.non_form_errors())
403+        if self._is_valid is None:
404+            self.full_clean()
405+        return self._is_valid
406 
407     def full_clean(self):
408         """
409         Cleans all of self.data and populates self._errors.
410         """
411+        self._is_valid = True # Assume the form is valid until proven otherwise.
412         self._errors = []
413+        self._non_form_errors = self.error_class()
414         if not self.is_bound: # Stop further processing.
415+            self._is_valid = False
416             return
417         for i in range(0, self._total_form_count):
418             form = self.forms[i]
419             self._errors.append(form.errors)
420+            if form.errors:
421+                self._is_valid = False
422         # Give self.clean() a chance to do cross-form validation.
423         try:
424             self.clean()
425         except ValidationError, e:
426-            self._non_form_errors = e.messages
427+            self._non_form_errors = self.error_class(e.messages)
428+            self._is_valid = False
429 
430     def clean(self):
431         """
432@@ -264,6 +289,39 @@
433     media = property(_get_media)
434 
435     def as_table(self):
436+        "Returns this form rendered as HTML <tr>s."
437+        return self._html_output_non_form_errors() + u'\n'.join(u'<table>\n%s\n</table>' % form.as_table() for form in [self.management_form] + self.forms)
438+
439+    def as_ul(self):
440+        "Returns this form rendered as HTML <li>s."
441+        return self._html_output_non_form_errors() + u'\n'.join(u'<ul>\n%s\n</ul>' % form.as_ul() for form in [self.management_form] + self.forms)
442+
443+    def as_p(self):
444+        "Returns this form rendered as HTML <p>s."
445+        return self._html_output_non_form_errors() + u'\n'.join(u'<div>\n%s\n</div>' % form.as_p() for form in [self.management_form] + self.forms)
446+
447+    def as_tr(self):
448+        "Returns this form rendered as HTML <td>s."
449+        output = [self.management_form.as_tr()]
450+        if self.non_form_errors:
451+            output.append(u'<tr><td colspan="%s">%s</td></tr>' % (
452+                len([bf for bf in self.forms[0] if not bf.is_hidden]),
453+                self._html_output_non_form_errors(),
454+            )) 
455+        if self.forms:
456+            output.append(u'<tr>')
457+            output.extend(u'<th>%s</th>' % bf.label for bf in self.forms[0] if not bf.is_hidden)
458+            output.append(u'</tr>')
459+        output.extend(form.as_tr() for form in self.forms)
460+        return '\n'.join(output)
461+   
462+    def _html_output_non_form_errors(self):
463+        if self.non_form_errors:
464+            return u'<div>%s</div>' % unicode(self.non_form_errors())
465+        else:
466+            return u''
467+
468+    def as_original_table(self):
469         "Returns this formset rendered as HTML <tr>s -- excluding the <table></table>."
470         # XXX: there is no semantic division between forms here, there
471         # probably should be. It might make sense to render each form as a
472@@ -271,13 +329,17 @@
473         forms = u' '.join([form.as_table() for form in self.forms])
474         return mark_safe(u'\n'.join([unicode(self.management_form), forms]))
475 
476-def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
477-                    can_delete=False, max_num=0):
478+class FormSet(BaseFormSet):
479+    __metaclass__ = FormSetMetaclass
480+    _options = FormSetOptions
481+
482+def formset_factory(form, formset=FormSet, extra=1, can_order=False,
483+                    can_delete=False, max_num=0, **kwargs):
484     """Return a FormSet for the given form class."""
485-    attrs = {'form': form, 'extra': extra,
486-             'can_order': can_order, 'can_delete': can_delete,
487-             'max_num': max_num}
488-    return type(form.__name__ + 'FormSet', (formset,), attrs)
489+    kwargs.update(locals())
490+    meta_class = type('Meta', (), kwargs)
491+    bases = (formset == FormSet and (FormSet,) or (formset, FormSet))
492+    return type(form.__name__ + 'FormSet', bases, {'Meta': meta_class})
493 
494 def all_valid(formsets):
495     """Returns true if every formset in formsets is valid."""
496
497=== modified file 'django/forms/metaclassing.py'
498--- django/forms/metaclassing.py        2008-08-01 08:59:48 +0000
499+++ django/forms/metaclassing.py        2008-08-01 09:03:19 +0000
500@@ -11,7 +11,7 @@
501 def create_meta(cls, attrs):
502     cls._meta = cls._options(getattr(cls, 'Meta', None))
503     for name, attr in cls._meta.__dict__.items():
504-        if name not in ('fieldsets', 'fields', 'exclude'):
505+        if name not in ('fieldsets', 'fields', 'exclude', 'inlines', 'base_form'):
506             setattr(cls, name, attr)
507 
508 def create_declared_fields(cls, attrs):
509@@ -60,7 +60,8 @@
510     if cls._meta.fieldsets:
511         names = []
512         for fieldset in cls._meta.fieldsets:
513-            names.extend(fieldset['fields'])
514+            if isinstance(fieldset, dict):
515+                names.extend(fieldset['fields'])
516     elif cls._meta.fields:
517         names = cls._meta.fields
518     elif cls._meta.exclude:
519@@ -72,3 +73,59 @@
520 def create_media(cls, attrs):
521     if not 'media' in attrs:
522         cls.media = media_property(cls)
523+
524+def create_fieldsets_if_inlines_exist(cls, attrs):
525+    if cls._meta.inlines is not None:
526+        if cls._meta.fieldsets is not None:
527+            raise ImproperlyConfigured("%s cannot have more than one option from fieldsets and inlines." % cls.__name__)
528+        cls._meta.fieldsets = [{'fields': cls.base_fields.keys()}] + list(cls._meta.inlines)
529+
530+def create_form_if_not_exists(cls, attrs):
531+    if not cls.form:
532+        form_attrs = {
533+            'Meta': type('Meta', (), cls._meta.__dict__),
534+        }
535+        for name, possible_field in attrs.items():
536+            if isinstance(possible_field, Field):
537+                form_attrs[name] = possible_field
538+                delattr(cls, name)
539+        cls.form = type(cls.__name__ + 'Form', (cls._meta.base_form,), form_attrs)
540+
541+def check_no_fieldsets_in_inner_form(cls, attrs):
542+    if cls.form._meta.fieldsets:
543+        raise ImproperlyConfigured("%s cannot have form with fieldsets." % cls.__name__)
544+
545+def add_fk_attribute_and_remove_fk_from_base_fields(cls, attrs):
546+    # If models are not set, this class would not be used directly.
547+    if not (cls.parent_model and cls.model):
548+        return
549+    # Try to discover what the foreign key from model to parent_model is.
550+    fks_to_parent = []
551+    for field in cls.model._meta.fields:
552+        # Exceptions are neccessary here - ForeignKey cannot be imported for circular dependancy.
553+        try:
554+            if field.rel.to == cls.parent_model or field.rel.to in cls.parent_model._meta.parents.keys():
555+                fks_to_parent.append(field)
556+        except AttributeError:
557+            pass
558+    if cls.fk_name:
559+        fks_to_parent = [fk for fk in fks_to_parent if fk.name == cls.fk_name]
560+        if len(fks_to_parent) == 0:
561+            raise ImproperlyConfigured("%s has no ForeignKey with name %s to %s." %
562+                (cls.model, cls.fk_name, cls.parent_model))
563+        elif len(fks_to_parent) > 1:
564+            raise ImproperlyConfigured("%s has more than one ForeignKey with name %s to %s." %
565+                (cls.model, cls.fk_name, cls.parent_model))
566+    else:
567+        if len(fks_to_parent) == 0:
568+            raise ImproperlyConfigured("%s has no ForeignKey to %s." %
569+                (cls.model, cls.parent_model))
570+        if len(fks_to_parent) > 1:
571+            raise ImproperlyConfigured("%s has more than one ForeignKey to %s." %
572+                (cls.model, cls.parent_model))
573+    cls.fk = fks_to_parent[0]
574+    # Try to remove the foreign key from base_fields to keep it transparent to the form.
575+    try:
576+        del cls.form.base_fields[cls.fk.name]
577+    except KeyError:
578+        pass
579
580=== modified file 'django/forms/models.py'
581--- django/forms/models.py      2008-08-01 21:12:51 +0000
582+++ django/forms/models.py      2008-08-01 21:12:56 +0000
583@@ -11,15 +11,18 @@
584 
585 from util import ValidationError
586 from forms import FormOptions, FormMetaclass, BaseForm
587+from formsets import FormSetOptions, FormSetMetaclass
588 from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
589 from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
590-from formsets import BaseFormSet, formset_factory, DELETION_FIELD_NAME
591+from formsets import BaseFormSet, DELETION_FIELD_NAME
592 import metaclassing
593 
594 __all__ = (
595     'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
596     'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
597-    'ModelChoiceField', 'ModelMultipleChoiceField',
598+    'ModelChoiceField', 'ModelMultipleChoiceField', 'BaseModelForm',
599+    'ModelForm', 'BaseInlineFormSet', 'InlineFormSet', 'modelform_factory',
600+    'modelformset_factory', 'inlineformset_factory',
601 )
602 
603 def save_instance(form, instance, fields=None, fail_message='saved',
604@@ -219,6 +222,7 @@
605         metaclassing.create_declared_fields(new_class, attrs)
606         metaclassing.create_base_fields_pool_from_model_fields_and_declared_fields(new_class, attrs)
607         metaclassing.create_base_fields_from_base_fields_pool(new_class, attrs)
608+        metaclassing.create_fieldsets_if_inlines_exist(new_class, attrs)
609         metaclassing.create_media(new_class, attrs)
610         return new_class
611 
612@@ -240,6 +244,16 @@
613         super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
614                                             error_class, label_suffix, empty_permitted)
615 
616+    def _construct_inlines(self):
617+        # this class can create inlines which are subclass of BaseInlineFormSet
618+        self.inlines = []
619+        if self.has_fieldsets():
620+            for fieldset in self._meta.fieldsets:
621+                if not isinstance(fieldset, dict):
622+                    if not issubclass(fieldset, BaseInlineFormSet):
623+                        raise ValueError('%s cannot create instance of %s.' % (self.__class__.__name__, fieldset.__name__))
624+                    self.inlines.append(fieldset(self.data, self.files, self.instance))
625+
626     def save(self, commit=True):
627         """
628         Saves this ``form``'s cleaned_data into model instance
629@@ -252,28 +266,31 @@
630             fail_message = 'created'
631         else:
632             fail_message = 'changed'
633-        return save_instance(self, self.instance, self.base_fields.keys(), fail_message, commit)
634+        self.saved_instance = save_instance(self, self.instance, self.base_fields.keys(), fail_message, commit)
635+        self.saved_inline_instances = [inline.save(commit) for inline in self.inlines]
636+        return self.saved_instance
637 
638 class ModelForm(BaseModelForm):
639     __metaclass__ = ModelFormMetaclass
640     _options = ModelFormOptions
641 
642 def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
643-                       formfield_callback=lambda f: f.formfield()):
644-    # HACK: we should be able to construct a ModelForm without creating
645-    # and passing in a temporary inner class
646-    class Meta:
647-        pass
648-    setattr(Meta, 'model', model)
649-    setattr(Meta, 'fields', fields)
650-    setattr(Meta, 'exclude', exclude)
651-    class_name = model.__name__ + 'Form'
652-    return ModelFormMetaclass(class_name, (form,), {'Meta': Meta,
653-                              'formfield_callback': formfield_callback})
654+                       formfield_callback=lambda f: f.formfield(), **kwargs):
655+    kwargs.update(locals())
656+    meta_class = type('Meta', (), kwargs)
657+    bases = (form == ModelForm and (ModelForm,) or (form, ModelForm))
658+    return ModelFormMetaclass(model.__name__ + 'Form', bases,
659+        {'Meta': meta_class, 'formfield_callback': formfield_callback})
660 
661 
662 # ModelFormSets ##############################################################
663 
664+class ModelFormSetOptions(FormSetOptions, ModelFormOptions):
665+    def __init__(self, options=None):
666+        super(ModelFormSetOptions, self).__init__(options)
667+        # options changed compared to superclass
668+        self.base_form = getattr(options, 'base_form', ModelForm)
669+
670 class BaseModelFormSet(BaseFormSet):
671     """
672     A ``FormSet`` for editing a queryset and/or adding new objects to it.
673@@ -362,24 +379,45 @@
674             form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
675         super(BaseModelFormSet, self).add_fields(form, index)
676 
677+class ModelFormSet(BaseModelFormSet):
678+    __metaclass__ = FormSetMetaclass # no changes are needed
679+    _options =  ModelFormSetOptions
680+
681 def modelformset_factory(model, form=ModelForm, formfield_callback=lambda f: f.formfield(),
682-                         formset=BaseModelFormSet,
683+                         formset=ModelFormSet,
684                          extra=1, can_delete=False, can_order=False,
685-                         max_num=0, fields=None, exclude=None):
686+                         max_num=0, fields=None, exclude=None, **kwargs):
687     """
688     Returns a FormSet class for the given Django model class.
689     """
690-    form = modelform_factory(model, form=form, fields=fields, exclude=exclude,
691-                             formfield_callback=formfield_callback)
692-    FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
693-                              can_order=can_order, can_delete=can_delete)
694-    FormSet.model = model
695-    return FormSet
696+    kwargs.update(locals())
697+    kwargs['form'] = modelform_factory(**kwargs)
698+    meta_class = type('Meta', (), kwargs)
699+    bases = (formset == ModelFormSet and (ModelFormSet,) or (formset, ModelFormSet))
700+    return type(form.__name__ + 'FormSet', bases, {'Meta': meta_class})
701 
702 
703 # InlineFormSets #############################################################
704 
705-class BaseInlineFormset(BaseModelFormSet):
706+class InlineFormSetOptions(ModelFormSetOptions):
707+    def __init__(self, options=None):
708+        super(InlineFormSetOptions, self).__init__(options)
709+        self.parent_model = getattr(options, 'parent_model', None)
710+        self.fk_name = getattr(options, 'fk_name', None)
711+        # options changed compared to superclass
712+        self.can_delete = getattr(options, 'can_delete', True)
713+        self.extra = getattr(options, 'extra', 3)
714+
715+class InlineFormSetMetaclass(FormSetMetaclass):
716+    def __new__(cls, name, bases, attrs):
717+        new_class = type.__new__(cls, name, bases, attrs)
718+        metaclassing.create_meta(new_class, attrs)
719+        metaclassing.create_form_if_not_exists(new_class, attrs)
720+        metaclassing.check_no_fieldsets_in_inner_form(new_class, attrs)
721+        metaclassing.add_fk_attribute_and_remove_fk_from_base_fields(new_class, attrs)
722+        return new_class
723+
724+class BaseInlineFormSet(BaseModelFormSet):
725     """A formset for child objects related to a parent."""
726     def __init__(self, data=None, files=None, instance=None,
727                  save_as_new=False, prefix=None):
728@@ -388,13 +426,13 @@
729         self.save_as_new = save_as_new
730         # is there a better way to get the object descriptor?
731         self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
732-        super(BaseInlineFormset, self).__init__(data, files, prefix=prefix or self.rel_name)
733+        super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix or self.rel_name)
734     
735     def _construct_forms(self):
736         if self.save_as_new:
737             self._total_form_count = self._initial_form_count
738             self._initial_form_count = 0
739-        super(BaseInlineFormset, self)._construct_forms()
740+        super(BaseInlineFormSet, self)._construct_forms()
741 
742     def get_queryset(self):
743         """
744@@ -409,66 +447,26 @@
745         new_obj = self.model(**kwargs)
746         return save_instance(form, new_obj, commit=commit)
747 
748-def _get_foreign_key(parent_model, model, fk_name=None):
749-    """
750-    Finds and returns the ForeignKey from model to parent if there is one.
751-    If fk_name is provided, assume it is the name of the ForeignKey field.
752-    """
753-    # avoid circular import
754-    from django.db.models import ForeignKey
755-    opts = model._meta
756-    if fk_name:
757-        fks_to_parent = [f for f in opts.fields if f.name == fk_name]
758-        if len(fks_to_parent) == 1:
759-            fk = fks_to_parent[0]
760-            if not isinstance(fk, ForeignKey) or \
761-                    (fk.rel.to != parent_model and
762-                     fk.rel.to not in parent_model._meta.parents.keys()):
763-                raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
764-        elif len(fks_to_parent) == 0:
765-            raise Exception("%s has no field named '%s'" % (model, fk_name))
766-    else:
767-        # Try to discover what the ForeignKey from model to parent_model is
768-        fks_to_parent = [
769-            f for f in opts.fields
770-            if isinstance(f, ForeignKey)
771-            and (f.rel.to == parent_model
772-                or f.rel.to in parent_model._meta.parents.keys())
773-        ]
774-        if len(fks_to_parent) == 1:
775-            fk = fks_to_parent[0]
776-        elif len(fks_to_parent) == 0:
777-            raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
778-        else:
779-            raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
780-    return fk
781-
782+class InlineFormSet(BaseInlineFormSet):
783+    __metaclass__ = InlineFormSetMetaclass
784+    _options = InlineFormSetOptions
785 
786 def inlineformset_factory(parent_model, model, form=ModelForm,
787-                          formset=BaseInlineFormset, fk_name=None,
788+                          formset=InlineFormSet, fk_name=None,
789                           fields=None, exclude=None,
790                           extra=3, can_order=False, can_delete=True, max_num=0,
791-                          formfield_callback=lambda f: f.formfield()):
792+                          formfield_callback=lambda f: f.formfield(), **kwargs):
793     """
794     Returns an ``InlineFormset`` for the given kwargs.
795 
796     You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
797     to ``parent_model``.
798     """
799-    fk = _get_foreign_key(parent_model, model, fk_name=fk_name)
800-    # let the formset handle object deletion by default
801-   
802-    if exclude is not None:
803-        exclude.append(fk.name)
804-    else:
805-        exclude = [fk.name]
806-    FormSet = modelformset_factory(model, form=form,
807-                                    formfield_callback=formfield_callback,
808-                                    formset=formset,
809-                                    extra=extra, can_delete=can_delete, can_order=can_order,
810-                                    fields=fields, exclude=exclude, max_num=max_num)
811-    FormSet.fk = fk
812-    return FormSet
813+    kwargs.update(locals())
814+    kwargs['form'] = modelform_factory(**kwargs)
815+    meta_class = type('Meta', (), kwargs)
816+    bases = (formset == InlineFormSet and (InlineFormSet,) or (form, InlineFormSet))
817+    return type(form.__name__ + 'FormSet', bases, {'Meta': meta_class})
818 
819 
820 # Fields #####################################################################
821
822=== modified file 'tests/regressiontests/inline_formsets/models.py'
823--- tests/regressiontests/inline_formsets/models.py     2008-07-20 21:58:25 +0000
824+++ tests/regressiontests/inline_formsets/models.py     2008-07-31 10:28:23 +0000
825@@ -24,7 +24,7 @@
826 >>> ifs = inlineformset_factory(Parent, Child)
827 Traceback (most recent call last):
828     ...
829-Exception: <class 'regressiontests.inline_formsets.models.Child'> has more than 1 ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>
830+ImproperlyConfigured: <class 'regressiontests.inline_formsets.models.Child'> has more than one ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>.
831 
832 
833 These two should both work without a problem.
834@@ -39,7 +39,7 @@
835 >>> ifs = inlineformset_factory(Parent, Child, fk_name='school')
836 Traceback (most recent call last):
837     ...
838-Exception: fk_name 'school' is not a ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>
839+ImproperlyConfigured: <class 'regressiontests.inline_formsets.models.Child'> has no ForeignKey with name school to <class 'regressiontests.inline_formsets.models.Parent'>.
840 
841 
842 If the field specified in fk_name is not a ForeignKey, we should get an
843@@ -48,7 +48,7 @@
844 >>> ifs = inlineformset_factory(Parent, Child, fk_name='test')
845 Traceback (most recent call last):
846     ...
847-Exception: <class 'regressiontests.inline_formsets.models.Child'> has no field named 'test'
848+ImproperlyConfigured: <class 'regressiontests.inline_formsets.models.Child'> has no ForeignKey with name test to <class 'regressiontests.inline_formsets.models.Parent'>.
849 
850 
851 """
852