Django-rest-framework: ネストされたシリアライザーはビューコンテキストを受け取りません

作成日 2015年02月13日  ·  26コメント  ·  ソース: encode/django-rest-framework

_スタックオーバーフローの質問IMOではないので、そうである場合はすみません_

ネストされたシリアライザーを使用しようとしていますが、v2ブランチで非常に多くの問題が発生したため、説明するのが難しいことはわかっています。 ネストされたシリアライザーフィールドで、選択したプロジェクトに基づいたデフォルトを使用したい。

プロジェクトIDはQUERY_PARAMSで提供され、v3-branchでは現在のようにperform_createに適用されます。 ただし、保存する前に、ネストされたシリアライザーフィールドにプロジェクトが必要です-シリアライザーメタデータでオブジェクトのデフォルト値を提供したいので、ユーザーはいくつかの変更を行うことができます。これは、オプションでプロジェクトオブジェクトから取得できます。

そこで、 get_serializer_contextcontext $にプロジェクトオブジェクトを入力します。 そして、ネストされたシリアライザーで__init__を書き直して、デフォルトを設定する必要がありますね。

問題は、ネストされたシリアライザーが$# __init__ $でcontextparent $も受け取らないことです。 どうやら、コンテキストをまったく受け取らないようです! したがって、 pull- request497以降のリグレッションになるはずです。

リグレッションではなく、デザインの変更に似ていると思いますが、serizliserの動作を変更できるようにするには、コンテキストが本当に必要です。

最も参考になるコメント

回避策の1つは、フィールド宣言ではなく、 ParentSerializer__init__メソッドでネストされたChildSerializerを常にインスタンス化することで、コンテキストを手動で転送できると思います。

このような:

class ChildSerializer(serializers.Serializer):
    ...

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

それは最初のテストでうまくいくようです。 これに問題があるかどうかはわかりません。

全てのコメント26件

コンテキストは、初期化の時点でフィールド(またはネストされたシリアライザー)に渡されません(できません)。これは、フィールドが親シリアライザーで宣言されたときに初期化されるためです。

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

MySerializer.__init__でそのような動作を実行するのが最善です。たとえば、...

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

その時点で、コンテキストにアクセスできます。

本当に必要な場合は、フィールドがその親にバインドされるポイントでいくつかのアクションを実行できます...

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

これはプライベートAPIと見なす必要があり、上記の親__init__スタイルを優先する必要があります。

ええ、私はあなたが説明することを理解しています。 だから少しトリッキーだと思います。 そのため、私の回避策は、親の__ini__にネストされたシリアライザーを作成することでした。 ただし、ネストされたシリアライザー__init__は、ディープコピーを使用するため、新しいシリアライザーインスタンスが作成されるたびに呼び出されますが、親インスタンスをその中にこっそり入れることはできません...
したがって、v2でわかるように、フィールドにはinitializeメソッドがあり、親シリアライザーに基づいてデフォルトを初期化するのに最適でした。

したがって、 bindは良いはずです。

これはプライベートAPIと見なす必要があり、上記の親__init__スタイルを優先する必要があります。

しかし、 __init__を使用できないため、現時点ではビューによって作成された利用可能なすべての環境にアクセスできないため、すべてのものが揃ったときにフィールド(シリアライザー)で呼び出されるパブリックAPIメソッドが必要です。利用可能。 initializeのように。

DRF 2で機能しますか?

残念なコンテキストは、ネストされたシリアライザーに伝播されなくなりました。

残念なコンテキストは、ネストされたシリアライザーに伝播されなくなりました。

どこでそれを手に入れたのですか ? Afaik、コンテキストはネストされたシリアライザーに伝播されます。

@xordoquyそれは奇妙です–私は最新のdrfに対してテストケースを作成しましたが、問題を再現できません:)さらに、自分のコードに対して再現することはできません。 問題が解決しました。

@xiaohanyu実際に私はこの問題をもう一度見つけました。 そしてそれはちょっと奇妙です:

