=== modified file 'django/forms/forms.py'
|
|
|
28 | 28 | self.fieldsets = getattr(options, 'fieldsets', None) |
29 | 29 | self.fields = getattr(options, 'fields', None) |
30 | 30 | self.exclude = getattr(options, 'exclude', None) |
| 31 | self.inlines = getattr(options, 'inlines', None) |
31 | 32 | |
32 | 33 | def create_declared_fields(cls, attrs): |
33 | 34 | """ |
… |
… |
|
72 | 73 | if cls._meta.fieldsets: |
73 | 74 | names = [] |
74 | 75 | for fieldset in cls._meta.fieldsets: |
75 | | names.extend(fieldset['fields']) |
| 76 | if isinstance(fieldset, dict): |
| 77 | names.extend(fieldset['fields']) |
76 | 78 | elif cls._meta.fields: |
77 | 79 | names = cls._meta.fields |
78 | 80 | elif cls._meta.exclude: |
… |
… |
|
81 | 83 | names = cls.base_fields_pool.keys() |
82 | 84 | cls.base_fields = SortedDict([(name, cls.base_fields_pool[name]) for name in names]) |
83 | 85 | |
| 86 | def create_fieldsets_if_inlines_exist(cls, attrs): |
| 87 | if cls._meta.inlines is not None: |
| 88 | if cls._meta.fieldsets is not None: |
| 89 | raise ImproperlyConfigured("%s cannot have more than one option from fieldsets and inlines." % cls.__name__) |
| 90 | cls._meta.fieldsets = [{'fields': cls.base_fields.keys()}] + list(cls._meta.inlines) |
| 91 | |
84 | 92 | class FormMetaclass(type): |
85 | 93 | """ |
86 | 94 | Metaclass that converts Field attributes to a dictionary called |
… |
… |
|
94 | 102 | create_declared_fields(new_class, attrs) |
95 | 103 | create_base_fields_pool_from_declared_fields(new_class, attrs) |
96 | 104 | create_base_fields_from_base_fields_pool(new_class, attrs) |
| 105 | create_fieldsets_if_inlines_exist(new_class, attrs) |
97 | 106 | if 'media' not in attrs: |
98 | 107 | new_class.media = media_property(new_class) |
99 | 108 | return new_class |
… |
… |
|
115 | 124 | self.error_class = error_class |
116 | 125 | self.label_suffix = label_suffix |
117 | 126 | self.empty_permitted = empty_permitted |
118 | | self._errors = None # Stores the errors after clean() has been called. |
| 127 | self._is_valid = None # Stores validation state after full_clean() has been called. |
119 | 128 | self._changed_data = None |
120 | 129 | |
121 | 130 | # The base_fields class attribute is the *class-wide* definition of |
… |
… |
|
124 | 133 | # Instances should always modify self.fields; they should not modify |
125 | 134 | # self.base_fields. |
126 | 135 | self.fields = deepcopy(self.base_fields) |
| 136 | self._construct_inlines() |
| 137 | |
| 138 | def _construct_inlines(self): |
| 139 | # This class cannot create any inlines. |
| 140 | self.inlines = [] |
| 141 | if self.has_fieldsets(): |
| 142 | for fieldset in self._meta.fieldsets: |
| 143 | if not isinstance(fieldset, dict): |
| 144 | raise ValueError('%s cannot create instance of %s.' % (self.__class__.__name__, fieldset.__name__)) |
127 | 145 | |
128 | 146 | def __unicode__(self): |
129 | 147 | return self.as_table() |
… |
… |
|
145 | 163 | |
146 | 164 | def fieldsets(self): |
147 | 165 | if self.has_fieldsets(): |
| 166 | fieldset_counter, formset_counter = 0, 0 |
148 | 167 | for fieldset in self._meta.fieldsets: |
149 | | yield { |
150 | | 'attrs': fieldset.get('attrs', {}), |
151 | | 'legend': fieldset.get('legend', u''), |
152 | | 'fields': [self[name] for name in fieldset['fields']], |
153 | | } |
154 | | |
| 168 | if isinstance(fieldset, dict): |
| 169 | yield { |
| 170 | 'order': fieldset_counter, |
| 171 | 'attrs': fieldset.get('attrs', {}), |
| 172 | 'legend': fieldset.get('legend', u''), |
| 173 | 'fields': [self[name] for name in fieldset['fields']], |
| 174 | } |
| 175 | fieldset_counter += 1 |
| 176 | else: |
| 177 | yield { |
| 178 | 'order': formset_counter, |
| 179 | 'formset': self.inlines[formset_counter] |
| 180 | } |
| 181 | formset_counter += 1 |
| 182 | |
155 | 183 | def _get_errors(self): |
156 | 184 | "Returns an ErrorDict for the data provided for the form" |
157 | | if self._errors is None: |
| 185 | if self._is_valid is None: |
158 | 186 | self.full_clean() |
159 | 187 | return self._errors |
160 | 188 | errors = property(_get_errors) |
… |
… |
|
164 | 192 | Returns True if the form has no errors. Otherwise, False. If errors are |
165 | 193 | being ignored, returns False. |
166 | 194 | """ |
167 | | return self.is_bound and not bool(self.errors) |
| 195 | if self._is_valid is None: |
| 196 | self.full_clean() |
| 197 | return self._is_valid |
168 | 198 | |
169 | 199 | def add_prefix(self, field_name): |
170 | 200 | """ |
… |
… |
|
254 | 284 | |
255 | 285 | def full_clean(self): |
256 | 286 | """ |
257 | | Cleans all of self.data and populates self._errors and |
| 287 | Cleans all of self.data and populates self._is_valid, self._errors and |
258 | 288 | self.cleaned_data. |
259 | 289 | """ |
| 290 | self._is_valid = True # Assume the form is valid until proven otherwise. |
260 | 291 | self._errors = ErrorDict() |
261 | 292 | if not self.is_bound: # Stop further processing. |
| 293 | self._is_valid = False |
262 | 294 | return |
263 | 295 | self.cleaned_data = {} |
264 | 296 | # If the form is permitted to be empty, and none of the form data has |
… |
… |
|
284 | 316 | self._errors[name] = e.messages |
285 | 317 | if name in self.cleaned_data: |
286 | 318 | del self.cleaned_data[name] |
| 319 | self._is_valid = False |
287 | 320 | try: |
288 | 321 | self.cleaned_data = self.clean() |
289 | 322 | except ValidationError, e: |
290 | 323 | self._errors[NON_FIELD_ERRORS] = e.messages |
291 | | if self._errors: |
| 324 | self._is_valid = False |
| 325 | for inline in self.inlines: |
| 326 | inline.full_clean() |
| 327 | if not inline.is_valid(): |
| 328 | self._is_valid = False |
| 329 | if not self._is_valid: |
292 | 330 | delattr(self, 'cleaned_data') |
| 331 | for inline in self.inlines: |
| 332 | inline._is_valid = False |
293 | 333 | |
294 | 334 | def clean(self): |
295 | 335 | """ |
… |
… |
|
337 | 377 | media = Media() |
338 | 378 | for field in self.fields.values(): |
339 | 379 | media = media + field.widget.media |
| 380 | for inline in self.inlines: |
| 381 | media = media + inline.media |
340 | 382 | return media |
341 | 383 | media = property(_get_media) |
342 | 384 | |
… |
… |
|
348 | 390 | for field in self.fields.values(): |
349 | 391 | if field.widget.needs_multipart_form: |
350 | 392 | return True |
| 393 | for inline in self.inlines: |
| 394 | if inline.is_multipart(): |
| 395 | return True |
351 | 396 | return False |
352 | 397 | |
353 | 398 | class Form(BaseForm): |
=== modified file 'django/forms/formsets.py'
|
|
|
38 | 38 | self.files = files |
39 | 39 | self.initial = initial |
40 | 40 | self.error_class = error_class |
41 | | self._errors = None |
42 | | self._non_form_errors = None |
| 41 | self._is_valid = None # Stores validation state after full_clean() has been called. |
43 | 42 | # initialization is different depending on whether we recieved data, initial, or nothing |
44 | 43 | if data or files: |
45 | 44 | self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix) |
… |
… |
|
182 | 181 | form -- i.e., from formset.clean(). Returns an empty ErrorList if there |
183 | 182 | are none. |
184 | 183 | """ |
185 | | if self._non_form_errors is not None: |
186 | | return self._non_form_errors |
187 | | return self.error_class() |
| 184 | if self._is_valid is None: |
| 185 | self.full_clean() |
| 186 | return self._non_form_errors |
188 | 187 | |
189 | 188 | def _get_errors(self): |
190 | 189 | """ |
191 | 190 | Returns a list of form.errors for every form in self.forms. |
192 | 191 | """ |
193 | | if self._errors is None: |
| 192 | if self._is_valid is None: |
194 | 193 | self.full_clean() |
195 | 194 | return self._errors |
196 | 195 | errors = property(_get_errors) |
… |
… |
|
199 | 198 | """ |
200 | 199 | Returns True if form.errors is empty for every form in self.forms. |
201 | 200 | """ |
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()) |
| 201 | if self._is_valid is None: |
| 202 | self.full_clean() |
| 203 | return self._is_valid |
211 | 204 | |
212 | 205 | def full_clean(self): |
213 | 206 | """ |
214 | 207 | Cleans all of self.data and populates self._errors. |
215 | 208 | """ |
| 209 | self._is_valid = True # Assume the form is valid until proven otherwise. |
216 | 210 | self._errors = [] |
| 211 | self._non_form_errors = self.error_class() |
217 | 212 | if not self.is_bound: # Stop further processing. |
| 213 | self._is_valid = False |
218 | 214 | return |
219 | 215 | for i in range(0, self._total_form_count): |
220 | 216 | form = self.forms[i] |
221 | 217 | self._errors.append(form.errors) |
| 218 | if form.errors: |
| 219 | self._is_valid = False |
222 | 220 | # Give self.clean() a chance to do cross-form validation. |
223 | 221 | try: |
224 | 222 | self.clean() |
225 | 223 | except ValidationError, e: |
226 | | self._non_form_errors = e.messages |
| 224 | self._non_form_errors = self.error_class(e.messages) |
| 225 | self._is_valid = False |
227 | 226 | |
228 | 227 | def clean(self): |
229 | 228 | """ |
=== modified file 'django/forms/models.py'
|
|
|
11 | 11 | from util import ValidationError, ErrorList |
12 | 12 | from forms import FormOptions, BaseForm, create_declared_fields |
13 | 13 | from forms import create_base_fields_from_base_fields_pool |
| 14 | from forms import create_fieldsets_if_inlines_exist |
14 | 15 | from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES |
15 | 16 | from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput |
16 | 17 | from widgets import media_property |
… |
… |
|
60 | 61 | continue |
61 | 62 | if f.name in cleaned_data: |
62 | 63 | f.save_form_data(instance, cleaned_data[f.name]) |
| 64 | if not commit: |
| 65 | for inline in form.inlines: |
| 66 | inline.save_m2m() |
63 | 67 | if commit: |
64 | 68 | # If we are committing, save the instance and the m2m data immediately. |
65 | 69 | instance.save() |
… |
… |
|
209 | 213 | create_declared_fields(new_class, attrs) |
210 | 214 | create_base_fields_pool_from_model_fields_and_declared_fields(new_class, attrs) |
211 | 215 | create_base_fields_from_base_fields_pool(new_class, attrs) |
| 216 | create_fieldsets_if_inlines_exist(new_class, attrs) |
212 | 217 | if 'media' not in attrs: |
213 | 218 | new_class.media = media_property(new_class) |
214 | 219 | return new_class |
… |
… |
|
233 | 238 | self.validate_unique() |
234 | 239 | return self.cleaned_data |
235 | 240 | |
| 241 | def _construct_inlines(self): |
| 242 | # This class can create inlines which are subclass of BaseInlineFormSet. |
| 243 | self.inlines = [] |
| 244 | if self.has_fieldsets(): |
| 245 | for fieldset in self._meta.fieldsets: |
| 246 | if not isinstance(fieldset, dict): |
| 247 | if not issubclass(fieldset, BaseInlineFormSet): |
| 248 | raise ValueError('%s cannot create instance of %s.' % (self.__class__.__name__, fieldset.__name__)) |
| 249 | self.inlines.append(fieldset(self.data, self.files, self.instance)) |
| 250 | |
236 | 251 | def validate_unique(self): |
237 | 252 | from django.db.models.fields import FieldDoesNotExist |
238 | 253 | |
… |
… |
|
295 | 310 | {'model_name': unicode(model_name), |
296 | 311 | 'field_label': unicode(field_label)} |
297 | 312 | ]) |
| 313 | self._is_valid = False |
298 | 314 | # unique_together |
299 | 315 | else: |
300 | 316 | field_labels = [self.fields[field_name].label for field_name in unique_check] |
… |
… |
|
304 | 320 | {'model_name': unicode(model_name), |
305 | 321 | 'field_label': unicode(field_labels)} |
306 | 322 | ) |
307 | | |
| 323 | self._is_valid = False |
| 324 | |
308 | 325 | # Mark these fields as needing to be removed from cleaned data |
309 | 326 | # later. |
310 | 327 | for field_name in unique_check: |
… |
… |
|
329 | 346 | fail_message = 'created' |
330 | 347 | else: |
331 | 348 | fail_message = 'changed' |
332 | | return save_instance(self, self.instance, self.fields.keys(), fail_message, commit) |
| 349 | self.saved_instance = save_instance(self, self.instance, self.fields.keys(), fail_message, commit) |
| 350 | self.saved_inline_instances = [inline.save(commit) for inline in self.inlines] |
| 351 | return self.saved_instance |
333 | 352 | |
334 | 353 | class ModelForm(BaseModelForm): |
335 | 354 | __metaclass__ = ModelFormMetaclass |
336 | 355 | |
337 | 356 | def modelform_factory(model, form=ModelForm, fields=None, exclude=None, fieldsets=None, |
338 | | formfield_callback=lambda f: f.formfield()): |
| 357 | inlines=None, formfield_callback=lambda f: f.formfield()): |
339 | 358 | # HACK: we should be able to construct a ModelForm without creating |
340 | 359 | # and passing in a temporary inner class |
341 | 360 | class Meta: |
… |
… |
|
344 | 363 | setattr(Meta, 'fieldsets', fieldsets) |
345 | 364 | setattr(Meta, 'fields', fields) |
346 | 365 | setattr(Meta, 'exclude', exclude) |
| 366 | setattr(Meta, 'inlines', inlines) |
347 | 367 | class_name = model.__name__ + 'Form' |
348 | 368 | return ModelFormMetaclass(class_name, (form,), {'Meta': Meta, |
349 | 369 | 'formfield_callback': formfield_callback}) |
… |
… |
|
451 | 471 | def modelformset_factory(model, form=ModelForm, formfield_callback=lambda f: f.formfield(), |
452 | 472 | formset=BaseModelFormSet, |
453 | 473 | extra=1, can_delete=False, can_order=False, |
454 | | max_num=0, fields=None, exclude=None, fieldsets=None): |
| 474 | max_num=0, fields=None, exclude=None, fieldsets=None, inlines=None): |
455 | 475 | """ |
456 | 476 | Returns a FormSet class for the given Django model class. |
457 | 477 | """ |
458 | 478 | form = modelform_factory(model, form=form, fields=fields, exclude=exclude, |
459 | | fieldsets=fieldsets, |
| 479 | fieldsets=fieldsets, inlines=inlines, |
460 | 480 | formfield_callback=formfield_callback) |
461 | 481 | FormSet = formset_factory(form, formset, extra=extra, max_num=max_num, |
462 | 482 | can_order=can_order, can_delete=can_delete) |
… |
… |
|
546 | 566 | |
547 | 567 | def inlineformset_factory(parent_model, model, form=ModelForm, |
548 | 568 | formset=BaseInlineFormSet, fk_name=None, |
549 | | fields=None, exclude=None, fieldsets=None, |
| 569 | fields=None, exclude=None, fieldsets=None, inlines=None, |
550 | 570 | extra=3, can_order=False, can_delete=True, max_num=0, |
551 | 571 | formfield_callback=lambda f: f.formfield()): |
552 | 572 | """ |
… |
… |
|
574 | 594 | 'fieldsets': fieldsets, |
575 | 595 | 'fields': fields, |
576 | 596 | 'exclude': exclude, |
| 597 | 'inlines': inlines, |
577 | 598 | 'max_num': max_num, |
578 | 599 | } |
579 | 600 | FormSet = modelformset_factory(model, **kwargs) |