— Ты не работал с пакетом django-moderation? И если нет, были ли у тебя задачи по модерации объектов, а конкретно: пользователь предлагает изменения, а другой пользователь либо отклоняет, либо принимает их?

— Не, не делал такого. Вот я все таки ох обескуражен от твоих занятий. Зачем такое вообще нужно?

... как всегда вырвано из контекста.

И правда зачем нужна модерация объектов и так все усложнять? Это были мои первые мысли, перед тем, как я начал разбираться с новой задачей, которая стояла перед моим уже окрепшим телом и духом (нет).

Поиск ответа на вопрос начался с изучения библиотеки django-moderation. Рабочий день уже подходил к концу, когда я понял, что ничего не понял в пакете и его логике. Бывалые программисты могут спросить: "А почему ты не спросил у более опытных сотрудников как делать задачу?" Я вам отвечу: "Я спрашивал, но поскольку ответы приводили все дальше меня в тупик, пакет не хотел работать, настройки пакета не позволяли сделать то, что требовалось, а время еще было, я принял решение написать что-то работающее для задачи и понятное хотя-бы для себя (конечно, согласовав этот вопрос с ПМ и тимлидом)".

Кому интересно ниже будут приведены проблемы с которыми я  столкнулся при работе с пакетом.

Проблемы, возникшие при подключении пакета django-moderation:
  1. Если в INSTALLED_APPS указать пакет “moderation” и указать “django_test_migrations.contrib.django_checks.AutoNames” , то ждите ошибку ModuleNotFoundError: No module named 'django_test_migrations.contrib.django_checks.AutoNames'; 'django_test_migrations.contrib.django_checks' is not a package. Данная ошибка появляется не только при использовании пакета django-test-migrations. Насколько удалось разобраться и понять - данная ошибка возникает из-за класса ModerationConfig внутри пакета moderation и логики внутри файлов apps.py и helpers.py.

  2. После переписывания файлов apps.py и helpers.py, или временного удаления из INSTALLED_APPS пакета django-test-migrations, проблемы начались с логикой пакета, которая не подходила, а именно: 

    1. При изменении разными пользователями объекта модели, создается один объект модерации на всех пользователей, а не по одному объекты модерации для каждого пользователя. Если включать историю изменений, то для одного пользователя создавалось несколько объектов модерации, но в api при обращении к объекту модели получал ошибку Multiple moderations found for object/s: <QuerySet [...]>.

    2. При изменении объекта, он делался невидимым в системе (не отображался в api и в админке), включение настройки видимости, которая предоставляется пакетом, не помогла.

    3. Существующая система ролей в пакете, завязанная на группы, не подходила.

  3. Только позднее я обратил внимание что в пакете написано о том, что он является многоразовым приложением для Django Framework, а проект пишется на Django Rest Framework. Я подумал, что все мои неудачи как то с этим связаны и перестал мучить себя и пакет django-moderation.

Прежде чем приступить к описанию своего решения, поясню используемые словосочетания:

  1. объект модели - это объект, который мы хотим изменить;

  2. объект модерации - это объект, который создается в БД с информацией об объекте модели, и изменениях, которые хотят к нему применить.

Также представлю часть модели User, в которой есть поле role, отвечающее за ролевую модель в системе.

from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext_lazy as _


class UserRole(models.TextChoices):
    """Роли пользователей."""

    LIDER = 'lr', _('Руководитель компании')
    EMPLOYEE = 'ee', _('Сотрудник компании')

class User(AbstractUser):
    """Класс пользователя"""

    role = models.CharField(
        _('роль'),
        max_length=2,
        help_text=_('роль пользователя в системе'),
        choices=UserRole.choices,
        default=UserRole.EMPLOYEE,
    )

И представлю упрощенную модель Company, CompanyViewSet и CompanySerializer(), на примере которых, хотелось бы показать работу модерации. Поле avatar модели ссылается на модель, которая хранит информацию о загруженных файлах (модель CustomFile наследуется от File из пакета djago-filer).

