Django-rest-framework: Serializer bersarang tidak menerima konteks tampilan

Dibuat pada 13 Feb 2015  ·  26Komentar  ·  Sumber: encode/django-rest-framework

_Ini bukan pertanyaan stackoverflow IMO, jadi maafkan saya jika itu_

Saya mencoba menggunakan serializer bersarang, saya tahu ini sulit untuk didiskusikan karena kami memiliki begitu banyak masalah di cabang v2. Saya ingin bidang serializer bersarang menggunakan default berdasarkan proyek yang dipilih.

ID proyek disediakan di QUERY_PARAMS dan berlaku di perform_create seperti seharusnya sekarang, di cabang v3. Tetapi saya memerlukan proyek di beberapa bidang serializer bersarang SEBELUM menyimpan - Saya ingin memberikan nilai default untuk objek dalam metadata serializer, sehingga pengguna akan dapat mengubah beberapa hal, yang secara opsional dapat diambil dari objek proyek.

Jadi, saya mengisi context di get_serializer_context dengan objek proyek. Dan saya harus menulis ulang __init__ di nested serializer untuk mengatur default, ya?

Masalahnya adalah, serializer bersarang tidak menerima context atau parent dalam __init__ . Tampaknya, itu tidak menerima konteks sama sekali! Jadi, itu harus menjadi regresi sejak pull-request 497 .

Saya pikir, ini bukan regresi, lebih seperti perubahan desain, tetapi saya benar-benar membutuhkan konteks untuk dapat mengubah perilaku serizliser!

Komentar yang paling membantu

Saya kira satu solusinya adalah selalu membuat instance ChildSerializer yang disarangkan dalam metode __init__ dari ParentSerializer alih-alih dalam deklarasi bidang, maka seseorang dapat meneruskan konteksnya secara manual.

Seperti ini:

class ChildSerializer(serializers.Serializer):
    ...

class ParentSerializer(serializers.Serializer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['child'] = ChildSerializer(context=self.context)

Itu tampaknya berhasil dalam tes awal. Tidak yakin apakah ada tangkapan dengan ini.

Semua 26 komentar

Konteks tidak (tidak dapat) diteruskan ke bidang (atau serializer bersarang) pada titik init, karena mereka diinisialisasi ketika dideklarasikan pada serializer induk...

class MySerializer(Serializer):
    child = ChildSerializer()  <--- We've just initialized this.

Yang terbaik adalah melakukan perilaku seperti itu di MySerializer.__init__ , misalnya...

def __init__(self, *args, **kwargs):
    super(MySerializer, self).__init__(*args, **kwargs)
    self.fields['child'].default = ...

Pada saat itu Anda _do_ memiliki akses ke konteks.

Jika Anda benar-benar ingin, Anda dapat melakukan beberapa tindakan pada titik bidang tersebut menjadi terikat pada induknya...

class ChildSerializer(Serializer):
    def bind(self, *args, **kwargs):
        super(ChildSerializer, self).bind(*args, **kwargs)
        # do stuff with self.context.

Ini harus dianggap sebagai API pribadi, dan gaya induk __init__ yang tercantum di atas harus lebih disukai.

Ya, saya mengerti apa yang Anda jelaskan. Makanya menurut saya agak ribet. Itu sebabnya solusi saya adalah membuat serializer bersarang di __ini__ induknya. Tapi serializer bersarang __init__ dipanggil setelah setiap instance serializer baru dibuat, karena menggunakan deepcopy, tetapi kami tidak dapat menyelipkan instance induk di dalamnya...
Jadi, seperti yang saya lihat di v2, bidang memiliki metode initialize , yang sempurna untuk init default, berdasarkan serializer induk.

Jadi, sepertinya bind seharusnya bagus.

Ini harus dianggap sebagai API pribadi, dan gaya induk __init__ yang tercantum di atas harus lebih disukai.

Tetapi karena kami tidak dapat menggunakan __init__ , karena kami tidak dapat mengakses semua lingkungan yang tersedia, yang dibuat oleh tampilan saat ini, kami harus memiliki metode API publik, yang disebut pada field(serializer) ketika kami memiliki semua hal tersedia. Seperti initialize dulu.

Apakah ini bekerja untuk DRF 2?

sayang sekali konteksnya tidak lagi disebarkan ke serializer bersarang.

sayang sekali konteksnya tidak lagi disebarkan ke serializer bersarang.

Di mana Anda mendapatkan itu? Afaik, konteks _is_ disebarkan ke serializer bersarang.

@xordoquy itu aneh - Saya telah membuat testcase terhadap drf terbaru dan saya tidak dapat mereproduksi masalah :) apalagi saya tidak dapat mereproduksinya dengan kode saya sendiri. Masalah terpecahkan.

