Как создать сериализатор, который повторно использует уникальный ключ моей модели? - PullRequest
1 голос
/ 25 мая 2020

Я использую Python 3.7, Django 2.2, Django rest framework и 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

Ответы [ 3 ]

2 голосов
/ 28 мая 2020

Когда вы используете ключ unique=True в модели, сериализатор автоматически добавит уникальный валидатор в это поле. Достаточно отменить проверку уникальности, написав собственное поле 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 in 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)

Метод save в сериализаторе проверяет, является ли поле self.instance нулевым или не. Если есть непустой self.instance, он вызовет метод update; else - метод create.
Итак, если существует CoopType с именем из вашего serializer_data словаря - будет вызван метод update. В противном случае вы увидите вызов метода create.

1 голос
/ 30 мая 2020

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]

The 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')
0 голосов
/ 30 мая 2020

Я предлагаю не использовать 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
...