Author: jkocherhans
Date: 2007-11-06 15:53:15 -0600 (Tue, 06 Nov 2007)
New Revision: 6655

Added:
   django/branches/newforms-admin/tests/modeltests/model_formsets/
   django/branches/newforms-admin/tests/modeltests/model_formsets/__init__.py
   django/branches/newforms-admin/tests/modeltests/model_formsets/models.py
Modified:
   django/branches/newforms-admin/django/newforms/models.py
Log:
newforms-admin: Fixed #5733. Added formset_for_queryset. This is backwards 
incompatble for anyone using formset_for_model or BaseModelFormSet.


Modified: django/branches/newforms-admin/django/newforms/models.py
===================================================================
--- django/branches/newforms-admin/django/newforms/models.py    2007-11-06 
02:38:12 UTC (rev 6654)
+++ django/branches/newforms-admin/django/newforms/models.py    2007-11-06 
21:53:15 UTC (rev 6655)
@@ -15,8 +15,8 @@
 
 __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 @@
 
 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 @@
         """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 @@
         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()}

Added: 
django/branches/newforms-admin/tests/modeltests/model_formsets/__init__.py
===================================================================

Added: django/branches/newforms-admin/tests/modeltests/model_formsets/models.py
===================================================================
--- django/branches/newforms-admin/tests/modeltests/model_formsets/models.py    
                        (rev 0)
+++ django/branches/newforms-admin/tests/modeltests/model_formsets/models.py    
2007-11-06 21:53:15 UTC (rev 6655)
@@ -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
+
+"""}


--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/django-updates?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to