1

I want to add the request context to my serializer in the Django REST framework. In particular to a nested serializer, i (successfully) tried to do that with a SerializerMethodField ( as my solution per: context in nested serializers django rest framework ). This is the setup i use:

class VehicleTypeSerializer(RsModelSerializer):

    class Meta:
        model = VehicleType


class VehicleSerializer(RsModelSerializer):

    vehicletype = SerializerMethodField()

    class Meta:
        model = Vehicle
        fields = ('vehiclename', 'vehicledescription', 'vehicletype')

    def get_vehicletype(self, obj):
        return self.get_serializermethodfield_data(obj, VehicleType, VehicleTypeSerializer, 'vehicle')


    def get_serializermethodfield_data(self, obj, model_class, serializer_class, filter_field):
        filter = {filter_field: obj}
        objs = model_class.objects.all().filter(**filter)

        # We need the request-context for checking field permissions in the serializer
        s = serializer_class(objs, many=True, context={'request': self.context.get('request')})
        return s.data   

Problem : I need a SerializerMethodField to pass the request-context to the nested-serializer (VehicleTypeSerializer) But now i am stuck dealing with POST's since the SerializerMethodField is read-only. I can't POST an object to /api/v1/vehicle with:

{
    "vehiclename": "test",
    "vehicledescription": "test"
    "vehicletype": "1" <---- get's ignored since SerializerMethodField is read-only
}

Question : Can someone point me in the right direction to add the request-context (especially the user information) to a nested serializer which i can write to?

I need the request context (request.user) in the VehicleSerializer as well as in the VechileTypeSerializer, because in the RsModelSerializer that i have defined, i check on a per-field-basis if the user that is doing the request has permission to read or update a field.

In the RsModelSerializer:

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

    # Make sure that there is a user mapped in the context (we need a user
    # for checking permissions on a field). If there is no user, we set
    # the user to None.
    if not self.context:
        self._context = getattr(self.Meta, 'context', {})
    try:
        self.user = self.context['request'].user
    except (KeyError, AttributeError):
        print('No request')
        self.user = None


def get_fields(self):
    """
    Override get_fields to ensure only fields that are allowed
    by model-field-permissions are returned to the serializer
    :return: Dict with allowed fields
    """

    ret = OrderedDict()
    fields = super().get_fields()

    # If no user is associated with the serializer, return no fields
    if self.user == None:
        return None

    # A superuser bypasses the permissions-check and gets all
    # available fields
    if self.user.is_superuser:
        print_without_test("user is superuser, bypassing permissions")
        return fields

    # Walk through all available fields and check if a user has permission for
    # it. If he does, add them to a return-array. This way all fields that
    # are not allowed to 'read' will be dropped. Note: this is only used
    # for read access. Write access is handled in the views (modelviewsets).
    for f in fields:
        if has_permission(user=self.user, app_label=self.Meta.model._meta.app_label,
                          table=self.Meta.model.__name__.lower(),
                          field=f,
                          permission='read'):
            ret[f] = fields[f]

    return ret
Community
  • 1
  • 1
Robin van Leeuwen
  • 2,625
  • 3
  • 24
  • 35

2 Answers2

2

If you need to pass a context to Serializer class. You can use Serializer's context

And you will be able to use it in a SerializerMethodField

class MySerializer(serializer.Serializer)

    field = serializer.SerializerMethodField()

    def get_field(self, obj):
        return self.context.get('my_key')

You call it from view:

...
s = MySerializer(data=data, context={'my_key': 'my_value'})
...

EDIT:

If you need use this context in another Serializer class, pass to the first serializer in the pass to the nexted serializer:

# views.py
...
s = MySerializer(data=data, context={'my_key': 'my_value'})
...

# serializers.py

class MySerializer(serializer.Serializer):
    field = serializer.SerializerMethodField()

    def get_field(self, obj):
        return MySecondSerializer(..., context=self.context)
Gocht
  • 9,924
  • 3
  • 42
  • 81
  • 1
    This was indeed the solution i had figured out a while ago (see link), but the SerializerMethodField doesn't allow POST requests, it is readonly. How can i write to a serializer that has nested fields *and* be able to pass the reuqest-context to it? – Robin van Leeuwen Sep 23 '15 at 17:52
  • I guess you need request.data, which is a dictionary, you can pass it via context attr in Serializer call. Or please, explain a little bit more. – Gocht Sep 23 '15 at 17:54
  • I updated my question to describe the problems i have with some examples and code – Robin van Leeuwen Sep 23 '15 at 22:32
  • You call a serializer in a SerializerMethodField and you need pass a context to that serializer? – Gocht Sep 23 '15 at 22:36
  • Yes, i need to pass a context value in the serializer ( context={'request': self.context.get('request')}) which adds the request from the ModelViewSet ) so that i can drop fields in the serializer when request.user doesn't have permission on a specific field. This works great. Except only for GET's since SerializerMethodFields are read-only. When i POST a value, it drops the data i put in the SerializerMethodField. So what equalivent field-type to SerializerMethodField must i use to be able to a) pass context in it , and b) use it to write data to with POST's requests from my API?? – Robin van Leeuwen Sep 23 '15 at 22:44
2

Method-1: Overriding the __init__() method of parent serializer

You can add the context to nested/child serializer in the __init__() method of parent serializer.

class RsModelSerializer(serializers.ModelSerializer):

    def __init__(self, *args, **kwargs):
        super(RsModelSerializer, self).__init__(*args, **kwargs)
        request_obj = self.context.get('request') # get the request from parent serializer's context
        # assign request object to nested serializer context
        self.fields['nested_serializer_field'].context['request'] = request_obj 

We cannot pass the context to nested serializer at the time of their __init__() because they get initialized at the time of declaration in the parent serializer.

class SomeParentSerializer(serializers.Serializer):

    some_child = SomeChildSerializer()  # gets initialized here

Method-2: Passing context when child serializer gets binded to its parent

Another option is to add the context when a child/nested serializer gets binded to the parent.

class SomeChildSerializer(Serializer):

    def bind(self, field_name, parent):
        super(SomeChildSerializer, self).bind(field_name, parent) # child gets binded to parent
        request_obj = parent.context.get('request') # get the request from parent serializer context
        self.context['request'] = request_obj

Quoting the DRF author's suggested option in the related ticket:

This should be considered private API, and the parent __init__ style listed above should be preferred.

So, the better option is to override the __init__() method of ParentSerializer and pass the context to child/nested serializer.

(Source: check this related ticket on Github.)

Rahul Gupta
  • 46,769
  • 10
  • 112
  • 126