@xiaohanyu sebenarnya saya menemukan masalah ini sekali lagi. Dan itu agak aneh:

>>> serializer
Out[12]: 
# Note there is context passed in constructor
CouponSerializer(<Coupon: Coupon Coupon #0 for offer Offer #0000>, context={'publisher': <User: John ZusPJRryIzYZ>}):
    id = IntegerField(label='ID', read_only=True)
    title = CharField(max_length=100, required=True)
    short_title = CharField(max_length=30)
    offer_details = OfferLimitedSerializer(read_only=True, source='offer'):
        id = IntegerField(label='ID', read_only=True)
        title = CharField(help_text='The Offer Title will be used for the Network, Advertisers and Publishers to identify the specific offer within the application.', read_only=True)       
        status_name = CharField(read_only=True, source='get_status_display')
        is_access_limited = SerializerMethodField()    
    exclusive = BooleanField(required=False)    
    categories_details = OfferCategorySerializer(many=True, read_only=True, source='categories'):
        id = IntegerField(read_only=True)
        category = CharField(read_only=True, source='code')
        category_name = CharField(read_only=True, source='get_code_display')    

# all fields except one got context propagated
>>> [f.field_name for f in serializer.fields.values() if not f.context]
Out[20]: 
['offer_details']

# offer_details is no different than categories_details, both are nested, read only, model serializers..
>>> serializer.fields['categories_details'].context
Out[21]: 
{'publisher': <User: John ZusPJRryIzYZ>}

# lets look inside problematic serializer
>>> offer_details = serializer.fields['offer_details']
>>> offer_details
Out[25]: 

OfferLimitedSerializer(read_only=True, source='offer'):
    id = IntegerField(label='ID', read_only=True)
    title = CharField(help_text='The Offer Title will be used for the Network, Advertisers and Publishers to identify the specific offer within the application.', read_only=True)    
    status_name = CharField(read_only=True, source='get_status_display')
    url = SerializerMethodField()
    is_access_limited = SerializerMethodField()
>>> offer_details.context
Out[23]: 
{}
>>> offer_details._context
Out[24]: 
{}
# ! surprisingly context is available inside fields... but not in the parent
>>> offer_details.fields['is_access_limited'].context
Out[26]: 
{'publisher': <User: John ZusPJRryIzYZ>}

# context is available in all of them!
>>> [x.field_name for x in offer_details.fields.values() if not x.context]
Out[27]: 
[]

@tomchristie maaf mengganggu, tapi mungkin Anda punya ide bagaimana itu mungkin?

Sementara itu saya terpaksa menggunakan peretasan ini untuk menyelesaikan masalah:

class Serializer(serializers.Serializer):

    def __init__(self, *args, **kwargs):
        super(Serializer, self).__init__(*args, **kwargs)
        # propagate context to nested complex serializers
        if self.context:
            for field in six.itervalues(self.fields):
                if not field.context:
                    delattr(field, 'context')
                    setattr(field, '_context', self.context)

@pySilver kami tidak dapat membantu banyak tanpa kasus uji sederhana. Anda menggunakan beberapa serializer berbeda yang mungkin memiliki beberapa kode yang ditentukan pengguna yang merusak propagasi konteks.

Ternyata itu terjadi karena kompleks mro , di mana beberapa kelas adalah anak-anak dari serializers.Serializer. Sesuatu seperti itu

class MyMixin(serializers.Serializer):
   # some code within __init__, calling parent too

class MyModelSerializer(serializers.ModelSerializer):
   # some code within __init__, calling parent too

class MyProblematicSerializer(MyModelSerializer, MyMixin)
    # this one will not receive context somehow.
     pass

anyway, itu saya pikir mungkin dianggap sebagai masalah lokal ..

Terima kasih atas tanggapannya 👍

@xordoquy @tomchristie sebenarnya masalah menjadi sangat aneh :) setiap kali Anda mengakses self.context di overriden serializer masalah dengan konteks yang hilang muncul. Saya sudah mencoba melakukannya di properti fields tanpa hasil. Berikut ini adalah testcase:

