#30022: Doc how to combine post_save signal with on_commit to alter a m2m 
relation
when saving a model instance
-------------------------------------+-------------------------------------
     Reporter:  George Tantiras      |                    Owner:  nobody
         Type:                       |                   Status:  closed
  Cleanup/optimization               |
    Component:  Documentation        |                  Version:  2.1
     Severity:  Normal               |               Resolution:  wontfix
     Keywords:                       |             Triage Stage:
                                     |  Unreviewed
    Has patch:  0                    |      Needs documentation:  0
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  0                    |                    UI/UX:  0
-------------------------------------+-------------------------------------

Comment (by Simon Charette):

 Hello George,

 This is getting in into django-user territory but the `test_user` test
 happens to fail if you remove the `on_transaction_commit` decorator on the
 `post_save` receiver because of
 [https://docs.djangoproject.com/en/2.1/topics/forms/modelforms/#the-save-
 method how model forms m2m saving works].

 `ModelForm.save()` start by saving their attached instance, which triggers
 `post_save` signals, and then save their m2m values. In your case since
 `superuser_data` doesn't include any value for `groups` then the following
 chain of actions happen on `change_form.save()` if you remove the
 `on_transaction_commit` decorator.

 1. `user` get assigned `superuser_data` and its `save()` method is
 invoked.
 2. The `post_save` signal is fired and the group gets added.
 3. The form saves the `groups` m2m to an empty set because `groups` is
 missing from `superuser_data`.

 You can confirm this is the case by changing your `test_user` test to the
 following.

 {{{#!python
 def test_user(group, user_data, superuser_data):
     '''
     This test will fail if on_commit is not used in the signal.
     '''
     creation_form = UserCreationForm(data=user_data)
     user = creation_form.save()
     change_form = UserChangeForm(instance=user, data=superuser_data)
     assert change_form.is_valid()
     with transaction.atomic():
         superuser = change_form.save(commit=False)
         superuser.save()
         assert group in superuser.groups.all()
         superuser.save_m2m()
     assert group not in superuser.groups.all()
 }}}

 When your `post_save` receiver is decorated with `on_transaction_commit`
 the group addition happens to be performed ''after'' `save_m2m` is called
 which happens to make the test pass.

 What you should do to assert `user.is_superuser ==
 user.groups.filter(name='superusers').exists()` consistency is hook into
 the `m2m_changed` signal as well instead of relying on unrelated
 `on_commit` behaviour. For example,

 {{{#!python
 @receiver(
     m2m_changed,
     sender=User.groups.through
 )
 def ensure_groups_consistency(signal, sender, instance, action, pk_set,
 **kwargs):
     if instance.is_superuser and action in ('pre_remove', 'post_clear'):
         group = Group.objects.get(name='Superusers')
         if action == 'pre_remove' and group.pk in pk_set:
             pk_set.remove(group.pk)
         elif action == 'post_clear':
             instance.groups.add(group)
 }}}

 As I said earlier this is probably better discussed on support channels
 that here as this is expected behaviour.

-- 
Ticket URL: <https://code.djangoproject.com/ticket/30022#comment:4>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to django-updates+unsubscr...@googlegroups.com.
To post to this group, send email to django-updates@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/066.ee19edd35212e10c5491c2c33fe4d5b9%40djangoproject.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to