我使用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
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')
当你使用 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_create
在 create
方法将返回元组,而不是实例。
好了,现在想象一下你将用 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
方法调用。
我的建议是不要使用 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