且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

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

更新时间:2023-02-16 11:16:08

更新

对Django的工作方式有了更好的了解,我发现
的困惑以及解决方案都在 BaseModelForm.save()

Acquiring a better understanding of how Django works, I see that the confusion and also the solution lie in 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

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 on ModelForm.cleaned_data.

如果在 post_save 期间添加了任何m2m关系>信号或在
中使用 Model.save()方法,它将从
BaseModelForm._save_m2m( ),具体取决于
ModelForm.cleaned_data 的内容。

If any m2m relation has been added during the post_save signal or at the Model.save() method, it will be removed or overridden from BaseModelForm._save_m2m(), depending on the content of the ModelForm.cleaned_data.

transac tion.on_commit()-稍后在此
问答中作为解决方案进行讨论,在其他一些我得到
启发而又被否决的SO答案中,它将延迟直到
BaseModelForm._save_m2m()结束其操作为止的信号变化。

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 until BaseModelForm._save_m2m() has concluded its operations.

尽管如此,在某些特殊情况下 transaction.on_commit() 非常有用,在这种情况下是一个过大的杀伤力,不仅是因为它使
的情况复杂而笨拙(最合适的信号是 m2m_changed 如此处所述)但是因为完全避免信号,所以

Although, in some special cases the transaction.on_commit() is very useful, in this case is an overkill, not only because it is complexing the situation in an awkward manner (the most suitable signal is m2m_changed as explained here) but because avoiding signals altogether, is rather good.

因此,我将尝试给出一种能够迎合您需求的解决方案两次:

Therefore, I will try to give a solution that caters for both occasions:


  1. 如果实例是从Django Admin(ModelForm)保存的,则

  2. 如果实例已保存而不使用ModelForm

models.py

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()




旧答案

基于 SO问题以及题为 ; 如何在post_save
信号内添加ManytoMany模型
"我求助的解决方案是使用 on_commit(func,using = None)

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):


您传入的函数将在调用了on_commit()的
个假设数据库写入之后,将立即成功调用

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)

上面的代码没有考虑到每个登录会导致
后保存信号

The above code does not take into account that every login causes a post_save signal.

相关 Django票证就是上面代码中的
将不起作用如果在
原子事务内进行了 save()调用,并且验证取决于
结果group_delegation()
函数。

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 the group_delegation() function.

@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_com mit
#接收器在退出此功能之前不会被调用。
if request.user.has_perm('group_permission'):
do_something()
...

on_commit # receiver wouldn't be called until exiting this function. if request.user.has_perm('group_permission'): do_something() ...

Django文档更详细地描述了
on_commit()成功运行。

Django docs describe in more details the constraints under which on_commit() successfully works.

在测试期间,使用
TransactionTestCase
@ pytest.mark.django_db(transaction = True) 装饰器。

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.