How to connect a renderer to a specific endpoint in a viewset, and only that endpoint - django-rest-framework

The DRF documentation shows how to connect a renderer to an APIView, but not how to do it for a specific action in a ViewSet. Given:
class XViewSet(ViewSet):
serializer_class = XSerializer
#action(detail=True, methods=['get'])
def my_action(self, request, pk=None):
..
How do I set a specific renderer for my_action, that will not affect the other/default actions in the viewset?
I can make an APIView just for that action of course but that makes for a more messy urls.py

As far as I can tell, the action takes any argument that can be a class attribute:
class XViewSet(ViewSet):
serializer_class = XSerializer
#action(detail=True, methods=['get'], renderer_classes=[yourrenderer])
def my_action(self, request, pk=None):
..

Related

How to send PUT request to ModelViewSet without passing a primary key in the url?

I am particularly interested in using ModelViewSet
for solving the challenge of updating the logged in user's profile. I am using the following definition:
from rest_framework import viewsets
class ProfileRetrieveUpdate(viewsets.ModelViewSet):
serializer_class = UserProfileSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def get_queryset(self):
return UserProfile.objects.filter(user=self.request.user)
def perform_update(self, serializer):
serializer.save(user=self.request.user)
By overriding get_queryset, I am able to get the expected behavior, i.e. when I access the endpoints (say profile/), I get the profile of the logged in user. However, if I have to update the profile, I can only access PUT by going to profile/6/. Is there a way I can get ModelViewSet to expose PUT at the same endpoint i.e. profile/?
You can register ModelViewSet HTTP method under any path you want.
path(
"profile",
ProfileRetrieveUpdate.as_view(
{"put": "partial_update", "get": "retrieve"}
),
name="profile-retrieve-update",
)
You will have to adjust other things as well as you don't provide pk.
Instead of overriding get_queryset method you need to adjust get_object for your needs.
from rest_framework import viewsets
from rest_framework.generics import get_object_or_404
class ProfileRetrieveUpdate(viewsets.ModelViewSet):
serializer_class = UserProfileSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
queryset = UserProfile.objects.all()
def get_object(self):
obj = get_object_or_404(self.queryset, user=self.request.user)
self.check_object_permissions(self.request, obj)
return obj
def perform_update(self, serializer):
serializer.save(user=self.request.user)
Now if you PUT profile/ it should work as expected.

DRF Filter PrimaryKeyField Based on Current User

