Django rest framework, gunakan serializers berbeda di ModelViewSet yang sama

196

Saya ingin menyediakan dua serialisator yang berbeda namun dapat memperoleh manfaat dari semua fasilitas ModelViewSet:

  • Saat melihat daftar objek, saya ingin setiap objek memiliki url yang mengarahkan ulang ke rinciannya dan setiap relasi lain muncul menggunakan __unicode __model target;

contoh:

{
  "url": "http://127.0.0.1:8000/database/gruppi/2/",
  "nome": "universitari",
  "descrizione": "unitn!",
  "creatore": "emilio",
  "accesso": "CHI",
  "membri": [
    "emilio",
    "michele",
    "luisa",
    "ivan",
    "saverio"
  ]
}
  • Saat melihat detail objek, saya ingin menggunakan default HyperlinkedModelSerializer

contoh:

{
  "url": "http://127.0.0.1:8000/database/gruppi/2/",
  "nome": "universitari",
  "descrizione": "unitn!",
  "creatore": "http://127.0.0.1:8000/database/utenti/3/",
  "accesso": "CHI",
  "membri": [
    "http://127.0.0.1:8000/database/utenti/3/",
    "http://127.0.0.1:8000/database/utenti/4/",
    "http://127.0.0.1:8000/database/utenti/5/",
    "http://127.0.0.1:8000/database/utenti/6/",
    "http://127.0.0.1:8000/database/utenti/7/"
  ]
}

Saya berhasil membuat semua ini berfungsi seperti yang saya inginkan dengan cara berikut:

serializers.py

# serializer to use when showing a list
class ListaGruppi(serializers.HyperlinkedModelSerializer):
    membri = serializers.RelatedField(many = True)
    creatore = serializers.RelatedField(many = False)

    class Meta:
        model = models.Gruppi