class Company(BaseModel):
    """Компания."""

    avatar = models.ForeignKey(
        'name_app.CustomFile',
        on_delete=models.CASCADE,
        verbose_name=_('аватар компании'),
        related_name='companies',
        null=True,
        blank=True,
    )
    name = models.CharField(
        _('название компании'),
        max_length=250,
        help_text=_('уникальное название компании'),
        unique=True,
    )
    phone = PhoneNumberField(
        _('номер телефона'), blank=True,
    )
    email = models.EmailField(
        _('адрес электронной почты'), blank=True,
    )
    
    
 class CompanyViewSet(ModelViewSet):
    """Компании."""

    serializer_class = CompanySerializer
    queryset = Company.objects.all()
    ordering_fields = '__all__'

    def perform_update(self, serializer):
        """Отправляем в сериализатор данные о пользователе, сделавшем запрос."""
        serializer.save(request_user=self.request.user)

    def perform_create(self, serializer):
        """Отправляем в сериализатор данные о пользователе, сделавшем запрос."""
        serializer.save(request_user=self.request.user)


class CompanySerializer(ModerationModelSerializer):
    """Сериалайзер компании."""

    class Meta(object):
        model = Company
        fields = ('id', 'avatar', 'name', 'phone', 'email')
        moderation_fields = ('avatar', 'name', 'phone',  'email')
        moderation_enabled = (UserRole.EMPLOYEE,)
        moderators = (UserRole.LIDER,)
        forced_update = False

CompanySerializer() наследуется от класса ModerationModelSerializer, который является ключевым элементов в процессе модерации и о котором речь пойдет ниже. Также в сериализатор добавляются дополнительные атрибуты:

  1. moderation_fields - список полей, для которых включена модерация. Для полей, которые не вошли в moderation_fields будут возвращаться значения из исходного объекта модели. Если moderation_fields не установлен, то объект не подлежит изменению, будет возвращен исходный объект (при условии указания moderation_enabled);

  2. moderation_enabled - список ролей, которые не смогут напрямую вносить изменения в объект и для которых включена модерация. Если moderation_enabled не установлен, то ModerationModelSerializer работает как serializers.ModelSerializer, то есть изменение объекта принимается сразу;

  3. moderators - список ролей, которые могут вносить изменения напрямую, и которые могут согласовывать изменения других пользователей, то есть модераторы. Если moderators не установлен, то модераторами являются все роли, за исключением тех, что указаны в moderation_enabled;

  4. forced_update - флаг, отвечающий за принудительное обновление. Если установлено False, то поля которые, не указаны в moderation_fields, не будут обновляться, даже если их передать. То есть они не доступны для обновления и всегда имеют исходное состояние. Если установлено в True, то все поля кроме тех, что указаны в moderation_fields будут обновлены вне зависимости от роли изменяемого. Данный флаг необходим для того, чтобы можно было модерировать отдельные поля модели.

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


0. Процесс модерации в разрабатываемой системе.

Схематичный процесс модерации.
Схематичный процесс модерации.

В общем виде процесс модерации сводится к тому, что один пользователь хочет изменить объект, но у него ничего не получается, поскольку не обладает нужными правами, и исходный объект остается в первоначальном состоянии. Вместо изменения исходного объекта, создастся еще один объект, которых хранит информацию о желании первого пользователя что-то поменять.

В это время второй пользователь, с кучей прав, видит, какие изменения хотел внести первый пользователь и либо принимает их, либо нет. В зависимости от решения объект меняется или остается в исходном состоянии.

В случае если первый пользователь вносит несколько изменений подряд и по ним не принято решение, то в нашем случае будет сохраняться только последнее изменение, которое предложил первый пользователь.

1. Создание модели.

