After a user is saved, I need to make sure that its instance is associated with a group by default.
I have found two ways to achieve that:
Overriding the model's
save()
methodmodels.py:
from django.contrib.auth.models import AbstractUser, Group class Person(AbstractUser): def save(self, *args, **kwargs): super().save(*args, **kwargs) to_add = Group.objects.get(id=1) # get_or_create is a better option instance.groups.add(to_add)
Capturing a post_save signal:
signals.py:
from django.conf import settings from django.contrib.auth.models import Group from django.db.models.signals import post_save from django.dispatch import receiver @receiver( post_save, sender=settings.AUTH_USER_MODEL, ) def save_the_group(instance, raw, **kwargs): if not raw: to_add = Group.objects.get(id=1) # get_or_create is a better option instance.groups.add(to_add)
Are these methods equal in achieving their goal?
Is there a better one in Django terms of "Good Practice"?
Update
Acquiring a better understanding of how Django works, I see that the confusion and also the solution lie in
BaseModelForm.save()
:and in
BaseModelForm._save_m2m()
:The instance is first saved to acquire a primary key (
post_save
signal emmited) and then all its many to many relations are saved based onModelForm.cleaned_data
.If any m2m relation has been added during the
post_save
signal or at theModel.save()
method, it will be removed or overridden fromBaseModelForm._save_m2m()
, depending on the content of theModelForm.cleaned_data
.The
transaction.on_commit()
-which is discussed as a solution in this asnwer later on and in a few other SO answers from which I was inspired and got downvoted- will delay the changes in the signal untilBaseModelForm._save_m2m()
has concluded its operations.This is an overkill, not only because it is complexing the situation in an awkward manner but because avoiding signals altogether, is rather good.
Therefore, I will try to give a solution that caters for both occasions:
models.py
forms.py
This will either work with:
or with:
Old answer
Based on either this or that SO questions along with an article titled "How to add ManytoMany model inside a post_save signal" the solution I turned to, is to use
on_commit(func, using=None)
:The above code does not take into account that every login causes a post_save signal.
Digging Deeper
A crucial point made in the relevant Django ticket is that the above code will not work if a
save()
call is made inside an atomic transaction together with a validation that depends on the result of thegroup_delegation()
function.on_commit # receiver wouldn't be called until exiting this function. if request.user.has_perm('group_permission'): do_something() ...
Django docs describe in more details the constraints under which
on_commit()
successfully works.Testing
During testing, it is crucial to use the TransactionTestCase or the
@pytest.mark.django_db(transaction=True)
decorator when testing with pytest.This is an example of how I tested this signal.