# serializer to use when showing the details
class DettaglioGruppi(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = models.Gruppi

views.py

class DualSerializerViewSet(viewsets.ModelViewSet):
    """
    ViewSet providing different serializers for list and detail views.

    Use list_serializer and detail_serializer to provide them
    """
    def list(self, *args, **kwargs):
        self.serializer_class = self.list_serializer
        return viewsets.ModelViewSet.list(self, *args, **kwargs)

    def retrieve(self, *args, **kwargs):
        self.serializer_class = self.detail_serializer
        return viewsets.ModelViewSet.retrieve(self, *args, **kwargs)

class GruppiViewSet(DualSerializerViewSet):
    model = models.Gruppi
    list_serializer = serializers.ListaGruppi
    detail_serializer = serializers.DettaglioGruppi

    # etc.

Pada dasarnya saya mendeteksi ketika pengguna meminta tampilan daftar atau tampilan rinci dan berubah serializer_classsesuai dengan kebutuhan saya. Saya tidak benar-benar puas dengan kode ini, sepertinya hack kotor dan, yang paling penting, bagaimana jika dua pengguna meminta daftar dan detail pada saat yang sama?

Apakah ada cara yang lebih baik untuk mencapai ini menggunakan ModelViewSetsatau saya harus kembali menggunakan GenericAPIView?

EDIT:
Berikut cara melakukannya menggunakan basis kustom ModelViewSet:

class MultiSerializerViewSet(viewsets.ModelViewSet):
    serializers = { 
        'default': None,
    }

    def get_serializer_class(self):
            return self.serializers.get(self.action,
                        self.serializers['default'])

class GruppiViewSet(MultiSerializerViewSet):
    model = models.Gruppi

    serializers = {
        'list':    serializers.ListaGruppi,
        'detail':  serializers.DettaglioGruppi,
        # etc.
    }
Beruang hitam
sumber
bagaimana Anda mengimplementasikannya pada akhirnya? Menggunakan cara yang diusulkan oleh user2734679 atau menggunakan GenericAPIView?
andilabs
Seperti yang disarankan oleh user2734679; Saya membuat ViewSet generik menambahkan kamus untuk menentukan serializer untuk setiap tindakan dan serializer default saat tidak ditentukan
BlackBear
Saya memiliki masalah serupa ( stackoverflow.com/questions/24809737/… ) dan untuk saat ini berakhir dengan itu ( gist.github.com/andilab/a23a6370bd118bf5e858 ), tetapi saya tidak puas dengan itu.
andilabs
1
Dibuat paket kecil ini untuk ini. github.com/Darwesh27/drf-custom-viewsets
Adil Malik
1
Mengganti metode pengambilan OK.
gzerone

Jawaban:

288

Ganti get_serializer_classmetode Anda . Metode ini digunakan dalam mixins model Anda untuk mengambil kelas Serializer yang tepat.

Perhatikan bahwa ada juga get_serializermetode yang mengembalikan instance Serializer yang benar

class DualSerializerViewSet(viewsets.ModelViewSet):
    def get_serializer_class(self):
        if self.action == 'list':
            return serializers.ListaGruppi
        if self.action == 'retrieve':
            return serializers.DettaglioGruppi
        return serializers.Default # I dont' know what you want for create/destroy/update.                
pengguna133688
sumber
1
Ini bagus, terima kasih! Saya telah mengganti get_serializer_class
BlackBear
15
PERINGATAN: jjj resto swagger tidak menempatkan parameter self.action, jadi fungsi ini akan melempar pengecualian. Anda dapat menggunakan jawaban gonz atau menggunakanif hasattr(self, 'action') and self.action == 'list'
Tom Leys
Buat paket pypi kecil untuk ini. github.com/Darwesh27/drf-custom-viewsets
Adil Malik
Bagaimana cara mendapatkan pkobjek yang diminta, jika tindakan itu retrieve?
Pranjal Mittal
Self.action saya adalah None. Bisakah seseorang memberi tahu saya alasannya?
Kakaji
86

Anda mungkin menemukan mixin ini berguna, itu menimpa metode get_serializer_class dan memungkinkan Anda untuk mendeklarasikan dict yang memetakan class action dan serializer atau mundur ke perilaku biasa.

class MultiSerializerViewSetMixin(object):
    def get_serializer_class(self):
        """
        Look for serializer class in self.serializer_action_classes, which
        should be a dict mapping action name (key) to serializer class (value),
        i.e.:

        class MyViewSet(MultiSerializerViewSetMixin, ViewSet):
            serializer_class = MyDefaultSerializer
            serializer_action_classes = {
               'list': MyListSerializer,
               'my_action': MyActionSerializer,
            }

            @action
            def my_action:
                ...

        If there's no entry for that action then just fallback to the regular
        get_serializer_class lookup: self.serializer_class, DefaultSerializer.

        """
        try:
            return self.serializer_action_classes[self.action]
        except (KeyError, AttributeError):
            return super(MultiSerializerViewSetMixin, self).get_serializer_class()
gonz
sumber
Dibuat paket kecil ini untuk ini. github.com/Darwesh27/drf-custom-viewsets
Adil Malik
15

Jawaban ini sama dengan jawaban yang diterima tetapi saya lebih suka melakukannya dengan cara ini.

Pandangan umum

get_serializer_class(self):

Mengembalikan kelas yang harus digunakan untuk serializer. Default untuk mengembalikan serializer_classatribut.

Dapat diganti untuk memberikan perilaku dinamis, seperti menggunakan serialisator yang berbeda untuk operasi membaca dan menulis atau menyediakan serialisator yang berbeda untuk tipe pengguna yang berbeda. atribut serializer_class.

class DualSerializerViewSet(viewsets.ModelViewSet):
    # mapping serializer into the action
    serializer_classes = {
        'list': serializers.ListaGruppi,
        'retrieve': serializers.DettaglioGruppi,
        # ... other actions
    }
    default_serializer_class = DefaultSerializer # Your default serializer

    def get_serializer_class(self):
        return self.serializer_classes.get(self.action, self.default_serializer_class)
Mohammad Masoumi
sumber
Tidak dapat menggunakannya karena memberitahu saya bahwa pandangan saya tidak memiliki atribut "tindakan". Sepertinya ProductIndex (generics.ListCreateAPIView). Apakah ini berarti bahwa Anda benar-benar harus mengoper viewset sebagai argumen atau adakah cara untuk melakukannya menggunakan tampilan API generik?
Seb
1
balasan terlambat untuk komentar @Seb - mungkin seseorang bisa mendapat untung dari itu :) Contohnya menggunakan ViewSets, bukan Views :)
fanny
Jadi dikombinasikan dengan posting ini stackoverflow.com/questions/32589087/… , ViewSets tampaknya menjadi cara untuk pergi untuk memiliki kontrol lebih besar atas pandangan yang berbeda dan menghasilkan url secara otomatis untuk memiliki API yang konsisten? Awalnya berpikir bahwa obat generik.ListeCreateAPIView adalah yang paling efisien, tetapi terlalu mendasar bukan?
Seb
10

