Django: программно добавлять группы при сохранении пользователя - PullRequest
0 голосов
/ 04 декабря 2018

После сохранения пользователя мне нужно убедиться, что его экземпляр по умолчанию связан с группой.

Я нашел два способа добиться этого:

  • Переопределение метода save() модели

    models.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)
    
  • Захват сигнала post_save:

    signal.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)
    

Равны ли эти методы в достижении своей цели?

Есть ли в Django лучший термин "Хорошая практика"?

1 Ответ

0 голосов
/ 04 декабря 2018

Обновление

Чтобы лучше понять, как работает 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() не завершит своеоперации.

Это перебор, не только потому, что он усложняет ситуацию, но и потому, что вообще избегать сигналов - это скорее хорошо .

Поэтому я постараюсьдать решение, которое подходит для обоих случаев:

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

Старый ответ

На основе , или , ТАК вопросов и статьи под заголовком "* 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.

Этот является примером того, как я тестировал этот сигнал.

...