Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

newforms-admin: Fixed #5733. Added formset_for_queryset. This is back…

…wards 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
  • Loading branch information...
commit 6f15904669f884de75dbd4cc2b2e03880d81a9a8 1 parent 99e8171
Joseph Kocherhans authored November 06, 2007
112  django/newforms/models.py
@@ -15,8 +15,8 @@
15 15
 
16 16
 __all__ = (
17 17
     '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',
20 20
 )
21 21
 
22 22
 def save_instance(form, instance, fields=None, fail_message='saved', commit=True):
@@ -237,17 +237,20 @@ def initial_data(instance, fields=None):
237 237
 
238 238
 class BaseModelFormSet(BaseFormSet):
239 239
     """
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.
241 241
     """
242 242
     model = None
  243
+    queryset = None
243 244
 
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):
246 246
         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()]
249 249
         super(BaseModelFormSet, self).__init__(**kwargs)
250 250
 
  251
+    def get_queryset(self):
  252
+        return self.queryset._clone()
  253
+
251 254
     def save_new(self, form, commit=True):
252 255
         """Saves and returns a new model instance for the given form."""
253 256
         return save_instance(form, self.model(), commit=commit)
@@ -260,25 +263,36 @@ def save(self, commit=True):
260 263
         """Saves model instances for every form, adding and changing instances
261 264
         as necessary, and returns the list of instances.
262 265
         """
  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.get_queryset into a dict so they are easy to lookup by pk
  272
+        existing_objects = {}
  273
+        for obj in self.get_queryset():
  274
+            existing_objects[obj._get_pk_val()] = obj
263 275
         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 self.deletable and 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 = []
277 286
         for form in self.add_forms:
278 287
             if form.is_empty():
279 288
                 continue
280  
-            saved_instances.append(self.save_new(form, commit=commit))
281  
-        return saved_instances
  289
+            # If someone has marked an add form for deletion, don't save the
  290
+            # object. At some point it would be nice if we didn't display
  291
+            # the deletion widget for add forms.
  292
+            if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
  293
+                continue
  294
+            new_objects.append(self.save_new(form, commit=commit))
  295
+        return new_objects
282 296
 
283 297
     def add_fields(self, form, index):
284 298
         """Add a hidden field for the object's primary key."""
@@ -286,25 +300,61 @@ def add_fields(self, form, index):
286 300
         form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
287 301
         super(BaseModelFormSet, self).add_fields(form, index)
288 302
 
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)
  303
+def formset_for_queryset(queryset, form=BaseForm, formfield_callback=lambda f: f.formfield(),
  304
+                         formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None):
  305
+    """
  306
+    Returns a FormSet class for the given QuerySet. This FormSet will contain
  307
+    change forms for every instance in the QuerySet as well as the number of
  308
+    add forms specified by ``extra``.
  309
+    
  310
+    Provide ``extra`` to determine the number of add forms to display.
  311
+    
  312
+    Provide ``deletable`` if you want to allow the formset to delete any
  313
+    objects in the given queryset.
  314
+    
  315
+    Provide ``form`` if you want to use a custom BaseForm subclass.
  316
+    
  317
+    Provide ``formfield_callback`` if you want to define different logic for
  318
+    determining the formfield for a given database field. It's a callable that
  319
+    takes a database Field instance and returns a form Field instance.
  320
+    
  321
+    Provide ``formset`` if you want to use a custom BaseModelFormSet subclass.
  322
+    """
  323
+    form = form_for_model(queryset.model, form=form, fields=fields, formfield_callback=formfield_callback)
291 324
     FormSet = formset_for_form(form, formset, extra, orderable, deletable)
292  
-    FormSet.model = model
  325
+    FormSet.model = queryset.model
  326
+    FormSet.queryset = queryset
293 327
     return FormSet
294 328
 
  329
+def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(),
  330
+                      formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None):
  331
+    """
  332
+    Returns a FormSet class for the given Django model class. This FormSet
  333
+    will contain change forms for every instance of the given model as well
  334
+    as the number of add forms specified by ``extra``.
  335
+    
  336
+    This is essentially the same as ``formset_for_queryset``, but automatically
  337
+    uses the model's default manager to determine the queryset.
  338
+    """
  339
+    qs = model._default_manager.all()
  340
+    return formset_for_queryset(qs, form, formfield_callback, formset, extra, orderable, deletable, fields)
  341
+
295 342
 class InlineFormset(BaseModelFormSet):
296 343
     """A formset for child objects related to a parent."""
297  
-    def __init__(self, instance=None, data=None, files=None):
  344
+    def __init__(self, instance, data=None, files=None):
298 345
         from django.db.models.fields.related import RelatedObject
299 346
         self.instance = instance
300 347
         # is there a better way to get the object descriptor?
301 348
         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)
  349
+        super(InlineFormset, self).__init__(data, files, prefix=self.rel_name)
303 350
 
304  
-    def get_inline_objects(self):
305  
-        if self.instance is None:
306  
-            return []
307  
-        return getattr(self.instance, self.rel_name).all()
  351
+    def get_queryset(self):
  352
+        """
  353
+        Returns this FormSet's queryset, but restricted to children of 
  354
+        self.instance
  355
+        """
  356
+        kwargs = {self.fk.name: self.instance}
  357
+        return self.queryset.filter(**kwargs)
308 358
 
