Ticket #6241: formset_refactor_7125.diff
| File formset_refactor_7125.diff, 49.5 kB (added by jkocherhans, 10 months ago) |
|---|
-
a/django/contrib/admin/options.py
old new 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 … … 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 """ … … 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 """ … … 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) … … 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. … … 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.""" … … 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' … … 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()): … … 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 """ -
a/django/contrib/admin/templates/admin/edit_inline/tabular.html
old new 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 -
a/django/newforms/formsets.py
old new 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, get_declared_fields 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' … … 20 27 self.base_fields[FORM_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput) 21 28 super(ManagementForm, self).__init__(*args, **kwargs) 22 29 30 class FormSetMetaclass(type): 31 def __new__(cls, name, bases, attrs, **options): 32 attrs["base_fields"] = get_declared_fields(bases, attrs) 33 attrs["_meta"] = FormSetOptions(options and options or attrs.get("Meta", None)) 34 return type.__new__(cls, name, bases, attrs) 35 23 36 class BaseFormSet(object): 24 37 """A collection of instances of the same Form class.""" 25 38 … … 37 50 self.management_form = ManagementForm(data, files, auto_id=self.auto_id, prefix=self.prefix) 38 51 if self.management_form.is_valid(): 39 52 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_extra53 self.required_forms = self.total_forms - self._meta.num_extra 54 self.change_form_count = self.total_forms - self._meta.num_extra 42 55 else: 43 56 # not sure that ValidationError is the best thing to raise here 44 57 raise ValidationError('ManagementForm data is missing or has been tampered with') 45 58 elif initial: 46 59 self.change_form_count = len(initial) 47 60 self.required_forms = len(initial) 48 self.total_forms = self.required_forms + self. num_extra61 self.total_forms = self.required_forms + self._meta.num_extra 49 62 self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix) 50 63 else: 51 64 self.change_form_count = 0 52 65 self.required_forms = 0 53 self.total_forms = self. num_extra66 self.total_forms = self._meta.num_extra 54 67 self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix) 55 68 56 69 def _get_add_forms(self): 57 70 """Return a list of all the add forms in this ``FormSet``.""" 58 FormClass = self.form_class59 71 if not hasattr(self, '_add_forms'): 60 72 add_forms = [] 61 73 for i in range(self.change_form_count, self.total_forms): … … 64 76 kwargs['data'] = self.data 65 77 if self.files: 66 78 kwargs['files'] = self.files 67 add_form = FormClass(**kwargs)79 add_form = self.get_form_class()(**kwargs) 68 80 self.add_fields(add_form, i) 69 81 add_forms.append(add_form) 70 82 self._add_forms = add_forms … … 73 85 74 86 def _get_change_forms(self): 75 87 """Return a list of all the change forms in this ``FormSet``.""" 76 FormClass = self.form_class77 88 if not hasattr(self, '_change_forms'): 78 89 change_forms = [] 79 90 for i in range(0, self.change_form_count): … … 84 95 kwargs['files'] = self.files 85 96 if self.initial: 86 97 kwargs['initial'] = self.initial[i] 87 change_form = FormClass(**kwargs)98 change_form = self.get_form_class(change=True)(**kwargs) 88 99 self.add_fields(change_form, i) 89 100 change_forms.append(change_form) 90 self._change_forms = change_forms101 self._change_forms = change_forms 91 102 return self._change_forms 92 103 change_forms = property(_get_change_forms) 93 104 … … 117 128 # Process change forms 118 129 for form in self.change_forms: 119 130 if form.is_valid(): 120 if self. deletable and form.cleaned_data[DELETION_FIELD_NAME]:131 if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 121 132 self.deleted_data.append(form.cleaned_data) 122 133 else: 123 134 self.cleaned_data.append(form.cleaned_data) … … 144 155 add_errors.reverse() 145 156 errors.extend(add_errors) 146 157 # Sort cleaned_data if the formset is orderable. 147 if self. orderable:158 if self._meta.orderable: 148 159 self.cleaned_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME]) 149 160 # Give self.clean() a chance to do validation 150 161 try: … … 167 178 via formset.non_form_errors() 168 179 """ 169 180 return self.cleaned_data 181 182 def get_form_class(self, change=False): 183 """ 184 A hook to change a form class object. 185 """ 186 FormClass = self._meta.form 187 FormClass.base_fields = self.base_fields 188 return FormClass 170 189 171 190 def add_fields(self, form, index): 172 191 """A hook for adding extra fields on to each form instance.""" 173 if self. orderable:192 if self._meta.orderable: 174 193 form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1) 175 if self. deletable:194 if self._meta.deletable: 176 195 form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False) 177 196 178 197 def add_prefix(self, index): … … 192 211 else: 193 212 return Media() 194 213 media = property(_get_media) 195 196 def formset_for_form(form, formset=BaseFormSet, num_extra=1, orderable=False, deletable=False):197 """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 214 215 class FormSet(BaseFormSet): 216 __metaclass__ = FormSetMetaclass 217 218 def formset_for_form(form, formset=FormSet, num_extra=1, orderable=False, 219 deletable=False): 220 """Return a FormSet for the given form class.""" 221 warn("formset_for_form is deprecated, use FormSet instead.", 222 PendingDeprecationWarning, 223 stacklevel=3) 224 return BaseFormSetMetaclass( 225 form.__name__ + "FormSet", (formset,), form.base_fields, 226 form=form, num_extra=num_extra, orderable=orderable, 227 deletable=deletable) 228 201 229 def all_valid(formsets): 202 230 """Returns true if every formset in formsets is valid.""" 203 231 valid = True -
a/django/newforms/models.py
old new 13 13 from util import ValidationError, ErrorList 14 14 from forms import BaseForm, get_declared_fields 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 … … 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 217 211 class ModelFormMetaclass(type): 212 opts_class = ModelFormOptions 213 218 214 def __new__(cls, name, bases, attrs, 219 formfield_callback=lambda f: f.formfield() ):215 formfield_callback=lambda f: f.formfield(), **options): 220 216 try: 221 parents = [b for b in bases if issubclass(b, ModelForm)] 217 # XXX: The 3 issubclass checks here works, but calling this fragile 218 # is being terribly nice. ModelFormSet and InlineFormSet could 219 # possiblly inherit from ModelForm, but that seems wrong. More 220 # thought needed. 221 parents = [b for b in bases if issubclass(b, ModelForm) or issubclass(b, ModelFormSet) or issubclass(b, InlineFormSet)] 222 222 except NameError: 223 223 # We are defining ModelForm itself. 224 224 parents = None … … 228 228 229 229 new_class = type.__new__(cls, name, bases, attrs) 230 230 declared_fields = get_declared_fields(bases, attrs, False) 231 opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))231 opts = new_class._meta = cls.opts_class(options and options or getattr(new_class, 'Meta', None)) 232 232 if opts.model: 233 233 # If a model is defined, extract form fields from it. 234 234 fields = fields_for_model(opts.model, opts.fields, … … 276 276 class ModelForm(BaseModelForm): 277 277 __metaclass__ = ModelFormMetaclass 278 278 279 def modelform_for_model(model, form=ModelForm, 280 formfield_callback=lambda f: f.formfield(), **options): 281 opts = model._meta 282 options.update({"model": model}) 283 return ModelFormMetaclass(opts.object_name + "ModelForm", (form,), 284 {}, formfield_callback, **options) 285 279 286 280 287 # Fields ##################################################################### 281 288 … … 390 397 391 398 # Model-FormSet integration ################################################### 392 399 393 def initial_data(instance, fields=None): 394 """ 395 Return a dictionary from data in ``instance`` that is suitable for 396 use as a ``Form`` constructor's ``initial`` argument. 397 398 Provide ``fields`` to specify the names of specific fields to return. 399 All field values in the instance will be returned if ``fields`` is not 400 provided. 401 """ 402 # avoid a circular import 403 from django.db.models.fields.related import ManyToManyField 404 opts = instance._meta 405 initial = {} 406 for f in opts.fields + opts.many_to_many: 407 if not f.editable: 408 continue 409 if fields and not f.name in fields: 410 continue 411 if isinstance(f, ManyToManyField): 412 # MultipleChoiceWidget needs a list of ints, not object instances. 413 initial[f.name] = [obj.pk for obj in f.value_from_object(instance)] 414 else: 415 initial[f.name] = f.value_from_object(instance) 416 return initial 400 class ModelFormSetMetaclass(ModelFormMetaclass): 401 opts_class = ModelFormSetOptions 417 402 418 403 class BaseModelFormSet(BaseFormSet): 419 404 """ 420 405 A ``FormSet`` for editing a queryset and/or adding new objects to it. 421 406 """ 422 model = None423 queryset = None424 407 425 def __init__(self, qs, data=None, files=None, auto_id='id_%s', prefix=None): 408 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 409 queryset=None): 426 410 kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix} 427 self.queryset = qs 428 kwargs['initial'] = [initial_data(obj) for obj in qs] 411 opts = self._meta 412 if queryset is None: 413 self.queryset = self.get_queryset(**kwargs) 414 else: 415 self.queryset = queryset 416 initial_data = [] 417 for obj in self.queryset: 418 initial_data.append(model_to_dict(obj, opts.fields, opts.exclude)) 419 kwargs['initial'] = initial_data 429 420 super(BaseModelFormSet, self).__init__(**kwargs) 421 422 def get_queryset(self, **kwargs): 423 """ 424 Hook to returning a queryset for this model. 425 """ 426 return self._meta.model._default_manager.all() 430 427 431 428 def save_new(self, form, commit=True): 432 429 """Saves and returns a new model instance for the given form.""" 433 return save_instance(form, self. model(), commit=commit)430 return save_instance(form, self._meta.model(), commit=commit) 434 431 435 432 def save_instance(self, form, instance, commit=True): 436 433 """Saves and returns an existing model instance for the given form.""" … … 445 442 def save_existing_objects(self, commit=True): 446 443 if not self.queryset: 447 444 return [] 448 # Put the objects from self. get_queryset into a dict so they are easy to lookup by pk445 # Put the objects from self.queryset into a dict so they are easy to lookup by pk 449 446 existing_objects = {} 447 opts = self._meta 450 448 for obj in self.queryset: 451 449 existing_objects[obj.pk] = obj 452 450 saved_instances = [] 453 451 for form in self.change_forms: 454 obj = existing_objects[form.cleaned_data[ self.model._meta.pk.attname]]455 if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:452 obj = existing_objects[form.cleaned_data[opts.model._meta.pk.attname]] 453 if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 456 454 obj.delete() 457 455 else: 458 456 saved_instances.append(self.save_instance(form, obj, commit=commit)) … … 460 458 461 459 def save_new_objects(self, commit=True): 462 460 new_objects = [] 461 opts = self._meta 463 462 for form in self.add_forms: 464 463 if form.is_empty(): 465 464 continue 466 465 # If someone has marked an add form for deletion, don't save the 467 466 # object. At some point it would be nice if we didn't display 468 467 # the deletion widget for add forms. 469 if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:468 if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 470 469 continue 471 470 new_objects.append(self.save_new(form, commit=commit)) 472 471 return new_objects 473 472 474 473 def add_fields(self, form, index): 475 474 """Add a hidden field for the object's primary key.""" 476 self._pk_field_name = self. model._meta.pk.attname475 self._pk_field_name = self._meta.model._meta.pk.attname 477 476 form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput) 478 477 super(BaseModelFormSet, self).add_fields(form, index) 479 478 480 def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(), 481 formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None): 479 class ModelFormSet(BaseModelFormSet): 480 __metaclass__ = ModelFormSetMetaclass 481 482 def formset_for_model(model, formset=BaseModelFormSet, 483 formfield_callback=lambda f: f.formfield(), **options): 482 484 """ 483 485 Returns a FormSet class for the given Django model class. This FormSet 484 486 will contain change forms for every instance of the given model as well … … 487 489 This is essentially the same as ``formset_for_queryset``, but automatically 488 490 uses the model's default manager to determine the queryset. 489 491 """ 490 form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback) 491 FormSet = formset_for_form(form, formset, extra, orderable, deletable) 492 FormSet.model = model 493 return FormSet 492 opts = model._meta 493 options.update({"model": model}) 494 return ModelFormSetMetaclass(opts.object_name + "ModelFormSet", (formset,), 495 {}, formfield_callback, **options) 496 497 class InlineFormSetMetaclass(ModelFormSetMetaclass): 498 opts_class = InlineFormSetOptions 499 500 def __new__(cls, name, bases, attrs, 501 formfield_callback=lambda f: f.formfield(), **options): 502 formset = super(InlineFormSetMetaclass, cls).__new__(cls, name, bases, attrs, 503 formfield_callback, **options) 504 # If this isn't a subclass of InlineFormSet, don't do anything special. 505 try: 506 if not filter(lambda b: issubclass(b, InlineFormSet), bases): 507 return formset 508 except NameError: 509 # 'InlineFormSet' isn't defined yet, meaning we're looking at 510 # Django's own InlineFormSet class, defined below. 511 return formset 512 opts = formset._meta 513 # resolve the foreign key 514 fk = cls.resolve_foreign_key(opts.parent_model, opts.model, opts.fk_name) 515 # remove the fk from base_fields to keep it transparent to the form. 516 try: 517 del formset.base_fields[fk.name] 518 except KeyError: 519 pass 520 formset.fk = fk 521 return formset 522 523 def _resolve_foreign_key(cls, parent_model, model, fk_name=None): 524 """ 525 Finds and returns the ForeignKey from model to parent if there is one. 526 If fk_name is provided, assume it is the name of the ForeignKey field. 527 """ 528 # avoid a circular import 529 from django.db.models import ForeignKey 530 opts = model._meta 531 if fk_name: 532 fks_to_parent = [f for f in opts.fields if f.name == fk_name] 533 if len(fks_to_parent) == 1: 534 fk = fks_to_parent[0] 535 if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model: 536 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model)) 537 elif len(fks_to_parent) == 0: 538 raise Exception("%s has no field named '%s'" % (model, fk_name)) 539 else: 540 # Try to discover what the ForeignKey from model to parent_model is 541 fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model] 542 if len(fks_to_parent) == 1: 543 fk = fks_to_parent[0] 544 elif len(fks_to_parent) == 0: 545 raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) 546 else: 547 raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) 548 return fk 549 resolve_foreign_key = classmethod(_resolve_foreign_key) 494 550 495 class InlineFormset(BaseModelFormSet):551 class BaseInlineFormSet(BaseModelFormSet): 496 552 """A formset for child objects related to a parent.""" 497 def __init__(self, instance, data=None, files=None):553 def __init__(self, *args, **kwargs): 498 554 from django.db.models.fields.related import RelatedObject 499 self.instance = instance 555 opts = self._meta 556 self.instance = kwargs.pop("instance", None) 500 557 # is there a better way to get the object descriptor? 501 self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()502 qs = self.get_queryset()503 super( InlineFormset, self).__init__(qs, data, files, prefix=self.rel_name)558 rel_name = RelatedObject(self.fk.rel.to, opts.model, self.fk).get_accessor_name() 559 kwargs["prefix"] = rel_name 560 super(BaseInlineFormSet, self).__init__(*args, **kwargs) 504 561 505 def get_queryset(self ):562 def get_queryset(self, **kwargs): 506 563 """ 507 564 Returns this FormSet's queryset, but restricted to children of 508 565 self.instance 509 566 """ 510 kwargs = {self.fk.name: self.instance} 511 return self.model._default_manager.filter(**kwargs) 567 opts = self._meta 568 if self.instance is None: 569 return opts.model._default_manager.none() 570 queryset = super(BaseInlineFormSet, self).get_queryset(**kwargs) 571 return queryset.filter(**{self.fk.name: self.instance}) 512 572 513 573 def save_new(self, form, commit=True): 514 574 kwargs = {self.fk.get_attname(): self.instance.pk} 515 new_obj = self. model(**kwargs)575 new_obj = self._meta.model(**kwargs) 516 576 return save_instance(form, new_obj, commit=commit) 517 577 518 def get_foreign_key(parent_model, model, fk_name=None): 519 """ 520 Finds and returns the ForeignKey from model to parent if there is one. 521 If fk_name is provided, assume it is the name of the ForeignKey field. 522 """ 523 # avoid circular import 524 from django.db.models import ForeignKey 525 opts = model._meta 526 if fk_name: 527 fks_to_parent = [f for f in opts.fields if f.name == fk_name] 528 if len(fks_to_parent) == 1: 529 fk = fks_to_parent[0] 530 if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model: 531 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model)) 532 elif len(fks_to_parent) == 0: 533 raise Exception("%s has no field named '%s'" % (model, fk_name)) 534 else: 535 # Try to discover what the ForeignKey from model to parent_model is 536 fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model] 537 if len(fks_to_parent) == 1: 538 fk = fks_to_parent[0] 539 elif len(fks_to_parent) == 0: 540 raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) 541 else: 542 raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) 543 return fk 578 class InlineFormSet(BaseInlineFormSet): 579 __metaclass__ = InlineFormSetMetaclass 544 580 545 def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()): 581 def inline_formset(parent_model, model, formset=InlineFormSet, 582 formfield_callback=lambda f: f.formfield(), **options): 546 583 """ 547 Returns an ``InlineForm set`` for the given kwargs.584 Returns an ``InlineFormSet`` for the given kwargs. 548 585 549 586 You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` 550 587 to ``parent_model``. 551 588 """ 552 fk = get_foreign_key(parent_model, model, fk_name=fk_name) 553 # let the formset handle object deletion by default 554 FormSet = formset_for_model(model, formset=InlineFormset, fields=fields, 555 formfield_callback=formfield_callback, 556 extra=extra, orderable=orderable, 557 deletable=deletable) 558 # HACK: remove the ForeignKey to the parent from every form 559 # This should be done a line above before we pass 'fields' to formset_for_model 560 # an 'omit' argument would be very handy here 561 try: 562 del FormSet.form_class.base_fields[fk.name] 563 except KeyError: 564 pass 565 FormSet.fk = fk 566 return FormSet 589 opts = model._meta 590 options.update({"parent_model": parent_model, "model": model}) 591 return InlineFormSetMetaclass(opts.object_name + "InlineFormSet", (formset,), 592 {}, formfield_callback, **options) -
a/tests/modeltests/model_formsets/models.py
old new 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> … … 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 … … 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> … … 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 … … 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> … … 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 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> … … 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 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> … … 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 -
a/tests/regressiontests/forms/formsets.py
old new 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, … … 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" /&
