From 6f15904669f884de75dbd4cc2b2e03880d81a9a8 Mon Sep 17 00:00:00 2001 From: Joseph Kocherhans Date: Tue, 6 Nov 2007 21:53:15 +0000 Subject: [PATCH] newforms-admin: Fixed #5733. Added formset_for_queryset. This is backwards incompatble for anyone using formset_for_model or BaseModelFormSet. git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@6655 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/newforms/models.py | 112 ++++++++--- tests/modeltests/model_formsets/__init__.py | 0 tests/modeltests/model_formsets/models.py | 204 ++++++++++++++++++++ 3 files changed, 285 insertions(+), 31 deletions(-) create mode 100644 tests/modeltests/model_formsets/__init__.py create mode 100644 tests/modeltests/model_formsets/models.py diff --git a/django/newforms/models.py b/django/newforms/models.py index 31ed13c2b6..5b8bc153cc 100644 --- a/django/newforms/models.py +++ b/django/newforms/models.py @@ -15,8 +15,8 @@ from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput __all__ = ( 'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields', - 'ModelChoiceField', 'ModelMultipleChoiceField', 'formset_for_model', - 'inline_formset' + 'formset_for_model', 'formset_for_queryset', 'inline_formset', + 'ModelChoiceField', 'ModelMultipleChoiceField', ) def save_instance(form, instance, fields=None, fail_message='saved', commit=True): @@ -237,17 +237,20 @@ def initial_data(instance, fields=None): class BaseModelFormSet(BaseFormSet): """ - A ``FormSet`` attatched to a particular model or sequence of model instances. + A ``FormSet`` for editing a queryset and/or adding new objects to it. """ model = None + queryset = None - def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, instances=None): - self.instances = instances + def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None): kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix} - if instances: - kwargs['initial'] = [initial_data(instance) for instance in instances] + if self.queryset: + kwargs['initial'] = [initial_data(obj) for obj in self.get_queryset()] super(BaseModelFormSet, self).__init__(**kwargs) + def get_queryset(self): + return self.queryset._clone() + def save_new(self, form, commit=True): """Saves and returns a new model instance for the given form.""" return save_instance(form, self.model(), commit=commit) @@ -260,25 +263,36 @@ class BaseModelFormSet(BaseFormSet): """Saves model instances for every form, adding and changing instances as necessary, and returns the list of instances. """ + return self.save_existing_objects(commit) + self.save_new_objects(commit) + + def save_existing_objects(self, commit=True): + if not self.queryset: + return [] + # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk + existing_objects = {} + for obj in self.get_queryset(): + existing_objects[obj._get_pk_val()] = obj saved_instances = [] - # put self.instances into a dict so they are easy to lookup by pk - instances = {} - for instance in self.instances: - instances[instance._get_pk_val()] = instance - if self.instances: - # update/save existing instances - for form in self.change_forms: - instance = instances[form.cleaned_data[self.model._meta.pk.attname]] - if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]: - instance.delete() - else: - saved_instances.append(self.save_instance(form, instance, commit=commit)) - # create/save new instances + for form in self.change_forms: + obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]] + if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]: + obj.delete() + else: + saved_instances.append(self.save_instance(form, obj, commit=commit)) + return saved_instances + + def save_new_objects(self, commit=True): + new_objects = [] for form in self.add_forms: if form.is_empty(): continue - saved_instances.append(self.save_new(form, commit=commit)) - return saved_instances + # If someone has marked an add form for deletion, don't save the + # object. At some point it would be nice if we didn't display + # the deletion widget for add forms. + if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]: + continue + new_objects.append(self.save_new(form, commit=commit)) + return new_objects def add_fields(self, form, index): """Add a hidden field for the object's primary key.""" @@ -286,25 +300,61 @@ class BaseModelFormSet(BaseFormSet): form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput) super(BaseModelFormSet, self).add_fields(form, index) -def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(), formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None): - form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback) +def formset_for_queryset(queryset, form=BaseForm, formfield_callback=lambda f: f.formfield(), + formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None): + """ + Returns a FormSet class for the given QuerySet. This FormSet will contain + change forms for every instance in the QuerySet as well as the number of + add forms specified by ``extra``. + + Provide ``extra`` to determine the number of add forms to display. + + Provide ``deletable`` if you want to allow the formset to delete any + objects in the given queryset. + + Provide ``form`` if you want to use a custom BaseForm subclass. + + Provide ``formfield_callback`` if you want to define different logic for + determining the formfield for a given database field. It's a callable that + takes a database Field instance and returns a form Field instance. + + Provide ``formset`` if you want to use a custom BaseModelFormSet subclass. + """ + form = form_for_model(queryset.model, form=form, fields=fields, formfield_callback=formfield_callback) FormSet = formset_for_form(form, formset, extra, orderable, deletable) - FormSet.model = model + FormSet.model = queryset.model + FormSet.queryset = queryset return FormSet +def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(), + formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None): + """ + Returns a FormSet class for the given Django model class. This FormSet + will contain change forms for every instance of the given model as well + as the number of add forms specified by ``extra``. + + This is essentially the same as ``formset_for_queryset``, but automatically + uses the model's default manager to determine the queryset. + """ + qs = model._default_manager.all() + return formset_for_queryset(qs, form, formfield_callback, formset, extra, orderable, deletable, fields) + class InlineFormset(BaseModelFormSet): """A formset for child objects related to a parent.""" - def __init__(self, instance=None, data=None, files=None): + def __init__(self, instance, data=None, files=None): from django.db.models.fields.related import RelatedObject self.instance = instance # is there a better way to get the object descriptor? self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name() - super(InlineFormset, self).__init__(data, files, instances=self.get_inline_objects(), prefix=self.rel_name) + super(InlineFormset, self).__init__(data, files, prefix=self.rel_name) - def get_inline_objects(self): - if self.instance is None: - return [] - return getattr(self.instance, self.rel_name).all() + def get_queryset(self): + """ + Returns this FormSet's queryset, but restricted to children of + self.instance + """ + kwargs = {self.fk.name: self.instance} + return self.queryset.filter(**kwargs) def save_new(self, form, commit=True): kwargs = {self.fk.get_attname(): self.instance._get_pk_val()} diff --git a/tests/modeltests/model_formsets/__init__.py b/tests/modeltests/model_formsets/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py new file mode 100644 index 0000000000..cac73d707f --- /dev/null +++ b/tests/modeltests/model_formsets/models.py @@ -0,0 +1,204 @@ +from django.db import models + +class Author(models.Model): + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name + +class Book(models.Model): + author = models.ForeignKey(Author) + title = models.CharField(max_length=100) + + def __unicode__(self): + return self.title + + +__test__ = {'API_TESTS': """ + +>>> from django.newforms.models import formset_for_queryset, formset_for_model + +>>> qs = Author.objects.all() +>>> AuthorFormSet = formset_for_model(Author, extra=3) + +>>> formset = AuthorFormSet() +>>> for form in formset.forms: +... print form.as_p() +