>>> 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ご迷惑をおかけして申し訳ありませんが、それがどのように可能であるかについて何かアイデアがあるかもしれません。

その間、私は問題を解決するためにこのハックを使用することを余儀なくされています:

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単純なテストケースなしでは大したことはできません。 コンテキストの伝播を中断するユーザー定義のコードが含まれている可能性のある、いくつかの異なるシリアライザーを使用しています。

複数のクラスがserializers.Serializerの子である複雑なmroが原因で発生することが判明しました。 そんな感じ

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

とにかく、それはローカルな問題と見なされるかもしれないと思います。

フィードバックをありがとう👍

@xordoquy @tomchristieは、実際には問題が非常に奇妙になりました:)オーバーライドされたシリアライザーでself.contextにアクセスするたびに、コンテキストが欠落している問題が発生します。 私は運が悪かったfieldsプロパティでそれをやろうとしました。 テストケースは次のとおりです。

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}

しかし、これは合格するでしょう。 オーバーライドされたメソッドでコンテキストにアクセスすると問題が発生する可能性があることに注意してください...

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}

明確にするために、ここで行っているのは、コンテキストに基づいてフィールドの動作を変更することです。

UPD:最後のコードでさえ信頼できない場合があります。 コンテキストに触れることは危険であることが判明しました。

__init__がコンテキストにアクセスできないのは残念です。 コンテキスト内の何かに基づいてシリアライザーにフィールドを動的に作成したい場合、どこでそれを行いますか?

上で提案したように、 bindでは機能しないようです。 たとえば、 self.fields['asdf'] = serializers.CharField()bindが呼び出されたときに、フィールドはすでに評価されていると思います。

fieldsプロパティでそれを行うことも、それが頻繁に呼び出されるため、素晴らしいとは思えません。また、遅延評価のために、 self._fieldsにキャッシュメカニズムがあり、複製する必要があります。コンクリートシリアライザーで。 フィールドが内部にキャッシュされる前に、フィールドを一度変更できる方がよいでしょう。

シリアライザーを直接呼び出すのではなく、フィールドとしてネストが行われていれば、これは問題にならないだろうと思います。

このような:

class ChildSerializer(serializers.Serializer):
    ...

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

次に、 ChildSerializerは、 ParentSerializerのフィールド宣言で直接インスタンス化されていないため、 __init__のコンテキストにアクセスできます。

コンテキスト内の何かに基づいてシリアライザーにフィールドを動的に作成したい場合、どこでそれを行いますか?

特定の例の文脈でこれについて話しましょう。 素晴らしくシンプルなユースケースを考え出すと、それに取り組むための最良の方法を見つけることができます。

シリアライザーを直接呼び出すのではなく、フィールドとしてネストが行われていれば、これは問題にならないだろうと思います。

試してみてください。ただし、そのルートに行く可能性は低いと思います。 これは、現在行っていることの大きなブレークであり、さらに、子シリアライザーに引数を設定することはできません。 そのスタイルには有効なケースがあると思いますが、現時点では特に変化は見られません。

@tomchristie返信ありがとうございます。

私が今持っているユースケースの1つは、現在ログインしているユーザーのプロパティに応じて、ネストされた子シリアライザーの2つの異なるフィールドセットを切り替えることです。

タイプAのユーザーには、タイプBのユーザーに関連するフィールドが表示されないようにする必要があります。その逆も同様です。

次のようになります。

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()

しかし、私は定期的に、drfシリアライザーやdjangoフォームを使用して、APIフォームとhtmlフォームの両方でさまざまな方法でフィールドを変更しています。 フィールドの動的な追加、フィールドの削除、属性の変更など、上記の例は唯一の方法ではありません。 私は以前、ネストされた子でそれを行う必要がなかっただけで、これに完全に困惑しました。

