Ticket #5733: r6614_formset_for_queryset.diff

File r6614_formset_for_queryset.diff, 14.1 KB (added by Brian Rosner, 16 years ago)

updated patch to match r6614. also includes a missing file and passes all tests.

  • django/newforms/models.py

    === django/newforms/models.py
    ==================================================================
     
    1515
    1616__all__ = (
    1717    'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
    18     'ModelChoiceField', 'ModelMultipleChoiceField', 'formset_for_model',
    19     'inline_formset'
     18    'formset_for_model', 'formset_for_queryset', 'inline_formset',
     19    'ModelChoiceField', 'ModelMultipleChoiceField',
    2020)
    2121
    2222def save_instance(form, instance, fields=None, fail_message='saved', commit=True):
     
    237237
    238238class BaseModelFormSet(BaseFormSet):
    239239    """
    240     A ``FormSet`` attatched to a particular model or sequence of model instances.
     240    A ``FormSet`` for editing a queryset and/or adding new objects to it.
    241241    """
    242242    model = None
     243    queryset = None
    243244
    244     def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, instances=None):
    245         self.instances = instances
     245    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None):
    246246        kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
    247         if instances:
    248             kwargs['initial'] = [initial_data(instance) for instance in instances]
     247        if self.queryset:
     248            kwargs['initial'] = [initial_data(obj) for obj in self.get_queryset()]
    249249        super(BaseModelFormSet, self).__init__(**kwargs)
    250250
     251    def get_queryset(self):
     252        return self.queryset._clone()
     253
    251254    def save_new(self, form, commit=True):
    252255        """Saves and returns a new model instance for the given form."""
    253256        return save_instance(form, self.model(), commit=commit)
     
    260263        """Saves model instances for every form, adding and changing instances
    261264        as necessary, and returns the list of instances.
    262265        """
     266        return self.save_existing_objects(commit) + self.save_new_objects(commit)
     267
     268    def save_existing_objects(self, commit=True):
     269        if not self.queryset:
     270            return []
     271        # Put the objects from self.queryset into a dict so they are easy to lookup by pk
     272        existing_objects = {}
     273        for obj in self.queryset._clone():
     274            existing_objects[obj._get_pk_val()] = obj
    263275        saved_instances = []
    264         # put self.instances into a dict so they are easy to lookup by pk
    265         instances = {}
    266         for instance in self.instances:
    267             instances[instance._get_pk_val()] = instance
    268         if self.instances:
    269             # update/save existing instances
    270             for form in self.change_forms:
    271                 instance = instances[form.cleaned_data[self.model._meta.pk.attname]]
    272                 if form.cleaned_data[DELETION_FIELD_NAME]:
    273                     instance.delete()
    274                 else:
    275                     saved_instances.append(self.save_instance(form, instance, commit=commit))
    276         # create/save new instances
     276        for form in self.change_forms:
     277            obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]
     278            if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     279                obj.delete()
     280            else:
     281                saved_instances.append(self.save_instance(form, obj, commit=commit))
     282        return saved_instances
     283
     284    def save_new_objects(self, commit=True):
     285        new_objects = []
    277286        for form in self.add_forms:
    278287            if form.is_empty():
    279288                continue
    280             saved_instances.append(self.save_new(form, commit=commit))
    281         return saved_instances
     289            new_objects.append(self.save_new(form, commit=commit))
     290        return new_objects
    282291
    283292    def add_fields(self, form, index):
    284293        """Add a hidden field for the object's primary key."""
     
    286295        form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
    287296        super(BaseModelFormSet, self).add_fields(form, index)
    288297
    289 def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(), formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None):
    290     form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback)
     298def formset_for_queryset(queryset, form=BaseForm, formfield_callback=lambda f: f.formfield(),
     299                         formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None):
     300    """
     301    Returns a FormSet class for the given QuerySet. This FormSet will contain
     302    change forms for every instance in the QuerySet as well as the number of
     303    add forms specified by ``extra``.
     304   
     305    Provide ``extra`` to determine the number of add forms to display.
     306   
     307    Provide ``deletable`` if you want to allow the formset to delete any
     308    objects in the given queryset.
     309   
     310    Provide ``form`` if you want to use a custom BaseForm subclass.
     311   
     312    Provide ``formfield_callback`` if you want to define different logic for
     313    determining the formfield for a given database field. It's a callable that
     314    takes a database Field instance and returns a form Field instance.
     315   
     316    Provide ``formset`` if you want to use a custom BaseModelFormSet subclass.
     317    """
     318    form = form_for_model(queryset.model, form=form, fields=fields, formfield_callback=formfield_callback)
    291319    FormSet = formset_for_form(form, formset, extra, orderable, deletable)
    292     FormSet.model = model
     320    FormSet.model = queryset.model
     321    FormSet.queryset = queryset
    293322    return FormSet
    294323
     324def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(),
     325                      formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None):
     326    """
     327    Returns a FormSet class for the given Django model class. This FormSet
     328    will contain change forms for every instance of the given model as well
     329    as the number of add forms specified by ``extra``.
     330   
     331    This is essentially the same as ``formset_for_queryset``, but automatically
     332    uses the model's default manager to determine the queryset.
     333    """
     334    qs = model._default_manager.all()
     335    return formset_for_queryset(qs, form, formfield_callback, formset, extra, orderable, deletable, fields)
     336
    295337class InlineFormset(BaseModelFormSet):
    296338    """A formset for child objects related to a parent."""
    297     def __init__(self, instance=None, data=None, files=None):
     339    def __init__(self, instance, data=None, files=None):
    298340        from django.db.models.fields.related import RelatedObject
    299341        self.instance = instance
    300342        # is there a better way to get the object descriptor?
    301343        self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
    302         super(InlineFormset, self).__init__(data, files, instances=self.get_inline_objects(), prefix=self.rel_name)
     344        super(InlineFormset, self).__init__(data, files, prefix=self.rel_name)
    303345
    304     def get_inline_objects(self):
    305         if self.instance is None:
    306             return []
    307         return getattr(self.instance, self.rel_name).all()
     346    def get_queryset(self):
     347        """
     348        Returns this FormSet's queryset, but restricted to children of
     349        self.instance
     350        """
     351        kwargs = {self.fk.name: self.instance}
     352        return self.queryset.filter(**kwargs)
    308353
    309354    def save_new(self, form, commit=True):
    310355        kwargs = {self.fk.get_attname(): self.instance._get_pk_val()}
  • tests/modeltests/model_formsets/models.py

    === tests/modeltests/model_formsets	(new directory)
    ==================================================================
    === tests/modeltests/model_formsets/__init__.py
    ==================================================================
    === tests/modeltests/model_formsets/models.py
    ==================================================================
     
     1from django.db import models
     2
     3class Author(models.Model):
     4    name = models.CharField(max_length=100)
     5
     6    def __unicode__(self):
     7        return self.name
     8
     9class Book(models.Model):
     10    author = models.ForeignKey(Author)
     11    title = models.CharField(max_length=100)
     12
     13    def __unicode__(self):
     14        return self.title
     15
     16
     17__test__ = {'API_TESTS': """
     18
     19>>> from django.newforms.models import formset_for_queryset, formset_for_model
     20
     21>>> qs = Author.objects.all()
     22>>> AuthorFormSet = formset_for_model(Author, extra=3)
     23
     24>>> formset = AuthorFormSet()
     25>>> for form in formset.forms:
     26...     print form.as_p()
     27<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>
     28<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>
     29<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>
     30
     31>>> data = {
     32...     'form-COUNT': '3',
     33...     'form-0-name': 'Charles Baudelaire',
     34...     'form-1-name': 'Arthur Rimbaud',
     35...     'form-2-name': '',
     36... }
     37
     38>>> formset = AuthorFormSet(data=data)
     39>>> formset.is_valid()
     40True
     41
     42>>> formset.save()
     43[<Author: Charles Baudelaire>, <Author: Arthur Rimbaud>]
     44
     45>>> for author in Author.objects.order_by('name'):
     46...     print author.name
     47Arthur Rimbaud
     48Charles Baudelaire
     49
     50
     51Gah! We forgot Paul Verlaine. Let's create a formset to edit the existing
     52authors with an extra form to add him. This time we'll use formset_for_queryset.
     53We *could* use formset_for_queryset to restrict the Author objects we edit,
     54but 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_queryset(qs, extra=1, deletable=False)
     58
     59>>> formset = AuthorFormSet()
     60>>> for form in formset.forms:
     61...     print form.as_p()
     62<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>
     63<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>
     64<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>
     65
     66
     67>>> data = {
     68...     'form-COUNT': '3',
     69...     'form-0-id': '2',
     70...     'form-0-name': 'Arthur Rimbaud',
     71...     'form-1-id': '1',
     72...     'form-1-name': 'Charles Baudelaire',
     73...     'form-2-name': 'Paul Verlaine',
     74... }
     75
     76>>> formset = AuthorFormSet(data=data)
     77>>> formset.is_valid()
     78True
     79
     80>>> formset.save()
     81[<Author: Arthur Rimbaud>, <Author: Charles Baudelaire>, <Author: Paul Verlaine>]
     82
     83>>> for author in Author.objects.order_by('name'):
     84...     print author.name
     85Arthur Rimbaud
     86Charles Baudelaire
     87Paul Verlaine
     88
     89
     90
     91We can also create a formset that is tied to a parent model. This is how the
     92admin system's edit inline functionality works.
     93
     94>>> from django.newforms.models import inline_formset
     95
     96>>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=3)
     97>>> author = Author.objects.get(name='Charles Baudelaire')
     98
     99>>> formset = AuthorBooksFormSet(author)
     100>>> for form in formset.forms:
     101...     print form.as_p()
     102<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>
     103<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>
     104<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>
     105
     106>>> data = {
     107...     'book_set-COUNT': '3',
     108...     'book_set-0-title': 'Les Fleurs du Mal',
     109...     'book_set-1-title': '',
     110...     'book_set-2-title': '',
     111... }
     112
     113>>> formset = AuthorBooksFormSet(author, data=data)
     114>>> formset.is_valid()
     115True
     116
     117>>> formset.save()
     118[<Book: Les Fleurs du Mal>]
     119
     120>>> for book in author.book_set.all():
     121...     print book.title
     122Les Fleurs du Mal
     123
     124
     125Now that we've added a book to Charles Baudelaire, let's try adding another
     126one. This time though, an edit form will be available for every existing
     127book.
     128
     129>>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=2)
     130>>> author = Author.objects.get(name='Charles Baudelaire')
     131
     132>>> formset = AuthorBooksFormSet(author)
     133>>> for form in formset.forms:
     134...     print form.as_p()
     135<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>
     136<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>
     137<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>
     138
     139>>> data = {
     140...     'book_set-COUNT': '3',
     141...     'book_set-0-id': '1',
     142...     'book_set-0-title': 'Les Fleurs du Mal',
     143...     'book_set-1-title': 'Le Spleen de Paris',
     144...     'book_set-2-title': '',
     145... }
     146
     147>>> formset = AuthorBooksFormSet(author, data=data)
     148>>> formset.is_valid()
     149True
     150
     151>>> formset.save()
     152[<Book: Les Fleurs du Mal>, <Book: Le Spleen de Paris>]
     153
     154As you can see, 'Le Spleen de Paris' is now a book belonging to Charles Baudelaire.
     155
     156>>> for book in author.book_set.order_by('title'):
     157...     print book.title
     158Le Spleen de Paris
     159Les Fleurs du Mal
     160
     161
     162"""}
Back to Top