Pass request context to serializer from Viewset in Django Rest Framework - django

I have a case where the values for a serializer field depend on the identity of the currently logged in user. I have seen how to add the user to the context when initializing a serializer, but I am not sure how to do this when using a ViewSet, as you only supply the serializer class and not the actual serializer instance.
Basically I would like to know how to go from:
class myModelViewSet(ModelViewSet):
queryset = myModel.objects.all()
permission_classes = [DjangoModelPermissions]
serializer_class = myModelSerializer
to:
class myModelSerializer(serializers.ModelSerializer):
uploaded_by = serializers.SerializerMethodField()
special_field = serializers.SerializerMethodField()
class Meta:
model = myModel
def get_special_field(self, obj):
if self.context['request'].user.has_perm('something.add_something'):
return something
Sorry if it wasn't clear, from the DOCs:
Adding Extra Context
Which says to do
serializer = AccountSerializer(account, context={'request': request})
serializer.data
But I am not sure how to do that automatically from the viewset, as I only can change the serializer class, and not the serializer instance itself.

GenericViewSet has the get_serializer_context method which will let you update context:
class MyModelViewSet(ModelViewSet):
queryset = MyModel.objects.all()
permission_classes = [DjangoModelPermissions]
serializer_class = MyModelSerializer
def get_serializer_context(self):
context = super().get_serializer_context()
context.update({"request": self.request})
return context
For Python 2.7, use context = super(MyModelViewSet, self).get_serializer_context()

For Function based views you can pass request or user as follows:
serializer = ProductSerializer(context = {"request": request}, data=request.data)
Your Serializer may look like:
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ["id"]
def create(self, validated_data):
user = self.context["request"].user
print(f"User is: {user}")
Feel free to inform if there is any better way to do this.

just use get_serializer() in your viewsets
def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
return serializer_class(*args, **kwargs)

Return parent context in overrided function get_serializer_context will make it easy to access request and its data.
class myModelViewSet(ModelViewSet):
queryset = myModel.objects.all()
permission_classes = [DjangoModelPermissions]
serializer_class = myModelSerializer
def get_serializer_context(self):
"""
pass request attribute to serializer
"""
context = super(myModelViewSet, self).get_serializer_context()
return context
This is very stable as every time we request viewset, it returns context as well.

the values for a serializer field depend on the identity of the currently logged in user
This is how I handle such cases in my ModelViewSet:
def perform_create(self, serializer):
user = self.request.user
if user.username == 'myuser':
serializer.data['myfield'] = 'something'
serializer.save()

Simply add this 2 line method in your class and you are good to go.
def get_serializer_context(self):
return {'request': self.request}

since the posted answers had partial correctness, summarizing here in the interest of completeness.
override get_serializer_context..AND
use get_serializer in your views instead of manually calling the serializer

Related

How to get lookup_url_kwarg in serializer of django rest framework

this my Django view
class CreateForeignTableView(CreateAPIView):
"""
create foreign_table finally not difference a normal table ??
"""
serializer_class = CreateForiegnTableSerializer
queryset = None
lookup_url_kwarg = 'foreign_server_id'
I want get lookup_url_kwarg in my create serializer function
Simple you can override create method to achieve this.
def create(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data, context={
'request': request,
'id': self.kwargs.get(self.lookup_url_kwarg)})
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
In the serializer, you can use id = self.context['id']
You can override get_serializer_context method to achieve this.
class CreateForeignTableView(CreateAPIView):
"""
create foreign_table finally not difference a normal table ??
"""
serializer_class = CreateForiegnTableSerializer
queryset = None
lookup_url_kwarg = 'foreign_server_id'
def get_serializer_context(self):
context = super(CreateForeignTableView, self).get_serializer_context()
context.update({
"foreign_server_id": self.kwargs.get(self.lookup_url_kwarg)
})
return context
In the serializer you can use self.context.get("foreign_server_id", "") to get foreign_server_id.
You can follow this post to know farther.
You only need to access the serializer context. GenericApiView sets the view itself into the serializer context, so you may access lookup_url_kwarg like this:
def create(self, validated_data):
my_url_kwarg = self.context['view'].lookup_url_kwarg

Django rest framework - self.context doesn't have request attribute