I have a view set up to return a list of books to a user, which is retrieved from a simple book model based on the currently logged-in user. However, I also have ReadingSession model which has a foreign key relationship to both the Book, and the User.
When I'm retrieving the books for the user, I'd like to, at the very least, return a list of primary keys that I can use to get the length of in my client.
The following code will get the full set of readingsessions in my BookSerializer:
from rest_framework import serializers
from books.models import Book
class BookSerializer(serializers.ModelSerializer):
readingsession_set = serializers.PrimaryKeyRelatedField(
many=True, read_only=True)
class Meta:
model = Book
fields = ["id", "title", "author", "publisher",
"publish_date", "description", "category",
"language", "small_thumbnail", "thumbnail",
"readingsession_set"]
However, the problem with this is that it will return all of the readingsessions, regardless of whether or not the session belongs to that user.
I'd like to be able to filter that so that it will only return the readingsessions for the current user. Something along the lines of:
readingsession_set = serializers.PrimaryKeyRelatedField(queryset=ReadingSession.objects.filter(user=user), read_only=True)
But I've tried various ways of trying to pass the user (self.request.user) from the APIView but none seem to work. I've tried passing a context, and tried passing extra **kwargs in __init__ but none seem to work.
Is there a way of achieving this? Or am I taking the wrong approach?
Thanks
The user is not present on the serializer's declaration but during its instantiation.
Therefore, you can filter querysets by user within the __init__ method.
from rest_framework import serializers
from bar.models import Foo
class RandomSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
user_foos = Foo.objects.filter(user=self._user)
self.fields['foo_ids'] = serializers.PrimaryKeyRelatedField(
required=False,
many=True,
read_only=False,
queryset=user_foos,
default=user_foos)
#property
def _user(self):
request = self.context.get('request', None)
if request:
return request.user
Don't forget to pass the request object to the serializer in the context (if necessary, e.g., using a simple APIView.
from rest_framework import views
class RandomView(views.APIView):
serializer_class = RandomSerializer
def post(self, request):
serializer = self.serializer_class(
data=request.data, context={'request': request})
# ...
serializer = RandomSerializer(data=request.data, context={'request': request}
You can access the user of the request on the serializer by means of the context.
As mentioned in the documentation, you can always do:
serializer = AccountSerializer(account, context={'request': request})
Thus, you will be able to use self.context['request'].user inside your serializer.
Hope that's what you're after.

DRF Serializer custom create

I don't know if I'm doing this the right way, but I'm having problems with saving some relations with DRF serializers.
Let's say I have a MessageBoard and some MessageBoardPosts
Post has a serializer like:
class MessageBoardPostSerializer(serializers.ModelSerializer):
class Meta:
model = MessageBoardPost
fields = '__all__'
I want to add a Post by posting to /api/messageBoards/[PK]/create_post/
To do this, I added an action to the MessageBoard Viewset:
#action(detail=True, methods=['post'], permission_classes=[MatchesMessageboardVisibility])
def create_post(self, request, pk=None):
# Messageboard to post to
messageBoard = MessageBoard.objects.get(pk=pk)
if messageBoard is not None:
serializer = MessageBoardPostSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
Since MessageBoard is a required FK on a post, I get validation errors when creating the post in the viewset.
What's the best way to solve this?
Serializer (i assume that "message_board" is ForeignKey in MessageBoardPost model):
class MessageBoardPostSerializer(serializers.ModelSerializer):
# this is read only by default which means serializer will not require that field
message_board = serializers.StringRelatedField()
class Meta:
model = MessageBoardPost
fields = '__all__'
Viewset action:
#action(detail=True, methods=['post'], permission_classes=[MatchesMessageboardVisibility])
def create_post(self, request, pk=None):
# Messageboard to post to
messageBoard = get_object_or_404(MessageBoard, pk=pk)
serializer = MessageBoardPostSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# here you can pass data without validation directly to the save method
serializer.save(message_board=messageBoard)
return Response(serializer.data)
you should write your variables in snake_case style like "message_board" etc.
for more advanced crud you should make url like /api/message-boards/PK/posts/ where you will POST your posts and message_board should be prefetched in overrited initial method
You can pass the MessageBoard as context to the serializer:
views.py
from django.shortcuts import get_object_or_404
#action(detail=True, methods=['post'], permission_classes=[MatchesMessageboardVisibility])
def create_post(self, request, pk=None):
# Messageboard to post to
message_board = get_object_or_404(MessageBoard, pk=pk)
context = {'message_board': message_board}
serializer = MessageBoardPostSerializer(data=request.data, context=context)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
serializers.py
class MessageBoardPostSerializer(serializers.ModelSerializer):
class Meta:
model = MessageBoardPost
fields = '__all__'
def create(self, validated_data):
post = MessageBoardPost(**validated_data)
post.message_board = self.context['message_board']
post.save()
return post

Serializer `validate()` not getting called on `is_valid()` on POST

I want to create a non class-based form to facilitate uniform logging in by users across all front-end apps. Currently, it looks like this
My serializer class:
class EmployeeLoginSerializer(serializers.Serializer):
username = serializers.CharField(min_length=6)
password = serializers.CharField(min_length=6)
def validate_credentials(self, attrs):
print('validating')
try:
emp: Employee = Employee.objects.get(username=attrs['username'])
if crypto.verify_password(attrs['password'], emp.password_hash):
return attrs
except Exception:
raise serializers.ValidationError("Incorrect username or password")
raise serializers.ValidationError('Incorrect username or password')
My view class:
class TestView(APIView):
serializer_class = EmployeeLoginSerializer
def get(self, request, *args, **kwargs):
return Response({'Message': 'Get works'})
def post(self, request, *args, **kwargs):
print(request.POST)
serializer = self.serializer_class(data=request.POST)
if serializer.is_valid():
return Response({'Message': 'Credentials were correct'})
My issue is that serializer.is_valid() doesn't seem to be calling on validate automatically. I know I could just call serializer.validate() manually but all the docs and questions on StackOverflow seem to show validate() being called by is_valid() automatically so I get that feeling that that wouldn't be the best practice. Is there something I'm missing?
The is_valid() method will call validate() method of the serializer and validate_FIELD_NAME() methods.
In your code, the validate_credentials() seems a regular class method which won't detect by DRF since the credentials isn't a field on the serializer.

Django Rest Framework - separate serializer class per method in model based API view

Say I have a simple Django REST Framework view that's extending multiple model classes and serves all the methods in one URL endpoint:
class UserAPIView(RetrieveAPIView, DestroyAPIView, BaseObjectAPIView):
permission_classes = (IsAuthenticated, )
serializer_class = UserSerializer
def get_serializer_class(self, *args, **kwargs):
# return different serializer depending on method??
# return UserUpdateSerializer
return UserViewSerializer
def get(self, request, *args, **kwargs):
"""
Retrieve user details
"""
# ...
return Response(data={'result': "OK"}, status=200)
def delete(self, request, pk):
"""
Delete user
"""
# ...
return Response(data={'result': "OK"}, status=200)
def put(self, request, pk):
"""
Change user
"""
# ...
return Response(data={'result': "OK"}, status=200)
Now I need to use different serializers per method, as my get-method will use different fields than my put-method, example serializers:
class UserViewSerializer(serializers.ModelSerializer):
firstname = serializers.Field(source='firstname')
lastname = serializers.Field(source='lastname')
username = serializers.Field(source='username')
class Meta:
model = User
class UserUpdateSerializer(serializers.ModelSerializer):
firstname = serializers.Field(source='firstname')
lastname = serializers.Field(source='lastname')
class Meta:
model = User
Is it possible to use different serializers for each method in my model based API view?
UPDATE:
I know how to use different serializers inside the methods themselves.
But I need to get the Browsable API generated by Swagger (Django module rest_framework_swagger) to retrieve different serializers for each method.
I can see that loading the API browser page triggers get_serializer_class, but inside that method, I don't know what method Swagger tries to get the serializer for.
How can I get rest_framework_swagger to retrieve different serializers per method?
I think there are at least two ways to achieve this:
You simply set the serializer that you want in each of your methods. Like this:
def get(self, request, *args, **kwargs):
self.serializer_class = UserViewSerializer
# ...
return Response(data={'result': "OK"}, status=200)
You override the get_Serializer_class method. Like this:
def get_serializer_class(self, *args, **kwargs):
if self.request.method == 'POST':
return UserUpdateSerializer
return UserViewSerializer
Hope this helps.
I suppose you could use yaml docstring on each method to override serializers. Like:
def put(self, request, pk):
"""Change user
---
serializer: .serializers.UserUpdateSerializer
"""
# ...
return Response(data={'result': "OK"}, status=200)

Resources