Ticket #6632: 02-forms-inlines.2.diff
File 02-forms-inlines.2.diff, 35.9 KB (added by , 16 years ago) |
---|
-
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 django.utils.translation import ugettext as _ 5 5 from fields import IntegerField, BooleanField 6 6 from widgets import Media, HiddenInput 7 from util import ErrorList, ValidationError 7 from util import ValidationError 8 import metaclassing 8 9 9 __all__ = ('BaseFormSet', ' all_valid')10 __all__ = ('BaseFormSet', 'FormSet', 'formset_factory', 'all_valid') 10 11 11 12 # special field names 12 13 TOTAL_FORM_COUNT = 'TOTAL_FORMS' … … 25 26 self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput) 26 27 super(ManagementForm, self).__init__(*args, **kwargs) 27 28 29 class FormSetOptions(FormOptions): 30 def __init__(self, options=None): 31 super(FormSetOptions, self).__init__(options) 32 # form 33 self.form = getattr(options, 'form', None) 34 self.base_form = getattr(options, 'base_form', Form) 35 # other options 36 self.can_delete = getattr(options, 'can_delete', False) 37 self.can_order = getattr(options, 'can_order', False) 38 self.extra = getattr(options, 'extra', 1) 39 self.fieldset_attrs = getattr(options, 'fieldset_attrs', None) 40 self.fieldset_legend = getattr(options, 'fieldset_legend', None) 41 self.max_num = getattr(options, 'max_num', 0) 42 self.output_type = getattr(options, 'output_type', 'tr') 43 # self.output_type = getattr(options, 'output_type', 'original_table') # backward-compatible 44 45 class FormSetMetaclass(type): 46 def __new__(cls, name, bases, attrs): 47 new_class = type.__new__(cls, name, bases, attrs) 48 metaclassing.create_meta(new_class, attrs) 49 metaclassing.create_form_if_not_exists(new_class, attrs) 50 metaclassing.check_no_fieldsets_in_inner_form(new_class, attrs) 51 return new_class 52 28 53 class BaseFormSet(StrAndUnicode): 29 54 """ 30 55 A collection of instances of the same Form class. 31 56 """ 32 57 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 33 initial=None, error_class= ErrorList):58 initial=None, error_class=None): 34 59 self.is_bound = data is not None or files is not None 35 60 self.prefix = prefix or 'form' 36 61 self.auto_id = auto_id 37 62 self.data = data 38 63 self.files = files 39 64 self.initial = initial 40 self.error_class = error_class41 self._errors = None42 self._ non_form_errors = None65 if error_class is not None: 66 self.error_class = error_class 67 self._is_valid = None # Stores validation state after full_clean() has been called. 43 68 # initialization is different depending on whether we recieved data, initial, or nothing 44 69 if data or files: 45 70 self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix) … … 67 92 self._construct_forms() 68 93 69 94 def __unicode__(self): 70 return self.as_table()95 return getattr(self, 'as_%s' % self.output_type)() 71 96 72 97 def _construct_forms(self): 73 98 # instantiate all the forms and put them in self.forms … … 182 207 form -- i.e., from formset.clean(). Returns an empty ErrorList if there 183 208 are none. 184 209 """ 185 if self._ non_form_errors is notNone:186 return self._non_form_errors187 return self. error_class()210 if self._is_valid is None: 211 self.full_clean() 212 return self._non_form_errors 188 213 189 214 def _get_errors(self): 190 215 """ 191 216 Returns a list of form.errors for every form in self.forms. 192 217 """ 193 if self._ errorsis None:218 if self._is_valid is None: 194 219 self.full_clean() 195 220 return self._errors 196 221 errors = property(_get_errors) … … 199 224 """ 200 225 Returns True if form.errors is empty for every form in self.forms. 201 226 """ 202 if not self.is_bound: 203 return False 204 # We loop over every form.errors here rather than short circuiting on the 205 # first failure to make sure validation gets triggered for every form. 206 forms_valid = True 207 for errors in self.errors: 208 if bool(errors): 209 forms_valid = False 210 return forms_valid and not bool(self.non_form_errors()) 227 if self._is_valid is None: 228 self.full_clean() 229 return self._is_valid 211 230 212 231 def full_clean(self): 213 232 """ 214 233 Cleans all of self.data and populates self._errors. 215 234 """ 235 self._is_valid = True # Assume the form is valid until proven otherwise. 216 236 self._errors = [] 237 self._non_form_errors = self.error_class() 217 238 if not self.is_bound: # Stop further processing. 239 self._is_valid = False 218 240 return 219 241 for i in range(0, self._total_form_count): 220 242 form = self.forms[i] 221 243 self._errors.append(form.errors) 244 if form.errors: 245 self._is_valid = False 222 246 # Give self.clean() a chance to do cross-form validation. 223 247 try: 224 248 self.clean() 225 249 except ValidationError, e: 226 self._non_form_errors = e.messages 250 self._non_form_errors = self.error_class(e.messages) 251 self._is_valid = False 227 252 228 253 def clean(self): 229 254 """ … … 265 290 media = property(_get_media) 266 291 267 292 def as_table(self): 293 "Returns this form rendered as HTML <tr>s." 294 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) 295 296 def as_ul(self): 297 "Returns this form rendered as HTML <li>s." 298 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) 299 300 def as_p(self): 301 "Returns this form rendered as HTML <p>s." 302 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) 303 304 def as_tr(self): 305 "Returns this form rendered as HTML <td>s." 306 output = [self.management_form.as_tr()] 307 if self.non_form_errors: 308 output.append(u'<tr><td colspan="%s">%s</td></tr>' % ( 309 len([bf for bf in self.forms[0] if not bf.is_hidden]), 310 self._html_output_non_form_errors(), 311 )) 312 if self.forms: 313 output.append(u'<tr>') 314 output.extend(u'<th>%s</th>' % bf.label for bf in self.forms[0] if not bf.is_hidden) 315 output.append(u'</tr>') 316 output.extend(form.as_tr() for form in self.forms) 317 return '\n'.join(output) 318 319 def _html_output_non_form_errors(self): 320 if self.non_form_errors: 321 return u'<div>%s</div>' % unicode(self.non_form_errors()) 322 else: 323 return u'' 324 325 def as_original_table(self): 268 326 "Returns this formset rendered as HTML <tr>s -- excluding the <table></table>." 269 327 # XXX: there is no semantic division between forms here, there 270 328 # probably should be. It might make sense to render each form as a … … 272 330 forms = u' '.join([form.as_table() for form in self.forms]) 273 331 return mark_safe(u'\n'.join([unicode(self.management_form), forms])) 274 332 275 def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, 276 can_delete=False, max_num=0): 333 class FormSet(BaseFormSet): 334 __metaclass__ = FormSetMetaclass 335 _options = FormSetOptions 336 337 def formset_factory(form, formset=FormSet, extra=1, can_order=False, 338 can_delete=False, max_num=0, **kwargs): 277 339 """Return a FormSet for the given form class.""" 278 attrs = {'form': form, 'extra': extra,279 'can_order': can_order, 'can_delete': can_delete,280 'max_num': max_num}281 return type(form.__name__ + 'FormSet', (formset,), attrs)340 kwargs.update(locals()) 341 meta_class = type('Meta', (), kwargs) 342 bases = (formset in (FormSet, BaseFormSet) and (FormSet,) or (formset, FormSet)) 343 return type(form.__name__ + 'FormSet', bases, {'Meta': meta_class}) 282 344 283 345 def all_valid(formsets): 284 346 """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.get_parent_list(): 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'
9 9 10 10 from util import ValidationError 11 11 from forms import FormOptions, FormMetaclass, BaseForm 12 from formsets import FormSetOptions, FormSetMetaclass 12 13 from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES 13 14 from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput 14 from formsets import BaseFormSet, formset_factory,DELETION_FIELD_NAME15 from formsets import BaseFormSet, DELETION_FIELD_NAME 15 16 import metaclassing 16 17 17 18 __all__ = ( 18 19 'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model', 19 20 'save_instance', 'form_for_fields', 'ModelChoiceField', 20 'ModelMultipleChoiceField', 21 'ModelMultipleChoiceField', 'BaseModelForm', 'ModelForm', 22 'BaseInlineFormSet', 'InlineFormSet', 'modelform_factory', 23 'modelformset_factory', 'inlineformset_factory', 21 24 ) 22 25 23 26 def save_instance(form, instance, fields=None, fail_message='saved', … … 160 163 metaclassing.create_declared_fields(new_class, attrs) 161 164 metaclassing.create_base_fields_pool_from_model_fields_and_declared_fields(new_class, attrs) 162 165 metaclassing.create_base_fields_from_base_fields_pool(new_class, attrs) 166 metaclassing.create_fieldsets_if_inlines_exist(new_class, attrs) 163 167 metaclassing.create_media(new_class, attrs) 164 168 return new_class 165 169 … … 181 185 super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data, 182 186 error_class, label_suffix, empty_permitted) 183 187 188 def _construct_inlines(self): 189 # this class can create inlines which are subclass of BaseInlineFormSet 190 self.inlines = [] 191 if self.has_fieldsets(): 192 for fieldset in self._meta.fieldsets: 193 if not isinstance(fieldset, dict): 194 if not issubclass(fieldset, BaseInlineFormSet): 195 raise ValueError('%s cannot create instance of %s.' % (self.__class__.__name__, fieldset.__name__)) 196 self.inlines.append(fieldset(self.data, self.files, self.instance)) 197 184 198 def save(self, commit=True): 185 199 """ 186 200 Saves this ``form``'s cleaned_data into model instance … … 193 207 fail_message = 'created' 194 208 else: 195 209 fail_message = 'changed' 196 return save_instance(self, self.instance, self.fields.keys(), fail_message, commit) 210 self.saved_instance = save_instance(self, self.instance, self.fields.keys(), fail_message, commit) 211 self.saved_inline_instances = [inline.save(commit) for inline in self.inlines] 212 return self.saved_instance 197 213 198 214 class ModelForm(BaseModelForm): 199 215 __metaclass__ = ModelFormMetaclass 200 216 _options = ModelFormOptions 201 217 202 218 def modelform_factory(model, form=ModelForm, fields=None, exclude=None, 203 formfield_callback=lambda f: f.formfield()): 204 # HACK: we should be able to construct a ModelForm without creating 205 # and passing in a temporary inner class 206 class Meta: 207 pass 208 setattr(Meta, 'model', model) 209 setattr(Meta, 'fields', fields) 210 setattr(Meta, 'exclude', exclude) 211 class_name = model.__name__ + 'Form' 212 return ModelFormMetaclass(class_name, (form,), {'Meta': Meta, 213 'formfield_callback': formfield_callback}) 219 formfield_callback=lambda f: f.formfield(), **kwargs): 220 kwargs.update(locals()) 221 meta_class = type('Meta', (), kwargs) 222 bases = (form in (ModelForm, BaseModelForm) and (ModelForm,) or (form, ModelForm)) 223 return ModelFormMetaclass(model.__name__ + 'Form', bases, 224 {'Meta': meta_class, 'formfield_callback': formfield_callback}) 214 225 215 226 216 227 # ModelFormSets ############################################################## 217 228 229 class ModelFormSetOptions(FormSetOptions, ModelFormOptions): 230 def __init__(self, options=None): 231 super(ModelFormSetOptions, self).__init__(options) 232 # options changed compared to superclass 233 self.base_form = getattr(options, 'base_form', ModelForm) 234 218 235 class BaseModelFormSet(BaseFormSet): 219 236 """ 220 237 A ``FormSet`` for editing a queryset and/or adding new objects to it. … … 303 320 form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput) 304 321 super(BaseModelFormSet, self).add_fields(form, index) 305 322 323 class ModelFormSet(BaseModelFormSet): 324 __metaclass__ = FormSetMetaclass # no changes are needed 325 _options = ModelFormSetOptions 326 306 327 def modelformset_factory(model, form=ModelForm, formfield_callback=lambda f: f.formfield(), 307 formset= BaseModelFormSet,328 formset=ModelFormSet, 308 329 extra=1, can_delete=False, can_order=False, 309 max_num=0, fields=None, exclude=None ):330 max_num=0, fields=None, exclude=None, **kwargs): 310 331 """ 311 332 Returns a FormSet class for the given Django model class. 312 333 """ 313 form = modelform_factory(model, form=form, fields=fields, exclude=exclude, 314 formfield_callback=formfield_callback) 315 FormSet = formset_factory(form, formset, extra=extra, max_num=max_num, 316 can_order=can_order, can_delete=can_delete) 317 FormSet.model = model 318 return FormSet 334 kwargs.update(locals()) 335 kwargs['form'] = modelform_factory(**kwargs) 336 meta_class = type('Meta', (), kwargs) 337 bases = (formset in (ModelFormSet, BaseModelFormSet) and (ModelFormSet,) or (formset, ModelFormSet)) 338 return type(form.__name__ + 'FormSet', bases, {'Meta': meta_class}) 319 339 320 340 321 341 # InlineFormSets ############################################################# 322 342 343 class InlineFormSetOptions(ModelFormSetOptions): 344 def __init__(self, options=None): 345 super(InlineFormSetOptions, self).__init__(options) 346 self.parent_model = getattr(options, 'parent_model', None) 347 self.fk_name = getattr(options, 'fk_name', None) 348 # options changed compared to superclass 349 self.can_delete = getattr(options, 'can_delete', True) 350 self.extra = getattr(options, 'extra', 3) 351 352 class InlineFormSetMetaclass(FormSetMetaclass): 353 def __new__(cls, name, bases, attrs): 354 new_class = type.__new__(cls, name, bases, attrs) 355 metaclassing.create_meta(new_class, attrs) 356 metaclassing.create_form_if_not_exists(new_class, attrs) 357 metaclassing.check_no_fieldsets_in_inner_form(new_class, attrs) 358 metaclassing.add_fk_attribute_and_remove_fk_from_base_fields(new_class, attrs) 359 return new_class 360 323 361 class BaseInlineFormSet(BaseModelFormSet): 324 362 """A formset for child objects related to a parent.""" 325 363 def __init__(self, data=None, files=None, instance=None, … … 350 388 new_obj = self.model(**kwargs) 351 389 return save_instance(form, new_obj, commit=commit) 352 390 353 def _get_foreign_key(parent_model, model, fk_name=None): 354 """ 355 Finds and returns the ForeignKey from model to parent if there is one. 356 If fk_name is provided, assume it is the name of the ForeignKey field. 357 """ 358 # avoid circular import 359 from django.db.models import ForeignKey 360 opts = model._meta 361 if fk_name: 362 fks_to_parent = [f for f in opts.fields if f.name == fk_name] 363 if len(fks_to_parent) == 1: 364 fk = fks_to_parent[0] 365 if not isinstance(fk, ForeignKey) or \ 366 (fk.rel.to != parent_model and 367 fk.rel.to not in parent_model._meta.get_parent_list()): 368 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model)) 369 elif len(fks_to_parent) == 0: 370 raise Exception("%s has no field named '%s'" % (model, fk_name)) 371 else: 372 # Try to discover what the ForeignKey from model to parent_model is 373 fks_to_parent = [ 374 f for f in opts.fields 375 if isinstance(f, ForeignKey) 376 and (f.rel.to == parent_model 377 or f.rel.to in parent_model._meta.get_parent_list()) 378 ] 379 if len(fks_to_parent) == 1: 380 fk = fks_to_parent[0] 381 elif len(fks_to_parent) == 0: 382 raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) 383 else: 384 raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) 385 return fk 386 391 class InlineFormSet(BaseInlineFormSet): 392 __metaclass__ = InlineFormSetMetaclass 393 _options = InlineFormSetOptions 387 394 388 395 def inlineformset_factory(parent_model, model, form=ModelForm, 389 formset= BaseInlineFormSet, fk_name=None,396 formset=InlineFormSet, fk_name=None, 390 397 fields=None, exclude=None, 391 398 extra=3, can_order=False, can_delete=True, max_num=0, 392 formfield_callback=lambda f: f.formfield() ):399 formfield_callback=lambda f: f.formfield(), **kwargs): 393 400 """ 394 401 Returns an ``InlineFormSet`` for the given kwargs. 395 402 396 403 You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` 397 404 to ``parent_model``. 398 405 """ 399 fk = _get_foreign_key(parent_model, model, fk_name=fk_name) 400 # let the formset handle object deletion by default 401 402 if exclude is not None: 403 exclude.append(fk.name) 404 else: 405 exclude = [fk.name] 406 FormSet = modelformset_factory(model, form=form, 407 formfield_callback=formfield_callback, 408 formset=formset, 409 extra=extra, can_delete=can_delete, can_order=can_order, 410 fields=fields, exclude=exclude, max_num=max_num) 411 FormSet.fk = fk 412 return FormSet 406 kwargs.update(locals()) 407 kwargs['form'] = modelform_factory(**kwargs) 408 meta_class = type('Meta', (), kwargs) 409 bases = (formset in (InlineFormSet, BaseInlineFormSet) and (InlineFormSet,) or (form, InlineFormSet)) 410 return type(form.__name__ + 'FormSet', bases, {'Meta': meta_class}) 413 411 414 412 415 413 # Fields ##################################################################### -
tests/modeltests/model_formsets/models.py
=== modified file 'tests/modeltests/model_formsets/models.py'
440 440 <p><label for="id_form-0-my_pk">My pk:</label> <input id="id_form-0-my_pk" type="text" name="form-0-my_pk" maxlength="10" /></p> 441 441 <p><label for="id_form-0-some_field">Some field:</label> <input id="id_form-0-some_field" type="text" name="form-0-some_field" maxlength="100" /></p> 442 442 443 # Foreign keys in parents ########################################444 445 >>> from django.forms.models import _get_foreign_key446 447 >>> type(_get_foreign_key(Restaurant, Owner))448 <class 'django.db.models.fields.related.ForeignKey'>449 >>> type(_get_foreign_key(MexicanRestaurant, Owner))450 <class 'django.db.models.fields.related.ForeignKey'>451 452 443 """} -
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 """