class MyModelSerializer(serializers.ModelSerializer):
field1 = serializers.CharField()
field2 = serializers.SerializerMethodField('get_awesome_user')
def get_current_user(self):
request = self.context.get("request")
if request and hasattr(request, "user"):
return request.user
return None
def get_awesome_user(self, obj):
user = self.get_current_user()
## use this user object, do some stuff and return the value
return ...
My api(which uses authentication_classes and permission_classes) is using this serializer and the get_current_user function always returns None. when I debug it, I found that self.context is empty dictionary, i.e {}. to be double sure I also printed self.context.keys(), still it's empty list.
I followed this thread.
Get current user in Model Serializer
PS: I'm using djangorestframework==3.3.3, Django==1.9.1
EDIT: adding viewset code
class MyModelViewSet(viewsets.ModelViewSet):
authentication_classes = (SessionAuthentication, BasicAuthentication, TokenAuthentication)
permission_classes = (IsAuthenticated,)
def list(self, *args, **kwargs):
queryset = MyModel.objects.all()
page = self.paginate_queryset(queryset)
if page is not None:
serializer = MyModelSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = MyModelSerializer(queryset, many=True)
return Response(serializer.data)
How do you create serializer in your viewset's list() method? You should call
serializer = self.get_serializer(data=request.data)
to get your serializer context filled automatically as it is done in default implementation of this method in DRF mixins., but I have a feeling that you're just creating it manually, like this:
serializer = MyModelSerializer(instance)
So, to fix this, you should either call get_serializer(), or pass extra context argument to serializer constructor:
serializer = MyModelSerializer(instance, context={'request': request, ...})

Receiving a KeyError for 'request' in DRF serializer when using detail_route in my ViewSet

This is my ViewSet:
class PostViewSet(viewsets.ModelViewSet):
serializer_class = PostSerializer
permission_classes = (IsAuthenticated, IsOwnerDeleteOrReadOnly)
def get_queryset(self):
return Post.objects.filter(location=self.request.user.userextended.location)
def perform_create(self, serializer):
serializer.save(owner=self.request.user, location=self.request.user.userextended.location)
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'format': self.format_kwarg,
'view': self,
'location': self.request.user.userextended.location
}
#detail_route(methods=['post'], permission_classes=[IsAuthenticated, IsFromLocation])
def like(self, request, pk=None):
post = self.get_object()
post.usersVoted.add(request.user)
return Response(status=status.HTTP_204_NO_CONTENT)
#detail_route(methods=['get'], permission_classes=[IsAuthenticated, ValidPostPkInKwargs, IsFromPostLocation])
def replies(self, request, pk=None):
post = self.get_object()
replies = post.postreply_set.all()
serializer = PostReplySerializer(replies, many=True)
return Response(serializer.data)
And this is my PostReplySerializer:
class PostReplySerializer(serializers.ModelSerializer):
owner = serializers.SlugRelatedField(slug_field='username', read_only=True)
voted = serializers.SerializerMethodField()
def get_voted(self, obj):
return self.context['request'].user in obj.usersVoted.all()
class Meta:
model = PostReply
fields = ('id', 'owner', 'post', 'voted', 'location')
The error points to the line
return self.context['request'].user in obj.usersVoted.all()
and says:
KeyError at /URL/20/replies/
'request'
Any idea why DRF says 'request' is a key error even though (from my understanding) it should be automatically in self.context?
Note that PostViewSet works perfectly fine for all other requests (if I get a Post, get a list of posts etc.). It just doesn't work for replies.
It's not in self.context because you have overridden get_serializer_context. request object is attached to context via this method. Just add request: self.request in your return statement of get_serializer_context that would solve the problem. Take a look at default implementation of get_serializer_context here https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/generics.py to understand more. Hope it helps..
EDIT
Since you are using different serializer(PostReplySerializer) in detail_route, you need to create serializer instance like serializer = PostReplySerializer(replies, many=True, context={'request': self.request})

Django Rest Framework update field

