Ticket #6241: formset_refactor_5.diff
File formset_refactor_5.diff, 61.2 KB (added by , 17 years ago) |
---|
-
django/contrib/admin/options.py
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index c3289ea..70640c1 100644
a b class ModelAdmin(BaseModelAdmin): 342 342 fields = flatten_fieldsets(self.declared_fieldsets) 343 343 else: 344 344 fields = None 345 return forms. form_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)345 return forms.modelform_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield) 346 346 347 347 def form_change(self, request, obj): 348 348 """ … … class ModelAdmin(BaseModelAdmin): 352 352 fields = flatten_fieldsets(self.declared_fieldsets) 353 353 else: 354 354 fields = None 355 return forms. form_for_instance(obj, fields=fields, formfield_callback=self.formfield_for_dbfield)355 return forms.modelform_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield) 356 356 357 357 def save_add(self, request, model, form, formsets, post_url_continue): 358 358 """ … … class ModelAdmin(BaseModelAdmin): 492 492 # Object list will give 'Permission Denied', so go back to admin home 493 493 post_url = '../../../' 494 494 495 ModelForm = self.form_add(request)495 Form = self.form_add(request) 496 496 inline_formsets = [] 497 497 obj = self.model() 498 498 if request.method == 'POST': 499 form = ModelForm(request.POST, request.FILES)499 form = Form(request.POST, request.FILES) 500 500 for FormSet in self.formsets_add(request): 501 inline_formset = FormSet( obj, data=request.POST, files=request.FILES)501 inline_formset = FormSet(request.POST, request.FILES) 502 502 inline_formsets.append(inline_formset) 503 503 if all_valid(inline_formsets) and form.is_valid(): 504 504 return self.save_add(request, model, form, inline_formsets, '../%s/') 505 505 else: 506 form = ModelForm(initial=request.GET)506 form = Form(initial=request.GET) 507 507 for FormSet in self.formsets_add(request): 508 inline_formset = FormSet( obj)508 inline_formset = FormSet() 509 509 inline_formsets.append(inline_formset) 510 510 511 511 adminForm = AdminForm(form, list(self.fieldsets_add(request)), self.prepopulated_fields) … … class ModelAdmin(BaseModelAdmin): 552 552 if request.POST and request.POST.has_key("_saveasnew"): 553 553 return self.add_view(request, form_url='../../add/') 554 554 555 ModelForm = self.form_change(request, obj)555 Form = self.form_change(request, obj) 556 556 inline_formsets = [] 557 557 if request.method == 'POST': 558 form = ModelForm(request.POST, request.FILES)558 form = Form(request.POST, request.FILES, instance=obj) 559 559 for FormSet in self.formsets_change(request, obj): 560 inline_formset = FormSet( obj, request.POST, request.FILES)560 inline_formset = FormSet(request.POST, request.FILES, instance=obj) 561 561 inline_formsets.append(inline_formset) 562 562 563 563 if all_valid(inline_formsets) and form.is_valid(): 564 564 return self.save_change(request, model, form, inline_formsets) 565 565 else: 566 form = ModelForm()566 form = Form(instance=obj, initial=request.GET) 567 567 for FormSet in self.formsets_change(request, obj): 568 inline_formset = FormSet( obj)568 inline_formset = FormSet(instance=obj) 569 569 inline_formsets.append(inline_formset) 570 570 571 571 ## Populate the FormWrapper. … … class InlineModelAdmin(BaseModelAdmin): 755 755 def fieldsets_add(self, request): 756 756 if self.declared_fieldsets: 757 757 return self.declared_fieldsets 758 form = self.formset_add(request).form_class759 return [(None, {'fields': form .base_fields.keys()})]758 formset = self.formset_add(request) 759 return [(None, {'fields': formset.base_fields.keys()})] 760 760 761 761 def fieldsets_change(self, request, obj): 762 762 if self.declared_fieldsets: 763 763 return self.declared_fieldsets 764 form = self.formset_change(request, obj).form_class765 return [(None, {'fields': form .base_fields.keys()})]764 formset = self.formset_change(request, obj) 765 return [(None, {'fields': formset.base_fields.keys()})] 766 766 767 767 class StackedInline(InlineModelAdmin): 768 768 template = 'admin/edit_inline/stacked.html' … … class InlineAdminFormSet(object): 778 778 self.opts = inline 779 779 self.formset = formset 780 780 self.fieldsets = fieldsets 781 # place orderable and deletable here since _meta is inaccesible in the 782 # templates. 783 self.orderable = formset._meta.orderable 784 self.deletable = formset._meta.deletable 781 785 782 786 def __iter__(self): 783 787 for form, original in zip(self.formset.change_forms, self.formset.get_queryset()): … … class InlineAdminFormSet(object): 787 791 788 792 def fields(self): 789 793 for field_name in flatten_fieldsets(self.fieldsets): 790 yield self.formset. form_class.base_fields[field_name]794 yield self.formset.base_fields[field_name] 791 795 792 796 class InlineAdminForm(AdminForm): 793 797 """ -
django/contrib/admin/templates/admin/edit_inline/tabular.html
diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html index 16bb14d..c91bb0e 100644
a b 11 11 <th {% if forloop.first %}colspan="2"{% endif %}>{{ field.label|capfirst|escape }}</th> 12 12 {% endif %} 13 13 {% endfor %} 14 {% if inline_admin_formset. formset.deletable %}<th>{% trans "Delete" %}?</th>{% endif %}14 {% if inline_admin_formset.deletable %}<th>{% trans "Delete" %}?</th>{% endif %} 15 15 </tr></thead> 16 16 17 17 {% for inline_admin_form in inline_admin_formset %} … … 45 45 {% endfor %} 46 46 {% endfor %} 47 47 48 {% if inline_admin_formset. formset.deletable %}<td class="delete">{{ inline_admin_form.deletion_field.field }}</td>{% endif %}48 {% if inline_admin_formset.deletable %}<td class="delete">{{ inline_admin_form.deletion_field.field }}</td>{% endif %} 49 49 50 50 </tr> 51 51 -
django/newforms/formsets.py
diff --git a/django/newforms/formsets.py b/django/newforms/formsets.py index 56179a9..d9b34cf 100644
a b 1 from forms import Form 2 from fields import IntegerField, BooleanField 1 2 from warnings import warn 3 4 from django.utils.datastructures import SortedDict 5 from django.utils.translation import ugettext_lazy as _ 6 7 from forms import BaseForm, Form 8 from fields import Field, IntegerField, BooleanField 9 from options import FormSetOptions 3 10 from widgets import HiddenInput, Media 4 11 from util import ErrorList, ValidationError 5 12 6 __all__ = ('BaseFormSet', ' formset_for_form', 'all_valid')13 __all__ = ('BaseFormSet', 'FormSet', 'formset_for_form', 'all_valid') 7 14 8 15 # special field names 9 16 FORM_COUNT_FIELD_NAME = 'COUNT' … … class ManagementForm(Form): 20 27 self.base_fields[FORM_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput) 21 28 super(ManagementForm, self).__init__(*args, **kwargs) 22 29 30 class BaseFormSetMetaclass(type): 31 def __new__(cls, name, bases, attrs, **options): 32 fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] 33 fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) 34 35 # If this class is subclassing another FormSet, ad that FormSet's fields. 36 # Note that we loop over the bases in *reverse*. This is necessary in 37 # order to preserve the correct order of fields. 38 for base in bases[::-1]: 39 if hasattr(base, "base_fields"): 40 fields = base.base_fields.items() + fields 41 attrs["base_fields"] = SortedDict(fields) 42 43 opts = FormSetOptions(options and options or attrs.get("Meta", None)) 44 attrs["_meta"] = opts 45 46 return type.__new__(cls, name, bases, attrs) 47 23 48 class BaseFormSet(object): 24 49 """A collection of instances of the same Form class.""" 25 50 … … class BaseFormSet(object): 37 62 self.management_form = ManagementForm(data, files, auto_id=self.auto_id, prefix=self.prefix) 38 63 if self.management_form.is_valid(): 39 64 self.total_forms = self.management_form.cleaned_data[FORM_COUNT_FIELD_NAME] 40 self.required_forms = self.total_forms - self. num_extra41 self.change_form_count = self.total_forms - self. num_extra65 self.required_forms = self.total_forms - self._meta.num_extra 66 self.change_form_count = self.total_forms - self._meta.num_extra 42 67 else: 43 68 # not sure that ValidationError is the best thing to raise here 44 69 raise ValidationError('ManagementForm data is missing or has been tampered with') 45 70 elif initial: 46 71 self.change_form_count = len(initial) 47 72 self.required_forms = len(initial) 48 self.total_forms = self.required_forms + self. num_extra73 self.total_forms = self.required_forms + self._meta.num_extra 49 74 self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix) 50 75 else: 51 76 self.change_form_count = 0 52 77 self.required_forms = 0 53 self.total_forms = self. num_extra78 self.total_forms = self._meta.num_extra 54 79 self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix) 55 80 56 81 def _get_add_forms(self): 57 82 """Return a list of all the add forms in this ``FormSet``.""" 58 FormClass = self.form_class59 83 if not hasattr(self, '_add_forms'): 60 84 add_forms = [] 61 85 for i in range(self.change_form_count, self.total_forms): … … class BaseFormSet(object): 64 88 kwargs['data'] = self.data 65 89 if self.files: 66 90 kwargs['files'] = self.files 67 add_form = FormClass(**kwargs)91 add_form = self.get_form_class(i)(**kwargs) 68 92 self.add_fields(add_form, i) 69 93 add_forms.append(add_form) 70 94 self._add_forms = add_forms … … class BaseFormSet(object): 73 97 74 98 def _get_change_forms(self): 75 99 """Return a list of all the change forms in this ``FormSet``.""" 76 FormClass = self.form_class77 100 if not hasattr(self, '_change_forms'): 78 101 change_forms = [] 79 102 for i in range(0, self.change_form_count): … … class BaseFormSet(object): 84 107 kwargs['files'] = self.files 85 108 if self.initial: 86 109 kwargs['initial'] = self.initial[i] 87 change_form = FormClass(**kwargs)110 change_form = self.get_form_class(i)(**kwargs) 88 111 self.add_fields(change_form, i) 89 112 change_forms.append(change_form) 90 self._change_forms = change_forms113 self._change_forms = change_forms 91 114 return self._change_forms 92 115 change_forms = property(_get_change_forms) 93 116 … … class BaseFormSet(object): 117 140 # Process change forms 118 141 for form in self.change_forms: 119 142 if form.is_valid(): 120 if self. deletable and form.cleaned_data[DELETION_FIELD_NAME]:143 if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 121 144 self.deleted_data.append(form.cleaned_data) 122 145 else: 123 146 self.cleaned_data.append(form.cleaned_data) … … class BaseFormSet(object): 144 167 add_errors.reverse() 145 168 errors.extend(add_errors) 146 169 # Sort cleaned_data if the formset is orderable. 147 if self. orderable:170 if self._meta.orderable: 148 171 self.cleaned_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME]) 149 172 # Give self.clean() a chance to do validation 150 173 try: … … class BaseFormSet(object): 167 190 via formset.non_form_errors() 168 191 """ 169 192 return self.cleaned_data 193 194 def get_form_class(self, index): 195 """ 196 A hook to change a form class object. 197 """ 198 FormClass = self._meta.form 199 FormClass.base_fields = self.base_fields 200 return FormClass 170 201 171 202 def add_fields(self, form, index): 172 203 """A hook for adding extra fields on to each form instance.""" 173 if self. orderable:204 if self._meta.orderable: 174 205 form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1) 175 if self. deletable:206 if self._meta.deletable: 176 207 form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False) 177 208 178 209 def add_prefix(self, index): … … class BaseFormSet(object): 192 223 else: 193 224 return Media() 194 225 media = property(_get_media) 226 227 class FormSet(BaseFormSet): 228 __metaclass__ = BaseFormSetMetaclass 195 229 196 def formset_for_form(form, formset=BaseFormSet, num_extra=1, orderable=False, deletable=False): 230 def formset_for_form(form, formset=FormSet, num_extra=1, orderable=False, 231 deletable=False): 197 232 """Return a FormSet for the given form class.""" 198 attrs = {'form_class': form, 'num_extra': num_extra, 'orderable': orderable, 'deletable': deletable} 199 return type(form.__name__ + 'FormSet', (formset,), attrs) 200 233 warn("formset_for_form is deprecated, use FormSet instead.", 234 PendingDeprecationWarning, 235 stacklevel=3) 236 return BaseFormSetMetaclass( 237 form.__name__ + "FormSet", (formset,), form.base_fields, 238 form=form, num_extra=num_extra, orderable=orderable, 239 deletable=deletable) 240 201 241 def all_valid(formsets): 202 242 """Returns true if every formset in formsets is valid.""" 203 243 valid = True -
django/newforms/models.py
diff --git a/django/newforms/models.py b/django/newforms/models.py index 3c9b43d..054e0f3 100644
a b from django.core.exceptions import ImproperlyConfigured 13 13 from util import ValidationError, ErrorList 14 14 from forms import BaseForm 15 15 from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES 16 from formsets import BaseFormSet, formset_for_form, DELETION_FIELD_NAME 16 from formsets import FormSetOptions, BaseFormSet, formset_for_form, DELETION_FIELD_NAME 17 from options import ModelFormOptions, ModelFormSetOptions, InlineFormSetOptions 17 18 from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput 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', 23 'ModelFormSet', 'InlineFormset', 'modelform_for_model', 22 24 'formset_for_model', 'inline_formset', 23 25 'ModelChoiceField', 'ModelMultipleChoiceField', 24 26 ) … … def fields_for_model(model, fields=None, exclude=None, formfield_callback=lambda 209 211 field_list.append((f.name, formfield)) 210 212 return SortedDict(field_list) 211 213 212 class ModelFormOptions(object):213 def __init__(self, options=None):214 self.model = getattr(options, 'model', None)215 self.fields = getattr(options, 'fields', None)216 self.exclude = getattr(options, 'exclude', None)217 218 214 class ModelFormMetaclass(type): 219 def __new__(cls, name, bases, attrs): 220 # TODO: no way to specify formfield_callback yet, do we need one, or 221 # should it be a special case for the admin? 215 opts_class = ModelFormOptions 216 217 def __new__(cls, name, bases, attrs, 218 formfield_callback=lambda f: f.formfield(), **options): 222 219 fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] 223 220 fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) 224 221 … … class ModelFormMetaclass(type): 230 227 fields = base.base_fields.items() + fields 231 228 declared_fields = SortedDict(fields) 232 229 233 opts = ModelFormOptions(attrs.get('Meta', None))230 opts = cls.opts_class(options and options or attrs.get('Meta', None)) 234 231 attrs['_meta'] = opts 235 232 236 233 # Don't allow more than one Meta model defenition in bases. The fields … … class ModelFormMetaclass(type): 255 252 base_model = getattr(base_opts, 'model', None) 256 253 if base_model is not None: 257 254 raise ImproperlyConfigured('%s defines more than one model.' % name) 258 model_fields = fields_for_model(opts.model, opts.fields, opts.exclude )255 model_fields = fields_for_model(opts.model, opts.fields, opts.exclude, formfield_callback) 259 256 # fields declared in base classes override fields from the model 260 257 model_fields.update(declared_fields) 261 258 attrs['base_fields'] = model_fields 262 259 else: 263 260 attrs['base_fields'] = declared_fields 264 261 return type.__new__(cls, name, bases, attrs) 265 262 266 263 class BaseModelForm(BaseForm): 267 def __init__(self, instance,data=None, files=None, auto_id='id_%s', prefix=None,268 initial=None, error_class=ErrorList, label_suffix=':' ):264 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 265 initial=None, error_class=ErrorList, label_suffix=':', instance=None): 269 266 self.instance = instance 270 267 opts = self._meta 271 object_data = model_to_dict(instance, opts.fields, opts.exclude) 268 if instance is None: 269 # if we didn't get an instance, instantiate a new one 270 self.instance = opts.model() 271 object_data = {} 272 else: 273 self.instance = instance 274 object_data = model_to_dict(instance, opts.fields, opts.exclude) 272 275 # if initial was provided, it should override the values from instance 273 276 if initial is not None: 274 277 object_data.update(initial) … … class BaseModelForm(BaseForm): 290 293 class ModelForm(BaseModelForm): 291 294 __metaclass__ = ModelFormMetaclass 292 295 296 # this should really be named form_for_model. 297 def modelform_for_model(model, form=ModelForm, 298 formfield_callback=lambda f: f.formfield(), **options): 299 opts = model._meta 300 options.update({"model": model}) 301 return ModelFormMetaclass(opts.object_name + "ModelForm", (form,), 302 {}, formfield_callback, **options) 303 293 304 294 305 # Fields ##################################################################### 295 306 … … class ModelMultipleChoiceField(ModelChoiceField): 404 415 405 416 # Model-FormSet integration ################################################### 406 417 407 def initial_data(instance, fields=None): 408 """ 409 Return a dictionary from data in ``instance`` that is suitable for 410 use as a ``Form`` constructor's ``initial`` argument. 411 412 Provide ``fields`` to specify the names of specific fields to return. 413 All field values in the instance will be returned if ``fields`` is not 414 provided. 415 """ 416 # avoid a circular import 417 from django.db.models.fields.related import ManyToManyField 418 opts = instance._meta 419 initial = {} 420 for f in opts.fields + opts.many_to_many: 421 if not f.editable: 422 continue 423 if fields and not f.name in fields: 424 continue 425 if isinstance(f, ManyToManyField): 426 # MultipleChoiceWidget needs a list of ints, not object instances. 427 initial[f.name] = [obj.pk for obj in f.value_from_object(instance)] 428 else: 429 initial[f.name] = f.value_from_object(instance) 430 return initial 418 class ModelFormSetMetaclass(ModelFormMetaclass): 419 opts_class = ModelFormSetOptions 431 420 432 421 class BaseModelFormSet(BaseFormSet): 433 422 """ 434 423 A ``FormSet`` for editing a queryset and/or adding new objects to it. 435 424 """ 436 model = None437 queryset = None438 425 439 def __init__(self, qs,data=None, files=None, auto_id='id_%s', prefix=None):426 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None): 440 427 kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix} 441 self.queryset = qs 442 kwargs['initial'] = [initial_data(obj) for obj in qs] 428 opts = self._meta 429 self.queryset = self.get_queryset(**kwargs) 430 initial_data = [] 431 for obj in self.queryset: 432 initial_data.append(model_to_dict(obj, opts.fields, opts.exclude)) 433 kwargs['initial'] = initial_data 443 434 super(BaseModelFormSet, self).__init__(**kwargs) 435 436 def get_queryset(self, **kwargs): 437 """ 438 Hook to returning a queryset for this model. 439 """ 440 return self._meta.model._default_manager.all() 444 441 445 442 def save_new(self, form, commit=True): 446 443 """Saves and returns a new model instance for the given form.""" 447 return save_instance(form, self. model(), commit=commit)444 return save_instance(form, self._meta.model(), commit=commit) 448 445 449 446 def save_instance(self, form, instance, commit=True): 450 447 """Saves and returns an existing model instance for the given form.""" … … class BaseModelFormSet(BaseFormSet): 461 458 return [] 462 459 # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk 463 460 existing_objects = {} 461 opts = self._meta 464 462 for obj in self.queryset: 465 463 existing_objects[obj.pk] = obj 466 464 saved_instances = [] 467 465 for form in self.change_forms: 468 obj = existing_objects[form.cleaned_data[ self.model._meta.pk.attname]]469 if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:466 obj = existing_objects[form.cleaned_data[opts.model._meta.pk.attname]] 467 if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 470 468 obj.delete() 471 469 else: 472 470 saved_instances.append(self.save_instance(form, obj, commit=commit)) … … class BaseModelFormSet(BaseFormSet): 474 472 475 473 def save_new_objects(self, commit=True): 476 474 new_objects = [] 475 opts = self._meta 477 476 for form in self.add_forms: 478 477 if form.is_empty(): 479 478 continue 480 479 # If someone has marked an add form for deletion, don't save the 481 480 # object. At some point it would be nice if we didn't display 482 481 # the deletion widget for add forms. 483 if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:482 if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 484 483 continue 485 484 new_objects.append(self.save_new(form, commit=commit)) 486 485 return new_objects 487 486 488 487 def add_fields(self, form, index): 489 488 """Add a hidden field for the object's primary key.""" 490 self._pk_field_name = self. model._meta.pk.attname489 self._pk_field_name = self._meta.model._meta.pk.attname 491 490 form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput) 492 491 super(BaseModelFormSet, self).add_fields(form, index) 493 492 494 def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(), 495 formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None): 493 class ModelFormSet(BaseModelFormSet): 494 __metaclass__ = ModelFormSetMetaclass 495 496 def formset_for_model(model, formset=BaseModelFormSet, 497 formfield_callback=lambda f: f.formfield(), **options): 496 498 """ 497 499 Returns a FormSet class for the given Django model class. This FormSet 498 500 will contain change forms for every instance of the given model as well … … def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formf 501 503 This is essentially the same as ``formset_for_queryset``, but automatically 502 504 uses the model's default manager to determine the queryset. 503 505 """ 504 form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback) 505 FormSet = formset_for_form(form, formset, extra, orderable, deletable) 506 FormSet.model = model 507 return FormSet 506 opts = model._meta 507 options.update({"model": model}) 508 return ModelFormSetMetaclass(opts.object_name + "ModelFormSet", (formset,), 509 {}, **options) 510 511 class InlineFormSetMetaclass(ModelFormSetMetaclass): 512 opts_class = InlineFormSetOptions 513 514 def __new__(cls, name, bases, attrs, 515 formfield_callback=lambda f: f.formfield(), **options): 516 formset = super(InlineFormSetMetaclass, cls).__new__(cls, name, bases, attrs, 517 formfield_callback, **options) 518 # If this isn't a subclass of InlineFormset, don't do anything special. 519 try: 520 if not filter(lambda b: issubclass(b, InlineFormset), bases): 521 return formset 522 except NameError: 523 # 'InlineFormset' isn't defined yet, meaning we're looking at 524 # Django's own InlineFormset class, defined below. 525 return formset 526 opts = formset._meta 527 # resolve the foreign key 528 fk = cls.resolve_foreign_key(opts.parent_model, opts.model, opts.fk_name) 529 # remove the fk from base_fields to keep it transparent to the form. 530 try: 531 del formset.base_fields[fk.name] 532 except KeyError: 533 pass 534 formset.fk = fk 535 return formset 536 537 def _resolve_foreign_key(cls, parent_model, model, fk_name=None): 538 """ 539 Finds and returns the ForeignKey from model to parent if there is one. 540 If fk_name is provided, assume it is the name of the ForeignKey field. 541 """ 542 # avoid a circular import 543 from django.db.models import ForeignKey 544 opts = model._meta 545 if fk_name: 546 fks_to_parent = [f for f in opts.fields if f.name == fk_name] 547 if len(fks_to_parent) == 1: 548 fk = fks_to_parent[0] 549 if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model: 550 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model)) 551 elif len(fks_to_parent) == 0: 552 raise Exception("%s has no field named '%s'" % (model, fk_name)) 553 else: 554 # Try to discover what the ForeignKey from model to parent_model is 555 fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model] 556 if len(fks_to_parent) == 1: 557 fk = fks_to_parent[0] 558 elif len(fks_to_parent) == 0: 559 raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) 560 else: 561 raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) 562 return fk 563 resolve_foreign_key = classmethod(_resolve_foreign_key) 508 564 509 class InlineFormset(BaseModelFormSet):565 class BaseInlineFormSet(BaseModelFormSet): 510 566 """A formset for child objects related to a parent.""" 511 def __init__(self, instance, data=None, files=None):567 def __init__(self, *args, **kwargs): 512 568 from django.db.models.fields.related import RelatedObject 513 self.instance = instance 569 opts = self._meta 570 self.instance = kwargs.pop("instance", None) 514 571 # is there a better way to get the object descriptor? 515 self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()516 qs = self.get_queryset()517 super( InlineFormset, self).__init__(qs, data, files, prefix=self.rel_name)572 rel_name = RelatedObject(self.fk.rel.to, opts.model, self.fk).get_accessor_name() 573 kwargs["prefix"] = rel_name 574 super(BaseInlineFormSet, self).__init__(*args, **kwargs) 518 575 519 def get_queryset(self ):576 def get_queryset(self, **kwargs): 520 577 """ 521 578 Returns this FormSet's queryset, but restricted to children of 522 579 self.instance 523 580 """ 524 kwargs = {self.fk.name: self.instance}525 return self.model._default_manager.filter(**kwargs)581 queryset = super(BaseInlineFormSet, self).get_queryset(**kwargs) 582 return queryset.filter(**{self.fk.name: self.instance}) 526 583 527 584 def save_new(self, form, commit=True): 528 585 kwargs = {self.fk.get_attname(): self.instance.pk} 529 new_obj = self. model(**kwargs)586 new_obj = self._meta.model(**kwargs) 530 587 return save_instance(form, new_obj, commit=commit) 531 588 532 def get_foreign_key(parent_model, model, fk_name=None): 533 """ 534 Finds and returns the ForeignKey from model to parent if there is one. 535 If fk_name is provided, assume it is the name of the ForeignKey field. 536 """ 537 # avoid circular import 538 from django.db.models import ForeignKey 539 opts = model._meta 540 if fk_name: 541 fks_to_parent = [f for f in opts.fields if f.name == fk_name] 542 if len(fks_to_parent) == 1: 543 fk = fks_to_parent[0] 544 if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model: 545 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model)) 546 elif len(fks_to_parent) == 0: 547 raise Exception("%s has no field named '%s'" % (model, fk_name)) 548 else: 549 # Try to discover what the ForeignKey from model to parent_model is 550 fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model] 551 if len(fks_to_parent) == 1: 552 fk = fks_to_parent[0] 553 elif len(fks_to_parent) == 0: 554 raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) 555 else: 556 raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) 557 return fk 589 class InlineFormset(BaseInlineFormSet): 590 __metaclass__ = InlineFormSetMetaclass 558 591 559 def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()): 592 def inline_formset(parent_model, model, formset=InlineFormset, 593 formfield_callback=lambda f: f.formfield(), **options): 560 594 """ 561 595 Returns an ``InlineFormset`` for the given kwargs. 562 596 563 597 You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` 564 598 to ``parent_model``. 565 599 """ 566 fk = get_foreign_key(parent_model, model, fk_name=fk_name) 567 # let the formset handle object deletion by default 568 FormSet = formset_for_model(model, formset=InlineFormset, fields=fields, 569 formfield_callback=formfield_callback, 570 extra=extra, orderable=orderable, 571 deletable=deletable) 572 # HACK: remove the ForeignKey to the parent from every form 573 # This should be done a line above before we pass 'fields' to formset_for_model 574 # an 'omit' argument would be very handy here 575 try: 576 del FormSet.form_class.base_fields[fk.name] 577 except KeyError: 578 pass 579 FormSet.fk = fk 580 return FormSet 600 opts = model._meta 601 options.update({"parent_model": parent_model, "model": model}) 602 return InlineFormSetMetaclass(opts.object_name + "InlineFormset", (formset,), 603 {}, formfield_callback, **options) -
new file django/newforms/options.py
diff --git a/django/newforms/options.py b/django/newforms/options.py new file mode 100644 index 0000000..f9696b8
- + 1 2 from forms import BaseForm 3 4 class BaseFormOptions(object): 5 """ 6 The base class for all options that are associated to a form object. 7 """ 8 def __init__(self, options=None): 9 self.fields = self._dynamic_attribute(options, "fields") 10 self.exclude = self._dynamic_attribute(options, "exclude") 11 12 def _dynamic_attribute(self, obj, key, default=None): 13 try: 14 return getattr(obj, key) 15 except AttributeError: 16 try: 17 return obj[key] 18 except (TypeError, KeyError): 19 # key doesnt exist in obj or obj is None 20 return default 21 22 class ModelFormOptions(BaseFormOptions): 23 """ 24 Encapsulates the options on a ModelForm class. 25 """ 26 def __init__(self, options=None): 27 self.model = self._dynamic_attribute(options, "model") 28 super(ModelFormOptions, self).__init__(options) 29 30 class FormSetOptions(BaseFormOptions): 31 """ 32 Encapsulates the options on a FormSet class. 33 """ 34 def __init__(self, options=None): 35 self.form = self._dynamic_attribute(options, "form", BaseForm) 36 self.num_extra = self._dynamic_attribute(options, "num_extra", 1) 37 self.orderable = self._dynamic_attribute(options, "orderable", False) 38 self.deletable = self._dynamic_attribute(options, "deletable", False) 39 super(FormSetOptions, self).__init__(options) 40 41 class ModelFormSetOptions(FormSetOptions, ModelFormOptions): 42 def __init__(self, options=None): 43 super(ModelFormSetOptions, self).__init__(options) 44 self.deletable = True 45 46 class InlineFormSetOptions(ModelFormSetOptions): 47 def __init__(self, options=None): 48 super(InlineFormSetOptions, self).__init__(options) 49 self.parent_model = self._dynamic_attribute(options, "parent_model") 50 self.fk_name = self._dynamic_attribute(options, "fk_name") 51 52 No newline at end of file -
tests/modeltests/model_forms/models.py
diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py index cd92956..5e95844 100644
a b ImproperlyConfigured: BadForm's base classes define more than one model. 167 167 >>> class CategoryForm(ModelForm): 168 168 ... class Meta: 169 169 ... model = Category 170 >>> f = CategoryForm( Category())170 >>> f = CategoryForm() 171 171 >>> print f 172 172 <tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" maxlength="20" /></td></tr> 173 173 <tr><th><label for="id_slug">Slug:</label></th><td><input id="id_slug" type="text" name="slug" maxlength="20" /></td></tr> … … ImproperlyConfigured: BadForm's base classes define more than one model. 179 179 >>> print f['name'] 180 180 <input id="id_name" type="text" name="name" maxlength="20" /> 181 181 182 >>> f = CategoryForm( Category(),auto_id=False)182 >>> f = CategoryForm(auto_id=False) 183 183 >>> print f.as_ul() 184 184 <li>Name: <input type="text" name="name" maxlength="20" /></li> 185 185 <li>Slug: <input type="text" name="slug" maxlength="20" /></li> 186 186 <li>The URL: <input type="text" name="url" maxlength="40" /></li> 187 187 188 >>> f = CategoryForm( Category(),{'name': 'Entertainment', 'slug': 'entertainment', 'url': 'entertainment'})188 >>> f = CategoryForm({'name': 'Entertainment', 'slug': 'entertainment', 'url': 'entertainment'}) 189 189 >>> f.is_valid() 190 190 True 191 191 >>> f.cleaned_data … … True 196 196 >>> Category.objects.all() 197 197 [<Category: Entertainment>] 198 198 199 >>> f = CategoryForm( Category(),{'name': "It's a test", 'slug': 'its-test', 'url': 'test'})199 >>> f = CategoryForm({'name': "It's a test", 'slug': 'its-test', 'url': 'test'}) 200 200 >>> f.is_valid() 201 201 True 202 202 >>> f.cleaned_data … … True 210 210 If you call save() with commit=False, then it will return an object that 211 211 hasn't yet been saved to the database. In this case, it's up to you to call 212 212 save() on the resulting model instance. 213 >>> f = CategoryForm( Category(),{'name': 'Third test', 'slug': 'third-test', 'url': 'third'})213 >>> f = CategoryForm({'name': 'Third test', 'slug': 'third-test', 'url': 'third'}) 214 214 >>> f.is_valid() 215 215 True 216 216 >>> f.cleaned_data … … True 225 225 [<Category: Entertainment>, <Category: It's a test>, <Category: Third test>] 226 226 227 227 If you call save() with invalid data, you'll get a ValueError. 228 >>> f = CategoryForm( Category(),{'name': '', 'slug': '', 'url': 'foo'})228 >>> f = CategoryForm({'name': '', 'slug': '', 'url': 'foo'}) 229 229 >>> f.errors 230 230 {'name': [u'This field is required.'], 'slug': [u'This field is required.']} 231 231 >>> f.cleaned_data … … AttributeError: 'CategoryForm' object has no attribute 'cleaned_data' 236 236 Traceback (most recent call last): 237 237 ... 238 238 ValueError: The Category could not be created because the data didn't validate. 239 >>> f = CategoryForm( Category(),{'name': '', 'slug': '', 'url': 'foo'})239 >>> f = CategoryForm({'name': '', 'slug': '', 'url': 'foo'}) 240 240 >>> f.save() 241 241 Traceback (most recent call last): 242 242 ... … … fields with the 'choices' attribute are represented by a ChoiceField. 253 253 >>> class ArticleForm(ModelForm): 254 254 ... class Meta: 255 255 ... model = Article 256 >>> f = ArticleForm( Article(),auto_id=False)256 >>> f = ArticleForm(auto_id=False) 257 257 >>> print f 258 258 <tr><th>Headline:</th><td><input type="text" name="headline" maxlength="50" /></td></tr> 259 259 <tr><th>Slug:</th><td><input type="text" name="slug" maxlength="50" /></td></tr> … … from the form can't provide a value for that field! 286 286 ... class Meta: 287 287 ... model = Article 288 288 ... fields = ('headline','pub_date') 289 >>> f = PartialArticleForm( Article(),auto_id=False)289 >>> f = PartialArticleForm(auto_id=False) 290 290 >>> print f 291 291 <tr><th>Headline:</th><td><input type="text" name="headline" maxlength="50" /></td></tr> 292 292 <tr><th>Pub date:</th><td><input type="text" name="pub_date" /></td></tr> … … current values are inserted as 'initial' data in each Field. 298 298 >>> class RoykoForm(ModelForm): 299 299 ... class Meta: 300 300 ... model = Writer 301 >>> f = RoykoForm( w, auto_id=False)301 >>> f = RoykoForm(auto_id=False, instance=w) 302 302 >>> print f 303 303 <tr><th>Name:</th><td><input type="text" name="name" value="Mike Royko" maxlength="50" /><br />Use both first and last names.</td></tr> 304 304 … … current values are inserted as 'initial' data in each Field. 309 309 >>> class TestArticleForm(ModelForm): 310 310 ... class Meta: 311 311 ... model = Article 312 >>> f = TestArticleForm(a rt, auto_id=False)312 >>> f = TestArticleForm(auto_id=False, instance=art) 313 313 >>> print f.as_ul() 314 314 <li>Headline: <input type="text" name="headline" value="Test article" maxlength="50" /></li> 315 315 <li>Slug: <input type="text" name="slug" value="test-article" maxlength="50" /></li> … … current values are inserted as 'initial' data in each Field. 331 331 <option value="2">It's a test</option> 332 332 <option value="3">Third test</option> 333 333 </select> Hold down "Control", or "Command" on a Mac, to select more than one.</li> 334 >>> f = TestArticleForm( art, {'headline': u'Test headline', 'slug': 'test-headline', 'pub_date': u'1984-02-06', 'writer': u'1', 'article': 'Hello.'})334 >>> f = TestArticleForm({'headline': u'Test headline', 'slug': 'test-headline', 'pub_date': u'1984-02-06', 'writer': u'1', 'article': 'Hello.'}, instance=art) 335 335 >>> f.is_valid() 336 336 True 337 337 >>> test_art = f.save() … … by specifying a 'fields' argument to form_for_instance. 347 347 ... class Meta: 348 348 ... model = Article 349 349 ... fields=('headline', 'slug', 'pub_date') 350 >>> f = PartialArticleForm( art, {'headline': u'New headline', 'slug': 'new-headline', 'pub_date': u'1988-01-04'}, auto_id=False)350 >>> f = PartialArticleForm({'headline': u'New headline', 'slug': 'new-headline', 'pub_date': u'1988-01-04'}, auto_id=False, instance=art) 351 351 >>> print f.as_ul() 352 352 <li>Headline: <input type="text" name="headline" value="New headline" maxlength="50" /></li> 353 353 <li>Slug: <input type="text" name="slug" value="new-headline" maxlength="50" /></li> … … Add some categories and test the many-to-many form output. 370 370 >>> class TestArticleForm(ModelForm): 371 371 ... class Meta: 372 372 ... model = Article 373 >>> f = TestArticleForm( new_art, auto_id=False)373 >>> f = TestArticleForm(auto_id=False, instance=new_art) 374 374 >>> print f.as_ul() 375 375 <li>Headline: <input type="text" name="headline" value="New headline" maxlength="50" /></li> 376 376 <li>Slug: <input type="text" name="slug" value="new-headline" maxlength="50" /></li> … … Add some categories and test the many-to-many form output. 393 393 <option value="3">Third test</option> 394 394 </select> Hold down "Control", or "Command" on a Mac, to select more than one.</li> 395 395 396 >>> f = TestArticleForm( new_art,{'headline': u'New headline', 'slug': u'new-headline', 'pub_date': u'1988-01-04',397 ... 'writer': u'1', 'article': u'Hello.', 'categories': [u'1', u'2']} )396 >>> f = TestArticleForm({'headline': u'New headline', 'slug': u'new-headline', 'pub_date': u'1988-01-04', 397 ... 'writer': u'1', 'article': u'Hello.', 'categories': [u'1', u'2']}, instance=new_art) 398 398 >>> new_art = f.save() 399 399 >>> new_art.id 400 400 1 … … Add some categories and test the many-to-many form output. 403 403 [<Category: Entertainment>, <Category: It's a test>] 404 404 405 405 Now, submit form data with no categories. This deletes the existing categories. 406 >>> f = TestArticleForm( new_art,{'headline': u'New headline', 'slug': u'new-headline', 'pub_date': u'1988-01-04',407 ... 'writer': u'1', 'article': u'Hello.'} )406 >>> f = TestArticleForm({'headline': u'New headline', 'slug': u'new-headline', 'pub_date': u'1988-01-04', 407 ... 'writer': u'1', 'article': u'Hello.'}, instance=new_art) 408 408 >>> new_art = f.save() 409 409 >>> new_art.id 410 410 1 … … Create a new article, with categories, via the form. 416 416 >>> class ArticleForm(ModelForm): 417 417 ... class Meta: 418 418 ... model = Article 419 >>> f = ArticleForm( Article(),{'headline': u'The walrus was Paul', 'slug': u'walrus-was-paul', 'pub_date': u'1967-11-01',419 >>> f = ArticleForm({'headline': u'The walrus was Paul', 'slug': u'walrus-was-paul', 'pub_date': u'1967-11-01', 420 420 ... 'writer': u'1', 'article': u'Test.', 'categories': [u'1', u'2']}) 421 421 >>> new_art = f.save() 422 422 >>> new_art.id … … Create a new article, with no categories, via the form. 429 429 >>> class ArticleForm(ModelForm): 430 430 ... class Meta: 431 431 ... model = Article 432 >>> f = ArticleForm( Article(),{'headline': u'The walrus was Paul', 'slug': u'walrus-was-paul', 'pub_date': u'1967-11-01',432 >>> f = ArticleForm({'headline': u'The walrus was Paul', 'slug': u'walrus-was-paul', 'pub_date': u'1967-11-01', 433 433 ... 'writer': u'1', 'article': u'Test.'}) 434 434 >>> new_art = f.save() 435 435 >>> new_art.id … … The m2m data won't be saved until save_m2m() is invoked on the form. 443 443 >>> class ArticleForm(ModelForm): 444 444 ... class Meta: 445 445 ... model = Article 446 >>> f = ArticleForm( Article(),{'headline': u'The walrus was Paul', 'slug': 'walrus-was-paul', 'pub_date': u'1967-11-01',446 >>> f = ArticleForm({'headline': u'The walrus was Paul', 'slug': 'walrus-was-paul', 'pub_date': u'1967-11-01', 447 447 ... 'writer': u'1', 'article': u'Test.', 'categories': [u'1', u'2']}) 448 448 >>> new_art = f.save(commit=False) 449 449 … … existing Category instance. 474 474 <Category: Third test> 475 475 >>> cat.id 476 476 3 477 >>> form = ShortCategory( cat, {'name': 'Third', 'slug': 'third', 'url': '3rd'})477 >>> form = ShortCategory({'name': 'Third', 'slug': 'third', 'url': '3rd'}, instance=cat) 478 478 >>> form.save() 479 479 <Category: Third> 480 480 >>> Category.objects.get(id=3) … … the data in the database when the form is instantiated. 486 486 >>> class ArticleForm(ModelForm): 487 487 ... class Meta: 488 488 ... model = Article 489 >>> f = ArticleForm( Article(),auto_id=False)489 >>> f = ArticleForm(auto_id=False) 490 490 >>> print f.as_ul() 491 491 <li>Headline: <input type="text" name="headline" maxlength="50" /></li> 492 492 <li>Slug: <input type="text" name="slug" maxlength="50" /></li> … … ValidationError: [u'Select a valid choice. 4 is not one of the available choices 690 690 >>> class PhoneNumberForm(ModelForm): 691 691 ... class Meta: 692 692 ... model = PhoneNumber 693 >>> f = PhoneNumberForm( PhoneNumber(),{'phone': '(312) 555-1212', 'description': 'Assistance'})693 >>> f = PhoneNumberForm({'phone': '(312) 555-1212', 'description': 'Assistance'}) 694 694 >>> f.is_valid() 695 695 True 696 696 >>> f.cleaned_data -
tests/modeltests/model_formsets/models.py
diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py index 19bdeed..d1e72ea 100644
a b class Book(models.Model): 16 16 17 17 __test__ = {'API_TESTS': """ 18 18 19 >>> from django.newforms.models import formset_for_model 19 >>> from django import newforms as forms 20 >>> from django.newforms.models import formset_for_model, ModelFormSet 20 21 21 >>> qs = Author.objects.all() 22 >>> AuthorFormSet = formset_for_model(Author, extra=3) 22 A bare bones verion. 23 23 24 >>> formset = AuthorFormSet(qs) 24 >>> class AuthorFormSet(ModelFormSet): 25 ... class Meta: 26 ... model = Author 27 >>> AuthorFormSet.base_fields.keys() 28 ['name'] 29 30 Extra fields. 31 32 >>> class AuthorFormSet(ModelFormSet): 33 ... published = forms.BooleanField() 34 ... 35 ... class Meta: 36 ... model = Author 37 >>> AuthorFormSet.base_fields.keys() 38 ['name', 'published'] 39 40 Lets create a formset that is bound to a model. 41 42 >>> class AuthorFormSet(ModelFormSet): 43 ... class Meta: 44 ... model = Author 45 ... num_extra = 3 46 47 >>> formset = AuthorFormSet() 25 48 >>> for form in formset.forms: 26 49 ... print form.as_p() 27 50 <p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /><input type="hidden" name="form-0-id" id="id_form-0-id" /></p> … … __test__ = {'API_TESTS': """ 35 58 ... 'form-2-name': '', 36 59 ... } 37 60 38 >>> formset = AuthorFormSet( qs, data=data)61 >>> formset = AuthorFormSet(data) 39 62 >>> formset.is_valid() 40 63 True 41 64 … … Charles Baudelaire 49 72 50 73 51 74 Gah! We forgot Paul Verlaine. Let's create a formset to edit the existing 52 authors with an extra form to add him. This time we'll use formset_for_queryset. 53 We *could* use formset_for_queryset to restrict the Author objects we edit, 54 but in that case we'll use it to display them in alphabetical order by name. 55 56 >>> qs = Author.objects.order_by('name') 57 >>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=False) 58 59 >>> formset = AuthorFormSet(qs) 75 authors with an extra form to add him. When subclassing ModelFormSet you can 76 override the get_queryset method to return any queryset we like, but in this 77 case we'll use it to display it in alphabetical order by name. 78 79 >>> class AuthorFormSet(ModelFormSet): 80 ... class Meta: 81 ... model = Author 82 ... num_extra = 1 83 ... deletable = False 84 ... 85 ... def get_queryset(self, **kwargs): 86 ... qs = super(AuthorFormSet, self).get_queryset(**kwargs) 87 ... return qs.order_by('name') 88 89 >>> formset = AuthorFormSet() 60 90 >>> for form in formset.forms: 61 91 ... print form.as_p() 62 92 <p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /><input type="hidden" name="form-0-id" value="2" id="id_form-0-id" /></p> … … but in that case we'll use it to display them in alphabetical order by name. 73 103 ... 'form-2-name': 'Paul Verlaine', 74 104 ... } 75 105 76 >>> formset = AuthorFormSet( qs, data=data)106 >>> formset = AuthorFormSet(data) 77 107 >>> formset.is_valid() 78 108 True 79 109 … … Paul Verlaine 90 120 This probably shouldn't happen, but it will. If an add form was marked for 91 121 deltetion, make sure we don't save that form. 92 122 93 >>> qs = Author.objects.order_by('name') 94 >>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=True) 95 96 >>> formset = AuthorFormSet(qs) 123 >>> class AuthorFormSet(ModelFormSet): 124 ... class Meta: 125 ... model = Author 126 ... num_extra = 1 127 ... deletable = True 128 ... 129 ... def get_queryset(self, **kwargs): 130 ... qs = super(AuthorFormSet, self).get_queryset(**kwargs) 131 ... return qs.order_by('name') 132 133 >>> formset = AuthorFormSet() 97 134 >>> for form in formset.forms: 98 135 ... print form.as_p() 99 136 <p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /></p> … … deltetion, make sure we don't save that form. 117 154 ... 'form-3-DELETE': 'on', 118 155 ... } 119 156 120 >>> formset = AuthorFormSet( qs, data=data)157 >>> formset = AuthorFormSet(data) 121 158 >>> formset.is_valid() 122 159 True 123 160 … … Paul Verlaine 134 171 We can also create a formset that is tied to a parent model. This is how the 135 172 admin system's edit inline functionality works. 136 173 137 >>> from django.newforms.models import inline_formset 174 >>> from django.newforms.models import inline_formset, InlineFormset 175 176 >>> class AuthorBooksFormSet(InlineFormset): 177 ... class Meta: 178 ... parent_model = Author 179 ... model = Book 180 ... num_extra = 3 181 ... deletable = False 138 182 139 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=3)140 183 >>> author = Author.objects.get(name='Charles Baudelaire') 141 184 142 >>> formset = AuthorBooksFormSet( author)185 >>> formset = AuthorBooksFormSet(instance=author) 143 186 >>> for form in formset.forms: 144 187 ... print form.as_p() 145 188 <p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" maxlength="100" /><input type="hidden" name="book_set-0-id" id="id_book_set-0-id" /></p> … … admin system's edit inline functionality works. 153 196 ... 'book_set-2-title': '', 154 197 ... } 155 198 156 >>> formset = AuthorBooksFormSet( author, data=data)199 >>> formset = AuthorBooksFormSet(data, instance=author) 157 200 >>> formset.is_valid() 158 201 True 159 202 … … Now that we've added a book to Charles Baudelaire, let's try adding another 169 212 one. This time though, an edit form will be available for every existing 170 213 book. 171 214 172 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=2) 215 >>> class AuthorBooksFormSet(InlineFormset): 216 ... class Meta: 217 ... parent_model = Author 218 ... model = Book 219 ... num_extra = 2 220 ... deletable = False 221 173 222 >>> author = Author.objects.get(name='Charles Baudelaire') 174 223 175 >>> formset = AuthorBooksFormSet( author)224 >>> formset = AuthorBooksFormSet(instance=author) 176 225 >>> for form in formset.forms: 177 226 ... print form.as_p() 178 227 <p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" value="Les Fleurs du Mal" maxlength="100" /><input type="hidden" name="book_set-0-id" value="1" id="id_book_set-0-id" /></p> … … book. 187 236 ... 'book_set-2-title': '', 188 237 ... } 189 238 190 >>> formset = AuthorBooksFormSet( author, data=data)239 >>> formset = AuthorBooksFormSet(data, instance=author) 191 240 >>> formset.is_valid() 192 241 True 193 242 -
tests/regressiontests/forms/formsets.py
diff --git a/tests/regressiontests/forms/formsets.py b/tests/regressiontests/forms/formsets.py index a6da2fe..f5cb2ee 100644
a b 2 2 formset_tests = """ 3 3 # Basic FormSet creation and usage ############################################ 4 4 5 FormSet allows us to use multiple instance of the same form on 1 page. For now,6 the best way to create a FormSet is by using the formset_for_form function.5 FormSet allows us to use multiple instance of the same form on 1 page. Create 6 the formset as you would a regular form by defining the fields declaratively. 7 7 8 8 >>> from django.newforms import Form, CharField, IntegerField, ValidationError 9 >>> from django.newforms.formsets import formset_for_form, BaseFormSet 9 >>> from django.newforms import BooleanField 10 >>> from django.newforms.formsets import formset_for_form, BaseFormSet, FormSet 10 11 11 >>> class Choice (Form):12 >>> class ChoiceFormSet(FormSet): 12 13 ... choice = CharField() 13 14 ... votes = IntegerField() 14 15 15 >>> ChoiceFormSet = formset_for_form(Choice)16 17 16 18 17 A FormSet constructor takes the same arguments as Form. Let's create a FormSet 19 18 for adding data. By default, it displays 1 blank form. It can display more, … … False 145 144 >>> formset.errors 146 145 [{'votes': [u'This field is required.'], 'choice': [u'This field is required.']}] 147 146 147 # Subclassing a FormSet class ################################################# 148 149 We can subclass a FormSet to add addition fields to an already exisiting 150 FormSet. 151 152 >>> class SecondChoiceFormSet(ChoiceFormSet): 153 ... is_public = BooleanField() 154 155 >>> formset = SecondChoiceFormSet(auto_id=False, prefix="choices") 156 >>> for form in formset.forms: 157 ... print form.as_ul() 158 <li>Choice: <input type="text" name="choices-0-choice" /></li> 159 <li>Votes: <input type="text" name="choices-0-votes" /></li> 160 <li>Is public: <input type="checkbox" name="choices-0-is_public" /></li> 148 161 149 162 # Displaying more than 1 blank form ########################################### 150 163 151 We can also display more than 1 empty form at a time. To do so, pass a152 num_extra argument to formset_for_form.164 We can also display more than 1 empty form at a time. To do so, create an inner 165 Meta class with an attribute num_extra. 153 166 154 >>> ChoiceFormSet = formset_for_form(Choice, num_extra=3) 167 >>> class NumExtraChoiceFormSet(ChoiceFormSet): 168 ... class Meta: 169 ... num_extra = 3 155 170 156 >>> formset = ChoiceFormSet(auto_id=False, prefix='choices')171 >>> formset = NumExtraChoiceFormSet(auto_id=False, prefix='choices') 157 172 >>> for form in formset.forms: 158 173 ... print form.as_ul() 159 174 <li>Choice: <input type="text" name="choices-0-choice" /></li> … … number of forms to be completed. 177 192 ... 'choices-2-votes': '', 178 193 ... } 179 194 180 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')195 >>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices') 181 196 >>> formset.is_valid() 182 197 True 183 198 >>> formset.cleaned_data … … We can just fill out one of the forms. 196 211 ... 'choices-2-votes': '', 197 212 ... } 198 213 199 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')214 >>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices') 200 215 >>> formset.is_valid() 201 216 True 202 217 >>> formset.cleaned_data … … And once again, if we try to partially complete a form, validation will fail. 215 230 ... 'choices-2-votes': '', 216 231 ... } 217 232 218 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')233 >>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices') 219 234 >>> formset.is_valid() 220 235 False 221 236 >>> formset.errors … … The num_extra argument also works when the formset is pre-filled with initial 226 241 data. 227 242 228 243 >>> initial = [{'choice': u'Calexico', 'votes': 100}] 229 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')244 >>> formset = NumExtraChoiceFormSet(initial=initial, auto_id=False, prefix='choices') 230 245 >>> for form in formset.forms: 231 246 ... print form.as_ul() 232 247 <li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> … … get an error. 254 269 ... 'choices-3-votes': '', 255 270 ... } 256 271 257 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')272 >>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices') 258 273 >>> formset.is_valid() 259 274 False 260 275 >>> formset.errors … … False 263 278 264 279 # FormSets with deletion ###################################################### 265 280 266 We can easily add deletion ability to a FormSet with an agrument to267 formset_for_form. This will add a boolean field to each form instance. When 268 that boolean field is True, the cleaned data will be in formset.deleted_data281 We can easily add deletion ability to a FormSet by setting deletable to True 282 in the inner Meta class. This will add a boolean field to each form instance. 283 When that boolean field is True, the cleaned data will be in formset.deleted_data 269 284 rather than formset.cleaned_data 270 285 271 >>> ChoiceFormSet = formset_for_form(Choice, deletable=True) 286 >>> class DeletableChoiceFormSet(ChoiceFormSet): 287 ... class Meta: 288 ... deletable = True 272 289 273 290 >>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}] 274 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')291 >>> formset = DeletableChoiceFormSet(initial=initial, auto_id=False, prefix='choices') 275 292 >>> for form in formset.forms: 276 293 ... print form.as_ul() 277 294 <li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> … … To delete something, we just need to set that form's special delete field to 300 317 ... 'choices-2-DELETE': '', 301 318 ... } 302 319 303 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')320 >>> formset = DeletableChoiceFormSet(data, auto_id=False, prefix='choices') 304 321 >>> formset.is_valid() 305 322 True 306 323 >>> formset.cleaned_data … … True 310 327 311 328 # FormSets with ordering ###################################################### 312 329 313 We can also add ordering ability to a FormSet with an agrument to314 formset_for_form. This will add a integer field to each form instance. When330 We can also add ordering ability to a FormSet by setting orderable to True in 331 the inner Meta class. This will add a integer field to each form instance. When 315 332 form validation succeeds, formset.cleaned_data will have the data in the correct 316 333 order specified by the ordering fields. If a number is duplicated in the set 317 334 of ordering fields, for instance form 0 and form 3 are both marked as 1, then 318 335 the form index used as a secondary ordering criteria. In order to put 319 336 something at the front of the list, you'd need to set it's order to 0. 320 337 321 >>> ChoiceFormSet = formset_for_form(Choice, orderable=True) 338 >>> class OrderableChoiceFormSet(ChoiceFormSet): 339 ... class Meta: 340 ... orderable = True 322 341 323 342 >>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}] 324 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')343 >>> formset = OrderableChoiceFormSet(initial=initial, auto_id=False, prefix='choices') 325 344 >>> for form in formset.forms: 326 345 ... print form.as_ul() 327 346 <li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> … … something at the front of the list, you'd need to set it's order to 0. 347 366 ... 'choices-2-ORDER': '0', 348 367 ... } 349 368 350 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')369 >>> formset = OrderableChoiceFormSet(data, auto_id=False, prefix='choices') 351 370 >>> formset.is_valid() 352 371 True 353 372 >>> for cleaned_data in formset.cleaned_data: … … True 359 378 # FormSets with ordering + deletion ########################################### 360 379 361 380 Let's try throwing ordering and deletion into the same form. 381 TODO: Perhaps handle Meta class inheritance so you can subclass 382 OrderableChoiceFormSet and DeletableChoiceFormSet? 362 383 363 >>> ChoiceFormSet = formset_for_form(Choice, orderable=True, deletable=True) 384 >>> class MixedChoiceFormSet(ChoiceFormSet): 385 ... class Meta: 386 ... orderable = True 387 ... deletable = True 364 388 365 389 >>> initial = [ 366 390 ... {'choice': u'Calexico', 'votes': 100}, 367 391 ... {'choice': u'Fergie', 'votes': 900}, 368 392 ... {'choice': u'The Decemberists', 'votes': 500}, 369 393 ... ] 370 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')394 >>> formset = MixedChoiceFormSet(initial=initial, auto_id=False, prefix='choices') 371 395 >>> for form in formset.forms: 372 396 ... print form.as_ul() 373 397 <li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> … … Let's delete Fergie, and put The Decemberists ahead of Calexico. 409 433 ... 'choices-3-DELETE': '', 410 434 ... } 411 435 412 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')436 >>> formset = MixedChoiceFormSet(data, auto_id=False, prefix='choices') 413 437 >>> formset.is_valid() 414 438 True 415 439 >>> for cleaned_data in formset.cleaned_data: … … particular form. It follows the same pattern as the clean hook on Forms. 428 452 Let's define a FormSet that takes a list of favorite drinks, but raises am 429 453 error if there are any duplicates. 430 454 431 >>> class FavoriteDrink Form(Form):455 >>> class FavoriteDrinksFormSet(FormSet): 432 456 ... name = CharField() 433 ... 434 435 >>> class FavoriteDrinksFormSet(BaseFormSet): 436 ... form_class = FavoriteDrinkForm 437 ... num_extra = 2 438 ... orderable = False 439 ... deletable = False 457 ... 458 ... class Meta: 459 ... num_extra = 2 460 ... orderable = False 461 ... deletable = False 440 462 ... 441 463 ... def clean(self): 442 464 ... seen_drinks = [] -
tests/regressiontests/inline_formsets/models.py
diff --git a/tests/regressiontests/inline_formsets/models.py b/tests/regressiontests/inline_formsets/models.py index f84be84..180bba1 100644
a b __test__ = {'API_TESTS': """ 21 21 Child has two ForeignKeys to Parent, so if we don't specify which one to use 22 22 for the inline formset, we should get an exception. 23 23 24 >>> i fs = inline_formset(Parent, Child)24 >>> inline_formset(Parent, Child)() 25 25 Traceback (most recent call last): 26 26 ... 27 27 Exception: <class 'regressiontests.inline_formsets.models.Child'> has more than 1 ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'> … … Exception: <class 'regressiontests.inline_formsets.models.Child'> has more than 29 29 30 30 These two should both work without a problem. 31 31 32 >>> ifs = inline_formset(Parent, Child, fk_name='mother') 33 >>> ifs = inline_formset(Parent, Child, fk_name='father') 32 >>> ifs = inline_formset(Parent, Child, fk_name='mother')() 33 >>> ifs = inline_formset(Parent, Child, fk_name='father')() 34 34 35 35 36 36 If we specify fk_name, but it isn't a ForeignKey from the child model to the 37 37 parent model, we should get an exception. 38 38 39 >>> i fs = inline_formset(Parent, Child, fk_name='school')39 >>> inline_formset(Parent, Child, fk_name='school')() 40 40 Traceback (most recent call last): 41 41 ... 42 42 Exception: fk_name 'school' is not a ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'> … … Exception: fk_name 'school' is not a ForeignKey to <class 'regressiontests.inlin 45 45 If the field specified in fk_name is not a ForeignKey, we should get an 46 46 exception. 47 47 48 >>> i fs = inline_formset(Parent, Child, fk_name='test')48 >>> inline_formset(Parent, Child, fk_name='test')() 49 49 Traceback (most recent call last): 50 50 ... 51 51 Exception: <class 'regressiontests.inline_formsets.models.Child'> has no field named 'test'