def test_serializer_context(self):
        class MyBaseSerializer(serializers.Serializer):
            def __init__(self, *args, **kwargs):
                super(MyBaseSerializer, self).__init__(*args, **kwargs)
                x = 0
                if self.context.get('x'):
                    x = self.context['x']

        class SubSerializer(MyBaseSerializer):
            char = serializers.CharField()
            integer = serializers.IntegerField()

        class ParentSerializer(MyBaseSerializer):
            with_context = serializers.CharField()
            without_context = SubSerializer()

        serializer = ParentSerializer(data={}, context={'what': 42})
        assert serializer.context == {'what': 42}
        assert serializer.fields['with_context'].context == {'what': 42}
        assert serializer.fields['without_context'].context == {'what': 42}

Namun yang satu ini akan berlalu. imho perlu dicatat dalam sumber bahwa mengakses konteks dalam metode yang diganti mungkin bermasalah ...

def test_serializer_context(self):
        class MyBaseSerializer(serializers.Serializer):
            <strong i="12">@property</strong>
            def fields(self):
                fields = super(MyBaseSerializer, self).fields
                x = 0
                if self.root.context.get('x'):
                    x = 1
                return fields


        class SubSerializer(MyBaseSerializer):
            char = serializers.CharField()
            integer = serializers.IntegerField()

        class ParentSerializer(MyBaseSerializer):
            with_context = serializers.CharField()
            without_context = SubSerializer()

        serializer = ParentSerializer(data={}, context={'what': 42})
        assert serializer.context == {'what': 42}
        assert serializer.fields['with_context'].context == {'what': 42}
        assert serializer.fields['without_context'].context == {'what': 42}

Untuk lebih jelasnya - apa yang saya lakukan di sini adalah mengubah perilaku bidang berdasarkan konteks.

UPD: bahkan kode terakhir tidak dapat diandalkan dalam beberapa kasus. ternyata menyentuh konteks itu berbahaya.

Sangat disayangkan bahwa __init__ tidak memiliki akses ke konteks. Jika seseorang ingin secara dinamis membuat bidang di serializer berdasarkan sesuatu dalam konteks, di mana Anda akan melakukannya?

Tampaknya tidak berfungsi di bind seperti yang disarankan di atas. Misalnya self.fields['asdf'] = serializers.CharField() . Saya berasumsi bahwa bidang telah dievaluasi ketika bind dipanggil.

Melakukannya di properti fields sepertinya juga tidak bagus, karena hal itu disebut banyak, dan karena evaluasi yang malas, ia memiliki mekanisme caching di self._fields yang harus diduplikasi dalam serializer beton. Akan lebih baik untuk dapat memodifikasi bidang sekali, sebelum di-cache secara internal.

Saya pikir ini tidak akan menjadi masalah jika nesting dilakukan sebagai sebuah field, daripada memanggil serializer secara langsung.

Seperti ini:

class ChildSerializer(serializers.Serializer):
    ...

class ParentSerializer(serializers.Serializer):
    child = serializers.NestedField(serializer=ChildSerializer)

Kemudian ChildSerializer dapat memiliki akses ke konteks di __init__ , karena tidak dipakai secara langsung dalam deklarasi bidang di ParentSerializer .

Jika seseorang ingin secara dinamis membuat bidang di serializer berdasarkan sesuatu dalam konteks, di mana Anda akan melakukannya?

Mari kita bicarakan ini dalam konteks contoh spesifik. Munculkan kasus penggunaan sederhana yang bagus dan kami dapat menemukan cara terbaik untuk mengatasinya.

Saya pikir ini tidak akan menjadi masalah jika nesting dilakukan sebagai sebuah field, daripada memanggil serializer secara langsung.

Cobalah, meskipun saya tidak berpikir kita mungkin akan pergi ke rute itu. Ini adalah terobosan besar dalam apa yang kami lakukan saat ini, ditambah itu tidak memungkinkan Anda untuk mengatur argumen pada serializer anak. Saya pikir ada kasus yang valid untuk gaya itu, tetapi saya tidak secara khusus melihat kami berubah pada saat ini.

@tomchristie terima kasih atas jawabannya.

Satu kasus penggunaan yang saya miliki saat ini adalah untuk beralih di antara dua set bidang yang berbeda dalam serializer anak bersarang tergantung pada properti pengguna yang saat ini masuk.

Pengguna Tipe A tidak boleh melihat bidang yang relevan dengan pengguna Tipe B, dan sebaliknya.

