web-dev-qa-db-fra.com

Modifier un champ dans un Django REST Framework ModelSerializer basé sur le type de demande?

Considérez ce cas où j'ai un modèle Book et Author.

serializers.py

class AuthorSerializer(serializers.ModelSerializer):

    class Meta:
        model = models.Author
        fields = ('id', 'name')

class BookSerializer(serializers.ModelSerializer):
    author = AuthorSerializer(read_only=True)

    class Meta:
        model = models.Book
        fields = ('id', 'title', 'author')

viewsets.py

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

Cela fonctionne très bien si j'envoie une demande GET pour un livre. J'obtiens une sortie avec un sérialiseur imbriqué contenant les détails du livre et les détails de l'auteur imbriqué, ce que je veux.

Cependant, quand je veux créer/mettre à jour un livre, je dois envoyer un POST/PUT/PATCH avec les détails imbriqués de l'auteur au lieu de juste leur id. Je veux pouvoir créer/mettre à jour un objet livre en spécifiant un identifiant d'auteur et non l'intégralité de l'objet auteur.

Donc, quelque chose où mon sérialiseur ressemble à ceci pour une demande GET

class BookSerializer(serializers.ModelSerializer):
    author = AuthorSerializer(read_only=True)

    class Meta:
        model = models.Book
        fields = ('id', 'title', 'author')

et mon sérialiseur ressemble à ceci pour une demande POST, PUT, PATCH

class BookSerializer(serializers.ModelSerializer):
    author = PrimaryKeyRelatedField(queryset=Author.objects.all())

    class Meta:
        model = models.Book
        fields = ('id', 'title', 'author')

Je ne veux pas non plus créer deux sérialiseurs entièrement distincts pour chaque type de demande. Je voudrais juste modifier le champ author dans le BookSerializer.

Enfin, existe-t-il une meilleure façon de faire tout cela?

14
him229

À mon humble avis, plusieurs sérialiseurs ne feront que créer de plus en plus de confusion.

Je préfère plutôt la solution ci-dessous:

  1. Ne changez pas votre ensemble de vues (laissez-le par défaut)
  2. Ajoutez la méthode .validate () dans votre sérialiseur; avec les autres .create ou .update () etc. requis. Ici, la vraie logique ira dans la méthode validate (). Lorsque basé sur le type de demande, nous créerons un dict validated_data tel que requis par notre sérialiseur.

Je pense que c'est l'approche la plus propre.

Voir mon problème et solution similaire sur DRF: autoriser tous les champs dans la demande GET mais restreindre POST à un seul champ

7
Jadav Bheda

Il y a une fonctionnalité de DRF où vous pouvez changer dynamiquement les champs sur le sérialiseur http://www.Django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields

Mon cas d'utilisation: utilisez le champ slug sur GET pour que nous puissions voir le bon représentant d'une relation, mais sur POST/PUT, revenez à la mise à jour de la clé primaire classique. Ajustez votre sérialiseur à quelque chose comme ceci:

class FooSerializer(serializers.ModelSerializer):
    bar = serializers.SlugRelatedField(slug_field='baz', queryset=models.Bar.objects.all())

    class Meta:
        model = models.Foo
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super(FooSerializer, self).__init__(*args, **kwargs)

        try:
            if self.context['request'].method in ['POST', 'PUT']:
                self.fields['bar'] = serializers.PrimaryKeyRelatedField(queryset=models.Bar.objects.all())
        except KeyError:
            pass

Le KeyError est parfois lancé lors de l'initialisation du code sans demande, éventuellement des tests unitaires.

Profitez-en et utilisez de manière responsable.

7
jmoz

Vous recherchez le get_serializer_class sur la méthode ViewSet. Cela vous permet d'activer le type de demande pour quel sérialiseur vous souhaitez utiliser.

from rest_framework import viewsets

class MyModelViewSet(viewsets.ModelViewSet):

    model = MyModel
    queryset = MyModel.objects.all()

    def get_serializer_class(self):
        if self.action in ('create', 'update', 'partial_update'):
            return MySerializerWithPrimaryKeysForCreatingOrUpdating
        else:
            return MySerializerWithNestedData
4
Aaron Lelevier

La façon dont j'ai fini par résoudre ce problème était d'avoir un autre sérialiseur pour quand c'est un domaine connexe.

class HumanSerializer(PersonSerializer):

    class Meta:
        model = Human
        fields = PersonSerializer.Meta.fields + (
            'firstname',
            'middlename',
            'lastname',
            'sex',
            'date_of_birth',
            'balance'
        )
        read_only_fields = ('name',)


class HumanRelatedSerializer(HumanSerializer):
    def to_internal_value(self, data):
        return self.Meta.model.objects.get(id=data['id'])


class PhoneNumberSerializer(serializers.ModelSerializer):
    contact = HumanRelatedSerializer()

    class Meta:
        model = PhoneNumber
        fields = (
            'id',
            'contact',
            'phone',
            'extension'
        )

Vous pouvez faire quelque chose comme ça, mais pour le RelatedSerializer, faites:

 def to_internal_value(self, data):
     return self.Meta.model.objects.get(id=data)

Ainsi, lors de la sérialisation, vous sérialisez l'objet associé et lors de la désérialisation, vous n'avez besoin que de l'ID pour obtenir l'objet associé.

0
miyamoto

Je sais qu'il est un peu tard, mais juste au cas où quelqu'un d'autre en aurait besoin. Il existe des packages tiers pour drf qui permettent le réglage dynamique des champs de sérialiseur inclus via les paramètres de requête de requête (répertoriés dans les documents officiels: https://www.Django-rest-framework.org/api-guide/serializers/#third-party-packages ).

Les OMI les plus complètes sont:

  1. https://github.com/AltSchool/dynamic-rest
  2. https://github.com/rsinger86/drf-flex-fields

où (1) a plus de fonctionnalités que (2) (peut-être trop, selon ce que vous voulez faire).

Avec (2), vous pouvez faire des choses telles que (extraites du readme du repo):

class CountrySerializer(FlexFieldsModelSerializer):
    class Meta:
        model = Country
        fields = ['name', 'population']


class PersonSerializer(FlexFieldsModelSerializer):
    country = serializers.PrimaryKeyRelatedField(read_only=True)

    class Meta:
        model = Person
        fields = ['id', 'name', 'country', 'occupation']

    expandable_fields = {
        'country': (CountrySerializer, {'source': 'country', 'fields': ['name']})
    }

La réponse par défaut:

{
  "id" : 13322,
  "name" : "John Doe",
  "country" : 12,
  "occupation" : "Programmer"
}

Lorsque vous faites un GET/person/13322? Expand = country, la réponse changera en:

{
  "id" : 13322,
  "name" : "John Doe",
  "country" : {
    "name" : "United States"
  },
  "occupation" : "Programmer",
}

Remarquez comment la population a été supprimée de l'objet pays imbriqué. En effet, les champs ont été définis sur ['nom'] lorsqu'ils ont été transmis au CountrySerializer intégré.

De cette façon, vous pouvez conserver vos POST requêtes incluant juste un identifiant et "étendre" les réponses GET pour inclure plus de détails.

0
aleclara95