How can I access the serializer in extra actions? - python

I have the following two classes and I want two merge EventInvitationCreateView into EventInvitationViewSet.
However, I am struggling to bring perform_create into create_invitation as I still need to access serializer. Do you have any input on how to achieve that?
class EventInvitationCreateView(CreateAPIView):
serializer_class = InvitationSerializer
permission_classes = [RetoolPermission]
queryset = Invitation.objects.none()
def perform_create(self, serializer):
email = serializer.validated_data["email"]
event = self.request.event
invitation_exists = Invitation.objects.filter(
email__iexact=email, event=event
).exists()
if invitation_exists:
raise ValidationError("Email already exists")
serializer.save(event=event)
class EventInvitationViewSet(
mixins.CreateModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet
):
permission_classes = [RetoolPermission]
serializer_class = InvitationSerializer
filter_backends = [filters.SearchFilter]
search_fields = ["email"]
queryset = Invitation.objects.none()
def get_queryset(self) -> QuerySet:
return self.request.event.invitations.all()
#action(detail=True, methods=["post"])
def create_invitation(self, request, pk=None):
[...perform_create]

the action is still a standard view's method, so you can use self.get_serializer()
for instance
#action(detail=True, methods=["post"])
def create_invitation(self, request, pk=None):
s = self.get_serializer(data=request.data)
....
If you need to use a "action specific serializer" you can specify the serializer class in the decorator
#action(detail=True, methods=["post"], serializer_class=MyOtherSerializer)
def create_invitation(self, request, pk=None):
s = self.get_serializer(data=request.data) # <-- instance of MyOtherSerializer
....

Related

How to write permissions in a viewset with conditional statements in DRF?

I have a viewset written in DRF:
class MyViewSet(ModelViewSet):
serializer_class = MySerializer
queryset = models.MyClass.objects.all()
def get_serializer_class(self):
permission = self.request.user.permission
if permission=='owner' or permission=='admin':
return self.serializer_class
else:
return OtherSerializer
def perform_create(self, serializer):
permission = self.request.user.permission
if permission=='owner' or permission=='admin':
serializer.save()
else:
employee = models.Employee.objects.get(user=self.request.user)
serializer.save(employee=employee)
Here, I am using the following statements in both get_serializer_class and perform_create which looks like a repetitive code:
permission = self.request.user.permission
if permission=='owner' or permission=='admin':
Is there any way to write it once and then use it as a permission_class somehow?
Create a Custom Permission class
https://www.django-rest-framework.org/api-guide/permissions/#custom-permissions
from rest_framework.permissions import BasePermission, SAFE_METHODS
class CustomPermission(BasePermission):
def has_permission(self, request, view):
if request.method in SAFE_METHODS:
return True
permission = self.request.user.permission
if permission=='owner' or permission=='admin':
return True
return False
in Views.py
class MyViewSet(ModelViewSet):
serializer_class = MySerializer
queryset = models.MyClass.objects.all()
permission_classes = (CustomPermission,)

pass a parameter into serilazer under ListModelMixin

I am passing a parameter to a serilaizer like this:
serializer = AttractionTicketSerializer(attraction, context={'api_consumer':request.auth.application})
I have a view which inherits from ListModelMixin, I need to pass this context param to the serilizer as well.
here is a summarized view:
class AttractionView(mixins.ListModelMixin, generics.GenericAPIView):
authentication_classes = AUTHENTICATION_CLASSES
permission_classes = [IsAuthenticatedOrTokenHasReadWriteScope]
queryset = Attraction.objects.all()
serializer_class = AttractionSerializer
def get(self, request: Request, *args, **kwargs):
attractions: Dict[str, Any] = self.list(request, *args, **kwargs)
return attractions
Is there a way to do it?
Thanx in advance
You can override the get_serializer_context() function to add additional context to be passed to the serializer.
class AttractionView(mixins.ListModelMixin, generics.GenericAPIView):
<...already_defined_attributes...>
def get_serializer_context(self):
# you can access self.request here
# if you need to get some data from your request
context = super().get_serializer_context()
context.update({
'new_key': <new_value>
})
return context
You can also override the list function to achieve it.
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
context = self.get_serializer_context()
context.update({
'new_key': 'new_value'
})
if page is not None:
serializer = self.get_serializer(page, context=context, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, context=context, many=True)
return Response(serializer.data)
Based on your specific requirement you can choose any of the above approaches.

How to get url to action method from ModelViewSet(Django Rest Framework) with reverse function?

I have this ModelViewSet class:
class DriveInvoiceViewSet(viewsets.ModelViewSet):
filter_fields = ('location_id', 'logical_deleted')
permission_classes = (UserCanManageFacilityPermission,)
pagination_class = None
def get_serializer_class(self):
...
def get_queryset(self):
...
#action(detail=False, methods=['GET'])
def get_subtotals_by_unit(self, request):
invoices_list = self.filter_queryset(self.get_queryset())
grouped_invoices = get_subtotals_by_unit(invoices_list)
return Response(grouped_invoices)
How can I get the URL from reverse function to test the get_subtotals_by_unit action?
The ViewSet registred by the router router.register('drive_invoices', DriveInvoiceViewSet, base_name='drive_invoices')
Change the action decorator slightly as below,
class DriveInvoiceViewSet(viewsets.ModelViewSet):
# other code
#action(detail=False, methods=['GET'], url_path="/some/path/", url_name="some-view-name")
def get_subtotals_by_unit(self, request):
invoices_list = self.filter_queryset(self.get_queryset())
grouped_invoices = get_subtotals_by_unit(invoices_list)
return Response(grouped_invoices)
Thus, DRF will create a url pattern with a view name with a syntax as <router_base_name>-<action_view_name>
Thus, the view name in your case will be, drive_invoices-some-view-name