309 359
     def save_new(self, form, commit=True):
310 360
         kwargs = {self.fk.get_attname(): self.instance._get_pk_val()}
0  tests/modeltests/model_formsets/__init__.py
No changes.
204  tests/modeltests/model_formsets/models.py
... ...
@@ -0,0 +1,204 @@
  1
+from django.db import models
  2
+
  3
+class Author(models.Model):
  4
+    name = models.CharField(max_length=100)
  5
+
  6
+    def __unicode__(self):
  7
+        return self.name
  8
+
  9
+class 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()
  40
+True
  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
  47
+Arthur Rimbaud
  48
+Charles Baudelaire
  49
+
  50
+
  51
+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_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()
  78
+True
  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
  85
+Arthur Rimbaud
  86
+Charles Baudelaire
  87
+Paul Verlaine
  88
+
  89
+
  90
+This probably shouldn't happen, but it will. If an add form was marked for
  91
+deltetion, make sure we don't save that form.
  92
+
  93
+>>> qs = Author.objects.order_by('name')
  94
+>>> AuthorFormSet = formset_for_queryset(qs, extra=1, deletable=True)
  95
+
  96
+>>> formset = AuthorFormSet()
  97
+>>> for form in formset.forms:
  98
+...     print form.as_p()
  99
+<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>
  100
+<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>
  101
+<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>
  102
+<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>
  103
+<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>
  104
+<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>
  105
+<p><label for="id_form-3-name">Name:</label> <input id="id_form-3-name" type="text" name="form-3-name" maxlength="100" /></p>
  106
+<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>
  107
+
  108
+>>> data = {
  109
+...     'form-COUNT': '4',
  110
+...     'form-0-id': '2',
  111
+...     'form-0-name': 'Arthur Rimbaud',
  112
+...     'form-1-id': '1',
  113
+...     'form-1-name': 'Charles Baudelaire',
  114
+...     'form-2-id': '3',
  115
+...     'form-2-name': 'Paul Verlaine',
  116
+...     'form-3-name': 'Walt Whitman',
  117
+...     'form-3-DELETE': 'on',
  118
+... }
  119
+
  120
+>>> formset = AuthorFormSet(data=data)
  121
+>>> formset.is_valid()
  122
+True
  123
+
  124
+>>> formset.save()
  125
+[<Author: Arthur Rimbaud>, <Author: Charles Baudelaire>, <Author: Paul Verlaine>]
  126
+
  127
+>>> for author in Author.objects.order_by('name'):
  128
+...     print author.name
  129
+Arthur Rimbaud
  130
+Charles Baudelaire
  131
+Paul Verlaine
  132
+
  133
+
  134
+We can also create a formset that is tied to a parent model. This is how the
  135
+admin system's edit inline functionality works.
  136
+
  137
+>>> from django.newforms.models import inline_formset
  138
+
  139
+>>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=3)
  140
+>>> author = Author.objects.get(name='Charles Baudelaire')
  141
+
  142
+>>> formset = AuthorBooksFormSet(author)
  143
+>>> for form in formset.forms:
  144
+...     print form.as_p()
  145
+<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>
  146
+<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>
  147
+<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>
  148
+
  149
+>>> data = {
  150
+...     'book_set-COUNT': '3',
  151
+...     'book_set-0-title': 'Les Fleurs du Mal',
  152
+...     'book_set-1-title': '',
  153
+...     'book_set-2-title': '',
  154
+... }
  155
+
  156
+>>> formset = AuthorBooksFormSet(author, data=data)
  157
+>>> formset.is_valid()
  158
+True
  159
+
  160
+>>> formset.save()
  161
+[<Book: Les Fleurs du Mal>]
  162
+
  163
+>>> for book in author.book_set.all():
  164
+...     print book.title
  165
+Les Fleurs du Mal
  166
+
  167
+
  168
+Now that we've added a book to Charles Baudelaire, let's try adding another
  169
+one. This time though, an edit form will be available for every existing
  170
+book.
  171
+
  172
+>>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=2)
  173
+>>> author = Author.objects.get(name='Charles Baudelaire')
  174
+
  175
+>>> formset = AuthorBooksFormSet(author)
  176
+>>> for form in formset.forms:
  177
+...     print form.as_p()
  178
+<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>
  179
+<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>
  180
+<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>
  181
+
  182
+>>> data = {
  183
+...     'book_set-COUNT': '3',
  184
+...     'book_set-0-id': '1',
  185
+...     'book_set-0-title': 'Les Fleurs du Mal',
  186
+...     'book_set-1-title': 'Le Spleen de Paris',
  187
+...     'book_set-2-title': '',
  188
+... }
  189
+
  190
+>>> formset = AuthorBooksFormSet(author, data=data)
  191
+>>> formset.is_valid()
  192
+True
  193
+
  194
+>>> formset.save()
  195
+[<Book: Les Fleurs du Mal>, <Book: Le Spleen de Paris>]
  196
+
  197
+As you can see, 'Le Spleen de Paris' is now a book belonging to Charles Baudelaire.
  198
+
  199
+>>> for book in author.book_set.order_by('title'):
  200
+...     print book.title
  201
+Le Spleen de Paris
  202
+Les Fleurs du Mal
  203
+
  204
+"""}

0 notes on commit 6f15904

Please sign in to comment.
Something went wrong with that request. Please try again.