Основная структура модели, которая отвечает за хранение объектов модерации была взята из пакета django-moderation и доработана следующим образом:

  1. Поле serializer_class - хранит в себе информацию о названии сериализатора, в котором произошло изменение или добавление объекта (в нашем примере будет хранить CompanySerializer) . Назначение данного поля будет рассмотрено далее.

  2. Поле changed_object - хранит новую информацию об изменениях в объекте модели в json виде. Ключами такой информации являются названия полей из объекта модели, а значения содержат в себе новую информацию, которую хотели внести в соответствующие поля объекта модели.

  3. Поле comment - хранит информацию от модератора (замечания или пожелания).

from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils.translation import gettext_lazy as _

User = get_user_model()


class StatusModeration(models.TextChoices):
    """Статус модерации объекта."""

    PENDING = 'p', _('Ожидает модерации')
    APPROVED = 'a', _('Изменения приняты')
    REJECTED = 'r', _('Изменения отклонены')


class ModeratedObject(models.Model):
    """Хранение информации об объекте, который нужно модерировать."""

    content_type = models.ForeignKey(
        ContentType,
        null=True,
        blank=True,
        verbose_name=_('тип изменяемого объекта'),
        on_delete=models.SET_NULL,
    )
    object_pk = models.PositiveIntegerField(
        _('id изменяемого объекта'), null=True, blank=True, db_index=True,
    )
    serializer_class = models.CharField(
        _('сериализатор класса'), max_length=200,
    )
    creation_date = models.DateTimeField(
    		_('дата создания'), auto_now_add=True,
    )
    update_date = models.DateTimeField(
				_('дата изменения'), auto_now=True,
		)
    status = models.CharField(
        _('статус объекта'),
        max_length=2,
        help_text=_('статус объекта модерации'),
        choices=StatusModeration.choices,
        default=StatusModeration.PENDING,
    )
    suggested = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        null=True,
        verbose_name=_('пользователь предложивший изменения'),
        related_name='suggested_moderated_objects',
    )
    moderation_date = models.DateTimeField(
        _('дата модерации'), blank=True, null=True,
    )
    comment = models.TextField(
        _('комментарий к модерируемому объекту'), blank=True,
    )
    changed_object = models.JSONField(
        _('измененные данные'), null=True, blank=True,
    )
    changed = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        verbose_name=_('пользователь - модератор'),
        related_name='changed_moderated_objects',
    )

    class Meta(BaseModel.Meta):
        verbose_name = _('модерация объекта')
        verbose_name_plural = _('модерация объектов')

    def __str__(self):
        return f'{self.content_type}: id - {self.object_pk}'

2. Создание view и serializer

Представление вышло совершенно обычное, кроме передачи в serializer данных о пользователе, который совершает запрос. Передача информации необходима для проверки роли и возможности принимать решения по объектам модерирования.

class ModeratedObjectViewSet(NestedViewSetMixin, BaseViewSet):
    """Модерируемый объект."""

    serializer_class = ModeratedObjectSerializer
    queryset = ModeratedObject.objects.all()
    ordering_fields = '__all__'
    filterset_class = ModeratedObjectFilter

    def perform_update(self, serializer):
        """Отправляем в сериализатор данные о пользователе, сделавшем запрос."""
        serializer.save(request_user=self.request.user)

    def perform_create(self, serializer):
        """Отправляем в сериализатор данные о пользователе, сделавшем запрос."""
        serializer.save(request_user=self.request.user)

С serializer пришлось повозиться подольше.

import datetime
from typing import Any, Dict

from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError

from server.apps.bizone_bug_bounty.models import CustomFile, ModeratedObject
from server.apps.bizone_bug_bounty.models.moderated_object import (
    StatusModeration,
)
from server.apps.bizone_users.models import User


