Django:以编程方式在用户保存时添加组

Django: Programmatically add Groups on User save

保存用户后,我需要确保其实例默认与组相关联。

我找到了两种实现方法:

这些方法在实现目标方面是否相同?

Django 有没有更好的"Good Practice"?

更新

为了更好地理解 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 信号期间或在 Model.save() 方法,它将被删除或覆盖 BaseModelForm._save_m2m(),取决于内容 ModelForm.cleaned_data.

transaction.on_commit() - 在此作为解决方案进行讨论 asnwer later and in a few other SO answers from which I was inspired from 并被否决-将延迟信号的变化直到 BaseModelForm._save_m2m() 已结束运营。

虽然,在某些特殊情况下 transaction.on_commit() is very useful,在这种情况下是大材小用,不仅因为它使情况复杂化 一种尴尬的方式(最合适的信号是 m2m_changed as explained here)但是因为完全避免信号,而是 good.

因此,我会尝试给出一个适合这两种情况的解决方案:

  1. 如果实例是从 Django Admin (ModelForm) 保存的
  2. 如果不使用 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()

旧答案

基于 this or that SO 问题以及 标题为“如何在 post_save 中添加 ManytoMany 模型的文章 signal" 我转向的解决方案是使用 on_commit(func, using=None):

The function you pass in will be called immediately after a hypothetical database write made where on_commit() is called would be successfully committed.

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 ticket中的一个关键点是 上面的代码 将不起作用 如果 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 docs 更详细地描述了约束条件 on_commit() 成功运行。

测试

During testing,关键是要使用 TransactionTestCase@pytest.mark.django_db(transaction=True) 用 pytest 测试时的装饰器。

This 是我如何测试此信号的示例。