使用DJANGO归还的DRF中的实例版本控制
#python #django #drf #djangoreversion

实例记录

实例记录或模型记录是一种广泛使用的实践,可存储模型的所有实例更新,它可以确保该特定模型中的每个实例都有其自己的版本历史记录,可以在实例进行多个更新时跟踪。

考虑一个简单的博客应用程序,其中博客是博客模型的实例,当与此博客实例进行交互时,我们检索了当前博客的属性,但是,此博客实例可能已经更新了几次,直到它达到了当前状态,为了确保实例数据的完整性,我们需要保存实例的所有过去状态,因为它创建了实例,也需要为每个记录状态包含一些元数据,包括已执行的系统用户更新,以及指示执行此更新的日期时间。

django reververion

django reverversion是一个强大的模块,允许我们跟踪实例版本历史记录,它还提供了可用于为任何模型的每个实例创建“版本控制”类似功能的多个操作实例到较早的状态。

教程

在本文中,我们将使用DRF创建一个简单的博客应用程序并集成Django reverversion,我们将创建相应的视图,该视图将负责检索实例的版本历史记录并将实例转移到较早的状态。

首先,让我们创建一个简单的DRF应用程序,我们将首先定义博客应用程序的models.py文件中的简单博客模型。

blog/models.py

from django.db import models


class Blog(models.Model):

    title = models.CharField(max_length=50, help_text="A title for the blog instance.")

    description = models.CharField(
        max_length=150, null=True, help_text="An optional short description of the blog instance."
    )

    content = models.TextField(help_text="The blog instance's content body.")

接下来,我们将为我们的博客应用程序创建一个序列化器,该应用程序将用于简单的CRUD操作,对于此应用程序,我们将在这里使用DRF的Modelserializer,这将在我们的serializers.py文件中完成。

blog/serializers.py

from rest_framework import serializers
from blog.models import Blog


class BlogSerializer(serializers.ModelSerializer):

    class Meta:
        model = Blog
        fields = '__all__'

现在,让我们在views.py文件中定义一个视图,该视图将处理博客模型上的不同操作,在此示例中,我们将使用DRF的GenericViewSet并根据我们的需求添加Mixins。

blog/views.py

from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, ListModelMixin
from blog.models import Blog
from blog.serializers import BlogSerializer


class BlogViewSet(UpdateModelMixin, RetrieveModelMixin, ListModelMixin, CreateModelMixin, GenericViewSet):

    queryset = Blog.objects.all()
    serializer_class = BlogSerializer

之后,我们只需要在urls.py文件中注册我们的视图。

blog/urls.py

from .views import BlogViewSet
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register('blog', BlogViewSet, basename='blog')
url_patterns = router.urls

请确保在主urls.py文件中导入并添加博客应用程序的所有URLPATTERN。

djangoProject/urls.py

from blog.urls import url_patterns as blog_url_patterns

urlpatterns = []
urlpatterns += blog_url_patterns

现在一切都设置了,我们准备将DJANGO回归模块实现到我们的应用中。

首先,我们必须安装Django归还模块。

  1. 使用以下命令安装DJANGO归还模块。
    pip install django-reversion

  2. reversion添加到您的INSTALLED_APPSINSTALLED_APPS

  3. python manage.py migrate

  4. 迁移您的应用程序

现在我们已经安装了Django恢复,我们将注册我们的Blog型号进行归还。

  1. 在我们的models.py文件中,从回归模块导入寄存器装饰器。

  2. 在我们的模型类上添加装饰器。

更新的models.py文件应该看起来像这样。

blog/models.py

from django.db import models
from reversion import register


@register()
class Blog(models.Model):

    title = models.CharField(max_length=50, help_text="A title for the blog instance.")

    description = models.CharField(
        max_length=150, null=True, help_text="An optional short description of the blog instance."
    )

    content = models.TextField(help_text="The blog instance's content body.")

接下来,我们将在视图集中实现回归模块,在本教程i中,我们有很多方法可以做到这一点,包括手动使用上下文API,使用reverversion Decorator或使用Reverversion View Mixin,将使用Mixin。

RevisionMixin将自动为视图集中的每个更新创建修订版,这很棒,但是我们希望能够检索每个实例的版本历史记录,我们也希望能够将实例还原为旧版本。

因此,我们将覆盖RevisionMixin,并在其中添加两个动作,一个用于检索旧版本的列表,一个用于将实例转移到旧版本中。

让我们创建一个RevisionMixin的自定义版本,可以通过将其添加到继承的类列表中,可以重复使用任何视图集。

  1. 在我们的博客目录中创建一个称为修订的新目录。

  2. views.py文件添加到新创建的目录。

  3. 在此文件中,我们从reversion.views导入RevisionMixin

  4. 创建一个新类,这将是我们的自定义版本的Mixin,我将命名为CustomRevisionMixin

  5. CustomRevisionMixin参数中添加回归模块的RevisionMixin,以继承当前的回归模块的功能。

