Ticket #6241: formset_refactor_6_tests_fail.diff
File formset_refactor_6_tests_fail.diff, 47.5 KB (added by , 17 years ago) |
---|
-
django/contrib/admin/options.py
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 """ … … 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 """ … … 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) … … 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. … … 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 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 768 767 769 class StackedInline(InlineModelAdmin): 768 770 template = 'admin/edit_inline/stacked.html' 769 771 … … 778 780 self.opts = inline 779 781 self.formset = formset 780 782 self.fieldsets = fieldsets 783 # place orderable and deletable here since _meta is inaccesible in the 784 # templates. 785 self.orderable = formset._meta.orderable 786 self.deletable = formset._meta.deletable 781 787 782 788 def __iter__(self): 783 789 for form, original in zip(self.formset.change_forms, self.formset.get_queryset()): … … 787 793 788 794 def fields(self): 789 795 for field_name in flatten_fieldsets(self.fieldsets): 790 yield self.formset. form_class.base_fields[field_name]796 yield self.formset.base_fields[field_name] 791 797 792 798 class InlineAdminForm(AdminForm): 793 799 """ -
django/contrib/admin/templates/admin/edit_inline/tabular.html
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/options.py
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 51 No newline at end of file -
django/newforms/formsets.py
1 from forms import Form 2 from fields import IntegerField, BooleanField 1 from warnings import warn 2 3 from django.utils.datastructures import SortedDict 4 from django.utils.translation import ugettext_lazy as _ 5 6 from forms import BaseForm, Form 7 from fields import Field, IntegerField, BooleanField 8 from options import FormSetOptions 9 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' … … 19 26 def __init__(self, *args, **kwargs): 20 27 self.base_fields[FORM_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput) 21 28 super(ManagementForm, self).__init__(*args, **kwargs) 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)) 22 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 … … 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): … … 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 … … 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): … … 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 … … 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) … … 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: … … 168 191 """ 169 192 return self.cleaned_data 170 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 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): … … 193 224 return Media() 194 225 media = property(_get_media) 195 226 196 def formset_for_form(form, formset=BaseFormSet, num_extra=1, orderable=False, deletable=False): 227 class FormSet(BaseFormSet): 228 __metaclass__ = BaseFormSetMetaclass 229 230 def formset_for_form(form, formset=FormSet, num_extra=1, orderable=False, 231 deletable=False): 232 197 233 """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) 234 warn("formset_for_form is deprecated, use FormSet instead.", 235 PendingDeprecationWarning, 236 stacklevel=3) 237 return BaseFormSetMetaclass( 238 form.__name__ + "FormSet", (formset,), form.base_fields, 239 form=form, num_extra=num_extra, orderable=orderable, 240 deletable=deletable) 200 241 201 242 def all_valid(formsets): 202 243 """Returns true if every formset in formsets is valid.""" -
django/newforms/models.py
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 ) … … 207 209 field_list.append((f.name, formfield)) 208 210 return SortedDict(field_list) 209 211 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 212 class ModelFormMetaclass(type): 213 214 opts_class = ModelFormOptions 215 217 216 def __new__(cls, name, bases, attrs, 218 formfield_callback=lambda f: f.formfield() ):217 formfield_callback=lambda f: f.formfield(), **options): 219 218 fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] 220 219 fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) 221 220 … … 227 226 fields = base.base_fields.items() + fields 228 227 declared_fields = SortedDict(fields) 229 228 230 opts = ModelFormOptions(attrs.get('Meta', None))229 opts = cls.opts_class(options and options or attrs.get('Meta', None)) 231 230 attrs['_meta'] = opts 232 231 233 232 # Don't allow more than one Meta model defenition in bases. The fields … … 293 292 class ModelForm(BaseModelForm): 294 293 __metaclass__ = ModelFormMetaclass 295 294 295 # this should really be named form_for_model. 296 def modelform_for_model(model, form=ModelForm, 297 formfield_callback=lambda f: f.formfield(), **options): 298 opts = model._meta 299 options.update({"model": model}) 300 return ModelFormMetaclass(opts.object_name + "ModelForm", (form,), 301 {}, formfield_callback, **options) 296 302 297 303 # Fields ##################################################################### 298 304 … … 407 413 408 414 # Model-FormSet integration ################################################### 409 415 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. 416 class ModelFormSetMetaclass(ModelFormMetaclass): 417 opts_class = ModelFormSetOptions 414 418 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 not417 provided.418 """419 # avoid a circular import420 from django.db.models.fields.related import ManyToManyField421 opts = instance._meta422 initial = {}423 for f in opts.fields + opts.many_to_many:424 if not f.editable:425 continue426 if fields and not f.name in fields:427 continue428 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 initial434 435 419 class BaseModelFormSet(BaseFormSet): 436 420 """ 437 421 A ``FormSet`` for editing a queryset and/or adding new objects to it. … … 439 423 model = None 440 424 queryset = None 441 425 442 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): 443 427 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] 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 434 446 435 super(BaseModelFormSet, self).__init__(**kwargs) 447 436 437 def get_queryset(self, **kwargs): 438 """ 439 Hook to returning a queryset for this model. 440 """ 441 return self._meta.model._default_manager.all() 442 448 443 def save_new(self, form, commit=True): 449 444 """Saves and returns a new model instance for the given form.""" 450 return save_instance(form, self. model(), commit=commit)445 return save_instance(form, self._meta.model(), commit=commit) 451 446 452 447 def save_instance(self, form, instance, commit=True): 453 448 """Saves and returns an existing model instance for the given form.""" 454 return save_instance(form, instance, commit=commit)449 return save_instance(form, self._meta.model(), commit=commit) 455 450 456 451 def save(self, commit=True): 457 452 """Saves model instances for every form, adding and changing instances … … 464 459 return [] 465 460 # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk 466 461 existing_objects = {} 462 opts = self._meta 467 463 for obj in self.queryset: 468 464 existing_objects[obj.pk] = obj 469 465 saved_instances = [] 470 466 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]:467 obj = existing_objects[form.cleaned_data[opts.model._meta.pk.attname]] 468 if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 473 469 obj.delete() 474 470 else: 475 471 saved_instances.append(self.save_instance(form, obj, commit=commit)) … … 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 opts = self._meta 483 if opts.self.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 487 484 continue 488 485 new_objects.append(self.save_new(form, commit=commit)) 489 486 return new_objects 490 487 491 488 def add_fields(self, form, index): 492 489 """Add a hidden field for the object's primary key.""" 493 self._pk_field_name = self. model._meta.pk.attname490 self._pk_field_name = self._meta.model._meta.pk.attname 494 491 form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput) 495 492 super(BaseModelFormSet, self).add_fields(form, index) 496 493 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): 494 class ModelFormSet(BaseModelFormSet): 495 __metaclass__ = ModelFormSetMetaclass 496 497 def formset_for_model(model, formset=BaseModelFormSet, 498 formfield_callback=lambda f: f.formfield(), **options): 499 499 """ 500 500 Returns a FormSet class for the given Django model class. This FormSet 501 501 will contain change forms for every instance of the given model as well … … 504 504 This is essentially the same as ``formset_for_queryset``, but automatically 505 505 uses the model's default manager to determine the queryset. 506 506 """ 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 FormSet507 opts = model._meta 508 options.update({"model": model}) 509 return ModelFormSetMetaclass(opts.object_name + "ModelFormSet", (formset,), 510 {}, **options) 511 511 512 class InlineFormset(BaseModelFormSet): 512 class InlineFormSetMetaclass(ModelFormSetMetaclass): 513 opts_class = InlineFormSetOptions 514 515 def __new__(cls, name, bases, attrs, 516 formfield_callback=lambda f: f.formfield(), **options): 517 formset = super(InlineFormSetMetaclass, cls).__new__(cls, name, bases, attrs, 518 formfield_callback, **options) 519 # If this isn't a subclass of InlineFormset, don't do anything special. 520 try: 521 if not filter(lambda b: issubclass(b, InlineFormset), bases): 522 return formset 523 except NameError: 524 # 'InlineFormset' isn't defined yet, meaning we're looking at 525 # Django's own InlineFormset class, defined below. 526 return formset 527 opts = formset._meta 528 # resolve the foreign key 529 fk = cls.resolve_foreign_key(opts.parent_model, opts.model, opts.fk_name) 530 # remove the fk from base_fields to keep it transparent to the form. 531 try: 532 del formset.base_fields[fk.name] 533 except KeyError: 534 pass 535 formset.fk = fk 536 return formset 537 538 def _resolve_foreign_key(cls, parent_model, model, fk_name=None): 539 """ 540 Finds and returns the ForeignKey from model to parent if there is one. 541 If fk_name is provided, assume it is the name of the ForeignKey field. 542 """ 543 # avoid a circular import 544 from django.db.models import ForeignKey 545 opts = model._meta 546 if fk_name: 547 fks_to_parent = [f for f in opts.fields if f.name == fk_name] 548 if len(fks_to_parent) == 1: 549 fk = fks_to_parent[0] 550 if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model: 551 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model)) 552 elif len(fks_to_parent) == 0: 553 raise Exception("%s has no field named '%s'" % (model, fk_name)) 554 else: 555 # Try to discover what the ForeignKey from model to parent_model is 556 fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model] 557 if len(fks_to_parent) == 1: 558 fk = fks_to_parent[0] 559 elif len(fks_to_parent) == 0: 560 raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) 561 else: 562 raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) 563 return fk 564 resolve_foreign_key = classmethod(_resolve_foreign_key) 565 566 class BaseInlineFormSet(BaseModelFormSet): 513 567 """A formset for child objects related to a parent.""" 514 def __init__(self, instance, data=None, files=None):568 def __init__(self, *args, **kwargs): 515 569 from django.db.models.fields.related import RelatedObject 516 self.instance = instance 570 opts = self._meta 571 self.instance = kwargs.pop("instance", None) 517 572 # 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)573 rel_name = RelatedObject(self.fk.rel.to, opts.model, self.fk).get_accessor_name() 574 kwargs["prefix"] = rel_name 575 super(BaseInlineFormSet, self).__init__(*args, **kwargs) 521 576 522 def get_queryset(self ):577 def get_queryset(self, **kwargs): 523 578 """ 524 579 Returns this FormSet's queryset, but restricted to children of 525 580 self.instance 526 581 """ 527 kwargs = {self.fk.name: self.instance}528 return self.model._default_manager.filter(**kwargs)582 queryset = super(BaseInlineFormSet, self).get_queryset(**kwargs) 583 return queryset.filter(**{self.fk.name: self.instance}) 529 584 530 585 def save_new(self, form, commit=True): 531 586 kwargs = {self.fk.get_attname(): self.instance.pk} 532 new_obj = self. model(**kwargs)587 new_obj = self._meta.model(**kwargs) 533 588 return save_instance(form, new_obj, commit=commit) 589 590 class InlineFormset(BaseInlineFormSet): 591 __metaclass__ = InlineFormSetMetaclass 534 592 535 def get_foreign_key(parent_model, model, fk_name=None): 593 def inline_formset(parent_model, model, formset=InlineFormset, 594 formfield_callback=lambda f: f.formfield(), **options): 536 595 """ 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 import541 from django.db.models import ForeignKey542 opts = model._meta543 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 is553 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 fk561 562 def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()):563 """564 596 Returns an ``InlineFormset`` for the given kwargs. 565 597 566 598 You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` 567 599 to ``parent_model``. 568 600 """ 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 601 opts = model._meta 602 options.update({"parent_model": parent_model, "model": model}) 603 return InlineFormSetMetaclass(opts.object_name + "InlineFormset", (formset,)) -
tests/modeltests/model_formsets/models.py
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> … … 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 … … 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 themin alphabetical order by name.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. 55 78 56 >>> qs = Author.objects.order_by('name') 57 >>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=False) 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') 58 88 59 >>> formset = AuthorFormSet( qs)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> … … 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 … … 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) 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') 95 132 96 >>> formset = AuthorFormSet(qs) 133 >>> formset = AuthorFormSet() 134 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> … … 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 … … 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 138 176 139 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=3) 177 >>> class AuthorBooksFormSet(InlineFormset): 178 ... class Meta: 179 ... parent_model = Author 180 ... model = Book 181 ... num_extra = 3 182 ... deletable = False 183 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> … … 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 … … 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 223 173 224 >>> author = Author.objects.get(name='Charles Baudelaire') 174 225 175 >>> formset = AuthorBooksFormSet( author)226 >>> formset = AuthorBooksFormSet(instance=author) 176 227 >>> for form in formset.forms: 177 228 ... print form.as_p() 178 229 <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> … … 187 238 ... 'book_set-2-title': '', 188 239 ... } 189 240 190 >>> formset = AuthorBooksFormSet( author, data=data)241 >>> formset = AuthorBooksFormSet(data, instance=author) 191 242 >>> formset.is_valid() 192 243 True 193 244 -
tests/regressiontests/forms/formsets.py
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 >>> from django.newforms import Form, CharField, IntegerField, ValidationError9 >>> from django.newforms.formsets import formset_for_form, BaseFormSet 8 >>> from django.newforms import BooleanField 9 >>> from django.newforms.formsets import formset_for_form, BaseFormSet, FormSet 10 10 11 >>> class Choice (Form):11 >>> class ChoiceFormSet(FormSet): 12 12 ... choice = CharField() 13 13 ... votes = IntegerField() 14 14 15 >>> ChoiceFormSet = formset_for_form(Choice)16 17 18 15 A FormSet constructor takes the same arguments as Form. Let's create a FormSet 19 16 for adding data. By default, it displays 1 blank form. It can display more, 20 17 but we'll look at how to do so later. … … 145 142 >>> formset.errors 146 143 [{'votes': [u'This field is required.'], 'choice': [u'This field is required.']}] 147 144 145 # Subclassing a FormSet class ################################################# 148 146 147 We can subclass a FormSet to add addition fields to an already exisiting 148 FormSet. 149 150 >>> class SecondChoiceFormSet(ChoiceFormSet): 151 ... is_public = BooleanField() 152 153 >>> formset = SecondChoiceFormSet(auto_id=False, prefix="choices") 154 >>> for form in formset.forms: 155 ... print form.as_ul() 156 <li>Choice: <input type="text" name="choices-0-choice" /></li> 157 <li>Votes: <input type="text" name="choices-0-votes" /></li> 158 <li>Is public: <input type="checkbox" name="choices-0-is_public" /></li> 159 149 160 # Displaying more than 1 blank form ########################################### 150 161 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.162 We can also display more than 1 empty form at a time. To do so, create an inner 163 Meta class with an attribute num_extra. 153 164 154 >>> ChoiceFormSet = formset_for_form(Choice, num_extra=3) 165 >>> class NumExtraChoiceFormSet(ChoiceFormSet): 166 ... class Meta: 167 ... num_extra = 3 155 168 156 >>> formset = ChoiceFormSet(auto_id=False, prefix='choices')169 >>> formset = NumExtraChoiceFormSet(auto_id=False, prefix='choices') 157 170 >>> for form in formset.forms: 158 171 ... print form.as_ul() 159 172 <li>Choice: <input type="text" name="choices-0-choice" /></li> … … 177 190 ... 'choices-2-votes': '', 178 191 ... } 179 192 180 >>> formset = ChoiceFormSet(data,auto_id=False, prefix='choices')193 >>> formset = NumExtraChoiceFormSet(auto_id=False, prefix='choices') 181 194 >>> formset.is_valid() 182 195 True 183 196 >>> formset.cleaned_data … … 196 209 ... 'choices-2-votes': '', 197 210 ... } 198 211 199 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')212 >>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices') 200 213 >>> formset.is_valid() 201 214 True 202 215 >>> formset.cleaned_data … … 215 228 ... 'choices-2-votes': '', 216 229 ... } 217 230 218 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')231 >>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices') 219 232 >>> formset.is_valid() 220 233 False 221 234 >>> formset.errors … … 226 239 data. 227 240 228 241 >>> initial = [{'choice': u'Calexico', 'votes': 100}] 229 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')242 >>> formset = NumExtraChoiceFormSet(initial=initial, auto_id=False, prefix='choices') 230 243 >>> for form in formset.forms: 231 244 ... print form.as_ul() 232 245 <li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> … … 254 267 ... 'choices-3-votes': '', 255 268 ... } 256 269 257 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')270 >>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices') 258 271 >>> formset.is_valid() 259 272 False 260 273 >>> formset.errors … … 263 276 264 277 # FormSets with deletion ###################################################### 265 278 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_data279 We can easily add deletion ability to a FormSet by setting deletable to True 280 in the inner Meta class. This will add a boolean field to each form instance. 281 When that boolean field is True, the cleaned data will be in formset.deleted_data 269 282 rather than formset.cleaned_data 270 283 271 >>> ChoiceFormSet = formset_for_form(Choice, deletable=True) 284 >>> class DeletableChoiceFormSet(ChoiceFormSet): 285 ... class Meta: 286 ... deletable = True 272 287 273 288 >>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}] 274 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')289 >>> formset = DeletableChoiceFormSet(data, auto_id=False, prefix='choices') 275 290 >>> for form in formset.forms: 276 291 ... print form.as_ul() 277 292 <li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> … … 300 315 ... 'choices-2-DELETE': '', 301 316 ... } 302 317 303 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')318 >>> formset = DeletableChoiceFormSet(data, auto_id=False, prefix='choices') 304 319 >>> formset.is_valid() 305 320 True 306 321 >>> formset.cleaned_data … … 310 325 311 326 # FormSets with ordering ###################################################### 312 327 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. When328 We can also add ordering ability to a FormSet by setting orderable to True in 329 the inner Meta class. This will add a integer field to each form instance. When 315 330 form validation succeeds, formset.cleaned_data will have the data in the correct 316 331 order specified by the ordering fields. If a number is duplicated in the set 317 332 of ordering fields, for instance form 0 and form 3 are both marked as 1, then 318 333 the form index used as a secondary ordering criteria. In order to put 319 334 something at the front of the list, you'd need to set it's order to 0. 320 335 321 >>> ChoiceFormSet = formset_for_form(Choice, orderable=True) 336 >>> class OrderableChoiceFormSet(ChoiceFormSet): 337 ... class Meta: 338 ... orderable = True 322 339 323 340 >>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}] 324 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')341 >>> formset = OrderableChoiceFormSet(initial=initial, auto_id=False, prefix='choices') 325 342 >>> for form in formset.forms: 326 343 ... print form.as_ul() 327 344 <li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> … … 347 364 ... 'choices-2-ORDER': '0', 348 365 ... } 349 366 350 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')367 >>> formset = OrderableChoiceFormSet(data, auto_id=False, prefix='choices') 351 368 >>> formset.is_valid() 352 369 True 353 370 >>> for cleaned_data in formset.cleaned_data: … … 359 376 # FormSets with ordering + deletion ########################################### 360 377 361 378 Let's try throwing ordering and deletion into the same form. 379 TODO: Perhaps handle Meta class inheritance so you can subclass 380 OrderableChoiceFormSet and DeletableChoiceFormSet? 362 381 363 >>> ChoiceFormSet = formset_for_form(Choice, orderable=True, deletable=True) 382 >>> class MixedChoiceFormSet(ChoiceFormSet): 383 ... class Meta: 384 ... orderable = True 385 ... deletable = True 364 386 387 >>> formset = MixedChoiceFormSet(initial=initial, auto_id=False, prefix='choices') 365 388 >>> initial = [ 366 389 ... {'choice': u'Calexico', 'votes': 100}, 367 390 ... {'choice': u'Fergie', 'votes': 900}, 368 391 ... {'choice': u'The Decemberists', 'votes': 500}, 369 392 ... ] 370 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')393 >>> formset = MixedChoiceFormSet(initial=initial, auto_id=False, prefix='choices') 371 394 >>> for form in formset.forms: 372 395 ... print form.as_ul() 373 396 <li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> … … 409 432 ... 'choices-3-DELETE': '', 410 433 ... } 411 434 412 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')435 >>> formset = MixedChoiceFormSet(data, auto_id=False, prefix='choices') 413 436 >>> formset.is_valid() 414 437 True 415 438 >>> for cleaned_data in formset.cleaned_data: … … 428 451 Let's define a FormSet that takes a list of favorite drinks, but raises am 429 452 error if there are any duplicates. 430 453 431 >>> class FavoriteDrinkForm(Form ):454 >>> class FavoriteDrinkForm(FormSet): 432 455 ... name = CharField() 433 456 ... 434 435 >>> class FavoriteDrinksFormSet(BaseFormSet): 436 ... form_class = FavoriteDrinkForm 437 ... num_extra = 2 438 ... orderable = False 439 ... deletable = False 457 ... class Meta: 458 ... num_extra = 2 459 ... orderable = False 460 ... deletable = False 440 461 ... 441 462 ... def clean(self): 442 463 ... seen_drinks = [] -
tests/regressiontests/inline_formsets/models.py
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'> … … 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'> … … 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'