Django Rest Framework override viewset list() method without loosing filter_backends functionality

I have a viewset and I override list() method, but filtering by field stop working. How can I call filtering options from my code:
This is my viewset:
class SupplementViewSet(viewsets.ModelViewSet):
permission_classes = (permissions.IsAuthenticated,)
queryset = models.Product.objects.filter()
serializer_class = serializers.SuplementSerializer
filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter,)
search_fields = ('hotel', 'name')
filter_fields = ('id', 'hotel', 'name')
def perform_create(self, instance):
instance.save(product_type=models.Product.SUPPLEMENT)
def list(self, request, pk=None):
if pk == None:
supplements = models.Product.objects.filter(product_type=models.Product.SUPPLEMENT)
else:
supplements = models.Product.objects.get(product_type=models.Product.SUPPLEMENT, id=pk)
page = self.paginate_queryset(supplements)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(page, many=True)
result_set = serializer.data
return Response(result_set)
def get_result_set(self, supplements):
result_set = serializers.ProductSerializer(supplements, many=True).data
return result_set
Thanks for the help.
def list(self, request, pk=None):
if pk == None:
supplements = models.Product.objects.filter(product_type=models.Product.SUPPLEMENT)
else:
supplements = models.Product.objects.filter(product_type=models.Product.SUPPLEMENT, id=pk)
supplements= self.filter_queryset(supplements)
page = self.paginate_queryset(supplements)
self.filter_queryset(queryset) is what you need, but it must accept queryset.
It would be helpful to see the actual implementation of the list method:
http://www.cdrf.co/3.9/rest_framework.viewsets/ReadOnlyModelViewSet.html#list
You can directly use the self.filter_queryset method (which is inherited from GenericAPIView in viewsets.ModelViewSet). In very rare scenarios you will need to override the self.filter_queryset method.
Both Ykh and Akshay Anurag answers are correct, for sake of simplicity you could simply call:
filter_queryset(queryset)
before calling
paginate_queryset(filtered_queryset)

How to return custom JSON in Django REST Framework

I am trying to return custom json with get_queryset but always get 404 error in response.
class TestViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows groups to be viewed or edited.
"""
queryset = Test.objects.all()
serializer_class = TestSerializer
def get_queryset(self):
if self.request.method == "GET":
content = {'user_count': '2'}
return HttpResponse(json.dumps(content), content_type='application/json')
If I delete everything starting from def I'll got correct response with standard json data. What I am doing wrong?
If you don't need a ModelViewSet and just want custom JSON on a GET request
You can also use an APIView, which doesn't require a model
class MyOwnView(APIView):
def get(self, request):
return Response({'some': 'data'})
and
urlpatterns = [
url(r'^my-own-view/$', MyOwnView.as_view()),
]
With a ModelViewSet
You've put the custom JSON into get_queryset, that's wrong. If you want to use a ModelViewSet, this by itself should be enough:
class TestViewSet(viewsets.ModelViewSet):
queryset = Test.objects.all()
serializer_class = TestSerializer
This ModelViewSet comes with default implementations for .list(), .retrieve(), .create(), .update(), and .destroy(). Which are available for you to override (customize) as needed
Returning custom JSON from .retrieve() and/or .list() in ModelViewSet
E.g. to override .retrieve() to return custom view when retrieving a single object. We can have a look at the default implementation which looks like this:
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
return Response(serializer.data)
So as an example to return custom JSON:
class TestViewSet(viewsets.ModelViewSet):
queryset = Test.objects.all()
serializer_class = TestSerializer
def retrieve(self, request, *args, **kwargs):
return Response({'something': 'my custom JSON'})
def list(self, request, *args, **kwargs):
return Response({'something': 'my custom JSON'})
There are 2 ways to custom the response in Class-based views with ModelViewSet
Solution 1: custom in views.py
class StoryViewSet(viewsets.ModelViewSet):
permission_classes = (permissions.AllowAny,)
queryset = Story.objects.all()
serializer_class = StorySerializer
def retrieve(self, request, *args, **kwargs):
# ret = super(StoryViewSet, self).retrieve(request)
return Response({'key': 'single value'})
def list(self, request, *args, **kwargs):
# ret = super(StoryViewSet, self).list(request)
return Response({'key': 'list value'})
Solution 2: custom in serializers.py (I recommend this solution)
class StorySerializer(serializers.ModelSerializer):
class Meta:
model = Story
fields = "__all__"
def to_representation(self, instance):
ret = super(StorySerializer, self).to_representation(instance)
# check the request is list view or detail view
is_list_view = isinstance(self.instance, list)
extra_ret = {'key': 'list value'} if is_list_view else {'key': 'single value'}
ret.update(extra_ret)
return ret

Categories