1
0
mirror of https://github.com/django/django.git synced 2025-07-04 17:59:13 +00:00

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
This commit is contained in:
Joseph Kocherhans 2007-11-06 21:53:15 +00:00
parent 99e8171a61
commit 6f15904669
3 changed files with 285 additions and 31 deletions

View File

@ -15,8 +15,8 @@ from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
__all__ = ( __all__ = (
'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields', 'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
'ModelChoiceField', 'ModelMultipleChoiceField', 'formset_for_model', 'formset_for_model', 'formset_for_queryset', 'inline_formset',
'inline_formset' 'ModelChoiceField', 'ModelMultipleChoiceField',
) )
def save_instance(form, instance, fields=None, fail_message='saved', commit=True): 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): 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 model = None
queryset = None
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, instances=None): def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None):
self.instances = instances
kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix} kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
if instances: if self.queryset:
kwargs['initial'] = [initial_data(instance) for instance in instances] kwargs['initial'] = [initial_data(obj) for obj in self.get_queryset()]
super(BaseModelFormSet, self).__init__(**kwargs) super(BaseModelFormSet, self).__init__(**kwargs)
def get_queryset(self):
return self.queryset._clone()
def save_new(self, form, commit=True): def save_new(self, form, commit=True):
"""Saves and returns a new model instance for the given form.""" """Saves and returns a new model instance for the given form."""
return save_instance(form, self.model(), commit=commit) 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 """Saves model instances for every form, adding and changing instances
as necessary, and returns the list of 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 = [] 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: for form in self.change_forms:
instance = instances[form.cleaned_data[self.model._meta.pk.attname]] obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]
if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]: if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
instance.delete() obj.delete()
else: else:
saved_instances.append(self.save_instance(form, instance, commit=commit)) saved_instances.append(self.save_instance(form, obj, commit=commit))
# create/save new instances return saved_instances
def save_new_objects(self, commit=True):
new_objects = []
for form in self.add_forms: for form in self.add_forms:
if form.is_empty(): if form.is_empty():
continue continue
saved_instances.append(self.save_new(form, commit=commit)) # If someone has marked an add form for deletion, don't save the
return saved_instances # 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): def add_fields(self, form, index):
"""Add a hidden field for the object's primary key.""" """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) form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
super(BaseModelFormSet, self).add_fields(form, index) 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): def formset_for_queryset(queryset, form=BaseForm, formfield_callback=lambda f: f.formfield(),
form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback) 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 = formset_for_form(form, formset, extra, orderable, deletable)
FormSet.model = model FormSet.model = queryset.model
FormSet.queryset = queryset
return FormSet 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): class InlineFormset(BaseModelFormSet):
"""A formset for child objects related to a parent.""" """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 from django.db.models.fields.related import RelatedObject
self.instance = instance self.instance = instance
# is there a better way to get the object descriptor? # 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() 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): def get_queryset(self):
if self.instance is None: """
return [] Returns this FormSet's queryset, but restricted to children of
return getattr(self.instance, self.rel_name).all() self.instance
"""
kwargs = {self.fk.name: self.instance}
return self.queryset.filter(**kwargs)
def save_new(self, form, commit=True): def save_new(self, form, commit=True):
kwargs = {self.fk.get_attname(): self.instance._get_pk_val()} kwargs = {self.fk.get_attname(): self.instance._get_pk_val()}

View File

@ -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()
<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>
<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" maxlength="100" /><input type="hidden" name="form-1-id" id="id_form-1-id" /></p>
<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" maxlength="100" /><input type="hidden" name="form-2-id" id="id_form-2-id" /></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()
[<Author: Charles Baudelaire>, <Author: Arthur Rimbaud>]
>>> 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()
<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>
<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" value="Charles Baudelaire" maxlength="100" /><input type="hidden" name="form-1-id" value="1" id="id_form-1-id" /></p>
<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" maxlength="100" /><input type="hidden" name="form-2-id" id="id_form-2-id" /></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()
[<Author: Arthur Rimbaud>, <Author: Charles Baudelaire>, <Author: Paul Verlaine>]
>>> 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()
<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>
<p><label for="id_form-0-DELETE">Delete:</label> <input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE" /><input type="hidden" name="form-0-id" value="2" id="id_form-0-id" /></p>
<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" value="Charles Baudelaire" maxlength="100" /></p>
<p><label for="id_form-1-DELETE">Delete:</label> <input type="checkbox" name="form-1-DELETE" id="id_form-1-DELETE" /><input type="hidden" name="form-1-id" value="1" id="id_form-1-id" /></p>
<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" value="Paul Verlaine" maxlength="100" /></p>
<p><label for="id_form-2-DELETE">Delete:</label> <input type="checkbox" name="form-2-DELETE" id="id_form-2-DELETE" /><input type="hidden" name="form-2-id" value="3" id="id_form-2-id" /></p>
<p><label for="id_form-3-name">Name:</label> <input id="id_form-3-name" type="text" name="form-3-name" maxlength="100" /></p>
<p><label for="id_form-3-DELETE">Delete:</label> <input type="checkbox" name="form-3-DELETE" id="id_form-3-DELETE" /><input type="hidden" name="form-3-id" id="id_form-3-id" /></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()
[<Author: Arthur Rimbaud>, <Author: Charles Baudelaire>, <Author: Paul Verlaine>]
>>> 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()
<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>
<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>
<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></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()
[<Book: Les Fleurs du Mal>]
>>> 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()
<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>
<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>
<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></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()
[<Book: Les Fleurs du Mal>, <Book: Le Spleen de Paris>]
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
"""}