Вместо первичного ключа отправьте другое поле в Django REST Framework - PullRequest
3 голосов
/ 14 марта 2019

serializers.py

class MovieSerializer(serializers.ModelSerializer):

    class Meta:
        model = Movie
        fields = [
            'popularity',
            'director',
            'genre',
            'imdb_score',
            'name',
        ]  

views.py

class MovieList(generics.ListCreateAPIView):
    queryset = Movie.objects.all().order_by('-id')[:10]
    serializer_class = MovieSerializer
    permission_classes = (IsAuthenticated,)

    def list(self, request):
        queryset = self.get_queryset()
        serializer = MovieSerializer(queryset, many=True)
        return Response(serializer.data)

    def post(self, request, format=None):
        data = request.data
        if isinstance(data, list):
            serializer = self.get_serializer(data=request.data, many=True)
        else:
            serializer = self.get_serializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Я отправляю свои данные вот так, и все работает правильно.

{
   "popularity": 83.0,
   "director": "Victor Fleming",
   "genre": [
      1,
      2,
      3,
      4
   ],
   "imdb_score": 8.3,
   "name": "The Wizard of Oz"
}

Но я хочу отправить данные примерно так:

{
   "popularity": 83.0,
   "director": "Victor Fleming",
   "genre": [
      "Adventure",
      "Family",
      "Fantasy",
      "Musical"
   ],
   "imdb_score": 8.3,
   "name": "The Wizard of Oz"
}

См. Список жанров - вместо первичного ключа я отправляю name.

Это мои модели:

class Genre(models.Model):
    name = models.CharField(max_length=30, unique=True)  # make unique

    def __str__(self):
        return self.name

class Movie(models.Model):
    popularity = models.FloatField(max_length=10)
    director = models.CharField(max_length=30)
    genre = models.ManyToManyField(Genre)
    imdb_score = models.FloatField(max_length=10)
    name = models.CharField(max_length=30)

Даже когда я перечисляю данные или получаю их, он должен посылать имя жанра вместо идентификатора.Я пытался использовать StringRelatedField, но он выдал ошибку.

NotImplementedError в / api / movie / StringRelatedField.to_internal_value () должен быть реализован.

Ответы [ 5 ]

3 голосов
/ 14 марта 2019

Переопределите метод create() сериализатора, как показано ниже,

class MovieSerializer(serializers.ModelSerializer):
    <b>genre = serializers.ListSerializer(child=serializers.CharField())</b>

    class Meta:
        model = Movie
        fields = [
            'popularity',
            'director',
            'genre',
            'imdb_score',
            'name',
        ]

    <b>def create(self, validated_data):
        genre = validated_data.pop('genre',[])
        movie = super().create(validated_data)
        genre_qs = Genre.objects.filter(name__in=genre)
        movie.genre.add(*genre_qs)
        return movie</b>
3 голосов
/ 14 марта 2019

Решение - переопределить поле genre в вашем сериализаторе, чтобы принять список строк, подобных этому:

class MovieSerializer(serializers.ModelSerializer):

    genre = serializer.ListField(child=serializers.CharField())

    class Meta:
        model = Movie
        fields = [
            'popularity',
            'director',
            'genre',
            'imdb_score',
            'name',
        ]  
    def validate(self, data):
        genre = data.get('genre', [])
        genre_obj_list = [Genre.objects.get(name=name) for name in genre.all()]
        data.update({'genre': genre_obj_list})
        return data

А в методе validate попробуйте выбрать каждый объект по их именам, поместить в новый список и обновить data результат новым списком объектов. (Я знаю, что это не самые чистые решения, но работает нормально)

Вы также можете попытаться использовать MethodSerializer или определить GenreSerializer и выбрать в нем объекты по их именам и использовать их в родительском сериализаторе в качестве входных данных.

1 голос
/ 14 марта 2019

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

class Genre(models.Model):
    name = models.CharField(max_length=30, primary_key=True)

Не то, чтобы здесь это имело большое значение, но это также сохранит столбецв базе данных, так как автоматически сгенерированный столбец id исчезнет:)


Обновление

После некоторого обсуждения в комментариях к этому ответу, я нахожуважно упомянуть, что, используя ЛЮБОЙ тип в качестве первичного ключа, следует также избегать изменения этого поля впоследствии.

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

1 голос
/ 14 марта 2019

Предполагая, что вы использовали StringRelatedField в вашем MovieSerializer следующим образом:

class MovieSerializer(serializers.ModelSerializer):
    genre = serializers.StringRelatedField(many=True)

    class Meta:
        model = Movie
        fields = [
            'popularity',
            'director',
            'genre',
            'imdb_score',
            'name',
        ]

результат будет выглядеть следующим образом при получении списка фильмов:

[
    {
        "popularity": 83.0,
        "director": "Victor Fleming",
        "genre": [
             "Adventure",
             "Family",
             "Fantasy",
             "Musical"
        ],
        "imdb_score": 8.3,
        "name": "The Wizard of Oz"
    }
]

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

Однако вы можете создать собственное связанное поле.

Это полное serializers.py:

from rest_framework import serializers

from .models import Genre, Movie


class GenreRelatedField(serializers.RelatedField):
    def display_value(self, instance):
        return instance

    def to_representation(self, value):
        return str(value)

    def to_internal_value(self, data):
        return Genre.objects.get(name=data)


class MovieSerializer(serializers.ModelSerializer):
    genre = GenreRelatedField(
        queryset=Genre.objects.all(),
        many=True
    )

    class Meta:
        model = Movie
        fields = (
            'popularity',  
            'director',     
            'genre',                         
            'imdb_score',
            'name',
        )   

Это простой пример, который можно настраивать различными способами.

Метод display_value определяет способ отображения жанра объекта.например в виде.Здесь он просто возвращает объект Genre, т.е. вывод __str__.

Метод to_representation определяет, как объект Genre отображается в выводе (JSON или XML).Это очень похоже на предыдущий метод, но здесь мы явно должны преобразовать жанр в строку.Конечно, вы можете создать более сложный вывод в соответствии с вашими требованиями.

Метод to_internal_value решает вашу реальную проблему путем получения жанра объекта для данного значения.Если у вас есть более сложный метод to_representation, вам потребуется расширенная логика для получения соответствующего объекта.

Используя этот подход, вы можете опубликовать JSON в желаемой форме, указав имена жанров вместо их идентификаторов.

Надеюсь, этот пример поможет и другим людям.

0 голосов
/ 14 марта 2019

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

class Movie(models.Model):
    popularity = models.FloatField(max_length=10)
    director = models.CharField(max_length=30)
    genre = models.ManyToManyField(Genre, db_column='name')
    imdb_score = models.FloatField(max_length=10)
    name = models.CharField(max_length=30)
...