diff --git a/django/newforms/models.py b/django/newforms/models.py index 56a08bc58e..7a86e30b1d 100644 --- a/django/newforms/models.py +++ b/django/newforms/models.py @@ -35,17 +35,23 @@ def save_instance(form, instance, fields=None, fail_message='saved', commit=True if fields and f.name not in fields: continue setattr(instance, f.name, cleaned_data[f.name]) - if commit: - instance.save() + # Wrap up the saving of m2m data as a function + def save_m2m(): + opts = instance.__class__._meta + cleaned_data = form.cleaned_data for f in opts.many_to_many: if fields and f.name not in fields: continue if f.name in cleaned_data: setattr(instance, f.attname, cleaned_data[f.name]) - # GOTCHA: If many-to-many data is given and commit=False, the many-to-many - # data will be lost. This happens because a many-to-many options cannot be - # set on an object until after it's saved. Maybe we should raise an - # exception in that case. + if commit: + # If we are committing, save the instance and the m2m data immediately + instance.save() + save_m2m() + else: + # We're not committing. Add a method to the form to allow deferred + # saving of m2m data + form.save_m2m = save_m2m return instance def make_model_save(model, fields, fail_message): diff --git a/docs/newforms.txt b/docs/newforms.txt index f22f61b8d1..718994678a 100644 --- a/docs/newforms.txt +++ b/docs/newforms.txt @@ -1502,6 +1502,36 @@ the database. In this case, it's up to you to call ``save()`` on the resulting model instance. This is useful if you want to do custom processing on the object before saving it. ``commit`` is ``True`` by default. +Another side effect of using ``commit=False`` is seen when your model has +a many-to-many relation with another model. If your model has a many-to-many +relation and you specify ``commit=False`` when you save a form, Django cannot +immediately save the form data for the many-to-many relation. This is because +it isn't possible to save many-to-many data for an instance until the instance +exists in the database. + +To work around this problem, every time you save a form using ``commit=False``, +Django adds a ``save_m2m()`` method to the form created by ``form_for_model``. +After you have manually saved the instance produced by the form, you can invoke +``save_m2m()`` to save the many-to-many form data:: + + # Create a form instance with POST data. + >>> f = AuthorForm(request.POST) + + # Create, but don't save the new author instance + >>> new_author = f.save(commit=False) + + # Modify the author in some way + ... + # Save the new instance + >>> new_author.save() + + # Now save the many-to-many data for the form + >>> f.save_m2m() + +Calling ``save_m2m()`` is only required if you use ``save(commit=False)``. +When you use a simple ``save()`` on a form, all data - include +many-to-many data - is saved without the need for any additional method calls. + Using an alternate base class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py index f5497bd515..d27f0b0e54 100644 --- a/tests/modeltests/model_forms/models.py +++ b/tests/modeltests/model_forms/models.py @@ -332,6 +332,28 @@ Create a new article, with no categories, via the form. >>> new_art.categories.all() [] +Create a new article, with categories, via the form, but use commit=False. +The m2m data won't be saved until save_m2m() is invoked on the form. +>>> ArticleForm = form_for_model(Article) +>>> f = ArticleForm({'headline': u'The walrus was Paul', 'pub_date': u'1967-11-01', +... 'writer': u'1', 'article': u'Test.', 'categories': [u'1', u'2']}) +>>> new_art = f.save(commit=False) + +# Manually save the instance +>>> new_art.save() +>>> new_art.id +4 + +# The instance doesn't have m2m data yet +>>> new_art = Article.objects.get(id=4) +>>> new_art.categories.all() +[] + +# Save the m2m data on the form +>>> f.save_m2m() +>>> new_art.categories.all() +[, ] + Here, we define a custom Form. Because it happens to have the same fields as the Category model, we can use save_instance() to apply its changes to an existing Category instance.