Ticket #6241: formset_refactor_7.diff
| File formset_refactor_7.diff, 49.6 kB (added by brosner, 1 year ago) |
|---|
-
a/django/contrib/admin/options.py
old new 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 def fieldsets_change(self, request, obj): 762 762 if self.declared_fieldsets: 763 763 return self.declared_fieldsets 764 form = self.formset_change(request, obj).form_class765 return [(None, {'fields': form .base_fields.keys()})]764 formset = self.formset_change(request, obj) 765 return [(None, {'fields': formset.base_fields.keys()})] 766 766 767 767 class StackedInline(InlineModelAdmin): 768 768 template = 'admin/edit_inline/stacked.html' … … 778 778 self.opts = inline 779 779 self.formset = formset 780 780 self.fieldsets = fieldsets 781 # place orderable and deletable here since _meta is inaccesible in the 782 # templates. 783 self.orderable = formset._meta.orderable 784 self.deletable = formset._meta.deletable 781 785 782 786 def __iter__(self): 783 787 for form, original in zip(self.formset.change_forms, self.formset.get_queryset()): … … 787 791 788 792 def fields(self): 789 793 for field_name in flatten_fieldsets(self.fieldsets): 790 yield self.formset. form_class.base_fields[field_name]794 yield self.formset.base_fields[field_name] 791 795 792 796 class InlineAdminForm(AdminForm): 793 797 """ -
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 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 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 … … 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: … … 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): … … 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 -
a/django/newforms/models.py
old new 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 opts_class = ModelFormOptions 214 217 215 def __new__(cls, name, bases, attrs, 218 formfield_callback=lambda f: f.formfield() ):216 formfield_callback=lambda f: f.formfield(), **options): 219 217 fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] 220 218 fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) 221 219 … … 227 225 fields = base.base_fields.items() + fields 228 226 declared_fields = SortedDict(fields) 229 227 230 opts = ModelFormOptions(attrs.get('Meta', None))228 opts = cls.opts_class(options and options or attrs.get('Meta', None)) 231 229 attrs['_meta'] = opts 232 230 233 231 # Don't allow more than one Meta model defenition in bases. The fields … … 260 258 else: 261 259 attrs['base_fields'] = declared_fields 262 260 return type.__new__(cls, name, bases, attrs) 263 261 264 262 class BaseModelForm(BaseForm): 265 263 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 266 264 initial=None, error_class=ErrorList, label_suffix=':', instance=None): … … 293 291 class ModelForm(BaseModelForm): 294 292 __metaclass__ = ModelFormMetaclass 295 293 294 # this should really be named form_for_model. 295 def modelform_for_model(model, form=ModelForm, 296 formfield_callback=lambda f: f.formfield(), **options): 297 opts = model._meta 298 options.update({"model": model}) 299 return ModelFormMetaclass(opts.object_name + "ModelForm", (form,), 300 {}, formfield_callback, **options) 301 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. 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 416 class ModelFormSetMetaclass(ModelFormMetaclass): 417 opts_class = ModelFormSetOptions 434 418 435 419 class BaseModelFormSet(BaseFormSet): 436 420 """ 437 421 A ``FormSet`` for editing a queryset and/or adding new objects to it. 438 422 """ 439 model = None440 queryset = None441 423 442 def __init__(self, qs,data=None, files=None, auto_id='id_%s', prefix=None):424 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None): 443 425 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] 426 opts = self._meta 427 self.queryset = self.get_queryset(**kwargs) 428 initial_data = [] 429 for obj in self.queryset: 430 initial_data.append(model_to_dict(obj, opts.fields, opts.exclude)) 431 kwargs['initial'] = initial_data 446 432 super(BaseModelFormSet, self).__init__(**kwargs) 433 434 def get_queryset(self, **kwargs): 435 """ 436 Hook to returning a queryset for this model. 437 """ 438 return self._meta.model._default_manager.all() 447 439 448 440 def save_new(self, form, commit=True): 449 441 """Saves and returns a new model instance for the given form.""" 450 return save_instance(form, self. model(), commit=commit)442 return save_instance(form, self._meta.model(), commit=commit) 451 443 452 444 def save_instance(self, form, instance, commit=True): 453 445 """Saves and returns an existing model instance for the given form.""" … … 464 456 return [] 465 457 # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk 466 458 existing_objects = {} 459 opts = self._meta 467 460 for obj in self.queryset: 468 461 existing_objects[obj.pk] = obj 469 462 saved_instances = [] 470 463 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]:464 obj = existing_objects[form.cleaned_data[opts.model._meta.pk.attname]] 465 if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 473 466 obj.delete() 474 467 else: 475 468 saved_instances.append(self.save_instance(form, obj, commit=commit)) … … 477 470 478 471 def save_new_objects(self, commit=True): 479 472 new_objects = [] 473 opts = self._meta 480 474 for form in self.add_forms: 481 475 if form.is_empty(): 482 476 continue 483 477 # If someone has marked an add form for deletion, don't save the 484 478 # object. At some point it would be nice if we didn't display 485 479 # the deletion widget for add forms. 486 if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:480 if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 487 481 continue 488 482 new_objects.append(self.save_new(form, commit=commit)) 489 483 return new_objects 490 484 491 485 def add_fields(self, form, index): 492 486 """Add a hidden field for the object's primary key.""" 493 self._pk_field_name = self. model._meta.pk.attname487 self._pk_field_name = self._meta.model._meta.pk.attname 494 488 form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput) 495 489 super(BaseModelFormSet, self).add_fields(form, index) 496 490 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): 491 class ModelFormSet(BaseModelFormSet): 492 __metaclass__ = ModelFormSetMetaclass 493 494 def formset_for_model(model, formset=BaseModelFormSet, 495 formfield_callback=lambda f: f.formfield(), **options): 499 496 """ 500 497 Returns a FormSet class for the given Django model class. This FormSet 501 498 will contain change forms for every instance of the given model as well … … 504 501 This is essentially the same as ``formset_for_queryset``, but automatically 505 502 uses the model's default manager to determine the queryset. 506 503 """ 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 = model 510 return FormSet 504 opts = model._meta 505 options.update({"model": model}) 506 return ModelFormSetMetaclass(opts.object_name + "ModelFormSet", (formset,), 507 {}, **options) 508 509 class InlineFormSetMetaclass(ModelFormSetMetaclass): 510 opts_class = InlineFormSetOptions 511 512 def __new__(cls, name, bases, attrs, 513 formfield_callback=lambda f: f.formfield(), **options): 514 formset = super(InlineFormSetMetaclass, cls).__new__(cls, name, bases, attrs, 515 formfield_callback, **options) 516 # If this isn't a subclass of InlineFormset, don't do anything special. 517 try: 518 if not filter(lambda b: issubclass(b, InlineFormset), bases): 519 return formset 520 except NameError: 521 # 'InlineFormset' isn't defined yet, meaning we're looking at 522 # Django's own InlineFormset class, defined below. 523 return formset 524 opts = formset._meta 525 # resolve the foreign key 526 fk = cls.resolve_foreign_key(opts.parent_model, opts.model, opts.fk_name) 527 # remove the fk from base_fields to keep it transparent to the form. 528 try: 529 del formset.base_fields[fk.name] 530 except KeyError: 531 pass 532 formset.fk = fk 533 return formset 534 535 def _resolve_foreign_key(cls, 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 a 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 561 resolve_foreign_key = classmethod(_resolve_foreign_key) 511 562 512 class InlineFormset(BaseModelFormSet):563 class BaseInlineFormSet(BaseModelFormSet): 513 564 """A formset for child objects related to a parent.""" 514 def __init__(self, instance, data=None, files=None):565 def __init__(self, *args, **kwargs): 515 566 from django.db.models.fields.related import RelatedObject 516 self.instance = instance 567 opts = self._meta 568 self.instance = kwargs.pop("instance", None) 517 569 # 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)570 rel_name = RelatedObject(self.fk.rel.to, opts.model, self.fk).get_accessor_name() 571 kwargs["prefix"] = rel_name 572 super(BaseInlineFormSet, self).__init__(*args, **kwargs) 521 573 522 def get_queryset(self ):574 def get_queryset(self, **kwargs): 523 575 """ 524 576 Returns this FormSet's queryset, but restricted to children of 525 577 self.instance 526 578 """ 527 kwargs = {self.fk.name: self.instance}528 return self.model._default_manager.filter(**kwargs)579 queryset = super(BaseInlineFormSet, self).get_queryset(**kwargs) 580 return queryset.filter(**{self.fk.name: self.instance}) 529 581 530 582 def save_new(self, form, commit=True): 531 583 kwargs = {self.fk.get_attname(): self.instance.pk} 532 new_obj = self. model(**kwargs)584 new_obj = self._meta.model(**kwargs) 533 585 return save_instance(form, new_obj, commit=commit) 534 586 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 587 class InlineFormset(BaseInlineFormSet): 588 __metaclass__ = InlineFormSetMetaclass 561 589 562 def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()): 590 def inline_formset(parent_model, model, formset=InlineFormset, 591 formfield_callback=lambda f: f.formfield(), **options): 563 592 """ 564 593 Returns an ``InlineFormset`` for the given kwargs. 565 594 566 595 You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` 567 596 to ``parent_model``. 568 597 """ 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 598 opts = model._meta 599 options.update({"parent_model": parent_model, "model": model}) 600 return InlineFormSetMetaclass(opts.object_name + "InlineFormset", (formset,), 601 {}, formfield_callback, **options) -
/dev/null
old new 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.fk_name = self._dynamic_attribute(options, "fk_name") 49 self.deletable = self._dynamic_attribute(options, "deletable", True) 50 -
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 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 them in alphabetical order by name. 55 56 >>> qs = Author.objects.order_by('name') 57 >>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=False) 58 59 >>> formset = AuthorFormSet(qs) 75 authors with an extra form to add him. When subclassing ModelFormSet you can 76 override the get_queryset method to return any queryset we like, but in this 77 case we'll use it to display it in alphabetical order by name. 78 79 >>> class AuthorFormSet(ModelFormSet): 80 ... class Meta: 81 ... model = Author 82 ... num_extra = 1 83 ... deletable = False 84 ... 85 ... def get_queryset(self, **kwargs): 86 ... qs = super(AuthorFormSet, self).get_queryset(**kwargs) 87 ... return qs.order_by('name') 88 89 >>> formset = AuthorFormSet() 60 90 >>> for form in formset.forms: 61 91 ... print form.as_p() 62 92 <p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /><input type="hidden" name="form-0-id" value="2" id="id_form-0-id" /></p> … … 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) 95 96 >>> formset = AuthorFormSet(qs) 123 >>> class AuthorFormSet(ModelFormSet): 124 ... class Meta: 125 ... model = Author 126 ... num_extra = 1 127 ... deletable = True 128 ... 129 ... def get_queryset(self, **kwargs): 130 ... qs = super(AuthorFormSet, self).get_queryset(**kwargs) 131 ... return qs.order_by('name') 132 133 >>> formset = AuthorFormSet() 97 134 >>> for form in formset.forms: 98 135 ... print form.as_p() 99 136 <p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /></p> … … 117 154 ... 'form-3-DELETE': 'on', 118 155 ... } 119 156 120 >>> formset = AuthorFormSet( qs, data=data)157 >>> formset = AuthorFormSet(data) 121 158 >>> formset.is_valid() 122 159 True 123 160 … … 134 171 We can also create a formset that is tied to a parent model. This is how the 135 172 admin system's edit inline functionality works. 136 173 137 >>> from django.newforms.models import inline_formset 174 >>> from django.newforms.models import inline_formset, InlineFormset 175 176 >>> class AuthorBooksFormSet(InlineFormset): 177 ... class Meta: 178 ... parent_model = Author 179 ... model = Book 180 ... num_extra = 3 181 ... deletable = False 138 182 139 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=3)140 183 >>> author = Author.objects.get(name='Charles Baudelaire') 141 184 142 >>> formset = AuthorBooksFormSet( author)185 >>> formset = AuthorBooksFormSet(instance=author) 143 186 >>> for form in formset.forms: 144 187 ... print form.as_p() 145 188 <p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" maxlength="100" /><input type="hidden" name="book_set-0-id" id="id_book_set-0-id" /></p> … … 153 196 ... 'book_set-2-title': '', 154 197 ... } 155 198 156 >>> formset = AuthorBooksFormSet( author, data=data)199 >>> formset = AuthorBooksFormSet(data, instance=author) 157 200 >>> formset.is_valid() 158 201 True 159 202 … … 169 212 one. This time though, an edit form will be available for every existing 170 213 book. 171 214 172 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=2) 215 >>> class AuthorBooksFormSet(InlineFormset): 216 ... class Meta: 217 ... parent_model = Author 218 ... model = Book 219 ... num_extra = 2 220 ... deletable = False 221 173 222 >>> author = Author.objects.get(name='Charles Baudelaire') 174 223 175 >>> formset = AuthorBooksFormSet( author)224 >>> formset = AuthorBooksFormSet(instance=author) 176 225 >>> for form in formset.forms: 177 226 ... print form.as_p() 178 227 <p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" value="Les Fleurs du Mal" maxlength="100" /><input type="hidden" name="book_set-0-id" value="1" id="id_book_set-0-id" /></p> … … 187 236 ... 'book_set-2-title': '', 188 237 ... } 189 238 190 >>> formset = AuthorBooksFormSet( author, data=data)239 >>> formset = AuthorBooksFormSet(data, instance=author) 191 240 >>> formset.is_valid() 192 241 True 193 242 -
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> 15
