如何创建一个重用我的模型的唯一键的序列器?

问题描述 投票:1回答:1

我使用Python 3.7,Django 2.2,Django rest框架和pytest。 我有以下模型,我想重用一个现有的模型,如果它的唯一键存在的话......。

class CoopTypeManager(models.Manager):

    def get_by_natural_key(self, name):
        return self.get_or_create(name=name)[0]

class CoopType(models.Model):
    name = models.CharField(max_length=200, null=False, unique=True)

    objects = CoopTypeManager()

然后我创建了下面的序列化器来从REST数据中生成这个模型。

class CoopTypeSerializer(serializers.ModelSerializer):
    class Meta:
        model = CoopType
        fields = ['id', 'name']

    def create(self, validated_data):
        """
        Create and return a new `CoopType` instance, given the validated data.
        """
        return CoopType.objects.get_or_create(**validated_data)

    def update(self, instance, validated_data):
        """
        Update and return an existing `CoopType` instance, given the validated data.
        """
        instance.name = validated_data.get('name', instance.name)
        instance.save()
        return instance

然而,当我运行下面的测试时,我故意使用了一个取自于的名字

@pytest.mark.django_db
def test_coop_type_create_with_existing(self):
    """ Test coop type serizlizer model if there is already a coop type by that name """
    coop_type = CoopTypeFactory()
    serializer_data = {
        "name": coop_type.name,
    }

    serializer = CoopTypeSerializer(data=serializer_data)
    serializer.is_valid()
    print(serializer.errors)
    assert serializer.is_valid(), serializer.errors
    result = serializer.save()
    assert result.name == name

我得到以下错误信息

python manage.py test --settings=directory.test_settings
...        ----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/davea/Documents/workspace/chicommons/maps/web/tests/test_serializers.py", line 46, in test_coop_type_create_with_existing
    assert serializer.is_valid(), serializer.errors
AssertionError: {'name': [ErrorDetail(string='coop type with this name already exists.', code='unique')]}

如何构造我的序列器,使我可以在其唯一键不存在的情况下创建我的模型,或者在它存在的情况下重新使用它?

编辑。 这里是GitHub的链接...

https://github.com/chicommons/maps/tree/master/web
django django-rest-framework pytest django-serializer unique-key
1个回答
1
投票

DRF验证了每个字段的唯一性,如果声明的是 unique=True 模型中,所以如果你想保持你的独特的反义词,你必须改变模型如下。name 字段。

class CoopType(models.Model):
    name = models.CharField(max_length=200, null=False)

    objects = CoopTypeManager()

    class Meta:
        # Creates a new unique constraint with the `name` field
        constraints = [models.UniqueConstraint(fields=['name'], name='coop_type_unq')]

另外,你还要改变你的序列化器,如果你使用的是默认行为的ViewSet,你只需要在序列化器中添加一个自定义验证。

from rest_framework import serializers
from rest_framework.exceptions import ValidationError

from .models import CoopType


class CoopTypeSerializer(serializers.ModelSerializer):
    default_error_messages = {'name_exists': 'The name already exists'}

    class Meta:
        model = CoopType
        fields = ['id', 'name']

    def validate(self, attrs):
        validated_attrs = super().validate(attrs)
        errors = {}

        # check if the new `name` doesn't exist for other db record, this is only for updates
        if (
            self.instance  # the instance to be updated
            and 'name' in validated_attrs  # if name is in the attributes
            and self.instance.name != validated_attrs['name']  # if the name is updated
        ):
            if (
                CoopType.objects.filter(name=validated_attrs['name'])
                .exclude(id=self.instance.id)
                .exists()
            ):
                errors['name'] = self.error_messages['name_exists']

        if errors:
            raise ValidationError(errors)

        return validated_attrs

    def create(self, validated_data):
        # get_or_create returns a tuple with (instance, boolean). The boolean is True if a new instance was created and False otherwise
        return CoopType.objects.get_or_create(**validated_data)[0]

update 方法被删除,因为不需要。

最后,测试。

class FactoryTest(TestCase):

    def test_coop_type_create_with_existing(self):
        """ Test coop type serializer model if there is already a coop type by that name """
        coop_type = CoopTypeFactory()
        serializer_data = {
            "name": coop_type.name,
        }

        # Creation
        serializer = CoopTypeSerializer(data=serializer_data)
        serializer.is_valid()
        self.assertTrue(serializer.is_valid(), serializer.errors)
        result = serializer.save()
        assert result.name == serializer_data['name']

        # update with no changes
        serializer = CoopTypeSerializer(coop_type, data=serializer_data)
        serializer.is_valid()
        serializer.save()
        self.assertTrue(serializer.is_valid(), serializer.errors)

        # update with the name changed
        serializer = CoopTypeSerializer(coop_type, data={'name': 'testname'})
        serializer.is_valid()
        serializer.save()
        self.assertTrue(serializer.is_valid(), serializer.errors)
        coop_type.refresh_from_db()
        self.assertEqual(coop_type.name, 'testname')

2
投票

当你使用 unique=True 模型中的键,Serializer将自动为该字段添加唯一性验证器。name 字段直接放在序列化器中,以防止出现错误。

class Ser(serializers.ModelSerializer):
    name = serializers.CharField()  # no unique validation here

    class Meta:
        model = CoopType
        fields = ['id', 'name']

    def create(self, validated_data):
        return CoopType.objects.get_or_create(**validated_data)

小心点: get_or_createcreate 方法将返回元组,而不是实例。

好了,现在想象一下你将用 id 领域,所以你真的需要一个 update 然后你可以在下面的方法中进行黑客攻击。validate 方法(也许它很脏,但它能用)。

class Ser(serializers.ModelSerializer):
    # no `read_only` option (default for primary keys in `ModelSerializer`)
    id = serializers.IntegerField(required=False)

    # no unique validators in charfield
    name = serializers.CharField()

    class Meta:
        model = CoopType
        fields = ["id", "name"]

    def validate(self, attrs):
        attrs = super().validate(attrs)

        if "id" in attrs:
            try:
                self.instance = CoopType.objects.get(name=attrs["name"])
            except CoopType.DoesNotExist:
                pass

            # to prevent manual changing ids in database
            del attrs["id"]

        return attrs

    def create(self, validated_data):
        return CoopType.objects.get_or_create(**validated_data)

    def update(self, instance, validated_data):
        # you can delete that method, it will be called anyway from parent class
        return super().update(instance, validated_data)

The save 方法,检查字段 self.instance 是否为空。如果有一个非空的 self.instance,它将调用 update 方法;否则 - create 方法。所以如果 CoopType 称呼 serializer_data 辞典存在- update 方法将被调用。在其他情况下,你会看到 create 方法调用。


0
投票

我的建议是不要使用 ModelSerializer 但却使用了一个普通的序列器。

class CoopTypeSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    name = serializers.CharField(max_length=200, required=True, allow_blank=False)

    def create(self, validated_data):
        """
        Create and return a new `CoopType` instance, given the validated data.
        """
        return CoopType.objects.get_or_create(**validated_data)[0]

    def update(self, instance, validated_data):
        """
        Update and return an existing `CoopType` instance, given the validated data.
        """
        instance.name = validated_data.get('name', instance.name)
        instance.save()
        return instance
© www.soinside.com 2019 - 2024. All rights reserved.