I'm trying to refactor my django project.
Therefore I want to refactor from:
#api_view([GET, POST])
#permission_classes((IsAuthenticated, VehiclePermissions, ))
def inactive_vehicle_view(request):
if request.method == "GET":
con = CBaseUtil.get_prod_instance()
vehicle_bo = VehicleBO()
dongle_dao = OBDDongleDAO(con)
since_days = int(request.GET.get("since", 28))
vehicles = vehicle_bo.find_vehicles_by_user_context(request.user.details)
return Response(vehicles, status=status_code, headers=get_headers(request))
To a class based view like this:
class InactiveVehicleView(View):
#authentication_classes((BasicAuthentication, WebsiteAuthentication))
#permission_classes((IsAuthenticated, VehiclePermissions, ))
def dispatch(self, *args, **kwargs):
return super(InactiveVehicleView, self).dispatch(*args, **kwargs)
def get(self, request):
con = CBaseUtil.get_prod_instance()
vehicle_bo = VehicleBO()
dongle_dao = OBDDongleDAO(con)
since_days = int(request.GET.get("since", 28))
vehicles = vehicle_bo.find_vehicles_by_user_context(request.user.details)
return Response(vehicles, status=status_code, headers=get_headers(request))
The issue I'm facing is that I can't get the user details like in the old version by request.user.details as the WSGI-Request does not contain an attribute user. I guess I did something wrong with the decorators but I can't figure it out.
FYI the view is mapped in the urls like this:
url(r'^vehicles/inactive/?$', InactiveVehicleView.as_view())
Does anyone have an idea what I did wrong with the authentication and/or the decorators?
According to the Django Rest Framework Authentication Docs, your view should subclass APIView, and set authentication_classes and permission_classes as attributes instead of using the decorators.
from rest_framework.views import APIView
class InactiveVehicleView(APIView):
authentication_classes = (BasicAuthentication, WebsiteAuthentication)
permission_classes = (IsAuthenticated, VehiclePermissions)
def get(self, request):
...
Related
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,)
I am new to django.
I have a model:
class Class(models.Model):
name = models.CharField(max_length=128)
students = models.ManyToManyField("Student")
def __str__(self) -> str:
return self.name
Now I want to create API to display the students in a particular class,the detail view, by giving the name of the class as a parameter using ModelViewClass.
Currently, I have following viewset written:
class ClassViewSet(ModelViewSet):
serializer_class = serializers.ClassSerializer
queryset = models.Class.objects.all()
How to do that?
You can use the #action(...) decorator to create a custom view.
from rest_framework.decorators import action
class ClassViewSet(ModelViewSet):
serializer_class = serializers.ClassSerializer
queryset = models.Class.objects.all()
#action(detail=True, methods=['get'], url_path='students', url_name='students')
def students(self, request, *args, **kwargs):
class_ = self.get_object()
students = class_.students.all()
serializer = serializers.StudentSerializer(students, many=True)
return Response(serializer.data)
If you are not using the DRF routers, you can specify the custom route as below,
urlpatterns = [
path('classes/', views.ClassViewSet.as_view({'get': 'list'})),
path('classes/<int:pk>/', views.ClassViewSet.as_view({'get': 'retrieve'})),
path('classes/<int:pk>/students/', views.ClassViewSet.as_view({'get': 'students'})),
]
I have a django model Donation that I expose as a ViewSet. Now I want to add an additional URL to a second model Shop where a related instance of Donation can be retrieved via the parameter order_id and custom actions can be executed.
# models.py
class Donation(models.Model):
id = models.AutoField(primary_key=True)
order_id = models.StringField(help_text='Only unique in combination with field `origin`')
origin = models.ForeignKey('Shop', on_delete=models.PROTECT)
class Shop(models.Model):
id = models.AutoField(primary_key=True)
# views.py
class DonationViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet):
def retrieve(self, request, *args, **kwargs):
if kwargs['pk'].isdigit():
return super(DonationViewSet, self).retrieve(request, *args, **kwargs)
else:
shop_id = self.request.query_params.get('shop_id', None)
order_id = self.request.query_params.get('order_id', None)
if shop_id is not None and order_id is not None:
instance = Donations.objects.filter(origin=shop_id, order_id=order_id).first()
if instance is None:
return Response(status=status.HTTP_404_NOT_FOUND)
return Response(self.get_serializer(instance).data)
return Response(status=status.HTTP_404_NOT_FOUND)
#action(methods=['post'], detail=True)
def custom_action(self, request, *args, **kwargs):
pass
class ShopViewSet(viewsets.ModelViewSet):
pass
# urls.py
router = routers.DefaultRouter()
router.register(r'donations', DonationViewSet)
router.register(r'shops', ShopViewSet)
router.register(r'shops/(?P<shop_id>[0-9]+)/donations/(?P<order_id>[0-9]+)', DonationViewSet)
My goal is to have http://localhost:8000/donations point at the entire DonationViewSet. Also I would like to lookup an individual donation, by its combination of shop_id and order_id like follows http://localhost:8000/shops/123/donations/1337/ and also executing the custom action like follows http://localhost:8000/shops/123/donations/1337/custom_action/. The problem I have is that the second url returns an entire queryset, not just a single instance of the model.
You can also use drf-nested-routers, which will have something like this:
from rest_framework_nested import routers
from django.conf.urls import url
# urls.py
router = routers.SimpleRouter()
router.register(r'donations', DonationViewSet, basename='donations')
router.register(r'shops', ShopViewSet, basename='shops')
shop_donations_router = routers.NestedSimpleRouter(router, r'', lookup='shops')
shop_donations_router.register(
r'donations', ShopViewSet, basename='shop-donations'
)
# views.py
class ShopViewSet(viewsets.ModelViewSet):
def retrieve(self, request, pk=None, donations_pk=None):
# pk for shops, donations_pk for donations
#action(detail=True, methods=['PUT'])
def custom_action(self, request, pk=None, donations_pk=None):
# pk for shops, donations_pk for donations
This is not tested! But in addition to what you already have, this will support:
donations/
donations/1337/
shops/123/donations/1337/
shops/123/donations/1337/custom_action
You can add urls by simply appending to the router's urls in the config like so. If all you want to do is add a single action from a view for one specifc url, and dont need all of the actions/urls for the viewset
# urls.py
urlpatterns = [
path('some_path', my_lookup_view)),
] # typical django url convention
urlpatterns += router.urls
# views.py
#api_view(['POST'])
def my_looup_view(request, shop_id, order_id):
# ...some lookup...
pass
You'll want to
derive from GenericViewSet since you're using models anyway
override get_object() instead with your custom lookup logic:
from rest_framework import mixins
from rest_framework.generics import get_object_or_404
from rest_framework.viewsets import GenericViewSet
class MyModelViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.ListModelMixin,
GenericViewSet,
):
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
lookup_value = self.kwargs["pk"]
if lookup_value.isdigit():
obj = get_object_or_404(queryset, pk=lookup_value)
else:
obj = get_object_or_404(queryset, third_party_service_id=lookup_value)
self.check_object_permissions(self.request, obj)
return obj
#action(methods=["post"], detail=True)
def custom_action(self, request, *args, **kwargs):
thing = self.get_object()
# urls.py
router.register(r"mymodels", MyModelViewSet)
This should let you do
mymodels/ to list models,
mymodels/123/ to get a model by PK,
mymodels/kasjdfg/ to get a model by third-party service id,
mymodels/123/custom_action/ to run custom_action on a model by PK
mymodels/kasjdfg/custom_action/ to run custom_action on a model by service id,
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
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