Ticket #5733: form_for_queryset.diff
File form_for_queryset.diff, 17.2 KB (added by , 17 years ago) |
---|
-
django/newforms/models.py
diff --git a/django/newforms/models.py b/django/newforms/models.py index 5c4dc08..8401296 100644
a b from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput 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): … … 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) … … class BaseModelFormSet(BaseFormSet): 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.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 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 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_instances289 new_objects.append(self.save_new(form, commit=commit)) 290 return new_objects 282 291 283 292 def add_fields(self, form, index): 284 293 """Add a hidden field for the object's primary key.""" … … class BaseModelFormSet(BaseFormSet): 286 295 form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput) 287 296 super(BaseModelFormSet, self).add_fields(form, index) 288 297 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) 298 def 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) 291 319 FormSet = formset_for_form(form, formset, extra, orderable, deletable) 292 FormSet.model = model 320 FormSet.model = queryset.model 321 FormSet.queryset = queryset 293 322 return FormSet 294 323 324 def 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 295 337 class InlineFormset(BaseModelFormSet): 296 338 """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): 298 340 from django.db.models.fields.related import RelatedObject 299 341 self.instance = instance 300 342 # is there a better way to get the object descriptor? 301 343 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) 303 345 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) 308 353 309 354 def save_new(self, form, commit=True): 310 355 kwargs = {self.fk.get_attname(): self.instance._get_pk_val()} 311 356 new_obj = self.model(**kwargs) 312 357 return save_instance(form, new_obj, commit=commit) 313 358 314 def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()):359 def get_foreign_key(parent_model, model, fk_name=None): 315 360 """ 316 Returns an ``InlineFormset`` for the given kwargs. 317 318 You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` 319 to ``parent_model``. 361 Finds and returns the ForeignKey from model to parent if there is one. 362 If fk_name is provided, assume it is the name of the ForeignKey field. 320 363 """ 364 # avoid circular import 321 365 from django.db.models import ForeignKey 322 366 opts = model._meta 323 # figure out what the ForeignKey from model to parent_model is 324 if fk_name is None: 325 fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model] 326 if len(fks_to_parent) == 1: 327 fk = fks_to_parent[0] 328 elif len(fks_to_parent) == 0: 329 raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) 330 else: 331 raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) 332 else: 367 if fk_name: 333 368 fks_to_parent = [f for f in opts.fields if f.name == fk_name] 334 369 if len(fks_to_parent) == 1: 335 370 fk = fks_to_parent[0] … … def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orde 337 372 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model)) 338 373 elif len(fks_to_parent) == 0: 339 374 raise Exception("%s has no field named '%s'" % (model, fk_name)) 375 else: 376 # Try to discover what the ForeignKey from model to parent_model is 377 fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model] 378 if len(fks_to_parent) == 1: 379 fk = fks_to_parent[0] 380 elif len(fks_to_parent) == 0: 381 raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) 382 else: 383 raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) 384 return fk 385 386 def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, 387 orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()): 388 """ 389 Returns an ``InlineFormset`` for the given kwargs. 390 391 You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` 392 to ``parent_model``. 393 """ 394 fk = get_foreign_key(parent_model, model, fk_name=fk_name) 340 395 # let the formset handle object deletion by default 341 396 FormSet = formset_for_model(model, formset=InlineFormset, fields=fields, 342 397 formfield_callback=formfield_callback, … … def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orde 349 404 del FormSet.form_class.base_fields[fk.name] 350 405 except KeyError: 351 406 pass 352 FormSet.parent_model = parent_model353 FormSet.fk_name = fk.name354 407 FormSet.fk = fk 355 408 return FormSet -
new file tests/modeltests/model_formsets/models.py
diff --git a/tests/modeltests/model_formsets/__init__.py b/tests/modeltests/model_formsets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py new file mode 100644 index 0000000..8abc9f8
- + 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 91 We can also create a formset that is tied to a parent model. This is how the 92 admin 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() 115 True 116 117 >>> formset.save() 118 [<Book: Les Fleurs du Mal>] 119 120 >>> for book in author.book_set.all(): 121 ... print book.title 122 Les Fleurs du Mal 123 124 125 Now that we've added a book to Charles Baudelaire, let's try adding another 126 one. This time though, an edit form will be available for every existing 127 book. 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() 149 True 150 151 >>> formset.save() 152 [<Book: Les Fleurs du Mal>, <Book: Le Spleen de Paris>] 153 154 As 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 158 Le Spleen de Paris 159 Les Fleurs du Mal 160 161 162 """}