I am new to DRF and I am trying to write custom view / serializer that I can use to update just one field of user object.
I need to make logic just to update the "name" of the user.
I wrote serializer:
class ClientNameSerializer(serializers.ModelSerializer):
class Meta:
model = ClientUser
fields = ('name',)
def update(self, instance, validated_data):
instance.name = validated_data.get('name', instance.name)
instance.save()
return instance
This method is never called. I tried setting breakpoint there and debug it, but it is never called, even if I use PUT, POST or PATCH methods. If I add create method it is being called when I use POST.
This is how my view looks like:
class UpdateName(generics.CreateAPIView):
queryset = ClientUser.objects.all()
serializer_class = ClientNameSerializer
permission_classes = (permissions.IsAuthenticated,)
Does anyone have some suggestion? Thanks!
My models.py looks like this
class ClientUser(models.Model):
owner = models.OneToOneField(User,unique=True,primary_key=True)
phone_number = models.CharField(validators=[PHONE_REGEX],max_length=20,unique=True)
name = models.CharField(max_length=100,blank=True)
status = models.IntegerField(default=1)
member_from = models.DateTimeField('member from',auto_now_add=True)
is_member = models.BooleanField(default=False)
The definition of what methods the endpoint can accept are done in the view, not in the serializer.
The update method you have under your serializer needs to be moved into your view so you'll have something like:
serializers.py
class ClientNameSerializer(serializers.ModelSerializer):
class Meta:
model = ClientUser
views.py
class UpdateName(generics.UpdateAPIView):
queryset = ClientUser.objects.all()
serializer_class = ClientNameSerializer
permission_classes = (permissions.IsAuthenticated,)
def update(self, request, *args, **kwargs):
instance = self.get_object()
instance.name = request.data.get("name")
instance.save()
serializer = self.get_serializer(instance)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data)
Take note that you're overriding the UpdateModelMixin and you might need to change the above code a little bit to get it right.
If you use class UpdateName(generics.CreateAPIView), this will only call a create() method on the serializer.
You should subclass generics.UpdateAPIView instead. And that's it.
You do not have to move your method to the view as suggested in this answer (it is basically copying/duplicating the UpdateModelMixin's update method)
For more information how serializers work regarding saving/updating see the docs here:
One other approach might be the following one:
serializer.py
class ClientNameSerializer(serializers.ModelSerializer):
class Meta:
model = ClientUser
fields = ('name',)
def update(self, instance, validated_data):
instance.name = validated_data.get('name', instance.name)
instance.save()
return instance
views.py
class UpdateName(generics.UpdateAPIView):
queryset = ClientUser.objects.all()
serializer_class = ClientNameSerializer
permission_classes = (permissions.IsAuthenticated,)
def update(self, request, *args, **kwargs):
data_to_change = {'name': request.data.get("name")}
# Partial update of the data
serializer = self.serializer_class(request.user, data=data_to_change, partial=True)
if serializer.is_valid():
self.perform_update(serializer)
return Response(serializer.data)

How do I set different Serializer for list and detail view with Django Rest Framework?

How do I set a different Serializer for list and detail view while using viewsets.ModelViewSet and HyperlinkedSerializer?
I see how to do it with viewsets.ViewSet by defining list and retrive, (here's an example) but I don't know how to elegantly adapt it to viewsets.ModelViewSet
I've adapted an answer from "Django rest framework, use different serializers in the same ModelViewSet" that serves me very well, and I hope you'll find useful:
class MyModelViewSet(viewsets.MyModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelListSerializer
detail_serializer_class = MyModelDetailSerializer
def get_serializer_class(self):
if self.action == 'retrieve':
if hasattr(self, 'detail_serializer_class'):
return self.detail_serializer_class
return super(MyModelViewSet, self).get_serializer_class()
In this case, you're just specifying your two serializers and using the one depending on the action. However, this can be made more general (for all actions) as follows:
class MyModelViewSet(viewsets.MyModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
action_serializers = {
'retrieve': MyModelDetailSerializer,
'list': MyModelListSerializer,
'create': MyModelCreateSerializer
}
def get_serializer_class(self):
if hasattr(self, 'action_serializers'):
return self.action_serializers.get(self.action, self.serializer_class)
return super(MyModelViewSet, self).get_serializer_class()
Viewsets extend the class GenericAPIView, so you can use this part of the documentation to solve your problem. Basically, what you need is to override get_serializer_class and to return a different serializer based on your request.
I've created this small package for this job. drf_custom_viewsets.
It has CustomSerializerViewSet, which inherits from ModelViewSet, which lets you set different serializers for different actions.
As of 2021, I will do it differently, a better and more generic way around is doing something like this:
class PlayersListViewSet(viewsets.ModelViewSet):
queryset = Player.objects.all()
serializer_class = PlayersListSerializer
http_method_names = ['get', 'post']
pagination_class = None
filter_backends = [filters.OrderingFilter]
ordering_fields = ['name']
serializer_class_by_action = {
'retrieve': PlayersDetailSerializer,
'list': PlayersListSerializer,
}
def get_serializer_class(self):
if hasattr(self, 'serializer_class_by_action'):
return self.serializer_class_by_action.get(self.action, self.serializer_class)
return super(MyModelViewSet, self).get_serializer_class()
def get_queryset(self):
queryset = Player.objects.all()
team_id = self.request.query_params.get('team', None)
if team_id:
try:
queryset = queryset.filter(team=team_id)
except ValueError:
raise exceptions.ParseError()
return queryset
Here action is the method used by the serializer, list in case of def list, retrieve in case of def retrieve and so on..
Thanks to #bbengfort, I've provided a simple solution without the need to create a new ViewSet.
Different Serializer Per Action In the ViewSet in Django
TL;DR
In the following code, we are overriding get_serializer_class based on Django documentation and we're specifying different serializers per action if it's needed:
Views.py
class TestAPIView(viewsets.ModelViewSet):
permission_classes = (IsAuthenticated,)
serializer_class = TestAPISerializer
serializer_class_by_action = {
'update_me': UpdateMeSerializer,
}
def get_serializer_class(self):
if hasattr(self, 'serializer_class_by_action'):
return self.serializer_class_by_action.get(self.action, self.serializer_class)
return self.serializer_class
#action(detail=True, methods=['patch'], url_name='Update Me', url_path='updateme')
def update_me(self, request, pk=None):
# Write your own logic
return Response("OK")
Serializers.py
class UpdateMeSerializer(serializers.Serializer):
count = serializers.CharField(required=False, allow_null=True, default=10)
class Meta:
fields = ['count']