更新
为了更好地理解 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
。
post_save
如果在信号期间或方法中添加了任何 m2m 关系Model.save()
,它将从 中删除或覆盖
BaseModelForm._save_m2m()
,具体取决于
ModelForm.cleaned_data
.
-transaction.on_commit()
稍后在此答案中以及在其他一些 SO 答案中被讨论为解决方案,我从中受到启发并被否决 - 将延迟信号的变化,直到
BaseModelForm._save_m2m()
结束其操作。
虽然,在某些特殊情况下transaction.on_commit()
非常有用,但在这种情况下是一种矫枉过正,不仅因为它以一种尴尬的方式使情况复杂化(最合适的信号m2m_changed
如此处所述),而且因为完全避免信号是相当
好的。
因此,我将尝试提供一个适合这两种情况的解决方案:
- 如果实例是从 Django Admin (ModelForm) 保存的
- 如果在不使用 ModelForm 的情况下保存实例
模型.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>
表格.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()
旧答案
基于这个或那个SO 问题以及一篇题为“如何在 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 # 在退出此函数之前不会调用接收器。如果 request.user.has_perm('group_permission'): do_something() ...
Django 文档更详细地描述了
on_commit()
成功工作的约束条件。
测试
在测试期间,使用pytest 进行测试时使用
TransactionTestCase或
装饰器至关重要。@pytest.mark.django_db(transaction=True)
这是我如何测试此信号的示例。