How to send PUT request to ModelViewSet without passing a primary key in the url? - django-rest-framework

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.

Related

Is it safe to use caching on get_object in django class-based view

I use some permission checks inside overridden check_permissions. I get the instance I will work with using get_object. I know that after this the get_object method will be called again - during retrieve or destroy methods. So I use caching (#lru_cache). See full example below.
from functools import lru_cache
from rest_framework.exceptions import PermissionDenied
from rest_framework.generics import RetrieveDestroyAPIView
class UserView(RetrieveDestroyAPIView):
permission_classes = [ProjectAdminOnly]
serializer_class = UserSerializer
#lru_cache
def get_object(self, *args, **kwargs):
return UserService.get(self.kwargs['user_id'])
def check_permissions(self, request):
super().check_permissions(request)
user = self.get_object()
# Only within the same project
if self.request.user.project != user.project:
raise PermissionDenied()
This code passes my tests successfully, but I wonder is there any unsafe situation in which the caching will do the bad service for me?

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.

django-rest-framework-simplejwt disable refresh

Is there a way to disable refresh token?
Take away refresh field from the response.
Thank you #alamshafi2263. I think you give the perfect direction.
Don't know why, I still git the refresh item from server response, not Django Shell. (also see the debug variables in photo).
So I just simply data.pop('refresh', None) it out, then problem solved.
Thank you for your time and code.
The Easy Way
Write a view of your own extending the TokenObtainPairView and override the post method.
# in your views.py
from rest_framework import status
from rest_framework.response import Response
from rest_framework_simplejwt.views import TokenObtainPairView
class MyTokenView(TokenObtainPairView):
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
try:
serializer.is_valid(raise_exception=True)
except TokenError as e:
raise InvalidToken(e.args[0])
serializer.validated_data.pop('refresh', None)
return Response(serializer.validated_data, status=status.HTTP_200_OK)
# in your urls.py
urlpatterns = [
path('api/token/', MyTokenView.as_view()),
]
The Complicated but Nicer Way
You need to create a serializer extending TokenObtainSerializer and then define a Custom View as above. This time put your new serializer as the serializer_class of this view and forget about the post method.
# in your serializers.py
from rest_framework_simplejwt.serializers import TokenObtainSerializer
from rest_framework_simplejwt.tokens import RefreshToken
class MyTokenObtainSerializer(TokenObtainSerializer):
#classmethod
def get_token(cls, user):
return RefreshToken.for_user(user)
def validate(self, attrs):
data = super().validate(attrs)
refresh = self.get_token(self.user)
data['access'] = str(refresh.access_token)
return data
# in your views.py
from rest_framework_simplejwt.views import TokenObtainPairView
from .serializers import MyTokenObtainSerializer
class MyTokenView(TokenObtainPairView):
serializer_class = MyTokenObtainSerializer
# in your urls.py
urlpatterns = [
path('api/token/', MyTokenView.as_view()),
]
You can parse the user.tokens() dictionary
user.tokens()['access']
user.tokens()['refresh]
And pass them to "tokens" key while in Response

Set 2 permmissions for 2 different actions in ListCreateAPIView in django-rest-framework

I am developing an end-point where users can get the categories and only admin can create them. I am using Django and DjangoRestFramework. More specifically I am using ListCreateAPIView.
Here is my code.
class TagView(ListCreateAPIView):
serializer_class = TagSerializer
queryset = Tag.objects.all()
permission_classes = [IsAdminUser, ]
My Task: I need to set two permission for list and create, they are AllowAny and IsAdminUser.
Any ideas or suggestions and answers are welcome. Thanks beforehand.
I recommend using rest_condition library. In your case, you can code like this:
...
from rest_condition import And, Or
from rest_framework.permissions import BasePermission
class IsPostMethod(BasePermission):
def has_permission(self, request, view):
return request.method.upper() == 'POST'
class IsSafeMethod(BasePermission):
def has_permission(self, request, view):
return request.method.upper() in ('OPTIONS', 'HEAD', 'GET')
class TagView(ListCreateAPIView):
serializer_class = TagSerializer
queryset = Tag.objects.all()
permission_classes = [
Or(
And(IsPostMethod, IsAdminUser),
And(IsSafeMethod, AllowAny),
)
]
Use method_decorator in combination with permission_required on top of your API class, like this:
#method_decorator(permission_required('permission_name'), name='method_name')
class TagView ...
I would suggest using custom permissions, It worked pretty well, and clean.
from rest_framework.permissions import SAFE_METHODS, BasePermission
class IsAdminOrReadOnly(BasePermission):
def has_permission(self, request, view):
if request.method in SAFE_METHODS:
return True
return request.user.is_superuser

DRF: Pagination without queryset

I am trying to make use of Django Rest Framework's pagination mechanisms in my case without success.
class TransactionView(viewsets.ViewSet):
serializer_class = TransactionSerializer
def list(self, request):
# fetching data from external API...
serializer = self.serializer_class(data=list_of_json, many=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.errors)
class TransactionSerializer(serializers.Serializer):
# Serializer (transaction's) fields ...
def create(self, validated_data):
return APITransaction(**validated_data)
class APITransaction(object):
def __init__(self, arg1, arg2, ...):
self.arg1 = arg1
...
The problem is that registering the pagination_class (like I have done for the rest of my resources which are represented by Models), doesn't work since the data are created/fetched on the fly, thus I don't have a Model/queryset.
Any ideas on how I could use DRF's pagination mechanism?
Here's the class I wound up creating and using locally for this sort of thing. (Thanks to stelios above for the initial clues.) Of course the contents of "data" must be JSONable.
from typing import List, Any
from collections import OrderedDict
from django.core.paginator import Paginator
from django.http.response import JsonResponse
from rest_framework.request import Request
class ListPaginator:
def __init__(self, request: Request):
# Hidden HtmlRequest properties/methods found in Request.
self._url_scheme = request.scheme
self._host = request.get_host()
self._path_info = request.path_info
def paginate_list(self, data: List[Any], page_size: int, page_number: int) -> JsonResponse:
paginator = Paginator(data, page_size)
page = paginator.page(page_number)
previous_url = None
next_url = None
if self._host and self._path_info:
if page.has_previous():
previous_url = '{}://{}{}?limit={}&page={}'.format(self._url_scheme, self._host, self._path_info, page_size, page.previous_page_number())
if page.has_next():
next_url = '{}://{}{}?limit={}&page={}'.format(self._url_scheme, self._host, self._path_info, page_size, page.next_page_number())
response_dict = OrderedDict([
('count', len(data)),
('next', next_url),
('previous', previous_url),
('results', page.object_list)
])
return JsonResponse(response_dict, status=200, safe=False)
You can't reuse existing DRF's pagination because they are supposed to work with queryset.
However, you may roll your own class by inheriting BasePagination though I haven't done myself.

Resources