@tomchristie私の典型的なユースケースは、コンテキストに基づいてフィールドが必要かどうかを制御することです(ユーザーと管理者のシリアライザーを想像してください。コンテキストを介して渡されるため、ユーザーIDを提供する必要はありません)。このスレッドの以前の投稿の解決策を使用してください。

回避策の1つは、フィールド宣言ではなく、 ParentSerializer__init__メソッドでネストされたChildSerializerを常にインスタンス化することで、コンテキストを手動で転送できると思います。

このような:

class ChildSerializer(serializers.Serializer):
    ...

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

それは最初のテストでうまくいくようです。 これに問題があるかどうかはわかりません。

同じことを提案するつもりだった、うん。

場合によってはうまくいきませんでした。 おそらく多重継承のためです。

2016年10月12日水曜日午後2時56分+0200に、「TomChristie」 [email protected]は次のように書いています。

同じことを提案するつもりだった、うん。


あなたが言及されたので、あなたはこれを受け取っています。
このメールに直接返信するか、GitHubで表示するか、スレッドをミュートしてください。

default属性(私の場合はCurrentUserDefault()値)を処理する__init__メソッドをサブクラス化した(ネストされた)シリアライザーフィールドに設定すると、同じ問題が発生しますself.context属性の場合、 KeyErrorが表示され、 requestがコンテキストで見つからないことを示します(これには何も含まれていません。つまり、親から渡されません)。
コード例:

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())
# ...

エラーログ:

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'

追加されたフィールドには、シリアライザーがメタクラスを介して実行するいくつかの機能がありません。 それらを設定するのはあなた次第です。

すでに上で述べたように、 __init__メソッドでコンテキストの取得を削除すると、すべてが正常に機能します...したがって、コンテキストは親から渡されるため、フィールド設定は確かに問題ではありません。場合。

そうです、私の電話にいて、その部分に気づいていませんでした。
次に、ネストされたシリアライザーが最上位のシリアライザーよりもはるかに前にインスタンス化されるため、これは正常です。
コンテキストの目的は、インスタンス化が発生した後、実際にそれを渡すことです。 したがって、initで機能しないのも不思議ではありません。

わかった。 最後の注意点として、エラーログが示すように、 __init__メソッドではなく、 CurrentUserDefault()オブジェクト( set_context )に表示されることを指摘しておきます。方法)。 ただし、コンテキストパラメータが直接呼び出された場合、つまりその存在を確認せずに、 __init__にも表示されることを確認してください(コンテキスト全体が空白であるため、渡されないため)。
また、他の誰かが上記で結論したことを繰り返したいと思います-ネストがない場合(およびインスタンス化メソッドでコンテキストが使用されている場合)、すべてが期待どおりに機能します。

コンテキストを子に伝達する際に問題がありました。https://github.com/encode/django-rest-framework/pull/5304で解決する必要がありますNoneのキャッシュに関連する多くの問題があったはずです。このPRで解決する必要がある子シリアライザーのルートとしての

私は最近この問題に遭遇しました。 これは私のために働いた解決策です:

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

上記では、 get_fields()をオーバーライドし、同じ基本クラスを持つすべての子シリアライザーにself._contextを渡すシリアライザー基本クラスを作成します。 ListSerializersの場合、コンテキストをその子にアタッチします。

次に、クエリパラメータ「omit_data」を確認し、要求された場合は「data」フィールドを削除します。

これがまだこれに対する答えを探している人にとって役立つことを願っています。

これが古いスレッドであることは知っていますが、__ init__の組み合わせを使用し、カスタムミックスインにバインドして、リクエストに基づいてクエリセットをフィルタリングすることを共有したいと思います(コンテキスト内)。 このミックスインは、get_objects_for_userメソッドがクエリセットに存在することを前提としています。そうでない場合は、guardian.shortcuts.get_objects_for_userを使用します。 ネストされたシリアライザーの場合、POSTまたはPATCHの場合にも非常にうまく機能します。

このページは役に立ちましたか?
0 / 5 - 0 評価