Django-rest-framework: Вложенный сериализатор не получает контекст представления

Созданный на 13 февр. 2015  ·  26Комментарии  ·  Источник: encode/django-rest-framework

_Это не вопрос о переполнении стека, IMO, так что извините, если это так_

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

Идентификатор проекта предоставляется в QUERY_PARAMS и применяется в perform_create , как и должно быть сейчас, в ветке v3. Но мне нужен проект в каком-то вложенном поле сериализатора ПЕРЕД сохранением - я хочу предоставить значения по умолчанию для объекта в метаданных сериализатора, чтобы пользователь мог изменить некоторые вещи, которые при желании можно взять из объекта проекта.

Итак, я заполняю context в get_serializer_context объектом проекта. И я должен просто переписать __init__ во вложенном сериализаторе, чтобы установить значения по умолчанию, да?

Проблема в том, что вложенный сериализатор не получает ни context , ни parent в __init__ . Кажется, он вообще не получает контекста! Таким образом, это должен быть регресс с момента запроса на включение 497 .

Я думаю, это не регрессия, а скорее изменение дизайна, но мне действительно нужен контекст, чтобы иметь возможность изменить поведение serizliser!

Самый полезный комментарий

Я предполагаю, что одним из обходных путей может быть всегда создание экземпляра вложенного ChildSerializer в методе __init__ ParentSerializer вместо объявления поля, тогда можно перенаправить контекст вручную.

Так:

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 = ...

В этот момент вы _do_ имеете доступ к контексту.

Если вы действительно хотите, вы можете выполнить некоторые действия в тот момент, когда поле становится привязанным к своему родителю...

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, контекст _is_ распространяется на вложенные сериализаторы.

@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 мы мало чем можем помочь без простого тестового примера. Вы используете несколько разных сериализаторов, которые могут иметь определенный пользователем код, который нарушает распространение контекста.

Оказывается, это происходит из-за сложного 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}

Однако этот пройдет. imho следует отметить в источниках, что доступ к контексту в переопределенных методах может быть проблематичным...

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 может иметь доступ к контексту в __init__ , поскольку он не создается непосредственно в объявлении поля в ParentSerializer .

Если кто-то хочет динамически создавать поля в сериализаторе на основе чего-то в контексте, где бы вы это сделали?

Поговорим об этом в контексте конкретного примера. Придумайте хороший простой вариант использования, и мы сможем найти лучший способ справиться с ним.

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

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

@tomchristie спасибо за ответ.

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

Пользователи типа А никогда не должны видеть поля, относящиеся к пользователям типа Б, и наоборот.

Это выглядит так:

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

Но я регулярно изменяю поля разными способами, как в API, так и в HTML-формах, используя сериализаторы drf и/или формы django. Динамическое добавление полей, удаление полей, изменение атрибутов, приведенный выше пример далеко не единственный способ. Мне просто никогда раньше не приходилось делать это во вложенном дочернем элементе, и я был полностью озадачен этим.

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

Я предполагаю, что одним из обходных путей может быть всегда создание экземпляра вложенного ChildSerializer в методе __init__ ParentSerializer вместо объявления поля, тогда можно перенаправить контекст вручную.

Так:

class ChildSerializer(serializers.Serializer):
    ...

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

Кажется, это работает в начальном тесте. Не уверен, что в этом есть какой-то подвох.

То же самое собирался предложить, ага.

Ну, это не сработало для меня в некоторых случаях. Вероятно, из-за множественного наследования.

В среду, 12 октября 2016 г., в 14:56 +0200, «Том Кристи» [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 или PATCHing.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги