Ticket #5372: formset_refactor_3.diff
File formset_refactor_3.diff, 47.2 KB (added by , 17 years ago) |
---|
-
django/contrib/admin/options.py
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index c3289ea..19f599b 100644
a b class ModelAdmin(BaseModelAdmin): 331 331 "Hook for specifying fieldsets for the change form." 332 332 if self.declared_fieldsets: 333 333 return self.declared_fieldsets 334 form = self.form_change(request , obj)334 form = self.form_change(request) 335 335 return [(None, {'fields': form.base_fields.keys()})] 336 336 337 337 def form_add(self, request): … … class ModelAdmin(BaseModelAdmin): 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 def form_change(self, request , obj):347 def form_change(self, request): 348 348 """ 349 349 Returns a Form class for use in the admin change view. 350 350 """ … … class ModelAdmin(BaseModelAdmin): 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 """ … … class ModelAdmin(BaseModelAdmin): 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) … … class ModelAdmin(BaseModelAdmin): 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) 556 556 inline_formsets = [] 557 557 if request.method == 'POST': 558 form = ModelForm(request.POST, request.FILES)558 form = Form(request.POST, request.FILES) 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. -
django/newforms/formsets.py
diff --git a/django/newforms/formsets.py b/django/newforms/formsets.py index 56179a9..d9b34cf 100644
a b 1 from forms import Form 2 from fields import IntegerField, BooleanField 1 2 from warnings import warn 3 4 from django.utils.datastructures import SortedDict 5 from django.utils.translation import ugettext_lazy as _ 6 7 from forms import BaseForm, Form 8 from fields import Field, IntegerField, BooleanField 9 from options import FormSetOptions 3 10 from widgets import HiddenInput, Media 4 11 from util import ErrorList, ValidationError 5 12 6 __all__ = ('BaseFormSet', ' formset_for_form', 'all_valid')13 __all__ = ('BaseFormSet', 'FormSet', 'formset_for_form', 'all_valid') 7 14 8 15 # special field names 9 16 FORM_COUNT_FIELD_NAME = 'COUNT' … … class ManagementForm(Form): 20 27 self.base_fields[FORM_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput) 21 28 super(ManagementForm, self).__init__(*args, **kwargs) 22 29 30 class BaseFormSetMetaclass(type): 31 def __new__(cls, name, bases, attrs, **options): 32 fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] 33 fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) 34 35 # If this class is subclassing another FormSet, ad that FormSet's fields. 36 # Note that we loop over the bases in *reverse*. This is necessary in 37 # order to preserve the correct order of fields. 38 for base in bases[::-1]: 39 if hasattr(base, "base_fields"): 40 fields = base.base_fields.items() + fields 41 attrs["base_fields"] = SortedDict(fields) 42 43 opts = FormSetOptions(options and options or attrs.get("Meta", None)) 44 attrs["_meta"] = opts 45 46 return type.__new__(cls, name, bases, attrs) 47 23 48 class BaseFormSet(object): 24 49 """A collection of instances of the same Form class.""" 25 50 … … class BaseFormSet(object): 37 62 self.management_form = ManagementForm(data, files, auto_id=self.auto_id, prefix=self.prefix) 38 63 if self.management_form.is_valid(): 39 64 self.total_forms = self.management_form.cleaned_data[FORM_COUNT_FIELD_NAME] 40 self.required_forms = self.total_forms - self. num_extra41 self.change_form_count = self.total_forms - self. num_extra65 self.required_forms = self.total_forms - self._meta.num_extra 66 self.change_form_count = self.total_forms - self._meta.num_extra 42 67 else: 43 68 # not sure that ValidationError is the best thing to raise here 44 69 raise ValidationError('ManagementForm data is missing or has been tampered with') 45 70 elif initial: 46 71 self.change_form_count = len(initial) 47 72 self.required_forms = len(initial) 48 self.total_forms = self.required_forms + self. num_extra73 self.total_forms = self.required_forms + self._meta.num_extra 49 74 self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix) 50 75 else: 51 76 self.change_form_count = 0 52 77 self.required_forms = 0 53 self.total_forms = self. num_extra78 self.total_forms = self._meta.num_extra 54 79 self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix) 55 80 56 81 def _get_add_forms(self): 57 82 """Return a list of all the add forms in this ``FormSet``.""" 58 FormClass = self.form_class59 83 if not hasattr(self, '_add_forms'): 60 84 add_forms = [] 61 85 for i in range(self.change_form_count, self.total_forms): … … class BaseFormSet(object): 64 88 kwargs['data'] = self.data 65 89 if self.files: 66 90 kwargs['files'] = self.files 67 add_form = FormClass(**kwargs)91 add_form = self.get_form_class(i)(**kwargs) 68 92 self.add_fields(add_form, i) 69 93 add_forms.append(add_form) 70 94 self._add_forms = add_forms … … class BaseFormSet(object): 73 97 74 98 def _get_change_forms(self): 75 99 """Return a list of all the change forms in this ``FormSet``.""" 76 FormClass = self.form_class77 100 if not hasattr(self, '_change_forms'): 78 101 change_forms = [] 79 102 for i in range(0, self.change_form_count): … … class BaseFormSet(object): 84 107 kwargs['files'] = self.files 85 108 if self.initial: 86 109 kwargs['initial'] = self.initial[i] 87 change_form = FormClass(**kwargs)110 change_form = self.get_form_class(i)(**kwargs) 88 111 self.add_fields(change_form, i) 89 112 change_forms.append(change_form) 90 self._change_forms = change_forms113 self._change_forms = change_forms 91 114 return self._change_forms 92 115 change_forms = property(_get_change_forms) 93 116 … … class BaseFormSet(object): 117 140 # Process change forms 118 141 for form in self.change_forms: 119 142 if form.is_valid(): 120 if self. deletable and form.cleaned_data[DELETION_FIELD_NAME]:143 if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 121 144 self.deleted_data.append(form.cleaned_data) 122 145 else: 123 146 self.cleaned_data.append(form.cleaned_data) … … class BaseFormSet(object): 144 167 add_errors.reverse() 145 168 errors.extend(add_errors) 146 169 # Sort cleaned_data if the formset is orderable. 147 if self. orderable:170 if self._meta.orderable: 148 171 self.cleaned_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME]) 149 172 # Give self.clean() a chance to do validation 150 173 try: … … class BaseFormSet(object): 167 190 via formset.non_form_errors() 168 191 """ 169 192 return self.cleaned_data 193 194 def get_form_class(self, index): 195 """ 196 A hook to change a form class object. 197 """ 198 FormClass = self._meta.form 199 FormClass.base_fields = self.base_fields 200 return FormClass 170 201 171 202 def add_fields(self, form, index): 172 203 """A hook for adding extra fields on to each form instance.""" 173 if self. orderable:204 if self._meta.orderable: 174 205 form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1) 175 if self. deletable:206 if self._meta.deletable: 176 207 form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False) 177 208 178 209 def add_prefix(self, index): … … class BaseFormSet(object): 192 223 else: 193 224 return Media() 194 225 media = property(_get_media) 226 227 class FormSet(BaseFormSet): 228 __metaclass__ = BaseFormSetMetaclass 195 229 196 def formset_for_form(form, formset=BaseFormSet, num_extra=1, orderable=False, deletable=False): 230 def formset_for_form(form, formset=FormSet, num_extra=1, orderable=False, 231 deletable=False): 197 232 """Return a FormSet for the given form class.""" 198 attrs = {'form_class': form, 'num_extra': num_extra, 'orderable': orderable, 'deletable': deletable} 199 return type(form.__name__ + 'FormSet', (formset,), attrs) 200 233 warn("formset_for_form is deprecated, use FormSet instead.", 234 PendingDeprecationWarning, 235 stacklevel=3) 236 return BaseFormSetMetaclass( 237 form.__name__ + "FormSet", (formset,), form.base_fields, 238 form=form, num_extra=num_extra, orderable=orderable, 239 deletable=deletable) 240 201 241 def all_valid(formsets): 202 242 """Returns true if every formset in formsets is valid.""" 203 243 valid = True -
django/newforms/models.py
diff --git a/django/newforms/models.py b/django/newforms/models.py index 3c9b43d..419389a 100644
a b from django.core.exceptions import ImproperlyConfigured 13 13 from util import ValidationError, ErrorList 14 14 from forms import BaseForm 15 15 from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES 16 from formsets import BaseFormSet, formset_for_form, DELETION_FIELD_NAME 16 from formsets import FormSetOptions, BaseFormSet, formset_for_form, DELETION_FIELD_NAME 17 from options import ModelFormOptions, ModelFormSetOptions, InlineFormSetOptions 17 18 from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput 18 19 19 20 __all__ = ( 20 21 'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model', 21 22 'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields', 22 ' formset_for_model', 'inline_formset',23 'modelform_for_model', 'formset_for_model', 'inline_formset', 23 24 'ModelChoiceField', 'ModelMultipleChoiceField', 24 25 ) 25 26 … … def fields_for_model(model, fields=None, exclude=None, formfield_callback=lambda 209 210 field_list.append((f.name, formfield)) 210 211 return SortedDict(field_list) 211 212 212 class ModelFormOptions(object):213 def __init__(self, options=None):214 self.model = getattr(options, 'model', None)215 self.fields = getattr(options, 'fields', None)216 self.exclude = getattr(options, 'exclude', None)217 218 213 class ModelFormMetaclass(type): 219 def __new__(cls, name, bases, attrs): 220 # TODO: no way to specify formfield_callback yet, do we need one, or 221 # should it be a special case for the admin? 214 def __new__(cls, name, bases, attrs, 215 formfield_callback=lambda f: f.formfield(), **options): 222 216 fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] 223 217 fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) 224 218 … … class ModelFormMetaclass(type): 230 224 fields = base.base_fields.items() + fields 231 225 declared_fields = SortedDict(fields) 232 226 233 opts = ModelFormOptions(attrs.get('Meta', None))227 opts = cls.get_options(options and options or attrs.get('Meta', None)) 234 228 attrs['_meta'] = opts 235 229 236 230 # Don't allow more than one Meta model defenition in bases. The fields … … class ModelFormMetaclass(type): 262 256 else: 263 257 attrs['base_fields'] = declared_fields 264 258 return type.__new__(cls, name, bases, attrs) 259 260 def _get_options(cls, options=None): 261 """ 262 Returns the options instance for this class. 263 """ 264 return ModelFormOptions(options) 265 get_options = classmethod(_get_options) 265 266 266 267 class BaseModelForm(BaseForm): 267 268 def __init__(self, instance, data=None, files=None, auto_id='id_%s', prefix=None, … … class BaseModelForm(BaseForm): 290 291 class ModelForm(BaseModelForm): 291 292 __metaclass__ = ModelFormMetaclass 292 293 294 # this should really be named form_for_model. 295 def modelform_for_model(model, form=BaseModelForm, 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 {}, **options) 301 293 302 294 303 # Fields ##################################################################### 295 304 … … class ModelMultipleChoiceField(ModelChoiceField): 404 413 405 414 # Model-FormSet integration ################################################### 406 415 407 def initial_data(instance, fields=None): 408 """ 409 Return a dictionary from data in ``instance`` that is suitable for 410 use as a ``Form`` constructor's ``initial`` argument. 411 412 Provide ``fields`` to specify the names of specific fields to return. 413 All field values in the instance will be returned if ``fields`` is not 414 provided. 415 """ 416 # avoid a circular import 417 from django.db.models.fields.related import ManyToManyField 418 opts = instance._meta 419 initial = {} 420 for f in opts.fields + opts.many_to_many: 421 if not f.editable: 422 continue 423 if fields and not f.name in fields: 424 continue 425 if isinstance(f, ManyToManyField): 426 # MultipleChoiceWidget needs a list of ints, not object instances. 427 initial[f.name] = [obj.pk for obj in f.value_from_object(instance)] 428 else: 429 initial[f.name] = f.value_from_object(instance) 430 return initial 416 class ModelFormSetMetaclass(ModelFormMetaclass): 417 def _get_options(cls, options=None): 418 return ModelFormSetOptions(options) 419 get_options = classmethod(_get_options) 431 420 432 421 class BaseModelFormSet(BaseFormSet): 433 422 """ 434 423 A ``FormSet`` for editing a queryset and/or adding new objects to it. 435 424 """ 436 model = None437 queryset = None438 425 439 def __init__(self, qs,data=None, files=None, auto_id='id_%s', prefix=None):426 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None): 440 427 kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix} 441 self.queryset = qs 442 kwargs['initial'] = [initial_data(obj) for obj in qs] 428 opts = self._meta 429 self.queryset = self.get_queryset(**kwargs) 430 initial_data = [] 431 for obj in self.queryset: 432 initial_data.append(model_to_dict(obj, opts.fields, opts.exclude)) 433 kwargs['initial'] = initial_data 443 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() 444 441 445 442 def save_new(self, form, commit=True): 446 443 """Saves and returns a new model instance for the given form.""" 447 return save_instance(form, self. model(), commit=commit)444 return save_instance(form, self._meta.model(), commit=commit) 448 445 449 446 def save_instance(self, form, instance, commit=True): 450 447 """Saves and returns an existing model instance for the given form.""" … … class BaseModelFormSet(BaseFormSet): 461 458 return [] 462 459 # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk 463 460 existing_objects = {} 461 opts = self._meta 464 462 for obj in self.queryset: 465 463 existing_objects[obj.pk] = obj 466 464 saved_instances = [] 467 465 for form in self.change_forms: 468 obj = existing_objects[form.cleaned_data[ self.model._meta.pk.attname]]469 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]: 470 468 obj.delete() 471 469 else: 472 470 saved_instances.append(self.save_instance(form, obj, commit=commit)) … … class BaseModelFormSet(BaseFormSet): 474 472 475 473 def save_new_objects(self, commit=True): 476 474 new_objects = [] 475 opts = self._meta 477 476 for form in self.add_forms: 478 477 if form.is_empty(): 479 478 continue 480 479 # If someone has marked an add form for deletion, don't save the 481 480 # object. At some point it would be nice if we didn't display 482 481 # the deletion widget for add forms. 483 if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:482 if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 484 483 continue 485 484 new_objects.append(self.save_new(form, commit=commit)) 486 485 return new_objects 487 486 488 487 def add_fields(self, form, index): 489 488 """Add a hidden field for the object's primary key.""" 490 self._pk_field_name = self. model._meta.pk.attname489 self._pk_field_name = self._meta.model._meta.pk.attname 491 490 form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput) 492 491 super(BaseModelFormSet, self).add_fields(form, index) 493 492 494 def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(), 495 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): 496 498 """ 497 499 Returns a FormSet class for the given Django model class. This FormSet 498 500 will contain change forms for every instance of the given model as well … … def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formf 501 503 This is essentially the same as ``formset_for_queryset``, but automatically 502 504 uses the model's default manager to determine the queryset. 503 505 """ 504 form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback)505 FormSet = formset_for_form(form, formset, extra, orderable, deletable)506 FormSet.model = model507 return FormSet506 opts = model._meta 507 options.update({"model": model}) 508 return ModelFormSetMetaclass(opts.object_name + "ModelFormSet", (formset,), 509 {}, **options) 508 510 509 class InlineFormset(BaseModelFormSet): 511 class InlineFormSetMetaclass(ModelFormMetaclass): 512 def _get_options(cls, options=None): 513 return InlineFormSetOptions(options) 514 get_options = classmethod(_get_options) 515 516 class BaseInlineFormSet(BaseModelFormSet): 510 517 """A formset for child objects related to a parent.""" 511 def __init__(self, instance, data=None, files=None):518 def __init__(self, *args, **kwargs): 512 519 from django.db.models.fields.related import RelatedObject 513 self.instance = instance 520 opts = self._meta 521 self.instance = kwargs.pop("instance", None) 522 self.fk = self.get_foreign_key(opts.parent_model, opts.model, opts.fk_name) 514 523 # is there a better way to get the object descriptor? 515 self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name() 516 qs = self.get_queryset() 517 super(InlineFormset, self).__init__(qs, data, files, prefix=self.rel_name) 524 rel_name = RelatedObject(self.fk.rel.to, opts.model, self.fk).get_accessor_name() 525 kwargs["prefix"] = rel_name 526 # remove the foreign key from base_fields 527 try: 528 del self.base_fields[self.fk.name] 529 except KeyError: 530 pass 531 super(BaseInlineFormSet, self).__init__(*args, **kwargs) 518 532 519 def get_queryset(self ):533 def get_queryset(self, **kwargs): 520 534 """ 521 535 Returns this FormSet's queryset, but restricted to children of 522 536 self.instance 523 537 """ 524 kwargs = {self.fk.name: self.instance} 525 return self.model._default_manager.filter(**kwargs) 538 queryset = super(BaseInlineFormSet, self).get_queryset(**kwargs) 539 return queryset.filter(**{self.fk.name: self.instance}) 540 541 def get_foreign_key(self, parent_model, model, fk_name=None): 542 """ 543 Finds and returns the ForeignKey from model to parent if there is one. 544 If fk_name is provided, assume it is the name of the ForeignKey field. 545 """ 546 from django.db.models import ForeignKey 547 opts = model._meta 548 if fk_name: 549 fks_to_parent = [f for f in opts.fields if f.name == fk_name] 550 if len(fks_to_parent) == 1: 551 fk = fks_to_parent[0] 552 if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model: 553 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model)) 554 elif len(fks_to_parent) == 0: 555 raise Exception("%s has no field named '%s'" % (model, fk_name)) 556 else: 557 # Try to discover what the ForeignKey from model to parent_model is 558 fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model] 559 if len(fks_to_parent) == 1: 560 fk = fks_to_parent[0] 561 elif len(fks_to_parent) == 0: 562 raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) 563 else: 564 raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) 565 return fk 526 566 527 567 def save_new(self, form, commit=True): 528 568 kwargs = {self.fk.get_attname(): self.instance.pk} 529 new_obj = self. model(**kwargs)569 new_obj = self._meta.model(**kwargs) 530 570 return save_instance(form, new_obj, commit=commit) 531 571 532 def get_foreign_key(parent_model, model, fk_name=None): 533 """ 534 Finds and returns the ForeignKey from model to parent if there is one. 535 If fk_name is provided, assume it is the name of the ForeignKey field. 536 """ 537 # avoid circular import 538 from django.db.models import ForeignKey 539 opts = model._meta 540 if fk_name: 541 fks_to_parent = [f for f in opts.fields if f.name == fk_name] 542 if len(fks_to_parent) == 1: 543 fk = fks_to_parent[0] 544 if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model: 545 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model)) 546 elif len(fks_to_parent) == 0: 547 raise Exception("%s has no field named '%s'" % (model, fk_name)) 548 else: 549 # Try to discover what the ForeignKey from model to parent_model is 550 fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model] 551 if len(fks_to_parent) == 1: 552 fk = fks_to_parent[0] 553 elif len(fks_to_parent) == 0: 554 raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) 555 else: 556 raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) 557 return fk 572 class InlineFormset(BaseInlineFormSet): 573 __metaclass__ = InlineFormSetMetaclass 558 574 559 def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()): 575 def inline_formset(parent_model, model, formset=BaseInlineFormSet, 576 formfield_callback=lambda f: f.formfield(), **options): 560 577 """ 561 578 Returns an ``InlineFormset`` for the given kwargs. 562 579 563 580 You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` 564 581 to ``parent_model``. 565 582 """ 566 fk = get_foreign_key(parent_model, model, fk_name=fk_name) 567 # let the formset handle object deletion by default 568 FormSet = formset_for_model(model, formset=InlineFormset, fields=fields, 569 formfield_callback=formfield_callback, 570 extra=extra, orderable=orderable, 571 deletable=deletable) 572 # HACK: remove the ForeignKey to the parent from every form 573 # This should be done a line above before we pass 'fields' to formset_for_model 574 # an 'omit' argument would be very handy here 575 try: 576 del FormSet.form_class.base_fields[fk.name] 577 except KeyError: 578 pass 579 FormSet.fk = fk 580 return FormSet 583 opts = model._meta 584 options.update({"parent_model": parent_model, "model": model}) 585 return InlineFormSetMetaclass(opts.object_name + "InlineFormset", (formset,), 586 {}, **options) -
new file django/newforms/options.py
diff --git a/django/newforms/options.py b/django/newforms/options.py new file mode 100644 index 0000000..4370aa7
- + 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 50 No newline at end of file -
tests/modeltests/model_formsets/models.py
diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py index 19bdeed..baaa38f 100644
a b class Book(models.Model): 16 16 17 17 __test__ = {'API_TESTS': """ 18 18 19 >>> from django.newforms.models import formset_for_model 19 >>> from django.newforms.models import formset_for_model, ModelFormSet 20 20 21 >>> qs = Author.objects.all() 22 >>> AuthorFormSet = formset_for_model(Author, extra=3) 21 Lets create a formset that is bound to a model. 23 22 24 >>> formset = AuthorFormSet(qs) 23 >>> class AuthorFormSet(ModelFormSet): 24 ... class Meta: 25 ... model = Author 26 ... num_extra = 3 27 28 >>> formset = AuthorFormSet() 25 29 >>> for form in formset.forms: 26 30 ... print form.as_p() 27 31 <p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /><input type="hidden" name="form-0-id" id="id_form-0-id" /></p> … … __test__ = {'API_TESTS': """ 35 39 ... 'form-2-name': '', 36 40 ... } 37 41 38 >>> formset = AuthorFormSet( qs, data=data)42 >>> formset = AuthorFormSet(data) 39 43 >>> formset.is_valid() 40 44 True 41 45 … … Charles Baudelaire 49 53 50 54 51 55 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) 56 authors with an extra form to add him. When subclassing ModelFormSet you can 57 override the get_queryset method to return any queryset we like, but in this 58 case we'll use it to display it in alphabetical order by name. 59 60 >>> class AuthorFormSet(ModelFormSet): 61 ... class Meta: 62 ... model = Author 63 ... num_extra = 1 64 ... deletable = False 65 ... 66 ... def get_queryset(self, **kwargs): 67 ... qs = super(AuthorFormSet, self).get_queryset(**kwargs) 68 ... return qs.order_by('name') 69 70 >>> formset = AuthorFormSet() 60 71 >>> for form in formset.forms: 61 72 ... print form.as_p() 62 73 <p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /><input type="hidden" name="form-0-id" value="2" id="id_form-0-id" /></p> … … but in that case we'll use it to display them in alphabetical order by name. 73 84 ... 'form-2-name': 'Paul Verlaine', 74 85 ... } 75 86 76 >>> formset = AuthorFormSet( qs, data=data)87 >>> formset = AuthorFormSet(data) 77 88 >>> formset.is_valid() 78 89 True 79 90 … … Paul Verlaine 90 101 This probably shouldn't happen, but it will. If an add form was marked for 91 102 deltetion, make sure we don't save that form. 92 103 93 >>> qs = Author.objects.order_by('name') 94 >>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=True) 95 96 >>> formset = AuthorFormSet(qs) 104 >>> class AuthorFormSet(ModelFormSet): 105 ... class Meta: 106 ... model = Author 107 ... num_extra = 1 108 ... deletable = True 109 ... 110 ... def get_queryset(self, **kwargs): 111 ... qs = super(AuthorFormSet, self).get_queryset(**kwargs) 112 ... return qs.order_by('name') 113 114 >>> formset = AuthorFormSet() 97 115 >>> for form in formset.forms: 98 116 ... print form.as_p() 99 117 <p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /></p> … … deltetion, make sure we don't save that form. 117 135 ... 'form-3-DELETE': 'on', 118 136 ... } 119 137 120 >>> formset = AuthorFormSet( qs, data=data)138 >>> formset = AuthorFormSet(data) 121 139 >>> formset.is_valid() 122 140 True 123 141 … … Paul Verlaine 134 152 We can also create a formset that is tied to a parent model. This is how the 135 153 admin system's edit inline functionality works. 136 154 137 >>> from django.newforms.models import inline_formset 155 >>> from django.newforms.models import inline_formset, InlineFormset 156 157 >>> class AuthorBooksFormSet(InlineFormset): 158 ... class Meta: 159 ... parent_model = Author 160 ... model = Book 161 ... num_extra = 3 162 ... deletable = False 138 163 139 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=3)140 164 >>> author = Author.objects.get(name='Charles Baudelaire') 141 165 142 >>> formset = AuthorBooksFormSet( author)166 >>> formset = AuthorBooksFormSet(instance=author) 143 167 >>> for form in formset.forms: 144 168 ... print form.as_p() 145 169 <p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" maxlength="100" /><input type="hidden" name="book_set-0-id" id="id_book_set-0-id" /></p> … … admin system's edit inline functionality works. 153 177 ... 'book_set-2-title': '', 154 178 ... } 155 179 156 >>> formset = AuthorBooksFormSet( author, data=data)180 >>> formset = AuthorBooksFormSet(data, instance=author) 157 181 >>> formset.is_valid() 158 182 True 159 183 … … Now that we've added a book to Charles Baudelaire, let's try adding another 169 193 one. This time though, an edit form will be available for every existing 170 194 book. 171 195 172 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=2) 196 >>> class AuthorBooksFormSet(InlineFormset): 197 ... class Meta: 198 ... parent_model = Author 199 ... model = Book 200 ... num_extra = 2 201 ... deletable = False 202 173 203 >>> author = Author.objects.get(name='Charles Baudelaire') 174 204 175 >>> formset = AuthorBooksFormSet( author)205 >>> formset = AuthorBooksFormSet(instance=author) 176 206 >>> for form in formset.forms: 177 207 ... print form.as_p() 178 208 <p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" value="Les Fleurs du Mal" maxlength="100" /><input type="hidden" name="book_set-0-id" value="1" id="id_book_set-0-id" /></p> … … book. 187 217 ... 'book_set-2-title': '', 188 218 ... } 189 219 190 >>> formset = AuthorBooksFormSet( author, data=data)220 >>> formset = AuthorBooksFormSet(data, instance=author) 191 221 >>> formset.is_valid() 192 222 True 193 223 -
tests/regressiontests/forms/formsets.py
diff --git a/tests/regressiontests/forms/formsets.py b/tests/regressiontests/forms/formsets.py index a6da2fe..f5cb2ee 100644
a b 2 2 formset_tests = """ 3 3 # Basic FormSet creation and usage ############################################ 4 4 5 FormSet allows us to use multiple instance of the same form on 1 page. For now,6 the best way to create a FormSet is by using the formset_for_form function.5 FormSet allows us to use multiple instance of the same form on 1 page. Create 6 the formset as you would a regular form by defining the fields declaratively. 7 7 8 8 >>> from django.newforms import Form, CharField, IntegerField, ValidationError 9 >>> from django.newforms.formsets import formset_for_form, BaseFormSet 9 >>> from django.newforms import BooleanField 10 >>> from django.newforms.formsets import formset_for_form, BaseFormSet, FormSet 10 11 11 >>> class Choice (Form):12 >>> class ChoiceFormSet(FormSet): 12 13 ... choice = CharField() 13 14 ... votes = IntegerField() 14 15 15 >>> ChoiceFormSet = formset_for_form(Choice)16 17 16 18 17 A FormSet constructor takes the same arguments as Form. Let's create a FormSet 19 18 for adding data. By default, it displays 1 blank form. It can display more, … … False 145 144 >>> formset.errors 146 145 [{'votes': [u'This field is required.'], 'choice': [u'This field is required.']}] 147 146 147 # Subclassing a FormSet class ################################################# 148 149 We can subclass a FormSet to add addition fields to an already exisiting 150 FormSet. 151 152 >>> class SecondChoiceFormSet(ChoiceFormSet): 153 ... is_public = BooleanField() 154 155 >>> formset = SecondChoiceFormSet(auto_id=False, prefix="choices") 156 >>> for form in formset.forms: 157 ... print form.as_ul() 158 <li>Choice: <input type="text" name="choices-0-choice" /></li> 159 <li>Votes: <input type="text" name="choices-0-votes" /></li> 160 <li>Is public: <input type="checkbox" name="choices-0-is_public" /></li> 148 161 149 162 # Displaying more than 1 blank form ########################################### 150 163 151 We can also display more than 1 empty form at a time. To do so, pass a152 num_extra argument to formset_for_form.164 We can also display more than 1 empty form at a time. To do so, create an inner 165 Meta class with an attribute num_extra. 153 166 154 >>> ChoiceFormSet = formset_for_form(Choice, num_extra=3) 167 >>> class NumExtraChoiceFormSet(ChoiceFormSet): 168 ... class Meta: 169 ... num_extra = 3 155 170 156 >>> formset = ChoiceFormSet(auto_id=False, prefix='choices')171 >>> formset = NumExtraChoiceFormSet(auto_id=False, prefix='choices') 157 172 >>> for form in formset.forms: 158 173 ... print form.as_ul() 159 174 <li>Choice: <input type="text" name="choices-0-choice" /></li> … … number of forms to be completed. 177 192 ... 'choices-2-votes': '', 178 193 ... } 179 194 180 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')195 >>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices') 181 196 >>> formset.is_valid() 182 197 True 183 198 >>> formset.cleaned_data … … We can just fill out one of the forms. 196 211 ... 'choices-2-votes': '', 197 212 ... } 198 213 199 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')214 >>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices') 200 215 >>> formset.is_valid() 201 216 True 202 217 >>> formset.cleaned_data … … And once again, if we try to partially complete a form, validation will fail. 215 230 ... 'choices-2-votes': '', 216 231 ... } 217 232 218 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')233 >>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices') 219 234 >>> formset.is_valid() 220 235 False 221 236 >>> formset.errors … … The num_extra argument also works when the formset is pre-filled with initial 226 241 data. 227 242 228 243 >>> initial = [{'choice': u'Calexico', 'votes': 100}] 229 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')244 >>> formset = NumExtraChoiceFormSet(initial=initial, auto_id=False, prefix='choices') 230 245 >>> for form in formset.forms: 231 246 ... print form.as_ul() 232 247 <li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> … … get an error. 254 269 ... 'choices-3-votes': '', 255 270 ... } 256 271 257 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')272 >>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices') 258 273 >>> formset.is_valid() 259 274 False 260 275 >>> formset.errors … … False 263 278 264 279 # FormSets with deletion ###################################################### 265 280 266 We can easily add deletion ability to a FormSet with an agrument to267 formset_for_form. This will add a boolean field to each form instance. When 268 that boolean field is True, the cleaned data will be in formset.deleted_data281 We can easily add deletion ability to a FormSet by setting deletable to True 282 in the inner Meta class. This will add a boolean field to each form instance. 283 When that boolean field is True, the cleaned data will be in formset.deleted_data 269 284 rather than formset.cleaned_data 270 285 271 >>> ChoiceFormSet = formset_for_form(Choice, deletable=True) 286 >>> class DeletableChoiceFormSet(ChoiceFormSet): 287 ... class Meta: 288 ... deletable = True 272 289 273 290 >>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}] 274 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')291 >>> formset = DeletableChoiceFormSet(initial=initial, auto_id=False, prefix='choices') 275 292 >>> for form in formset.forms: 276 293 ... print form.as_ul() 277 294 <li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> … … To delete something, we just need to set that form's special delete field to 300 317 ... 'choices-2-DELETE': '', 301 318 ... } 302 319 303 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')320 >>> formset = DeletableChoiceFormSet(data, auto_id=False, prefix='choices') 304 321 >>> formset.is_valid() 305 322 True 306 323 >>> formset.cleaned_data … … True 310 327 311 328 # FormSets with ordering ###################################################### 312 329 313 We can also add ordering ability to a FormSet with an agrument to314 formset_for_form. This will add a integer field to each form instance. When330 We can also add ordering ability to a FormSet by setting orderable to True in 331 the inner Meta class. This will add a integer field to each form instance. When 315 332 form validation succeeds, formset.cleaned_data will have the data in the correct 316 333 order specified by the ordering fields. If a number is duplicated in the set 317 334 of ordering fields, for instance form 0 and form 3 are both marked as 1, then 318 335 the form index used as a secondary ordering criteria. In order to put 319 336 something at the front of the list, you'd need to set it's order to 0. 320 337 321 >>> ChoiceFormSet = formset_for_form(Choice, orderable=True) 338 >>> class OrderableChoiceFormSet(ChoiceFormSet): 339 ... class Meta: 340 ... orderable = True 322 341 323 342 >>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}] 324 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')343 >>> formset = OrderableChoiceFormSet(initial=initial, auto_id=False, prefix='choices') 325 344 >>> for form in formset.forms: 326 345 ... print form.as_ul() 327 346 <li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> … … something at the front of the list, you'd need to set it's order to 0. 347 366 ... 'choices-2-ORDER': '0', 348 367 ... } 349 368 350 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')369 >>> formset = OrderableChoiceFormSet(data, auto_id=False, prefix='choices') 351 370 >>> formset.is_valid() 352 371 True 353 372 >>> for cleaned_data in formset.cleaned_data: … … True 359 378 # FormSets with ordering + deletion ########################################### 360 379 361 380 Let's try throwing ordering and deletion into the same form. 381 TODO: Perhaps handle Meta class inheritance so you can subclass 382 OrderableChoiceFormSet and DeletableChoiceFormSet? 362 383 363 >>> ChoiceFormSet = formset_for_form(Choice, orderable=True, deletable=True) 384 >>> class MixedChoiceFormSet(ChoiceFormSet): 385 ... class Meta: 386 ... orderable = True 387 ... deletable = True 364 388 365 389 >>> initial = [ 366 390 ... {'choice': u'Calexico', 'votes': 100}, 367 391 ... {'choice': u'Fergie', 'votes': 900}, 368 392 ... {'choice': u'The Decemberists', 'votes': 500}, 369 393 ... ] 370 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')394 >>> formset = MixedChoiceFormSet(initial=initial, auto_id=False, prefix='choices') 371 395 >>> for form in formset.forms: 372 396 ... print form.as_ul() 373 397 <li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> … … Let's delete Fergie, and put The Decemberists ahead of Calexico. 409 433 ... 'choices-3-DELETE': '', 410 434 ... } 411 435 412 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')436 >>> formset = MixedChoiceFormSet(data, auto_id=False, prefix='choices') 413 437 >>> formset.is_valid() 414 438 True 415 439 >>> for cleaned_data in formset.cleaned_data: … … particular form. It follows the same pattern as the clean hook on Forms. 428 452 Let's define a FormSet that takes a list of favorite drinks, but raises am 429 453 error if there are any duplicates. 430 454 431 >>> class FavoriteDrink Form(Form):455 >>> class FavoriteDrinksFormSet(FormSet): 432 456 ... name = CharField() 433 ... 434 435 >>> class FavoriteDrinksFormSet(BaseFormSet): 436 ... form_class = FavoriteDrinkForm 437 ... num_extra = 2 438 ... orderable = False 439 ... deletable = False 457 ... 458 ... class Meta: 459 ... num_extra = 2 460 ... orderable = False 461 ... deletable = False 440 462 ... 441 463 ... def clean(self): 442 464 ... seen_drinks = [] -
tests/regressiontests/inline_formsets/models.py
diff --git a/tests/regressiontests/inline_formsets/models.py b/tests/regressiontests/inline_formsets/models.py index f84be84..180bba1 100644
a b __test__ = {'API_TESTS': """ 21 21 Child has two ForeignKeys to Parent, so if we don't specify which one to use 22 22 for the inline formset, we should get an exception. 23 23 24 >>> i fs = inline_formset(Parent, Child)24 >>> inline_formset(Parent, Child)() 25 25 Traceback (most recent call last): 26 26 ... 27 27 Exception: <class 'regressiontests.inline_formsets.models.Child'> has more than 1 ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'> … … Exception: <class 'regressiontests.inline_formsets.models.Child'> has more than 29 29 30 30 These two should both work without a problem. 31 31 32 >>> ifs = inline_formset(Parent, Child, fk_name='mother') 33 >>> ifs = inline_formset(Parent, Child, fk_name='father') 32 >>> ifs = inline_formset(Parent, Child, fk_name='mother')() 33 >>> ifs = inline_formset(Parent, Child, fk_name='father')() 34 34 35 35 36 36 If we specify fk_name, but it isn't a ForeignKey from the child model to the 37 37 parent model, we should get an exception. 38 38 39 >>> i fs = inline_formset(Parent, Child, fk_name='school')39 >>> inline_formset(Parent, Child, fk_name='school')() 40 40 Traceback (most recent call last): 41 41 ... 42 42 Exception: fk_name 'school' is not a ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'> … … Exception: fk_name 'school' is not a ForeignKey to <class 'regressiontests.inlin 45 45 If the field specified in fk_name is not a ForeignKey, we should get an 46 46 exception. 47 47 48 >>> i fs = inline_formset(Parent, Child, fk_name='test')48 >>> inline_formset(Parent, Child, fk_name='test')() 49 49 Traceback (most recent call last): 50 50 ... 51 51 Exception: <class 'regressiontests.inline_formsets.models.Child'> has no field named 'test' -
tests/regressiontests/modeladmin/models.py
diff --git a/tests/regressiontests/modeladmin/models.py b/tests/regressiontests/modeladmin/models.py index 6275135..cdc6c8b 100644
a b displayed because you forgot to add it to fields/fielsets 72 72 >>> ma = BandAdmin(Band, site) 73 73 >>> ma.form_add(request).base_fields.keys() 74 74 ['name'] 75 >>> ma.form_change(request , band).base_fields.keys()75 >>> ma.form_change(request).base_fields.keys() 76 76 ['name'] 77 77 78 78 >>> class BandAdmin(ModelAdmin): … … displayed because you forgot to add it to fields/fielsets 81 81 >>> ma = BandAdmin(Band, site) 82 82 >>> ma.form_add(request).base_fields.keys() 83 83 ['name'] 84 >>> ma.form_change(request , band).base_fields.keys()84 >>> ma.form_change(request).base_fields.keys() 85 85 ['name'] 86 86 87 87