Ticket #6632: 02-forms-inlines.diff
File 02-forms-inlines.diff, 36.6 KB (added by , 16 years ago) |
---|
-
django/contrib/admin/options.py
=== modified file 'django/contrib/admin/options.py'
1 1 from django import forms, template 2 2 from django.forms.formsets import all_valid 3 3 from django.forms.models import modelform_factory, inlineformset_factory 4 from django.forms.models import BaseInlineForm set4 from django.forms.models import BaseInlineFormSet 5 5 from django.contrib.contenttypes.models import ContentType 6 6 from django.contrib.admin import widgets 7 7 from django.contrib.admin.util import quote, unquote, get_deleted_objects … … 714 714 """ 715 715 model = None 716 716 fk_name = None 717 formset = BaseInlineForm set717 formset = BaseInlineFormSet 718 718 extra = 3 719 719 max_num = 0 720 720 template = None -
django/forms/__init__.py
=== modified file 'django/forms/__init__.py'
14 14 from widgets import * 15 15 from fields import * 16 16 from forms import * 17 from formsets import * 17 18 from models import * -
django/forms/forms.py
=== modified file 'django/forms/forms.py'
30 30 self.fieldsets = getattr(options, 'fieldsets', None) 31 31 self.fields = getattr(options, 'fields', None) 32 32 self.exclude = getattr(options, 'exclude', None) 33 self.inlines = getattr(options, 'inlines', None) 33 34 # other options 34 35 self.error_class = getattr(options, 'error_class', ErrorList) 35 36 self.error_row_class = getattr(options, 'error_row_class', 'error') … … 38 39 self.label_capfirst = getattr(options, 'label_capfirst', True) 39 40 # self.label_capfirst = getattr(options, 'label_capfirst', False) # backward-compatible 40 41 self.label_suffix = getattr(options, 'label_suffix', ':') 42 self.output_type = getattr(options, 'output_type', 'table') 41 43 self.required_row_class = getattr(options, 'required_row_class', 'required') 42 44 # self.required_row_class = getattr(options, 'required_row_class', None) # backward-compatible 43 45 self.use_field_row_class = getattr(options, 'use_field_row_class', True) … … 50 52 metaclassing.create_declared_fields(new_class, attrs) 51 53 metaclassing.create_base_fields_pool_from_declared_fields(new_class, attrs) 52 54 metaclassing.create_base_fields_from_base_fields_pool(new_class, attrs) 55 metaclassing.create_fieldsets_if_inlines_exist(new_class, attrs) 53 56 metaclassing.create_media(new_class, attrs) 54 57 return new_class 55 58 … … 75 78 if label_suffix is not None: 76 79 self.label_suffix = label_suffix 77 80 self.empty_permitted = empty_permitted 78 self._ errors = None # Stores the errors afterclean() has been called.81 self._is_valid = None # Stores validation state after full_clean() has been called. 79 82 self._changed_data = None 80 81 83 # The base_fields class attribute is the *class-wide* definition of 82 84 # fields. Because a particular *instance* of the class might want to 83 85 # alter self.fields, we create self.fields here by copying base_fields. 84 86 # Instances should always modify self.fields; they should not modify 85 87 # self.base_fields. 86 88 self.fields = deepcopy(self.base_fields) 89 self._construct_inlines() 90 91 def _construct_inlines(self): 92 # this class cannot create any inlines 93 self.inlines = [] 94 if self.has_fieldsets(): 95 for fieldset in self._meta.fieldsets: 96 if not isinstance(fieldset, dict): 97 raise ValueError('%s cannot create instance of %s.' % (self.__class__.__name__, fieldset.__name__)) 87 98 88 99 def __unicode__(self): 89 return self.as_table()100 return getattr(self, 'as_%s' % self.output_type)() 90 101 91 102 def __iter__(self): 92 103 for name, field in self.fields.items(): … … 102 113 103 114 def _get_errors(self): 104 115 "Returns an ErrorDict for the data provided for the form." 105 if self._ errorsis None:116 if self._is_valid is None: 106 117 self.full_clean() 107 118 return self._errors 108 119 errors = property(_get_errors) … … 112 123 Returns True if the form has no errors. Otherwise, False. If errors are 113 124 being ignored, returns False. 114 125 """ 115 return self.is_bound and not bool(self.errors) 126 if self._is_valid is None: 127 self.full_clean() 128 return self._is_valid 116 129 117 130 def add_prefix(self, name): 118 131 """ … … 129 142 130 143 def first_fieldset_attrs(self): 131 144 "Returns attributes for first fieldset as HTML code." 132 if self.has_fieldsets() and 'attrs' in self._meta.fieldsets[0]: 133 return flatatt(self._meta.fieldsets[0]['attrs']) 145 if self.has_fieldsets(): 146 if isinstance(self._meta.fieldsets[0], dict): 147 attrs = self._meta.fieldsets[0].get('attrs') 148 else: 149 attrs = self.inlines[0].fieldset_attrs 150 else: 151 attrs = None 152 if attrs: 153 return flatatt(attrs) 134 154 else: 135 155 return u'' 136 156 137 157 def first_fieldset_legend_tag(self): 138 158 "Returns legend tag for first fieldset as HTML code." 139 if self.has_fieldsets() and 'legend' in self._meta.fieldsets[0]: 140 return mark_safe(u'<legend>%s</legend>' % conditional_escape(force_unicode(self._meta.fieldsets[0]['legend']))) 159 if self.has_fieldsets(): 160 if isinstance(self._meta.fieldsets[0], dict): 161 legend = self._meta.fieldsets[0].get('legend') 162 else: 163 legend = self.inlines[0].fieldset_legend 164 else: 165 legend = None 166 if legend: 167 return mark_safe(u'<legend>%s</legend>' % conditional_escape(force_unicode(legend))) 141 168 else: 142 169 return u'' 143 170 … … 205 232 output.append(fieldset_end_html) 206 233 return u'\n'.join(output) 207 234 235 def _inline_html_output(self, inline, is_first, is_last, fieldset_start_html, fieldset_end_html, legend_tag_html): 236 "Helper function for outputting HTML from a inline. Used by _html_output." 237 output = [] 238 if not is_first: 239 legend_tag = attrs = u'' 240 if inline.fieldset_legend: 241 legend_tag = legend_tag_html % { 242 'legend': conditional_escape(force_unicode(inline.fieldset_legend)), 243 } 244 if inline.fieldset_attrs: 245 attrs = flatatt(inline.fieldset_attrs) 246 output.append(fieldset_start_html % { 247 'legend_tag': legend_tag, 248 'attrs': attrs, 249 }) 250 output.append(unicode(inline)) 251 if not is_last: 252 output.append(fieldset_end_html) 253 return u'\n'.join(output) 254 208 255 def _hidden_fields_html_output(self, hidden_fields, hidden_fields_html): 209 256 "Helper function for outputting HTML from a hidden fields. Used by _html_output." 210 257 if self.hidden_row_class: … … 232 279 if top_errors: 233 280 output.append(self._top_errors_html_output(top_errors, top_errors_html)) 234 281 if self.has_fieldsets(): 282 inlines = list(self.inlines) # Copy it - method pop should not changed self.inlines. 235 283 for i, fieldset in enumerate(self._meta.fieldsets): 236 fields = dict((name, visible_fields[name]) for name in fieldset['fields'] if name in visible_fields)237 284 is_first = (i == 0) 238 285 is_last = (i + 1 == len(self._meta.fieldsets)) 239 output.append(self._fieldset_html_output(fields, fieldset, is_first, is_last, 240 fieldset_start_html, fieldset_end_html, legend_tag_html)) 286 if isinstance(fieldset, dict): 287 fields = dict((name, visible_fields[name]) for name in fieldset['fields'] if name in visible_fields) 288 output.append(self._fieldset_html_output(fields, fieldset, is_first, is_last, 289 fieldset_start_html, fieldset_end_html, legend_tag_html)) 290 else: 291 output.append(self._inline_html_output(inlines.pop(0), is_first, is_last, 292 fieldset_start_html, fieldset_end_html, legend_tag_html)) 241 293 else: 242 294 for name in self.fields: 243 295 if name in visible_fields: … … 288 340 } 289 341 return self._html_output(**kwargs) 290 342 343 def as_tr(self): 344 "Returns this form rendered as HTML <td>s." 345 if self.has_fieldsets(): 346 raise ValueError("%s has fieldsets or inlines so its method as_tr cannot be used." % self.__class__.__name__) 347 colspan = len([bf for bf in self if not bf.is_hidden]) 348 kwargs = { 349 'row_html': u'<td%(attrs)s>%(rendered_errors)s%(rendered_widget)s%(help_text)s</td>', 350 'label_tag_html': u'', 351 'help_text_html': u' %(help_text)s', 352 'top_errors_html': u'<tr><td colspan="%s">%%(top_errors)s</td></tr>\n<tr>' % colspan, 353 'fieldset_start_html': u'', 354 'fieldset_end_html': u'', 355 'legend_tag_html': u'', 356 'hidden_fields_html': u'</tr>\n<tr%%(attrs)s><td colspan="%s">%%(hidden_fields)s</td></tr>' % colspan, 357 } 358 html_output = self._html_output(**kwargs) 359 if not html_output.startswith('<tr>'): 360 html_output = u'<tr>\n%s' % html_output 361 if not html_output.endswith('</tr>'): 362 html_output = u'%s\n</tr>' % html_output 363 return html_output 364 291 365 def non_field_errors(self): 292 366 """ 293 367 Returns an ErrorList of errors that aren't associated with a particular 294 368 field -- i.e., from Form.clean(). Returns an empty ErrorList if there 295 369 are none. 296 370 """ 371 if self._is_valid is None: 372 self.full_clean() 297 373 return self.errors.get(NON_FIELD_ERRORS, self.error_class()) 298 374 299 375 def full_clean(self): 300 376 """ 301 Cleans all of self.data and populates self._ errors and377 Cleans all of self.data and populates self._is_valid, self._errors and 302 378 self.cleaned_data. 303 379 """ 380 self._is_valid = True # Assume the form is valid until proven otherwise. 304 381 self._errors = ErrorDict() 305 382 if not self.is_bound: # Stop further processing. 383 self._is_valid = False 306 384 return 307 385 self.cleaned_data = {} 308 386 # If the form is permitted to be empty, and none of the form data has … … 328 406 self._errors[name] = self.error_class(e.messages) 329 407 if name in self.cleaned_data: 330 408 del self.cleaned_data[name] 409 self._is_valid = False 331 410 try: 332 411 self.cleaned_data = self.clean() 333 412 except ValidationError, e: 334 413 self._errors[NON_FIELD_ERRORS] = self.error_class(e.messages) 335 if self._errors: 414 self._is_valid = False 415 for inline in self.inlines: 416 inline.full_clean() 417 if not inline.is_valid(): 418 self._is_valid = False 419 if not self._is_valid: 336 420 delattr(self, 'cleaned_data') 421 for inline in self.inlines: 422 inline._is_valid = False 337 423 338 424 def clean(self): 339 425 """ … … 375 461 media = Media() 376 462 for field in self.fields.values(): 377 463 media = media + field.widget.media 464 for inline in self.inlines: 465 media = media + inline.media 378 466 return media 379 467 media = property(_get_media) 380 468 381 469 def is_multipart(self): 382 """ 383 Returns True if the form needs to be multipart-encrypted, i.e. it has 384 FileInput. Otherwise, False. 385 """ 470 471 """ 472 Returns True if the form (including inlines) needs to be 473 multipart-encrypted, i.e. it has FileInput. Otherwise, False. 474 """ 386 475 for field in self.fields.values(): 387 476 if field.widget.needs_multipart_form: 388 477 return True 478 for inline in self.inlines: 479 if inline.is_multipart(): 480 return True 389 481 return False 390 482 391 483 class Form(BaseForm): -
django/forms/formsets.py
=== modified file 'django/forms/formsets.py'
1 from forms import Form 1 from forms import Form, FormOptions 2 2 from django.utils.encoding import StrAndUnicode 3 3 from django.utils.safestring import mark_safe 4 4 from fields import IntegerField, BooleanField 5 5 from widgets import Media, HiddenInput 6 from util import ErrorList, ValidationError 6 from util import ValidationError 7 import metaclassing 7 8 8 __all__ = ('BaseFormSet', ' all_valid')9 __all__ = ('BaseFormSet', 'FormSet', 'formset_factory', 'all_valid') 9 10 10 11 # special field names 11 12 TOTAL_FORM_COUNT = 'TOTAL_FORMS' … … 24 25 self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput) 25 26 super(ManagementForm, self).__init__(*args, **kwargs) 26 27 28 class FormSetOptions(FormOptions): 29 def __init__(self, options=None): 30 super(FormSetOptions, self).__init__(options) 31 # form 32 self.form = getattr(options, 'form', None) 33 self.base_form = getattr(options, 'base_form', Form) 34 # other options 35 self.can_delete = getattr(options, 'can_delete', False) 36 self.can_order = getattr(options, 'can_order', False) 37 self.extra = getattr(options, 'extra', 1) 38 self.fieldset_attrs = getattr(options, 'fieldset_attrs', None) 39 self.fieldset_legend = getattr(options, 'fieldset_legend', None) 40 self.max_num = getattr(options, 'max_num', 0) 41 self.output_type = getattr(options, 'output_type', 'tr') 42 # self.output_type = getattr(options, 'output_type', 'original_table') # backward-compatible 43 44 class FormSetMetaclass(type): 45 def __new__(cls, name, bases, attrs): 46 new_class = type.__new__(cls, name, bases, attrs) 47 metaclassing.create_meta(new_class, attrs) 48 metaclassing.create_form_if_not_exists(new_class, attrs) 49 metaclassing.check_no_fieldsets_in_inner_form(new_class, attrs) 50 return new_class 51 27 52 class BaseFormSet(StrAndUnicode): 28 53 """ 29 54 A collection of instances of the same Form class. 30 55 """ 31 56 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 32 initial=None, error_class= ErrorList):57 initial=None, error_class=None): 33 58 self.is_bound = data is not None or files is not None 34 59 self.prefix = prefix or 'form' 35 60 self.auto_id = auto_id 36 61 self.data = data 37 62 self.files = files 38 63 self.initial = initial 39 self.error_class = error_class40 self._errors = None41 self._ non_form_errors = None64 if error_class is not None: 65 self.error_class = error_class 66 self._is_valid = None # Stores validation state after full_clean() has been called. 42 67 # initialization is different depending on whether we recieved data, initial, or nothing 43 68 if data or files: 44 69 self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix) … … 66 91 self._construct_forms() 67 92 68 93 def __unicode__(self): 69 return self.as_table()94 return getattr(self, 'as_%s' % self.output_type)() 70 95 71 96 def _construct_forms(self): 72 97 # instantiate all the forms and put them in self.forms … … 181 206 form -- i.e., from formset.clean(). Returns an empty ErrorList if there 182 207 are none. 183 208 """ 184 if self._ non_form_errors is notNone:185 return self._non_form_errors186 return self. error_class()209 if self._is_valid is None: 210 self.full_clean() 211 return self._non_form_errors 187 212 188 213 def _get_errors(self): 189 214 """ 190 215 Returns a list of form.errors for every form in self.forms. 191 216 """ 192 if self._ errorsis None:217 if self._is_valid is None: 193 218 self.full_clean() 194 219 return self._errors 195 220 errors = property(_get_errors) … … 198 223 """ 199 224 Returns True if form.errors is empty for every form in self.forms. 200 225 """ 201 if not self.is_bound: 202 return False 203 # We loop over every form.errors here rather than short circuiting on the 204 # first failure to make sure validation gets triggered for every form. 205 forms_valid = True 206 for errors in self.errors: 207 if bool(errors): 208 forms_valid = False 209 return forms_valid and not bool(self.non_form_errors()) 226 if self._is_valid is None: 227 self.full_clean() 228 return self._is_valid 210 229 211 230 def full_clean(self): 212 231 """ 213 232 Cleans all of self.data and populates self._errors. 214 233 """ 234 self._is_valid = True # Assume the form is valid until proven otherwise. 215 235 self._errors = [] 236 self._non_form_errors = self.error_class() 216 237 if not self.is_bound: # Stop further processing. 238 self._is_valid = False 217 239 return 218 240 for i in range(0, self._total_form_count): 219 241 form = self.forms[i] 220 242 self._errors.append(form.errors) 243 if form.errors: 244 self._is_valid = False 221 245 # Give self.clean() a chance to do cross-form validation. 222 246 try: 223 247 self.clean() 224 248 except ValidationError, e: 225 self._non_form_errors = e.messages 249 self._non_form_errors = self.error_class(e.messages) 250 self._is_valid = False 226 251 227 252 def clean(self): 228 253 """ … … 264 289 media = property(_get_media) 265 290 266 291 def as_table(self): 292 "Returns this form rendered as HTML <tr>s." 293 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) 294 295 def as_ul(self): 296 "Returns this form rendered as HTML <li>s." 297 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) 298 299 def as_p(self): 300 "Returns this form rendered as HTML <p>s." 301 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) 302 303 def as_tr(self): 304 "Returns this form rendered as HTML <td>s." 305 output = [self.management_form.as_tr()] 306 if self.non_form_errors: 307 output.append(u'<tr><td colspan="%s">%s</td></tr>' % ( 308 len([bf for bf in self.forms[0] if not bf.is_hidden]), 309 self._html_output_non_form_errors(), 310 )) 311 if self.forms: 312 output.append(u'<tr>') 313 output.extend(u'<th>%s</th>' % bf.label for bf in self.forms[0] if not bf.is_hidden) 314 output.append(u'</tr>') 315 output.extend(form.as_tr() for form in self.forms) 316 return '\n'.join(output) 317 318 def _html_output_non_form_errors(self): 319 if self.non_form_errors: 320 return u'<div>%s</div>' % unicode(self.non_form_errors()) 321 else: 322 return u'' 323 324 def as_original_table(self): 267 325 "Returns this formset rendered as HTML <tr>s -- excluding the <table></table>." 268 326 # XXX: there is no semantic division between forms here, there 269 327 # probably should be. It might make sense to render each form as a … … 271 329 forms = u' '.join([form.as_table() for form in self.forms]) 272 330 return mark_safe(u'\n'.join([unicode(self.management_form), forms])) 273 331 274 def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, 275 can_delete=False, max_num=0): 332 class FormSet(BaseFormSet): 333 __metaclass__ = FormSetMetaclass 334 _options = FormSetOptions 335 336 def formset_factory(form, formset=FormSet, extra=1, can_order=False, 337 can_delete=False, max_num=0, **kwargs): 276 338 """Return a FormSet for the given form class.""" 277 attrs = {'form': form, 'extra': extra,278 'can_order': can_order, 'can_delete': can_delete,279 'max_num': max_num}280 return type(form.__name__ + 'FormSet', (formset,), attrs)339 kwargs.update(locals()) 340 meta_class = type('Meta', (), kwargs) 341 bases = (formset == FormSet and (FormSet,) or (formset, FormSet)) 342 return type(form.__name__ + 'FormSet', bases, {'Meta': meta_class}) 281 343 282 344 def all_valid(formsets): 283 345 """Returns true if every formset in formsets is valid.""" -
django/forms/metaclassing.py
=== modified file 'django/forms/metaclassing.py'
11 11 def create_meta(cls, attrs): 12 12 cls._meta = cls._options(getattr(cls, 'Meta', None)) 13 13 for name, attr in cls._meta.__dict__.items(): 14 if name not in ('fieldsets', 'fields', 'exclude' ):14 if name not in ('fieldsets', 'fields', 'exclude', 'inlines', 'base_form'): 15 15 setattr(cls, name, attr) 16 16 17 17 def create_declared_fields(cls, attrs): … … 60 60 if cls._meta.fieldsets: 61 61 names = [] 62 62 for fieldset in cls._meta.fieldsets: 63 names.extend(fieldset['fields']) 63 if isinstance(fieldset, dict): 64 names.extend(fieldset['fields']) 64 65 elif cls._meta.fields: 65 66 names = cls._meta.fields 66 67 elif cls._meta.exclude: … … 72 73 def create_media(cls, attrs): 73 74 if not 'media' in attrs: 74 75 cls.media = media_property(cls) 76 77 def create_fieldsets_if_inlines_exist(cls, attrs): 78 if cls._meta.inlines is not None: 79 if cls._meta.fieldsets is not None: 80 raise ImproperlyConfigured("%s cannot have more than one option from fieldsets and inlines." % cls.__name__) 81 cls._meta.fieldsets = [{'fields': cls.base_fields.keys()}] + list(cls._meta.inlines) 82 83 def create_form_if_not_exists(cls, attrs): 84 if not cls.form: 85 form_attrs = { 86 'Meta': type('Meta', (), cls._meta.__dict__), 87 } 88 for name, possible_field in attrs.items(): 89 if isinstance(possible_field, Field): 90 form_attrs[name] = possible_field 91 delattr(cls, name) 92 cls.form = type(cls.__name__ + 'Form', (cls._meta.base_form,), form_attrs) 93 94 def check_no_fieldsets_in_inner_form(cls, attrs): 95 if cls.form._meta.fieldsets: 96 raise ImproperlyConfigured("%s cannot have form with fieldsets." % cls.__name__) 97 98 def add_fk_attribute_and_remove_fk_from_base_fields(cls, attrs): 99 # If models are not set, this class would not be used directly. 100 if not (cls.parent_model and cls.model): 101 return 102 # Try to discover what the foreign key from model to parent_model is. 103 fks_to_parent = [] 104 for field in cls.model._meta.fields: 105 # Exceptions are neccessary here - ForeignKey cannot be imported for circular dependancy. 106 try: 107 if field.rel.to == cls.parent_model or field.rel.to in cls.parent_model._meta.parents.keys(): 108 fks_to_parent.append(field) 109 except AttributeError: 110 pass 111 if cls.fk_name: 112 fks_to_parent = [fk for fk in fks_to_parent if fk.name == cls.fk_name] 113 if len(fks_to_parent) == 0: 114 raise ImproperlyConfigured("%s has no ForeignKey with name %s to %s." % 115 (cls.model, cls.fk_name, cls.parent_model)) 116 elif len(fks_to_parent) > 1: 117 raise ImproperlyConfigured("%s has more than one ForeignKey with name %s to %s." % 118 (cls.model, cls.fk_name, cls.parent_model)) 119 else: 120 if len(fks_to_parent) == 0: 121 raise ImproperlyConfigured("%s has no ForeignKey to %s." % 122 (cls.model, cls.parent_model)) 123 if len(fks_to_parent) > 1: 124 raise ImproperlyConfigured("%s has more than one ForeignKey to %s." % 125 (cls.model, cls.parent_model)) 126 cls.fk = fks_to_parent[0] 127 # Try to remove the foreign key from base_fields to keep it transparent to the form. 128 try: 129 del cls.form.base_fields[cls.fk.name] 130 except KeyError: 131 pass -
django/forms/models.py
=== modified file 'django/forms/models.py'
11 11 12 12 from util import ValidationError 13 13 from forms import FormOptions, FormMetaclass, BaseForm 14 from formsets import FormSetOptions, FormSetMetaclass 14 15 from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES 15 16 from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput 16 from formsets import BaseFormSet, formset_factory,DELETION_FIELD_NAME17 from formsets import BaseFormSet, DELETION_FIELD_NAME 17 18 import metaclassing 18 19 19 20 __all__ = ( 20 21 'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model', 21 22 'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields', 22 'ModelChoiceField', 'ModelMultipleChoiceField', 23 'ModelChoiceField', 'ModelMultipleChoiceField', 'BaseModelForm', 24 'ModelForm', 'BaseInlineFormSet', 'InlineFormSet', 'modelform_factory', 25 'modelformset_factory', 'inlineformset_factory', 23 26 ) 24 27 25 28 def save_instance(form, instance, fields=None, fail_message='saved', … … 219 222 metaclassing.create_declared_fields(new_class, attrs) 220 223 metaclassing.create_base_fields_pool_from_model_fields_and_declared_fields(new_class, attrs) 221 224 metaclassing.create_base_fields_from_base_fields_pool(new_class, attrs) 225 metaclassing.create_fieldsets_if_inlines_exist(new_class, attrs) 222 226 metaclassing.create_media(new_class, attrs) 223 227 return new_class 224 228 … … 240 244 super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data, 241 245 error_class, label_suffix, empty_permitted) 242 246 247 def _construct_inlines(self): 248 # this class can create inlines which are subclass of BaseInlineFormSet 249 self.inlines = [] 250 if self.has_fieldsets(): 251 for fieldset in self._meta.fieldsets: 252 if not isinstance(fieldset, dict): 253 if not issubclass(fieldset, BaseInlineFormSet): 254 raise ValueError('%s cannot create instance of %s.' % (self.__class__.__name__, fieldset.__name__)) 255 self.inlines.append(fieldset(self.data, self.files, self.instance)) 256 243 257 def save(self, commit=True): 244 258 """ 245 259 Saves this ``form``'s cleaned_data into model instance … … 252 266 fail_message = 'created' 253 267 else: 254 268 fail_message = 'changed' 255 return save_instance(self, self.instance, self.base_fields.keys(), fail_message, commit) 269 self.saved_instance = save_instance(self, self.instance, self.base_fields.keys(), fail_message, commit) 270 self.saved_inline_instances = [inline.save(commit) for inline in self.inlines] 271 return self.saved_instance 256 272 257 273 class ModelForm(BaseModelForm): 258 274 __metaclass__ = ModelFormMetaclass 259 275 _options = ModelFormOptions 260 276 261 277 def modelform_factory(model, form=ModelForm, fields=None, exclude=None, 262 formfield_callback=lambda f: f.formfield()): 263 # HACK: we should be able to construct a ModelForm without creating 264 # and passing in a temporary inner class 265 class Meta: 266 pass 267 setattr(Meta, 'model', model) 268 setattr(Meta, 'fields', fields) 269 setattr(Meta, 'exclude', exclude) 270 class_name = model.__name__ + 'Form' 271 return ModelFormMetaclass(class_name, (form,), {'Meta': Meta, 272 'formfield_callback': formfield_callback}) 278 formfield_callback=lambda f: f.formfield(), **kwargs): 279 kwargs.update(locals()) 280 meta_class = type('Meta', (), kwargs) 281 bases = (form == ModelForm and (ModelForm,) or (form, ModelForm)) 282 return ModelFormMetaclass(model.__name__ + 'Form', bases, 283 {'Meta': meta_class, 'formfield_callback': formfield_callback}) 273 284 274 285 275 286 # ModelFormSets ############################################################## 276 287 288 class ModelFormSetOptions(FormSetOptions, ModelFormOptions): 289 def __init__(self, options=None): 290 super(ModelFormSetOptions, self).__init__(options) 291 # options changed compared to superclass 292 self.base_form = getattr(options, 'base_form', ModelForm) 293 277 294 class BaseModelFormSet(BaseFormSet): 278 295 """ 279 296 A ``FormSet`` for editing a queryset and/or adding new objects to it. … … 362 379 form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput) 363 380 super(BaseModelFormSet, self).add_fields(form, index) 364 381 382 class ModelFormSet(BaseModelFormSet): 383 __metaclass__ = FormSetMetaclass # no changes are needed 384 _options = ModelFormSetOptions 385 365 386 def modelformset_factory(model, form=ModelForm, formfield_callback=lambda f: f.formfield(), 366 formset= BaseModelFormSet,387 formset=ModelFormSet, 367 388 extra=1, can_delete=False, can_order=False, 368 max_num=0, fields=None, exclude=None ):389 max_num=0, fields=None, exclude=None, **kwargs): 369 390 """ 370 391 Returns a FormSet class for the given Django model class. 371 392 """ 372 form = modelform_factory(model, form=form, fields=fields, exclude=exclude, 373 formfield_callback=formfield_callback) 374 FormSet = formset_factory(form, formset, extra=extra, max_num=max_num, 375 can_order=can_order, can_delete=can_delete) 376 FormSet.model = model 377 return FormSet 393 kwargs.update(locals()) 394 kwargs['form'] = modelform_factory(**kwargs) 395 meta_class = type('Meta', (), kwargs) 396 bases = (formset == ModelFormSet and (ModelFormSet,) or (formset, ModelFormSet)) 397 return type(form.__name__ + 'FormSet', bases, {'Meta': meta_class}) 378 398 379 399 380 400 # InlineFormSets ############################################################# 381 401 382 class BaseInlineFormset(BaseModelFormSet): 402 class InlineFormSetOptions(ModelFormSetOptions): 403 def __init__(self, options=None): 404 super(InlineFormSetOptions, self).__init__(options) 405 self.parent_model = getattr(options, 'parent_model', None) 406 self.fk_name = getattr(options, 'fk_name', None) 407 # options changed compared to superclass 408 self.can_delete = getattr(options, 'can_delete', True) 409 self.extra = getattr(options, 'extra', 3) 410 411 class InlineFormSetMetaclass(FormSetMetaclass): 412 def __new__(cls, name, bases, attrs): 413 new_class = type.__new__(cls, name, bases, attrs) 414 metaclassing.create_meta(new_class, attrs) 415 metaclassing.create_form_if_not_exists(new_class, attrs) 416 metaclassing.check_no_fieldsets_in_inner_form(new_class, attrs) 417 metaclassing.add_fk_attribute_and_remove_fk_from_base_fields(new_class, attrs) 418 return new_class 419 420 class BaseInlineFormSet(BaseModelFormSet): 383 421 """A formset for child objects related to a parent.""" 384 422 def __init__(self, data=None, files=None, instance=None, 385 423 save_as_new=False, prefix=None): … … 388 426 self.save_as_new = save_as_new 389 427 # is there a better way to get the object descriptor? 390 428 self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name() 391 super(BaseInlineForm set, self).__init__(data, files, prefix=prefix or self.rel_name)429 super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix or self.rel_name) 392 430 393 431 def _construct_forms(self): 394 432 if self.save_as_new: 395 433 self._total_form_count = self._initial_form_count 396 434 self._initial_form_count = 0 397 super(BaseInlineForm set, self)._construct_forms()435 super(BaseInlineFormSet, self)._construct_forms() 398 436 399 437 def get_queryset(self): 400 438 """ … … 409 447 new_obj = self.model(**kwargs) 410 448 return save_instance(form, new_obj, commit=commit) 411 449 412 def _get_foreign_key(parent_model, model, fk_name=None): 413 """ 414 Finds and returns the ForeignKey from model to parent if there is one. 415 If fk_name is provided, assume it is the name of the ForeignKey field. 416 """ 417 # avoid circular import 418 from django.db.models import ForeignKey 419 opts = model._meta 420 if fk_name: 421 fks_to_parent = [f for f in opts.fields if f.name == fk_name] 422 if len(fks_to_parent) == 1: 423 fk = fks_to_parent[0] 424 if not isinstance(fk, ForeignKey) or \ 425 (fk.rel.to != parent_model and 426 fk.rel.to not in parent_model._meta.parents.keys()): 427 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model)) 428 elif len(fks_to_parent) == 0: 429 raise Exception("%s has no field named '%s'" % (model, fk_name)) 430 else: 431 # Try to discover what the ForeignKey from model to parent_model is 432 fks_to_parent = [ 433 f for f in opts.fields 434 if isinstance(f, ForeignKey) 435 and (f.rel.to == parent_model 436 or f.rel.to in parent_model._meta.parents.keys()) 437 ] 438 if len(fks_to_parent) == 1: 439 fk = fks_to_parent[0] 440 elif len(fks_to_parent) == 0: 441 raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) 442 else: 443 raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) 444 return fk 445 450 class InlineFormSet(BaseInlineFormSet): 451 __metaclass__ = InlineFormSetMetaclass 452 _options = InlineFormSetOptions 446 453 447 454 def inlineformset_factory(parent_model, model, form=ModelForm, 448 formset= BaseInlineFormset, fk_name=None,455 formset=InlineFormSet, fk_name=None, 449 456 fields=None, exclude=None, 450 457 extra=3, can_order=False, can_delete=True, max_num=0, 451 formfield_callback=lambda f: f.formfield() ):458 formfield_callback=lambda f: f.formfield(), **kwargs): 452 459 """ 453 460 Returns an ``InlineFormset`` for the given kwargs. 454 461 455 462 You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` 456 463 to ``parent_model``. 457 464 """ 458 fk = _get_foreign_key(parent_model, model, fk_name=fk_name) 459 # let the formset handle object deletion by default 460 461 if exclude is not None: 462 exclude.append(fk.name) 463 else: 464 exclude = [fk.name] 465 FormSet = modelformset_factory(model, form=form, 466 formfield_callback=formfield_callback, 467 formset=formset, 468 extra=extra, can_delete=can_delete, can_order=can_order, 469 fields=fields, exclude=exclude, max_num=max_num) 470 FormSet.fk = fk 471 return FormSet 465 kwargs.update(locals()) 466 kwargs['form'] = modelform_factory(**kwargs) 467 meta_class = type('Meta', (), kwargs) 468 bases = (formset == InlineFormSet and (InlineFormSet,) or (form, InlineFormSet)) 469 return type(form.__name__ + 'FormSet', bases, {'Meta': meta_class}) 472 470 473 471 474 472 # Fields ##################################################################### -
tests/regressiontests/inline_formsets/models.py
=== modified file 'tests/regressiontests/inline_formsets/models.py'
24 24 >>> ifs = inlineformset_factory(Parent, Child) 25 25 Traceback (most recent call last): 26 26 ... 27 Exception: <class 'regressiontests.inline_formsets.models.Child'> has more than 1 ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'> 27 ImproperlyConfigured: <class 'regressiontests.inline_formsets.models.Child'> has more than one ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>. 28 28 29 29 30 30 These two should both work without a problem. … … 39 39 >>> ifs = inlineformset_factory(Parent, Child, fk_name='school') 40 40 Traceback (most recent call last): 41 41 ... 42 Exception: fk_name 'school' is not a ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'> 42 ImproperlyConfigured: <class 'regressiontests.inline_formsets.models.Child'> has no ForeignKey with name school to <class 'regressiontests.inline_formsets.models.Parent'>. 43 43 44 44 45 45 If the field specified in fk_name is not a ForeignKey, we should get an … … 48 48 >>> ifs = inlineformset_factory(Parent, Child, fk_name='test') 49 49 Traceback (most recent call last): 50 50 ... 51 Exception: <class 'regressiontests.inline_formsets.models.Child'> has no field named 'test' 51 ImproperlyConfigured: <class 'regressiontests.inline_formsets.models.Child'> has no ForeignKey with name test to <class 'regressiontests.inline_formsets.models.Parent'>. 52 52 53 53 54 54 """