+

+

+ +>>> data = { +... 'form-COUNT': '3', +... 'form-0-name': 'Charles Baudelaire', +... 'form-1-name': 'Arthur Rimbaud', +... 'form-2-name': '', +... } + +>>> formset = AuthorFormSet(data=data) +>>> formset.is_valid() +True + +>>> formset.save() +[, ] + +>>> for author in Author.objects.order_by('name'): +... print author.name +Arthur Rimbaud +Charles Baudelaire + + +Gah! We forgot Paul Verlaine. Let's create a formset to edit the existing +authors with an extra form to add him. This time we'll use formset_for_queryset. +We *could* use formset_for_queryset to restrict the Author objects we edit, +but in that case we'll use it to display them in alphabetical order by name. + +>>> qs = Author.objects.order_by('name') +>>> AuthorFormSet = formset_for_queryset(qs, extra=1, deletable=False) + +>>> formset = AuthorFormSet() +>>> for form in formset.forms: +... print form.as_p() +

+

+

+ + +>>> data = { +... 'form-COUNT': '3', +... 'form-0-id': '2', +... 'form-0-name': 'Arthur Rimbaud', +... 'form-1-id': '1', +... 'form-1-name': 'Charles Baudelaire', +... 'form-2-name': 'Paul Verlaine', +... } + +>>> formset = AuthorFormSet(data=data) +>>> formset.is_valid() +True + +>>> formset.save() +[, , ] + +>>> for author in Author.objects.order_by('name'): +... print author.name +Arthur Rimbaud +Charles Baudelaire +Paul Verlaine + + +This probably shouldn't happen, but it will. If an add form was marked for +deltetion, make sure we don't save that form. + +>>> qs = Author.objects.order_by('name') +>>> AuthorFormSet = formset_for_queryset(qs, extra=1, deletable=True) + +>>> formset = AuthorFormSet() +>>> for form in formset.forms: +... print form.as_p() +

+

+

+

+

+

+

+

+ +>>> data = { +... 'form-COUNT': '4', +... 'form-0-id': '2', +... 'form-0-name': 'Arthur Rimbaud', +... 'form-1-id': '1', +... 'form-1-name': 'Charles Baudelaire', +... 'form-2-id': '3', +... 'form-2-name': 'Paul Verlaine', +... 'form-3-name': 'Walt Whitman', +... 'form-3-DELETE': 'on', +... } + +>>> formset = AuthorFormSet(data=data) +>>> formset.is_valid() +True + +>>> formset.save() +[, , ] + +>>> for author in Author.objects.order_by('name'): +... print author.name +Arthur Rimbaud +Charles Baudelaire +Paul Verlaine + + +We can also create a formset that is tied to a parent model. This is how the +admin system's edit inline functionality works. + +>>> from django.newforms.models import inline_formset + +>>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=3) +>>> author = Author.objects.get(name='Charles Baudelaire') + +>>> formset = AuthorBooksFormSet(author) +>>> for form in formset.forms: +... print form.as_p() +

+

+

+ +>>> data = { +... 'book_set-COUNT': '3', +... 'book_set-0-title': 'Les Fleurs du Mal', +... 'book_set-1-title': '', +... 'book_set-2-title': '', +... } + +>>> formset = AuthorBooksFormSet(author, data=data) +>>> formset.is_valid() +True + +>>> formset.save() +[] + +>>> for book in author.book_set.all(): +... print book.title +Les Fleurs du Mal + + +Now that we've added a book to Charles Baudelaire, let's try adding another +one. This time though, an edit form will be available for every existing +book. + +>>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=2) +>>> author = Author.objects.get(name='Charles Baudelaire') + +>>> formset = AuthorBooksFormSet(author) +>>> for form in formset.forms: +... print form.as_p() +

+

+

+ +>>> data = { +... 'book_set-COUNT': '3', +... 'book_set-0-id': '1', +... 'book_set-0-title': 'Les Fleurs du Mal', +... 'book_set-1-title': 'Le Spleen de Paris', +... 'book_set-2-title': '', +... } + +>>> formset = AuthorBooksFormSet(author, data=data) +>>> formset.is_valid() +True + +>>> formset.save() +[, ] + +As you can see, 'Le Spleen de Paris' is now a book belonging to Charles Baudelaire. + +>>> for book in author.book_set.order_by('title'): +... print book.title +Le Spleen de Paris +Les Fleurs du Mal + +"""}