Mengenai menyediakan berbagai serialisator, mengapa tidak ada orang yang menggunakan pendekatan yang memeriksa metode HTTP? Ini IMO lebih jelas dan tidak memerlukan pemeriksaan tambahan.

def get_serializer_class(self):
    if self.request.method == 'POST':
        return NewRackItemSerializer
    return RackItemSerializer

Kredit / sumber: https://github.com/encode/django-rest-framework/issues/1563#issuecomment-42357718

Luca Bezerra
sumber
12
Untuk kasus yang dimaksud, yaitu tentang menggunakan serializer listdan retrievetindakan yang berbeda, Anda memiliki masalah yang sama-sama menggunakan GETmetode. Inilah sebabnya mengapa kerangka istirahat Django ViewSets menggunakan konsep tindakan , yang serupa, tetapi sedikit berbeda dari metode http yang sesuai.
Tutup Håken
8

Berdasarkan @gonz dan @ user2734679 jawaban saya telah membuat paket python kecil ini yang memberikan fungsionalitas ini dalam bentuk kelas anak ModelViewset. Inilah cara kerjanya.

from drf_custom_viewsets.viewsets.CustomSerializerViewSet
from myapp.serializers import DefaltSerializer, CustomSerializer1, CustomSerializer2

class MyViewSet(CustomSerializerViewSet):
    serializer_class = DefaultSerializer
    custom_serializer_classes = {
        'create':  CustomSerializer1,
        'update': CustomSerializer2,
    }
Adil Malik
sumber
6
Lebih baik gunakan mixin yang jauh lebih umum.
iamsk
1

Meskipun mendefinisikan beberapa Serializer dengan cara atau cara lain tampaknya merupakan cara yang paling jelas didokumentasikan , FWIW ada pendekatan alternatif yang mengacu pada kode terdokumentasi lain dan yang memungkinkan lewat argumen ke serializer saat instantiated. Saya pikir itu mungkin akan cenderung lebih bermanfaat jika Anda perlu menghasilkan logika berdasarkan berbagai faktor, seperti tingkat admin pengguna, tindakan yang dipanggil, bahkan mungkin atribut dari instance.

Bagian pertama dari teka-teki adalah dokumentasi tentang memodifikasi serializer secara dinamis pada titik instantiasi . Dokumentasi itu tidak menjelaskan cara memanggil kode ini dari viewset atau bagaimana mengubah status bidang yang hanya dibaca setelah initated - tetapi itu tidak terlalu sulit.

Bagian kedua - metode get_serializer juga didokumentasikan - (hanya sedikit lebih jauh di bawah halaman dari get_serializer_class di bawah 'metode lain') sehingga harus aman untuk diandalkan (dan sumbernya sangat sederhana, yang semoga berarti lebih sedikit kesempatan untuk tidak sengaja) efek samping yang dihasilkan dari modifikasi). Periksa sumbernya di bawah GenericAPIView (ModelViewSet - dan semua kelas tampilan bawaan lainnya) - mewarisi dari GenericAPIView yang mendefinisikan get_serializer.