def is_moderator(moder_obj_meta, user: User) -> bool:
    """Проверка роли пользователя на возможность модерировать.
    
    :param moder_obj_meta: Meta класс сериализатора, где был создан объект на
                           модерацию и где создается/изменяется исходный объект.
    :param user: Пользователь, который вносит изменения в модерируемый объект.
    
    1) Если роль пользователя входит в группу модераторов - True.
    2) Если роль пользователя не входит в группу тех, для кого включена
    модерация - True.
    3) Если не установлен ни один из параметров (модерация отключена) - True.
    """
    if moderators := getattr(moder_obj_meta, 'moderators', None):
        return user.role in moderators
    if moderation_enabled := getattr(moder_obj_meta, 'moderation_enabled', None):
        return user.role not in moderation_enabled
    return True


def initial_fields(
    initial_object,
    modified_fields: Dict[str, Any],
) -> Dict[str, Any]:
    """Получение исходных полей (имеющиеся в БД) модерируемого объекта.
		
    Функция нужна для сопоставления новых и старых данных.
		"""
    # Переменная для хранения исходных данных об объекте модели.
    initial_fields_data = {}
    for field in modified_fields.keys():
      	# Получаем поле модели.
        object_field = initial_object._meta.get_field(field)
        # Для поля avatar отдельная логика из-за сложности объекта.
        if field == 'avatar':
            # Получаем значение pk, на который ссылается avatar и вытягиваем 
            # нужные данные
            pk = object_field.value_from_object(initial_object)
            avatar = CustomFile.objects.get(pk=pk)
            initial_fields_data.update(
                {
                    'avatar': {
                        'id': avatar.id,
                        'url': avatar.url,
                    },
                },
            )
        else:
          	# Получаем значение поля для поля объекта в str типе.
            initial_fields_data.update(
                {field: object_field.value_to_string(initial_object)},
            )

    return initial_fields_data


class ModeratedObjectSerializer(serializers.ModelSerializer):
    """Сериалайзер объекта для модерации."""

    initial_data_object = serializers.SerializerMethodField()

    class Meta(object):
        model = ModeratedObject
        fields = (
            'id',
            'content_type',
            'object_pk',
            'creation_date',
            'update_date',
            'status',
            'suggested',
            'moderation_date',
            'comment',
            'changed_object',
            'changed',
            'initial_data_object',
        )
        read_only_fields = (
            'id',
            'content_type',
            'object_pk',
            'creation_date',
            'update_date',
            'suggested',
            'moderation_date',
            'changed',
            'initial_data_object',
        )

    def get_initial_data_object(self, mod_obj):
        """Получение исходной информации об изменяемом объекте.

        Под исходной информацией понимается информация из БД о текущем
				состоянии объекта модели.
        """
        if mod_obj.object_pk:
          	# Конструкция для получения класса модели.
            object_model = mod_obj.content_type.model_class()
            try:
                initial_object = object_model.objects.get(id=mod_obj.object_pk)
            except object_model.DoesNotExist:
                raise ValidationError(
                    _(
                        'Модерируемый объект удален или перенесен. ' +
                        'Невозможно внести изменения.',
                    ),
                )

            return initial_fields(initial_object, mod_obj.changed_object)

        return ''

    def update(self, instance, validated_data):
        """Для согласования изменений необходимо изменить статус объекта."""
        user = validated_data.pop('request_user')

        if instance.status == StatusModeration.APPROVED:
            raise ValidationError(
                _('Объект уже прошел модерацию, действия с ним невозможны.'),
            )

        # Получаем экземпляр сериализатора, внутри которого необходимо создать
        # или изменить модерируемый объект.
        moder_obj_serializer = import_string(instance.serializer_class)
        # Статус и комментарии имеет право изменять/добавлять
        # только модератор. При этом пользователь, который внес изменения, не
        # может согласовать эти изменения. Также, пользователи с одинаковыми
        # ролями не могут согласовать изменения.
        if (
            not is_moderator(moder_obj_serializer.Meta, user) or
            user == instance.suggested or
            user.role == instance.suggested.role
        ):
            self.check_status(validated_data.get('status'))
            self.check_comment(validated_data.get('comment'))
				
        # Если изменения приняты, то при наличаии instance обновляем, 
        # если нет, то создаем.
        if validated_data.get('status') == StatusModeration.APPROVED:
          	# Получаем класс объекта модели. В нашем случае Company.
            object_model = instance.content_type.model_class()
            
            if instance.object_pk:
                # Используется filter, а не get потому что далее используется
                # метод update() для обнволения информации.
                qs_object_model = object_model.objects.filter(
                    id=instance.object_pk,
                )
                qs_object_model.update(**instance.changed_object)
            else:
                object_model.objects.create(**instance.changed_object)

            validated_data.update(
                {
                    'changed': user,
                    'moderation_date': datetime.datetime.now(),
                },
            )

        return super().update(instance, validated_data)

    def check_status(self, status) -> None:
        """Менять статус может только модератор."""
        if status:
            raise ValidationError(_('Вы не можете менять статус объекта.'))

    def check_comment(self, comment) -> None:
        """Добавлять/изменять комментарий может только модератор."""
        if comment:
            raise ValidationError(_('Вы не можете оставлять комментарий.'))

