Обновление
Чтобы лучше понять, как работает Django, я вижу, что путаница, а также ее решение заключаются в BaseModelForm.save()
:
...
if commit:
# If committing, save the instance and the m2m data immediately.
self.instance.save()
self._save_m2m()
...
и BaseModelForm._save_m2m()
:
...
if f.name in cleaned_data:
f.save_form_data(self.instance, cleaned_data[f.name])
...
Экземпляр сначала сохраняется для получения первичного ключа (сигнал post_save
выдается), а затем все его многие ко многимотношения сохраняются на основе ModelForm.cleaned_data
.
Если какое-либо отношение m2m было добавлено во время сигнала post_save
или при методе Model.save()
, оно будет удалено или переопределено с BaseModelForm._save_m2m()
, в зависимости от содержимого ModelForm.cleaned_data
.
transaction.on_commit()
- который обсуждается как решение в этом ответе позже и в нескольких других SO-ответах, на которые я был вдохновлен и опущен - задержит изменения в сигнале, пока BaseModelForm._save_m2m()
не завершит своеоперации.
Это перебор, не только потому, что он усложняет ситуацию, но и потому, что вообще избегать сигналов - это скорее хорошо .
Поэтому я постараюсьдать решение, которое подходит для обоих случаев:
- Если экземпляр сохранен из Django Admin (ModelForm)
- Если экземпляр сохранен без использования ModelForm
models.py
from django.contrib.auth.models import AbstractUser, Group
class Person(AbstractUser):
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if not getattr(self, 'from_modelform', False): # This flag is created in ModelForm
<add - remove groups logic>
forms.py
from django import forms
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.models import Group
from my_app.models import Person
class PersonChangeForm(UserChangeForm):
def clean(self):
cleaned_data = super().clean()
if self.errors:
return
group = cleaned_data['groups']
to_add = Group.objects.filter(id=1)
to_remove = Group.objects.filter(id=2)
cleaned_data['groups'] = group.union(to_add).difference(to_remove)
self.instance.from_modelform = True
return cleaned_data
class Meta:
model = Person
fields = '__all__'
Это будет работать с:
>>> p = Person()
>>> p.username = 'username'
>>> p.password = 'password'
>>> p.save()
или с:
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import get_user_model
from django.forms.models import modelform_factory
user_creationform_data = {
'username': 'george',
'password1': '123!trettb',
'password2': '123!trettb',
'email': 'email@yo.gr',
}
user_model_form = modelform_factory(
get_user_model(),
form=UserCreationForm,
)
user_creation_form = user_model_form(data=user_creationform_data)
new_user = user_creation_form.save()
Старый ответ
На основе , или , ТАК вопросов и статьи под заголовком "* 1064"* Как добавить модель ManytoMany в сигнал post_save"Решение, к которому я обратился, заключается в использовании on_commit(func,
using=None)
:
Будет вызвана передаваемая вами функциясразу после того, как будет произведена гипотетическая запись в базу данных, в которой вызывается on_commit (),
from django.conf import settings
from django.contrib.auth.models import Group
from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import receiver
def on_transaction_commit(func):
''' Create the decorator '''
def inner(*args, **kwargs):
transaction.on_commit(lambda: func(*args, **kwargs))
return inner
@receiver(
post_save,
sender=settings.AUTH_USER_MODEL,
)
@on_transaction_commit
def group_delegation(instance, raw, **kwargs):
to_add = Group.objects.get(id=1)
instance.groups.add(to_add)
Приведенный выше код не учитывает, что каждый логин вызывает сигнал post_save .
Копать глубже
Важным моментом, указанным в соответствующем билете Django , является то, что приведенный выше код не будет работать , если вызов save()
сделано внутри атомарной транзакции вместе с проверкой, которая зависит от результата функции group_delegation()
.
@transaction.atomic
def accept_group_invite(request, group_id):
validate_and_add_to_group(request.user, group_id)
# The below line would always fail in your case because the
on_commit # получатель не будет вызываться до выхода из этой функции.if request.user.has_perm ('group_permission'): do_something () ...
Django docs более подробно описывает ограничения, при которых on_commit()
успешно работает.
Тестирование
Во время тестирования крайне важно использовать декоратор TransactionTestCase или @pytest.mark.django_db(transaction=True)
при тестировании сpytest.
Этот является примером того, как я тестировал этот сигнал.