Ticket #6241: formset_refactor_9.diff
File formset_refactor_9.diff, 52.0 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..ee28cb4 100644
a b 1 1 from django import oldforms, template 2 2 from django import newforms as forms 3 3 from django.newforms.formsets import all_valid 4 from django.newforms.models import modelform_for_model, inline_formset 4 5 from django.contrib.contenttypes.models import ContentType 5 6 from django.contrib.admin import widgets 6 7 from django.contrib.admin.util import get_deleted_objects … … class ModelAdmin(BaseModelAdmin): 342 343 fields = flatten_fieldsets(self.declared_fieldsets) 343 344 else: 344 345 fields = None 345 return forms.form_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)346 return modelform_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield) 346 347 347 348 def form_change(self, request, obj): 348 349 """ … … class ModelAdmin(BaseModelAdmin): 352 353 fields = flatten_fieldsets(self.declared_fieldsets) 353 354 else: 354 355 fields = None 355 return forms.form_for_instance(obj, fields=fields, formfield_callback=self.formfield_for_dbfield)356 return modelform_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield) 356 357 357 358 def save_add(self, request, model, form, formsets, post_url_continue): 358 359 """ … … class ModelAdmin(BaseModelAdmin): 492 493 # Object list will give 'Permission Denied', so go back to admin home 493 494 post_url = '../../../' 494 495 495 ModelForm = self.form_add(request)496 Form = self.form_add(request) 496 497 inline_formsets = [] 497 498 obj = self.model() 498 499 if request.method == 'POST': 499 form = ModelForm(request.POST, request.FILES)500 form = Form(request.POST, request.FILES) 500 501 for FormSet in self.formsets_add(request): 501 inline_formset = FormSet( obj, data=request.POST, files=request.FILES)502 inline_formset = FormSet(request.POST, request.FILES) 502 503 inline_formsets.append(inline_formset) 503 504 if all_valid(inline_formsets) and form.is_valid(): 504 505 return self.save_add(request, model, form, inline_formsets, '../%s/') 505 506 else: 506 form = ModelForm(initial=request.GET)507 form = Form(initial=request.GET) 507 508 for FormSet in self.formsets_add(request): 508 inline_formset = FormSet( obj)509 inline_formset = FormSet() 509 510 inline_formsets.append(inline_formset) 510 511 511 512 adminForm = AdminForm(form, list(self.fieldsets_add(request)), self.prepopulated_fields) … … class ModelAdmin(BaseModelAdmin): 552 553 if request.POST and request.POST.has_key("_saveasnew"): 553 554 return self.add_view(request, form_url='../../add/') 554 555 555 ModelForm = self.form_change(request, obj)556 Form = self.form_change(request, obj) 556 557 inline_formsets = [] 557 558 if request.method == 'POST': 558 form = ModelForm(request.POST, request.FILES)559 form = Form(request.POST, request.FILES, instance=obj) 559 560 for FormSet in self.formsets_change(request, obj): 560 inline_formset = FormSet( obj, request.POST, request.FILES)561 inline_formset = FormSet(request.POST, request.FILES, instance=obj) 561 562 inline_formsets.append(inline_formset) 562 563 563 564 if all_valid(inline_formsets) and form.is_valid(): 564 565 return self.save_change(request, model, form, inline_formsets) 565 566 else: 566 form = ModelForm()567 form = Form(instance=obj, initial=request.GET) 567 568 for FormSet in self.formsets_change(request, obj): 568 inline_formset = FormSet( obj)569 inline_formset = FormSet(instance=obj) 569 570 inline_formsets.append(inline_formset) 570 571 571 572 ## Populate the FormWrapper. … … class InlineModelAdmin(BaseModelAdmin): 742 743 fields = flatten_fieldsets(self.declared_fieldsets) 743 744 else: 744 745 fields = None 745 return forms.inline_formset(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra)746 return inline_formset(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra) 746 747 747 748 def formset_change(self, request, obj): 748 749 """Returns an InlineFormSet class for use in admin change views.""" … … class InlineModelAdmin(BaseModelAdmin): 750 751 fields = flatten_fieldsets(self.declared_fieldsets) 751 752 else: 752 753 fields = None 753 return forms.inline_formset(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra)754 return inline_formset(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra) 754 755 755 756 def fieldsets_add(self, request): 756 757 if self.declared_fieldsets: 757 758 return self.declared_fieldsets 758 form = self.formset_add(request).form_class759 return [(None, {'fields': form .base_fields.keys()})]759 formset = self.formset_add(request) 760 return [(None, {'fields': formset.base_fields.keys()})] 760 761 761 762 def fieldsets_change(self, request, obj): 762 763 if self.declared_fieldsets: 763 764 return self.declared_fieldsets 764 form = self.formset_change(request, obj).form_class765 return [(None, {'fields': form .base_fields.keys()})]765 formset = self.formset_change(request, obj) 766 return [(None, {'fields': formset.base_fields.keys()})] 766 767 767 768 class StackedInline(InlineModelAdmin): 768 769 template = 'admin/edit_inline/stacked.html' … … class InlineAdminFormSet(object): 778 779 self.opts = inline 779 780 self.formset = formset 780 781 self.fieldsets = fieldsets 782 # place orderable and deletable here since _meta is inaccesible in the 783 # templates. 784 self.orderable = formset._meta.orderable 785 self.deletable = formset._meta.deletable 781 786 782 787 def __iter__(self): 783 788 for form, original in zip(self.formset.change_forms, self.formset.get_queryset()): … … class InlineAdminFormSet(object): 787 792 788 793 def fields(self): 789 794 for field_name in flatten_fieldsets(self.fieldsets): 790 yield self.formset. form_class.base_fields[field_name]795 yield self.formset.base_fields[field_name] 791 796 792 797 class InlineAdminForm(AdminForm): 793 798 """ -
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 e0f2cde..ecf983f 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', 22 ' formset_for_model', 'inline_formset',23 'ModelFormSet', 'InlineFormset', 23 24 'ModelChoiceField', 'ModelMultipleChoiceField', 24 25 ) 25 26 … … def fields_for_model(model, fields=None, exclude=None, formfield_callback=lambda 207 208 field_list.append((f.name, formfield)) 208 209 return SortedDict(field_list) 209 210 210 class ModelFormOptions(object):211 def __init__(self, options=None):212 self.model = getattr(options, 'model', None)213 self.fields = getattr(options, 'fields', None)214 self.exclude = getattr(options, 'exclude', None)215 216 211 class ModelFormMetaclass(type): 212 opts_class = ModelFormOptions 213 217 214 def __new__(cls, name, bases, attrs, 218 formfield_callback=lambda f: f.formfield() ):215 formfield_callback=lambda f: f.formfield(), **options): 219 216 fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] 220 217 fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) 221 218 … … class ModelFormMetaclass(type): 227 224 fields = base.base_fields.items() + fields 228 225 declared_fields = SortedDict(fields) 229 226 230 opts = ModelFormOptions(attrs.get('Meta', None))227 opts = cls.opts_class(options and options or attrs.get('Meta', None)) 231 228 attrs['_meta'] = opts 232 229 233 230 # Don't allow more than one Meta model defenition in bases. The fields … … class ModelFormMetaclass(type): 260 257 else: 261 258 attrs['base_fields'] = declared_fields 262 259 return type.__new__(cls, name, bases, attrs) 263 260 264 261 class BaseModelForm(BaseForm): 265 262 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 266 263 initial=None, error_class=ErrorList, label_suffix=':', instance=None): … … class BaseModelForm(BaseForm): 293 290 class ModelForm(BaseModelForm): 294 291 __metaclass__ = ModelFormMetaclass 295 292 293 def modelform_for_model(model, form=ModelForm, 294 formfield_callback=lambda f: f.formfield(), **options): 295 opts = model._meta 296 options.update({"model": model}) 297 return ModelFormMetaclass(opts.object_name + "ModelForm", (form,), 298 {}, formfield_callback, **options) 299 296 300 297 301 # Fields ##################################################################### 298 302 … … class ModelMultipleChoiceField(ModelChoiceField): 407 411 408 412 # Model-FormSet integration ################################################### 409 413 410 def initial_data(instance, fields=None): 411 """ 412 Return a dictionary from data in ``instance`` that is suitable for 413 use as a ``Form`` constructor's ``initial`` argument. 414 415 Provide ``fields`` to specify the names of specific fields to return. 416 All field values in the instance will be returned if ``fields`` is not 417 provided. 418 """ 419 # avoid a circular import 420 from django.db.models.fields.related import ManyToManyField 421 opts = instance._meta 422 initial = {} 423 for f in opts.fields + opts.many_to_many: 424 if not f.editable: 425 continue 426 if fields and not f.name in fields: 427 continue 428 if isinstance(f, ManyToManyField): 429 # MultipleChoiceWidget needs a list of ints, not object instances. 430 initial[f.name] = [obj.pk for obj in f.value_from_object(instance)] 431 else: 432 initial[f.name] = f.value_from_object(instance) 433 return initial 414 class ModelFormSetMetaclass(ModelFormMetaclass): 415 opts_class = ModelFormSetOptions 434 416 435 417 class BaseModelFormSet(BaseFormSet): 436 418 """ 437 419 A ``FormSet`` for editing a queryset and/or adding new objects to it. 438 420 """ 439 model = None440 queryset = None441 421 442 def __init__(self, qs, data=None, files=None, auto_id='id_%s', prefix=None): 422 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 423 queryset=None): 443 424 kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix} 444 self.queryset = qs 445 kwargs['initial'] = [initial_data(obj) for obj in qs] 425 opts = self._meta 426 if queryset is None: 427 self.queryset = self.get_queryset(**kwargs) 428 else: 429 self.queryset = queryset 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 446 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() 447 441 448 442 def save_new(self, form, commit=True): 449 443 """Saves and returns a new model instance for the given form.""" 450 return save_instance(form, self. model(), commit=commit)444 return save_instance(form, self._meta.model(), commit=commit) 451 445 452 446 def save_instance(self, form, instance, commit=True): 453 447 """Saves and returns an existing model instance for the given form.""" … … class BaseModelFormSet(BaseFormSet): 462 456 def save_existing_objects(self, commit=True): 463 457 if not self.queryset: 464 458 return [] 465 # Put the objects from self. get_queryset into a dict so they are easy to lookup by pk459 # Put the objects from self.queryset into a dict so they are easy to lookup by pk 466 460 existing_objects = {} 461 opts = self._meta 467 462 for obj in self.queryset: 468 463 existing_objects[obj.pk] = obj 469 464 saved_instances = [] 470 465 for form in self.change_forms: 471 obj = existing_objects[form.cleaned_data[ self.model._meta.pk.attname]]472 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]: 473 468 obj.delete() 474 469 else: 475 470 saved_instances.append(self.save_instance(form, obj, commit=commit)) … … class BaseModelFormSet(BaseFormSet): 477 472 478 473 def save_new_objects(self, commit=True): 479 474 new_objects = [] 475 opts = self._meta 480 476 for form in self.add_forms: 481 477 if form.is_empty(): 482 478 continue 483 479 # If someone has marked an add form for deletion, don't save the 484 480 # object. At some point it would be nice if we didn't display 485 481 # the deletion widget for add forms. 486 if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:482 if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 487 483 continue 488 484 new_objects.append(self.save_new(form, commit=commit)) 489 485 return new_objects 490 486 491 487 def add_fields(self, form, index): 492 488 """Add a hidden field for the object's primary key.""" 493 self._pk_field_name = self. model._meta.pk.attname489 self._pk_field_name = self._meta.model._meta.pk.attname 494 490 form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput) 495 491 super(BaseModelFormSet, self).add_fields(form, index) 496 492 497 def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(), 498 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): 499 498 """ 500 499 Returns a FormSet class for the given Django model class. This FormSet 501 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 504 503 This is essentially the same as ``formset_for_queryset``, but automatically 505 504 uses the model's default manager to determine the queryset. 506 505 """ 507 form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback)508 FormSet = formset_for_form(form, formset, extra, orderable, deletable)509 FormSet.model = model510 return FormSet506 opts = model._meta 507 options.update({"model": model}) 508 return ModelFormSetMetaclass(opts.object_name + "ModelFormSet", (formset,), 509 {}, formfield_callback, **options) 511 510 512 class InlineFormset(BaseModelFormSet): 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) 564 565 class BaseInlineFormSet(BaseModelFormSet): 513 566 """A formset for child objects related to a parent.""" 514 def __init__(self, instance, data=None, files=None):567 def __init__(self, *args, **kwargs): 515 568 from django.db.models.fields.related import RelatedObject 516 self.instance = instance 569 opts = self._meta 570 self.instance = kwargs.pop("instance", None) 517 571 # is there a better way to get the object descriptor? 518 self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()519 qs = self.get_queryset()520 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) 521 575 522 def get_queryset(self ):576 def get_queryset(self, **kwargs): 523 577 """ 524 578 Returns this FormSet's queryset, but restricted to children of 525 579 self.instance 526 580 """ 527 kwargs = {self.fk.name: self.instance} 528 return self.model._default_manager.filter(**kwargs) 581 opts = self._meta 582 if self.instance is None: 583 return opts.model._default_manager.none() 584 queryset = super(BaseInlineFormSet, self).get_queryset(**kwargs) 585 return queryset.filter(**{self.fk.name: self.instance}) 529 586 530 587 def save_new(self, form, commit=True): 531 588 kwargs = {self.fk.get_attname(): self.instance.pk} 532 new_obj = self. model(**kwargs)589 new_obj = self._meta.model(**kwargs) 533 590 return save_instance(form, new_obj, commit=commit) 534 591 535 def get_foreign_key(parent_model, model, fk_name=None): 536 """ 537 Finds and returns the ForeignKey from model to parent if there is one. 538 If fk_name is provided, assume it is the name of the ForeignKey field. 539 """ 540 # avoid circular import 541 from django.db.models import ForeignKey 542 opts = model._meta 543 if fk_name: 544 fks_to_parent = [f for f in opts.fields if f.name == fk_name] 545 if len(fks_to_parent) == 1: 546 fk = fks_to_parent[0] 547 if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model: 548 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model)) 549 elif len(fks_to_parent) == 0: 550 raise Exception("%s has no field named '%s'" % (model, fk_name)) 551 else: 552 # Try to discover what the ForeignKey from model to parent_model is 553 fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model] 554 if len(fks_to_parent) == 1: 555 fk = fks_to_parent[0] 556 elif len(fks_to_parent) == 0: 557 raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) 558 else: 559 raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) 560 return fk 592 class InlineFormset(BaseInlineFormSet): 593 __metaclass__ = InlineFormSetMetaclass 561 594 562 def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()): 595 def inline_formset(parent_model, model, formset=InlineFormset, 596 formfield_callback=lambda f: f.formfield(), **options): 563 597 """ 564 598 Returns an ``InlineFormset`` for the given kwargs. 565 599 566 600 You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` 567 601 to ``parent_model``. 568 602 """ 569 fk = get_foreign_key(parent_model, model, fk_name=fk_name) 570 # let the formset handle object deletion by default 571 FormSet = formset_for_model(model, formset=InlineFormset, fields=fields, 572 formfield_callback=formfield_callback, 573 extra=extra, orderable=orderable, 574 deletable=deletable) 575 # HACK: remove the ForeignKey to the parent from every form 576 # This should be done a line above before we pass 'fields' to formset_for_model 577 # an 'omit' argument would be very handy here 578 try: 579 del FormSet.form_class.base_fields[fk.name] 580 except KeyError: 581 pass 582 FormSet.fk = fk 583 return FormSet 603 opts = model._meta 604 options.update({"parent_model": parent_model, "model": model}) 605 return InlineFormSetMetaclass(opts.object_name + "InlineFormset", (formset,), 606 {}, 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..f8482fb
- + 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 pass 43 44 class InlineFormSetOptions(ModelFormSetOptions): 45 def __init__(self, options=None): 46 super(InlineFormSetOptions, self).__init__(options) 47 self.parent_model = self._dynamic_attribute(options, "parent_model") 48 self.num_extra = self._dynamic_attribute(options, "num_extra", 3) 49 self.fk_name = self._dynamic_attribute(options, "fk_name") 50 self.deletable = self._dynamic_attribute(options, "deletable", True) 51 52 No newline at end of file -
tests/modeltests/model_formsets/models.py
diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py index 19bdeed..cd16046 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 Lets test the most basic use case of a ModelFormSet. This basically says give 23 me a FormSet that edits all the objects found in the Author model. 23 24 24 >>> formset = AuthorFormSet(qs) 25 >>> class AuthorFormSet(ModelFormSet): 26 ... class Meta: 27 ... model = Author 28 >>> AuthorFormSet.base_fields.keys() 29 ['name'] 30 31 Lets add on an extra field. 32 33 >>> class AuthorFormSet(ModelFormSet): 34 ... published = forms.BooleanField() 35 ... 36 ... class Meta: 37 ... model = Author 38 >>> AuthorFormSet.base_fields.keys() 39 ['name', 'published'] 40 41 Lets create a formset that is bound to a model. 42 43 >>> class AuthorFormSet(ModelFormSet): 44 ... class Meta: 45 ... model = Author 46 ... num_extra = 3 47 48 >>> formset = AuthorFormSet() 25 49 >>> for form in formset.forms: 26 50 ... print form.as_p() 27 51 <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 59 ... 'form-2-name': '', 36 60 ... } 37 61 38 >>> formset = AuthorFormSet( qs, data=data)62 >>> formset = AuthorFormSet(data) 39 63 >>> formset.is_valid() 40 64 True 41 65 … … Charles Baudelaire 49 73 50 74 51 75 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) 76 authors with an extra form to add him. When subclassing ModelFormSet you can 77 override the get_queryset method to return any queryset we like, but in this 78 case we'll use it to display it in alphabetical order by name. 79 80 >>> class AuthorFormSet(ModelFormSet): 81 ... class Meta: 82 ... model = Author 83 ... num_extra = 1 84 ... deletable = False 85 ... 86 ... def get_queryset(self, **kwargs): 87 ... queryset = super(AuthorFormSet, self).get_queryset(**kwargs) 88 ... return queryset.order_by('name') 89 90 >>> formset = AuthorFormSet() 60 91 >>> for form in formset.forms: 61 92 ... print form.as_p() 62 93 <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 104 ... 'form-2-name': 'Paul Verlaine', 74 105 ... } 75 106 76 >>> formset = AuthorFormSet( qs, data=data)107 >>> formset = AuthorFormSet(data) 77 108 >>> formset.is_valid() 78 109 True 79 110 … … Paul Verlaine 90 121 This probably shouldn't happen, but it will. If an add form was marked for 91 122 deltetion, make sure we don't save that form. 92 123 93 >>> qs = Author.objects.order_by('name') 94 >>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=True) 95 96 >>> formset = AuthorFormSet(qs) 124 >>> class AuthorFormSet(ModelFormSet): 125 ... class Meta: 126 ... model = Author 127 ... num_extra = 1 128 ... deletable = True 129 ... 130 ... def get_queryset(self, **kwargs): 131 ... queryset = super(AuthorFormSet, self).get_queryset(**kwargs) 132 ... return queryset.order_by('name') 133 134 >>> formset = AuthorFormSet() 97 135 >>> for form in formset.forms: 98 136 ... print form.as_p() 99 137 <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 155 ... 'form-3-DELETE': 'on', 118 156 ... } 119 157 120 >>> formset = AuthorFormSet( qs, data=data)158 >>> formset = AuthorFormSet(data) 121 159 >>> formset.is_valid() 122 160 True 123 161 … … Paul Verlaine 134 172 We can also create a formset that is tied to a parent model. This is how the 135 173 admin system's edit inline functionality works. 136 174 137 >>> from django.newforms.models import inline_formset 175 >>> from django.newforms.models import inline_formset, InlineFormset 176 177 >>> class AuthorBooksFormSet(InlineFormset): 178 ... class Meta: 179 ... parent_model = Author 180 ... model = Book 181 ... num_extra = 3 182 ... deletable = False 138 183 139 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=3)140 184 >>> author = Author.objects.get(name='Charles Baudelaire') 141 185 142 >>> formset = AuthorBooksFormSet( author)186 >>> formset = AuthorBooksFormSet(instance=author) 143 187 >>> for form in formset.forms: 144 188 ... print form.as_p() 145 189 <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 197 ... 'book_set-2-title': '', 154 198 ... } 155 199 156 >>> formset = AuthorBooksFormSet( author, data=data)200 >>> formset = AuthorBooksFormSet(data, instance=author) 157 201 >>> formset.is_valid() 158 202 True 159 203 … … Now that we've added a book to Charles Baudelaire, let's try adding another 169 213 one. This time though, an edit form will be available for every existing 170 214 book. 171 215 172 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=2) 216 >>> class AuthorBooksFormSet(InlineFormset): 217 ... class Meta: 218 ... parent_model = Author 219 ... model = Book 220 ... num_extra = 2 221 ... deletable = False 222 173 223 >>> author = Author.objects.get(name='Charles Baudelaire') 174 224 175 >>> formset = AuthorBooksFormSet( author)225 >>> formset = AuthorBooksFormSet(instance=author) 176 226 >>> for form in formset.forms: 177 227 ... print form.as_p() 178 228 <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 237 ... 'book_set-2-title': '', 188 238 ... } 189 239 190 >>> formset = AuthorBooksFormSet( author, data=data)240 >>> formset = AuthorBooksFormSet(data, instance=author) 191 241 >>> formset.is_valid() 192 242 True 193 243 -
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..0e89441 100644
a b class Child(models.Model): 15 15 16 16 __test__ = {'API_TESTS': """ 17 17 18 >>> from django.newforms.models import inline_formset 19 18 >>> from django.newforms.models import InlineFormset 20 19 21 20 Child has two ForeignKeys to Parent, so if we don't specify which one to use 22 21 for the inline formset, we should get an exception. 23 22 24 >>> ifs = inline_formset(Parent, Child) 23 >>> class ChildrenFormSet(InlineFormset): 24 ... class Meta: 25 ... parent_model = Parent 26 ... model = Child 25 27 Traceback (most recent call last): 26 28 ... 27 29 Exception: <class 'regressiontests.inline_formsets.models.Child'> has more than 1 ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'> 28 30 29 30 31 These two should both work without a problem. 31 32 32 >>> ifs = inline_formset(Parent, Child, fk_name='mother') 33 >>> ifs = inline_formset(Parent, Child, fk_name='father') 33 >>> class ChildrenFormSet(InlineFormset): 34 ... class Meta: 35 ... parent_model = Parent 36 ... model = Child 37 ... fk_name = "mother" 34 38 39 >>> class ChildrenFormSet(InlineFormset): 40 ... class Meta: 41 ... parent_model = Parent 42 ... model = Child 43 ... fk_name = "father" 35 44 36 45 If we specify fk_name, but it isn't a ForeignKey from the child model to the 37 46 parent model, we should get an exception. 38 47 39 >>> ifs = inline_formset(Parent, Child, fk_name='school') 48 >>> class ChildrenFormSet(InlineFormset): 49 ... class Meta: 50 ... parent_model = Parent 51 ... model = Child 52 ... fk_name = "school" 40 53 Traceback (most recent call last): 41 54 ... 42 55 Exception: fk_name 'school' is not a ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'> 43 56 44 45 57 If the field specified in fk_name is not a ForeignKey, we should get an 46 58 exception. 47 59 48 >>> ifs = inline_formset(Parent, Child, fk_name='test') 60 >>> class ChildreFormSet(InlineFormset): 61 ... class Meta: 62 ... parent_model = Parent 63 ... model = Child 64 ... fk_name = "test" 49 65 Traceback (most recent call last): 50 66 ... 51 67 Exception: <class 'regressiontests.inline_formsets.models.Child'> has no field named 'test'