Функция def is_moderator() необходима для проверки роли пользователя, который хочет модерировать объект. Аргумент moder_obj_meta содержит в себе класс serializer, где происходило создание или изменение объекта модели (в нашем случае CompanySerializer), а аргумент user содержит информацию о пользователе, который хочет быть модератором (то есть делает запрос на изменение статуса объекта модерации).

Получение CompanySerializer осуществляется на 137 строчке с помощью import_string. Именно для актуального получения списка ролей в модели ModeratedObject было добавлено поле serializer_class.

Функция def initial_fields() необходима для api. Она позволяет представлять исходные данные (которые хранятся в БД на момент модерации) об объекте модели в json формате. Аргумент initial_object - это объект модели (в нашем случае Company), modified_fields - словарь полей, которые отправлены на модерацию, то есть поля, которые хотели бы изменить. Получение класса Company осуществляется на 111 строчке с помощью поля content_type, которое содержит pk нашего класса и метода model_class(), а получение объекта уже на 113 строчке с помощью метода objects.get() и поля object_pk в классе ModeratedObject, который хранит pk изменяемого объекта.

Функция def initial_fields() используется в поле initial_data_object ModeratedObjectSerializer. Общий вид объекта ModeratedObject представлен на картинке.

Пример полученного объекта модерации через api.
Пример полученного объекта модерации через api.

Конечно, можно было бы хранить информацию об исходном состоянии объекта модели (initial_data_object) в модели ModeratedObject, а не вычислять динамически, но в рамках задачи, этого делать не нужно было.

В методе def update() ModeratedObjectSerializer происходит проверка пользователя на возможность менять статус объекта модерации и происходит изменение объекта через метод update() при наличии атрибута ModeratedObject.object_pk и создание через метод create() при его отсутствии.

3. Написание своего класса для serializer

Заключительным этапом решения задачи стало написание своего класса для сериализаторов, который бы реализовывал логику по созданию дополнительного объекта, хранящего в себе информацию о том кто, куда и какие изменения хочет внести.

import traceback
from typing import Set

from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers, status
from rest_framework.exceptions import APIException
from rest_framework.utils import model_meta

from server.apps.bizone_bug_bounty.models import Company, Task


class CreateModerationObject(APIException):
    """Исключение, которое вызывается при создании объекта."""

    status_code = status.HTTP_204_NO_CONTENT
    default_detail = _('Объект на модерации.')
    default_code = 'moderation'