Ini terlihat seperti ini:

class ChildSerializer(serializers.Serializer):
  # relevant for all users
  name = serializers.CharField()

  # only relevant for users of Type A
  phone = serializers.CharField()

  # only relevant for users of Type B
  ssn = serializers.CharField()

Tetapi saya secara teratur memodifikasi bidang dalam berbagai cara, baik dalam bentuk api maupun html, menggunakan serializer drf dan/atau formulir Django. Menambahkan bidang secara dinamis, menghapus bidang, memodifikasi atribut, contoh di atas jauh dari satu-satunya cara. Saya tidak pernah melakukannya pada anak bersarang sebelumnya, dan benar-benar bingung dengan ini.

@tomchristie usecase khas saya adalah untuk mengontrol apakah bidang diperlukan atau tidak berdasarkan konteks (bayangkan serializer untuk pengguna dan admin, di mana salah satu dari mereka tidak perlu memberikan id pengguna karena diteruskan melalui konteks.) sekarang saya terpaksa melakukannya gunakan solusi dari posting saya sebelumnya di utas ini.

Saya kira satu solusinya adalah selalu membuat instance ChildSerializer yang disarangkan dalam metode __init__ dari ParentSerializer alih-alih dalam deklarasi bidang, maka seseorang dapat meneruskan konteksnya secara manual.

Seperti ini:

class ChildSerializer(serializers.Serializer):
    ...

class ParentSerializer(serializers.Serializer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['child'] = ChildSerializer(context=self.context)

Itu tampaknya berhasil dalam tes awal. Tidak yakin apakah ada tangkapan dengan ini.

Akan menyarankan hal yang sama, yup.

Yah itu tidak berhasil untuk saya untuk beberapa kasus. Mungkin karena pewarisan berganda.

Pada Rabu, 12 Oktober 2016 pukul 14:56 +0200, "Tom Christie" [email protected] menulis:

Akan menyarankan hal yang sama, yup.


Anda menerima ini karena Anda disebutkan.
Balas email ini secara langsung, lihat di GitHub, atau matikan utasnya.

Saya mengalami masalah yang sama, ketika saya menetapkan atribut default (dengan nilai CurrentUserDefault() dalam kasus saya) ke bidang serializer (bersarang) yang telah mensubklasifikasikan metode __init__ yang memproses self.context , KeyError muncul menunjukkan bahwa request tidak ditemukan dalam konteks (yang tidak berisi apa pun yaitu tidak diteruskan dari induknya).
Contoh kode:

class UserSerializer(serializers.ModelSerializer):
# ...
    def __init__(self, *args, **kwargs):
        kwargs.pop('fields', None)
        super().__init__(*args, **kwargs)
        if 'list' in self.context: # Once I remove this, it will work
            self.fields['friend_count'] = serializers.SerializerMethodField()
# ...

class CommentSerializer(serializers.ModelSerializer):
    person = UserSerializer(read_only=True, default=serializers.CurrentUserDefault())
# ...

Catatan eror:

Environment:


Request Method: POST
Request URL: http://127.0.0.1:8000/api/comments/98/

Django Version: 1.9.7
Python Version: 3.4.3
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django.contrib.sites',
 'rest_framework',
 'rest_framework.authtoken',
 'generic_relations',
 'decorator_include',
 'allauth',
 'allauth.account',
 'allauth.socialaccount',
 'allauth.socialaccount.providers.facebook',
 'allauth.socialaccount.providers.google',
 'phonenumber_field',
 'bootstrap3',
 'stronghold',
 'captcha',
 'django_settings_export',
 'main']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'stronghold.middleware.LoginRequiredMiddleware']



Traceback:

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/django/core/handlers/base.py" in get_response
  149.                     response = self.process_exception_by_middleware(e, request)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/django/core/handlers/base.py" in get_response
  147.                     response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/django/views/decorators/csrf.py" in wrapped_view
  58.         return view_func(*args, **kwargs)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/django/views/generic/base.py" in view
  68.             return self.dispatch(request, *args, **kwargs)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/views.py" in dispatch
  466.             response = self.handle_exception(exc)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/views.py" in dispatch
  463.             response = handler(request, *args, **kwargs)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/generics.py" in post
  246.         return self.create(request, *args, **kwargs)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/mixins.py" in create
  20.         serializer.is_valid(raise_exception=True)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/serializers.py" in is_valid
  213.                 self._validated_data = self.run_validation(self.initial_data)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/serializers.py" in run_validation
  407.         value = self.to_internal_value(data)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/serializers.py" in to_internal_value
  437.                 validated_value = field.run_validation(primitive_value)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/serializers.py" in run_validation
  403.         (is_empty_value, data) = self.validate_empty_values(data)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/fields.py" in validate_empty_values
  453.             return (True, self.get_default())

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/fields.py" in get_default
  437.                 self.default.set_context(self)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/fields.py" in set_context
  239.         self.user = serializer_field.context['request'].user