blog/revision/views.py

from reversion.views import RevisionMixin
class CustomRevisionMixin(RevisionMixin):
    pass

现在我们可以添加自己的功能,我们将在这里创建两个额外的操作。

rest_framework.decorators导入action装饰器。

from rest_framework.decorators import action

创建一个用于检索实例日志的操作,将methods参数设置为['GET'],而detail参数设置为True

@action(methods=['GET'], detail=True)
def instance_logs(self, request, **kwargs):
    pass

接下来,让我们创建一个将检索当前实例版本历史记录的函数,我们将调用函数get_versions

  1. reversion.models导入Version型号。

  2. reversion.errors导入RegistrationError异常类。

  3. 通过调用视图集的get_object方法来检索当前实例。

  4. 致电Version Model的经理的get_for_object方法,因为在文档中,有人指出该方法可能会抛出RegistrationError如果要求使用未注册的模型,我们将在try except子句中包装它,然后扔掉它此案的标准APIException(来自rest_framework.exceptions)。

  5. 通过取出最后一个版本的对象过滤QuerySet,因为它是在降序中订购的,因此该对象将是当前实例的版本,我们在版本的历史记录中不需要。

    <

    < /li>
  6. 将实例和版本QuerySet返回为元组。

def get_versions(self):
    instance = self.get_object()
    try:
        versions = Version.objects.get_for_object(instance)
    except RegistrationError:
        raise APIException(detail='model has not been registered for revision.')
    current_version = versions[0]
    versions = versions.exclude(pk=current_version.id)
    return instance, versions

下一步是为Version模型创建一个序列化器,像往常一样,我们将使用Modelserializer来实现这一目标。

  1. revision目录中创建一个serializers.py文件。

  2. rest_framework导入serializersVersion型号的Version型号。

  3. 定义了将模型设置为Version的序列化机,并根据Version模型上的可用数据定义字段,以便参考查看DJANGO归还模块的源代码或文档,对于本教程,我将添加以下四个字段:

  • version,版本实例的主要键。
  • updated_at,版本实例的创建日期,同时指示何时更新了实例。
  • updated_by,执行更新的用户的用户ID,如果实现了身份验证。
  • instance,更新后实例的JSON对象表示。

您的序列化器类看起来像这样。

blog/revision/serializers.py

from rest_framework import serializers
from reversion.models import Version


class RevisionSerializer(serializers.ModelSerializer):
    version = serializers.PrimaryKeyRelatedField(read_only=True, source='id')
    updated_at = serializers.DateTimeField(read_only=True, source='revision.date_created')
    updated_by = serializers.PrimaryKeyRelatedField(read_only=True, source='revision.user')
    instance = serializers.JSONField(read_only=True, source='field_dict')

    class Meta:
        model = Version
        fields = ['version', 'updated_at', 'updated_by', 'instance']

现在我们最终确定了我们的instance_logs动作。

  1. 将新创建的RevisionSerializer导入到我们的views.py文件中。

  2. rest_framework.response导入Response

  3. 呼叫get_versions函数。

  4. 将版本作为序列化实例传递,别忘了添加many=True kwarg。

  5. 返回序列化数据。

@action(methods=['GET'], detail=True)
def instance_logs(self, request, **kwargs):
    instance, versions = self.get_versions()
    serializer = RevisionSerializer(versions, many=True)
    return Response(serializer.data)

现在,我们定义了第二个动作,该操作将处理实例重新转换为旧版本。

  1. 创建一个revert_instance函数,然后在其顶部添加action装饰器。

  2. 此操作的方法将发布,详细信息应设置为true。

  3. 呼叫get_versions函数。

  4. 定义一个称为RevisionRevertSerializer的第二个序列化器,该序列化器在我们的serializers.py文件中覆盖了RevisionSerializer

  5. 覆盖__init__函数,要动态定义version字段,这是需要的,因为我们想将QuerySet作为上下文传递,以防止用户发送无效的版本IDS。

    <

    <

    <

    <

    <

    < /li>
  6. 覆盖序列化器的update函数以从validated_data提取版本实例。

  7. 调用修订实例API的revert方法,首先,使用DOT表示法访问提取版本实例的修订实例,然后调用revert方法。

  8. 因为revert可能会抛出RevertError,以防数据库模式已更改,我们将在try except子句中包装它,并抛出用于处理此情况的自定义消息。

  9. 导入RevisionRevertSerializer序列化器inside in of in of views.py文件。

  10. 验证并保存序列化器,然后将成功消息返回为响应。

最终代码应该看起来像这样。

blog/revision/views.py

from reversion.models import Version
from reversion.views import RevisionMixin
from reversion.errors import RegistrationError
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.exceptions import APIException
from blog.revision.serializers import RevisionSerializer, RevisionRevertSerializer