class ModerationModelSerializer(serializers.ModelSerializer):
    """Сериализитор для модерации объектов."""

    def get_moderation_fields(self) -> Set[str]:
        """Возвращает поля, которые участвуют в модерации объекта."""
        return getattr(self.Meta, 'moderation_fields', set())

    def get_moderation_enabled_role(self) -> Set[str]:
        """Возвращает роли, для которых включена модерация."""
        return getattr(self.Meta, 'moderation_enabled', set())

    def get_moderators_role(self) -> Set[str]:
        """Возвращает роли пользователей, которые имеют право модерировать."""
        return getattr(self.Meta, 'moderators', set())

    def get_forced_update(self) -> bool:
        """Принудительное обновление."""
        return getattr(self.Meta, 'forced_update', False)

    def update(self, instance, validated_data):
        """Обновление объекта, подлежащего модерации."""
        # Получаем пользователя, который совершил запрос на изменение и
        # проверяем нужна ли для роли пользователя модерация.
        user = validated_data.pop('request_user')
        if user.role in self.get_moderation_enabled_role():
            # Переменная moderation_data хранит значения из validated_data,
            # которые подлежат изменению.
            moderation_data = {}
            moderation_fields = self.get_moderation_fields()
            # Сохраняем ключи провалидированных данных. Необходимо для
            # сокращения итераций, когда мы проверяем не все поля для модерации,
            # а все поступившие поля в validated_data.
            validated_fields = validated_data.copy().keys()
            if moderation_fields:
                # формируем словарь с измененными полями объекта.
                for name_field in validated_fields:
                    # Если поле, которое изменяют или создают, входит в поля
                    # для модерации, то запоминаем его
                    # Отдельная логика для avatar, потому что сложный объект.
                    if name_field in moderation_fields:
                        if (
                            name_field == 'avatar' and
                            validated_data.get('avatar')
                        ):
                            avatar = validated_data.pop('avatar')
                            moderation_data.update(
                                {
                                    f'{name_field}': {
                                        'id': avatar.id,
                                        'name': avatar.name,
                                    },
                                },
                            )
                        else:
                            moderation_data.update(
                                {
                                    f'{name_field}':
                                        validated_data.pop(name_field, None),
                                },
                            )
                # Создаем или получаем объект. Может быть такое, что один и тот
                # же пользователь сделал несколько запросов на изменение.
                # В таком случае старый запрос на изменение перезаписывается.
                moderated_obj, created = ModeratedObject.objects.get_or_create(
                    content_type=ContentType.objects.get_for_model(
                        instance.__class__,
                    ),
                    serializer_class='{module}.{class_name}'.format(
                        module=self.__module__,
                        class_name=self.__class__.__name__,
                    ),
                    object_pk=instance.pk,
                    suggested=user,
                    status=StatusModeration.PENDING,
                )
                # Сохраняем изменения объекта отдельно, для того чтобы не
                # дублировались одинаковые объекты от одного и того-же
                # пользователя.
                moderated_obj.changed_object = moderation_data
                moderated_obj.save()

            # Проверка того, что при изменении объекта мы не меняем поля,
            # которые не участвуют в модерации. То есть если пользователь
            # меняет поля, которых нет в get_moderation_enabled_role(), то
            # они не будут изменены.
            if not self.get_forced_update():
                return instance

        # Код из стандартного метода update.
        serializers.raise_errors_on_nested_writes(
            'update', self, validated_data,
        )
        info = model_meta.get_field_info(instance)
        m2m_fields = []

        for attr, valid_value in validated_data.items():
            if attr in info.relations and info.relations[attr].to_many:
                m2m_fields.append((attr, valid_value))
            else:
                setattr(instance, attr, valid_value)

        instance.save()

        for attr, field_value in m2m_fields:
            field = getattr(instance, attr)
            field.set(field_value)

        return instance

    def create(self, validated_data):
        """Создание объекта, подлежащего модерации.

        При создании объекта учитывается роль пользователя. Если для роли
        нужна модерация, то объект создается в ModeratedObject, а в
        instance записывается информация о том, что необходима модерация.

        В остальных случаях метод работает без изменений.
        """
        serializers.raise_errors_on_nested_writes(
            'create', self, validated_data,
        )

        ModelClass = self.Meta.model

        info = model_meta.get_field_info(ModelClass)
        many_to_many = {}
        for field_name, relation_info in info.relations.items():
            if relation_info.to_many and (field_name in validated_data):
                many_to_many[field_name] = validated_data.pop(field_name)

        # serializer_class нужен для получения актуальных ролей
        # из Meta параметров сериализатора
        list_role = self.get_moderation_enabled_role()
        user = validated_data.pop('request_user')
        try:
            if user.role in list_role and not self.get_forced_update():
                ModeratedObject.objects.create(
                    content_type=ContentType.objects.get_for_model(ModelClass),
                    suggested=user,
                    serializer_class='{module}.{class_name}'.format(
                        module=self.__module__,
                        class_name=self.__class__.__name__,
                    ),
                    changed_object=validated_data,
                )
                instance = 'Объект на модерации.'
            else:
                instance = ModelClass._default_manager.create(**validated_data)
        except TypeError:
            tb = traceback.format_exc()
            msg = (
                'Got a `TypeError` when calling `%s.%s.create()`. '
                'This may be because you have a writable field on the '
                'serializer class that is not a valid argument to '
                '`%s.%s.create()`. You may need to make the field '
                'read-only, or override the %s.create() method to handle '
                'this correctly.\nOriginal exception was:\n %s' %
                (
                    ModelClass.__name__,
                    ModelClass._default_manager.name,
                    ModelClass.__name__,
                    ModelClass._default_manager.name,
                    self.__class__.__name__,
                    tb,
                )
            )
            raise TypeError(msg)

        if many_to_many:
            for field_name, field_value in many_to_many.items():
                field = getattr(instance, field_name)
                field.set(field_value)

        # Возвращаем исключение, если соблюдены условия для модерации.
        # Исключение возвращается потому что должен вернуться instance объект
        # ModelClass, но мы создаем объект ModeratedObject.
        if isinstance(instance, str):
            raise CreateModerationObject(instance)

        return instance