Exception Type: KeyError at /api/comments/98/
Exception Value: 'request'

Bidang yang ditambahkan tidak memiliki beberapa hal yang dilakukan serializer melalui metaclass. Terserah Anda untuk mengaturnya.

Seperti yang sudah saya katakan di atas - setelah saya menghapus pengambilan konteks dalam metode __init__ , semuanya berfungsi dengan baik ... Jadi pengaturan bidang tentu saja bukan masalah, karena konteksnya akan diteruskan dari induk dalam hal itu kasus.

Benar, ada di ponsel saya, tidak memperhatikan bagian itu.
Maka itu normal karena serializer bersarang dipakai jauh sebelum serializer teratas.
Maksud konteks memang untuk menyampaikannya jauh setelah instantiasi terjadi. Jadi tidak heran itu tidak berfungsi di init.

Baik. Untuk catatan terakhir, saya hanya ingin menunjukkan bahwa seperti yang ditunjukkan oleh log kesalahan, itu tidak muncul dalam metode __init__ , tetapi dalam objek CurrentUserDefault() (dalam set_context metode). Tapi yakin bahwa itu juga akan muncul di __init__ jika ada panggilan langsung ke parameter konteks, yaitu tanpa memeriksa keberadaannya (karena seluruh konteks kosong, karena tidak dilewati).
Juga, saya ingin mengulangi apa yang disimpulkan di atas oleh orang lain - dalam kasus ketika tidak ada sarang (dan ketika konteks digunakan dalam metode instantiasi), semuanya berfungsi seperti yang diharapkan.

Ada masalah dalam menyebarkan konteks ke anak-anak, itu harus diselesaikan di https://github.com/encode/Django-rest-framework/pull/5304 Saya kira seharusnya ada banyak masalah yang terkait dengan caching None sebagai root dari serializer anak yang harus diselesaikan dengan PR ini.

Saya mengalami masalah ini baru-baru ini. Ini adalah solusi yang berhasil untuk saya:

class MyBaseSerializer(serializers.HyperlinkedModelSerializer):

    def get_fields(self):
        '''
        Override get_fields() method to pass context to other serializers of this base class.

        If the context contains query param "omit_data" as set to true, omit the "data" field
        '''
        fields = super().get_fields()

        # Cause fields with this same base class to inherit self._context
        for field_name in fields:
            if isinstance(fields[field_name], serializers.ListSerializer):
                if isinstance(fields[field_name].child, MyBaseSerializer):
                    fields[field_name].child._context = self._context

            elif isinstance(fields[field_name], MyBaseSerializer):
                fields[field_name]._context = self._context

        # Check for "omit_data" in the query params and remove data field if true
        if 'request' in self._context:
            omit_data = self._context['request'].query_params.get('omit_data', False)

            if omit_data and omit_data.lower() in ['true', '1']:
                fields.pop('data')

        return fields

Di atas, saya membuat kelas dasar serializer yang menimpa get_fields() dan meneruskan self._context ke serializer anak mana pun yang memiliki kelas dasar yang sama. Untuk ListSerializers, saya melampirkan konteks ke turunannya.

Kemudian, saya memeriksa parameter kueri "omit_data" dan menghapus bidang "data" jika diminta.

Saya harap ini bermanfaat bagi siapa saja yang masih mencari jawaban untuk ini.

Saya tahu ini adalah utas lama, tetapi saya hanya ingin berbagi bahwa saya menggunakan kombinasi __init__ dan mengikat mixin khusus saya untuk memfilter kumpulan kueri berdasarkan permintaan (dalam konteks). Mixin ini mengasumsikan metode get_objects_for_user ada di queryset, atau menggunakan guardian.shortcuts.get_objects_for_user sebaliknya. Ini bekerja sangat bagus untuk serializer bersarang, juga saat POSTing atau PATCHing.

Apakah halaman ini membantu?
0 / 5 - 0 peringkat