class CustomRevisionMixin(RevisionMixin):

    def get_versions(self):
        instance = self.get_object()
        try:
            versions = Version.objects.get_for_object(instance)
        except RegistrationError:
            raise APIException(detail='model has not been registered for revision.')
        current_version = versions[0]
        versions = versions.exclude(pk=current_version.id)
        return instance, versions

    @action(methods=['GET'], detail=True)
    def instance_logs(self, request, **kwargs):
        instance, versions = self.get_versions()
        serializer = RevisionSerializer(versions, many=True)
        return Response(serializer.data)

    @action(methods=['POST'], detail=True)
    def revert_instance(self, request, **kwargs):
        instance, versions = self.get_versions()
        serializer = RevisionRevertSerializer(instance, data=request.data, context={'versions': versions})
        serializer.is_valid(raise_exception=True)
        serializer.save()
        version = serializer.validated_data.get('version')
        return Response({'message': 'instance reverted to version %d.' % version.id})

blog/revision/serializers.py

from rest_framework import serializers
from rest_framework.exceptions import APIException
from reversion.models import Version
from reversion.errors import RevertError


class RevisionSerializer(serializers.ModelSerializer):
    version = serializers.PrimaryKeyRelatedField(read_only=True, source='id')
    updated_at = serializers.DateTimeField(read_only=True, source='revision.date_created')
    updated_by = serializers.PrimaryKeyRelatedField(read_only=True, source='revision.user')
    instance = serializers.JSONField(read_only=True, source='field_dict')

    class Meta:
        model = Version
        fields = ['version', 'updated_at', 'updated_by', 'instance']


class RevisionRevertSerializer(RevisionSerializer):

    def __init__(self, *args, **kwargs):
        super(RevisionSerializer, self).__init__(*args, **kwargs)
        version_queryset = self.context.get('versions')
        self.fields['version'] = serializers.PrimaryKeyRelatedField(write_only=True, queryset=version_queryset)

    def update(self, instance, validated_data):
        version = validated_data['version']
        try:
            version.revision.revert()
            return validated_data
        except RevertError:
            raise APIException(detail='can not revert instance.')

    class Meta(RevisionSerializer.Meta):
        fields = ['version']

最后,我们可以将此自定义修订Mixin添加到任何视图集中,它将自动为任何实例更新创建修订版,并同时接收自定义操作,instance_logsrevert_instance

  1. 在我们的blog/views.py文件中导入CustomRevisionMixin

  2. 添加要通过视图集继承的混合蛋白。

blog/views.py

from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, ListModelMixin
from blog.models import Blog
from blog.serializers import BlogSerializer
from blog.revision.views import CustomRevisionMixin


class BlogViewSet(CustomRevisionMixin, UpdateModelMixin, RetrieveModelMixin, ListModelMixin, CreateModelMixin, GenericViewSet):

    queryset = Blog.objects.all()
    serializer_class = BlogSerializer

全部完成了!现在我们可以测试我们的代码。

首先,让我们创建一个博客实例。

METHOD: POST 
URL: /blog/
BODY: {
    "title": "version 1 title",
    "description": "version 1 description",
    "content": "version 1 content"
}

接下来,让我们更新我们创建的实例。

METHOD: PATCH
URL: /blog/1/
BODY: {
    "title": "version 2 title",
    "description": "version 2 description",
    "content": "version 2 content"
}

之后,我们将检查实例日志。

METHOD: GET
URL: /blog/1/instance_logs/

我们的响应应包含旧版本(更新之前)。

[
    {
        "version": 1,
        "updated_at": "2023-04-22T22:55:29.235430Z",
        "updated_by": null,
        "instance": {
            "id": 1,
            "title": "version 1 title",
            "description": "version 1 description",
            "content": "version 1 content"
        }
    }
]

在这里,我们可以看到版本ID是1,因此我们必须向{"version": 1}提供revert_instance端点才能将其恢复。

METHOD: POST
URL: /blog/1/revert_instance/
BODY: {
    "version": 1
}

现在让我们检查一下实例是否已通过检索实例来恢复。

METHOD: GET
URL: /blog/1/

正如预期的,该实例已成功恢复。

{
    "id": 1,
    "title": "version 1 title",
    "description": "version 1 description",
    "content": "version 1 content"
}

和另一个版本已添加到实例的日志中。

[
    {
        "version": 2,
        "updated_at": "2023-04-22T23:00:44.489784Z",
        "updated_by": null,
        "instance": {
            "id": 1,
            "title": "version 2 title",
            "description": "version 2 description",
            "content": "version 2 content"
        }
    },
    {
        "version": 1,
        "updated_at": "2023-04-22T22:55:29.235430Z",
        "updated_by": null,
        "instance": {
            "id": 1,
            "title": "version 1 title",
            "description": "version 1 description",
            "content": "version 1 content"
        }
    }
]

这就是本文的全部,我真的希望它有帮助。

源代码:https://github.com/saruar999/django-reversion-with-drf