Menyatukan keduanya Anda bisa melakukan sesuatu seperti ini:

Dalam file serializers (untuk saya base_serializers.py):

class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""
A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed.
"""

def __init__(self, *args, **kwargs):
    # Don't pass the 'fields' arg up to the superclass
    fields = kwargs.pop('fields', None)

    # Adding this next line to the documented example
    read_only_fields = kwargs.pop('read_only_fields', None)

    # Instantiate the superclass normally
    super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)

    if fields is not None:
        # Drop any fields that are not specified in the `fields` argument.
        allowed = set(fields)
        existing = set(self.fields)
        for field_name in existing - allowed:
            self.fields.pop(field_name)

    # another bit we're adding to documented example, to take care of readonly fields 
    if read_only_fields is not None:
        for f in read_only_fields:
            try:
                self.fields[f].read_only = True
            exceptKeyError:
                #not in fields anyway
                pass

Kemudian di viewset Anda, Anda mungkin melakukan sesuatu seperti ini:

class MyViewSet(viewsets.ModelViewSet):
    # ...permissions and all that stuff

    def get_serializer(self, *args, **kwargs):

        # the next line is taken from the source
        kwargs['context'] = self.get_serializer_context()

        # ... then whatever logic you want for this class e.g:
        if self.action == "list":
            rofs = ('field_a', 'field_b')
            fs = ('field_a', 'field_c')
        if self.action == retrieve”:
            rofs = ('field_a', 'field_c’, ‘field_d’)
            fs = ('field_a', 'field_b’)
        #  add all your further elses, elifs, drawing on info re the actions, 
        # the user, the instance, anything passed to the method to define your read only fields and fields ...
        #  and finally instantiate the specific class you want (or you could just
        # use get_serializer_class if you've defined it).  
        # Either way the class you're instantiating should inherit from your DynamicFieldsModelSerializer
        kwargs['read_only_fields'] = rofs
        kwargs['fields'] = fs
        return MyDynamicSerializer(*args, **kwargs)

Dan itu seharusnya! Menggunakan MyViewSet sekarang harus instantiate MyDynamicSerializer Anda dengan argumen yang Anda inginkan - dan dengan asumsi serializer Anda mewarisi dari DynamicFieldsModelSerializer Anda, ia harus tahu apa yang harus dilakukan.

Mungkin perlu disebutkan bahwa dapat masuk akal jika Anda ingin mengadaptasi serializer dengan beberapa cara lain ... misalnya untuk melakukan hal-hal seperti mengambil dalam daftar read_only_exceptions dan menggunakannya untuk daftar putih daripada bidang daftar hitam (yang cenderung saya lakukan). Saya juga merasa berguna untuk mengatur bidang ke tuple kosong jika tidak lulus dan kemudian hanya menghapus centang untuk Tidak Ada ... dan saya menetapkan definisi bidang saya pada Serializers bawaan saya ke ' semua '. Ini berarti tidak ada bidang yang tidak dilewati ketika membuat serializer selamat secara tidak sengaja dan saya juga tidak perlu membandingkan permintaan serializer dengan definisi kelas serializer bawaan untuk mengetahui apa yang disertakan ... misalnya dalam init dari DynamicFieldsModelSerializer:

# ....
fields = kwargs.pop('fields', ())
# ...
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
# ....

NB Jika saya hanya ingin dua atau tiga kelas yang dipetakan ke tindakan yang berbeda dan / atau saya tidak ingin perilaku serializer yang dinamis, saya mungkin akan menggunakan salah satu pendekatan yang disebutkan oleh orang lain di sini, tapi saya pikir ini layak disajikan sebagai alternatif , khususnya mengingat kegunaannya yang lain.

pengguna1936977
sumber