Первые четыре метода в ModerationModelSerializer необходимы для считывания новых атрибутов, установленных для сериализаторов, которые будут наследоваться от нашего ModerationModelSerializer.

Далее были переопределены методы create() и update(), основной код которых полностью взят из serializers.ModelSerializer.

Для метода create() была дописана логика, по которой проверяется роль пользователя, который желает создать объект. Если для роли необходима модерация, то мы создавали ModeratedObject, куда записывали информацию:

  • serializer_class - информация о сериализаторе, в котором объект создавался (в нашем примере CompanySerializer),

  • content_type - pk объекта, который берется из Meta свойств CompanySerializer

  • suggested - пользователь, который хотел создать объект модели (объект Company).

  • changed_object - информация о новом объекте (название полей и их значения).

В instance мы записали сообщение "Объект на модерации". Поскольку метод create() ждет от нас объект, а мы его не передаем, то перед return вызывается исключение, которое уведомляет пользователя о том, что все нормально - жди модерации (строки 208-209). Если роль не требует модерации, то объект создастся сразу.

В методе update() мы также получаем роль пользователя, который хочет изменить объект. В случае если для роли пользователя установлена модерация, то мы проверяем какие поля установлены для модерации в атрибуте moderation_fields сериализатора и какие данные нам поступили из serializer.validated_data. Если нашлось совпадения, то мы формируем словарь, куда вносим название поля и новое значение этого поля.
После анализа serializer.validated_data мы либо создаем либо находим ModeratedObject (как было сказано выше, если пользователь несколько раз подряд меняет объект, то будут сохранены только последние его изменения) и вносим туда новую информацию об изменениях. Далее просто возвращаем не измененный исходных объект пользователю. Также как и в методе create(), если роль не требует модерации, то объект изменяется сразу.

Так закончилось выполнение интересной для меня задачи. Тесты на функционал были написаны, но еще не в полном объеме, обещаю исправиться.
Всем кто дочитал, работающих библиотек и интересных задач)

P.S. Возможны проблемы с подсветкой кода (подсветка для django почему-то не работала, поэтому использовал python), если не подсвечивается попробуйте shift+ctrl+r.

Комментарии (0)