Ticket #6241: formset_refactor_10.diff
| File formset_refactor_10.diff, 52.0 kB (added by brosner, 11 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 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()(**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(change=True)(**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, change=False): 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', 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 211 class ModelFormMetaclass(type): 212 opts_class = ModelFormOptions 213 217 214 def __new__(cls, name, bases, attrs, 218 formfield_callback=lambda f: f.formfield() ):215 formfield_callback=lambda f: f.formfield(), **options): 219 216 fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] 220 217 fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) 221 218 … … 227 224 fields = base.base_fields.items() + fields 228 225 declared_fields = SortedDict(fields) 229 226 230 opts = ModelFormOptions(attrs.get('Meta', None))227 opts = cls.opts_class(options and options or attrs.get('Meta', None)) 231 228 attrs['_meta'] = opts 232 229 233 230 # Don't allow more than one Meta model defenition in bases. The fields … … 260 257 else: 261 258 attrs['base_fields'] = declared_fields 262 259 return type.__new__(cls, name, bases, attrs) 263 260 264 261 class BaseModelForm(BaseForm): 265 262 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 266 263 initial=None, error_class=ErrorList, label_suffix=':', instance=None): … … 293 290 class ModelForm(BaseModelForm): 294 291 __metaclass__ = ModelFormMetaclass 295 292 293 def modelform_for_model(model, form=ModelForm, 294 formfield_callback=lambda f: f.formfield(), **options): 295 opts = model._meta 296 options.update({"model": model}) 297 return ModelFormMetaclass(opts.object_name + "ModelForm", (form,), 298 {}, formfield_callback, **options) 299 296 300 297 301 # Fields ##################################################################### 298 302 … … 407 411 408 412 # Model-FormSet integration ################################################### 409 413 410 def initial_data(instance, fields=None): 411 """ 412 Return a dictionary from data in ``instance`` that is suitable for 413 use as a ``Form`` constructor's ``initial`` argument. 414 415 Provide ``fields`` to specify the names of specific fields to return. 416 All field values in the instance will be returned if ``fields`` is not 417 provided. 418 """ 419 # avoid a circular import 420 from django.db.models.fields.related import ManyToManyField 421 opts = instance._meta 422 initial = {} 423 for f in opts.fields + opts.many_to_many: 424 if not f.editable: 425 continue 426 if fields and not f.name in fields: 427 continue 428 if isinstance(f, ManyToManyField): 429 # MultipleChoiceWidget needs a list of ints, not object instances. 430 initial[f.name] = [obj.pk for obj in f.value_from_object(instance)] 431 else: 432 initial[f.name] = f.value_from_object(instance) 433 return initial 414 class ModelFormSetMetaclass(ModelFormMetaclass): 415 opts_class = ModelFormSetOptions 434 416 435 417 class BaseModelFormSet(BaseFormSet): 436 418 """ 437 419 A ``FormSet`` for editing a queryset and/or adding new objects to it. 438 420 """ 439 model = None440 queryset = None441 421 442 def __init__(self, qs, data=None, files=None, auto_id='id_%s', prefix=None): 422 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 423 queryset=None): 443 424 kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix} 444 self.queryset = qs 445 kwargs['initial'] = [initial_data(obj) for obj in qs] 425 opts = self._meta 426 if queryset is None: 427 self.queryset = self.get_queryset(**kwargs) 428 else: 429 self.queryset = queryset 430 initial_data = [] 431 for obj in self.queryset: 432 initial_data.append(model_to_dict(obj, opts.fields, opts.exclude)) 433 kwargs['initial'] = initial_data 446 434 super(BaseModelFormSet, self).__init__(**kwargs) 435 436 def get_queryset(self, **kwargs): 437 """ 438 Hook to returning a queryset for this model. 439 """ 440 return self._meta.model._default_manager.all() 447 441 448 442 def save_new(self, form, commit=True): 449 443 """Saves and returns a new model instance for the given form.""" 450 return save_instance(form, self. model(), commit=commit)444 return save_instance(form, self._meta.model(), commit=commit) 451 445 452 446 def save_instance(self, form, instance, commit=True): 453 447 """Saves and returns an existing model instance for the given form.""" … … 462 456 def save_existing_objects(self, commit=True): 463 457 if not self.queryset: 464 458 return [] 465 # Put the objects from self. get_queryset into a dict so they are easy to lookup by pk459 # Put the objects from self.queryset into a dict so they are easy to lookup by pk 466 460 existing_objects = {} 461 opts = self._meta 467 462 for obj in self.queryset: 468 463 existing_objects[obj.pk] = obj 469 464 saved_instances = [] 470 465 for form in self.change_forms: 471 obj = existing_objects[form.cleaned_data[ self.model._meta.pk.attname]]472 if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:466 obj = existing_objects[form.cleaned_data[opts.model._meta.pk.attname]] 467 if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 473 468 obj.delete() 474 469 else: 475 470 saved_instances.append(self.save_instance(form, obj, commit=commit)) … … 477 472 478 473 def save_new_objects(self, commit=True): 479 474 new_objects = [] 475 opts = self._meta 480 476 for form in self.add_forms: 481 477 if form.is_empty(): 482 478 continue 483 479 # If someone has marked an add form for deletion, don't save the 484 480 # object. At some point it would be nice if we didn't display 485 481 # the deletion widget for add forms. 486 if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:482 if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 487 483 continue 488 484 new_objects.append(self.save_new(form, commit=commit)) 489 485 return new_objects 490 486 491 487 def add_fields(self, form, index): 492 488 """Add a hidden field for the object's primary key.""" 493 self._pk_field_name = self. model._meta.pk.attname489 self._pk_field_name = self._meta.model._meta.pk.attname 494 490 form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput) 495 491 super(BaseModelFormSet, self).add_fields(form, index) 496 492 497 def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(), 498 formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None): 493 class ModelFormSet(BaseModelFormSet): 494 __metaclass__ = ModelFormSetMetaclass 495 496 def formset_for_model(model, formset=BaseModelFormSet, 497 formfield_callback=lambda f: f.formfield(), **options): 499 498 """ 500 499 Returns a FormSet class for the given Django model class. This FormSet 501 500 will contain change forms for every instance of the given model as well … … 504 503 This is essentially the same as ``formset_for_queryset``, but automatically 505 504 uses the model's default manager to determine the queryset. 506 505 """ 507 form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback)508 FormSet = formset_for_form(form, formset, extra, orderable, deletable)509 FormSet.model = model510 return FormSet506 opts = model._meta 507 options.update({"model": model}) 508 return ModelFormSetMetaclass(opts.object_name + "ModelFormSet", (formset,), 509 {}, formfield_callback, **options) 511 510 512 class InlineFormset(BaseModelFormSet): 511 class InlineFormSetMetaclass(ModelFormSetMetaclass): 512 opts_class = InlineFormSetOptions 513 514 def __new__(cls, name, bases, attrs, 515 formfield_callback=lambda f: f.formfield(), **options): 516 formset = super(InlineFormSetMetaclass, cls).__new__(cls, name, bases, attrs, 517 formfield_callback, **options) 518 # If this isn't a subclass of InlineFormset, don't do anything special. 519 try: 520 if not filter(lambda b: issubclass(b, InlineFormset), bases): 521 return formset 522 except NameError: 523 # 'InlineFormset' isn't defined yet, meaning we're looking at 524 # Django's own InlineFormset class, defined below. 525 return formset 526 opts = formset._meta 527 # resolve the foreign key 528 fk = cls.resolve_foreign_key(opts.parent_model, opts.model, opts.fk_name) 529 # remove the fk from base_fields to keep it transparent to the form. 530 try: 531 del formset.base_fields[fk.name] 532 except KeyError: 533 pass 534 formset.fk = fk 535 return formset 536 537 def _resolve_foreign_key(cls, parent_model, model, fk_name=None): 538 """ 539 Finds and returns the ForeignKey from model to parent if there is one. 540 If fk_name is provided, assume it is the name of the ForeignKey field. 541 """ 542 # avoid a circular import 543 from django.db.models import ForeignKey 544 opts = model._meta 545 if fk_name: 546 fks_to_parent = [f for f in opts.fields if f.name == fk_name] 547 if len(fks_to_parent) == 1: 548 fk = fks_to_parent[0] 549 if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model: 550 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model)) 551 elif len(fks_to_parent) == 0: 552 raise Exception("%s has no field named '%s'" % (model, fk_name)) 553 else: 554 # Try to discover what the ForeignKey from model to parent_model is 555 fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model] 556 if len(fks_to_parent) == 1: 557 fk = fks_to_parent[0] 558 elif len(fks_to_parent) == 0: 559 raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) 560 else: 561 raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) 562 return fk 563 resolve_foreign_key = classmethod(_resolve_foreign_key) 564 565 class BaseInlineFormSet(BaseModelFormSet): 513 566 """A formset for child objects related to a parent.""" 514 def __init__(self, instance, data=None, files=None):567 def __init__(self, *args, **kwargs): 515 568 from django.db.models.fields.related import RelatedObject 516 self.instance = instance 569 opts = self._meta 570 self.instance = kwargs.pop("instance", None) 517 571 # is there a better way to get the object descriptor? 518 self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()519 qs = self.get_queryset()520 super( InlineFormset, self).__init__(qs, data, files, prefix=self.rel_name)572 rel_name = RelatedObject(self.fk.rel.to, opts.model, self.fk).get_accessor_name() 573 kwargs["prefix"] = rel_name 574 super(BaseInlineFormSet, self).__init__(*args, **kwargs) 521 575 522 def get_queryset(self ):576 def get_queryset(self, **kwargs): 523 577 """ 524 578 Returns this FormSet's queryset, but restricted to children of 525 579 self.instance 526 580 """ 527 kwargs = {self.fk.name: self.instance} 528 return self.model._default_manager.filter(**kwargs) 581 opts = self._meta 582 if self.instance is None: 583 return opts.model._default_manager.none() 584 queryset = super(BaseInlineFormSet, self).get_queryset(**kwargs) 585 return queryset.filter(**{self.fk.name: self.instance}) 529 586 530 587 def save_new(self, form, commit=True): 531 588 kwargs = {self.fk.get_attname(): self.instance.pk} 532 new_obj = self. model(**kwargs)589 new_obj = self._meta.model(**kwargs) 533 590 return save_instance(form, new_obj, commit=commit) 534 591 535 def get_foreign_key(parent_model, model, fk_name=None): 536 """ 537 Finds and returns the ForeignKey from model to parent if there is one. 538 If fk_name is provided, assume it is the name of the ForeignKey field. 539 """ 540 # avoid circular import 541 from django.db.models import ForeignKey 542 opts = model._meta 543 if fk_name: 544 fks_to_parent = [f for f in opts.fields if f.name == fk_name] 545 if len(fks_to_parent) == 1: 546 fk = fks_to_parent[0] 547 if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model: 548 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model)) 549 elif len(fks_to_parent) == 0: 550 raise Exception("%s has no field named '%s'" % (model, fk_name)) 551 else: 552 # Try to discover what the ForeignKey from model to parent_model is 553 fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model] 554 if len(fks_to_parent) == 1: 555 fk = fks_to_parent[0] 556 elif len(fks_to_parent) == 0: 557 raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) 558 else: 559 raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) 560 return fk 592 class InlineFormset(BaseInlineFormSet): 593 __metaclass__ = InlineFormSetMetaclass 561 594 562 def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()): 595 def inline_formset(parent_model, model, formset=InlineFormset, 596 formfield_callback=lambda f: f.formfield(), **options): 563 597 """ 564 598 Returns an ``InlineFormset`` for the given kwargs. 565 599 566 600 You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` 567 601 to ``parent_model``. 568 602 """ 569 fk = get_foreign_key(parent_model, model, fk_name=fk_name) 570 # let the formset handle object deletion by default 571 FormSet = formset_for_model(model, formset=InlineFormset, fields=fields, 572 formfield_callback=formfield_callback, 573 extra=extra, orderable=orderable, 574 deletable=deletable) 575 # HACK: remove the ForeignKey to the parent from every form 576 # This should be done a line above before we pass 'fields' to formset_for_model 577 # an 'omit' argument would be very handy here 578 try: 579 del FormSet.form_class.base_fields[fk.name] 580 except KeyError: 581 pass 582 FormSet.fk = fk 583 return FormSet 603 opts = model._meta 604 options.update({"parent_model": parent_model, "model": model}) 605 return InlineFormSetMetaclass(opts.object_name + "InlineFormset", (formset,), 606 {}, formfield_callback, **options) -
/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.num_extra = self._dynamic_attribute(options, "num_extra", 3) 49 self.fk_name = self._dynamic_attribute(options, "fk_name") 50 self.deletable = self._dynamic_attribute(options, "deletable", True) 51 -
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
