DEV Community

Managing RESTful URLs in Django Rest Framework

Stanislav Kozlovski on August 14, 2017

We've all been taught about RESTful API design. It does not take much to realize that these endpoints POST /products/1/delete POST /products/1...
Collapse
 
khorolets profile image
Bohdan Khorolets • Edited

Thanks! Nice post and nice idea. Unfortunately the issues are starting when you're starting to add some security or permissions.

For example you need to make GET method public and protect others. If you add permissions to the classes you've mapped, they simply would skip the permissions check from your classes as and would apply the permissions from ProductManageView only.

Still the idea of yours is nice!

P.S. Correct me if I'm mistaken about permissions :)

Collapse
 
enether profile image
Stanislav Kozlovski

Fortunately, you are mistaken. You can very simply add a permission class to the view you want to protect and it works how you'd expect it.
Say we want only authorized users to delete our products. We'd simply add the IsAuthorized permission class to the delete view

from rest_framework.permissions import IsAuthenticated


class ProductDestroyView(DestroyAPIView):
    permission_classes = (IsAuthenticated, )
    queryset = Product.objects.all()
    serializer_class = ProductSerializer

Our new test

def test_destroy_view_requires_authentication(self):
        product = Product.objects.create(name='Apple Watch', price=500, stock=3)
        response = self.client.delete(f'/products/{product.id}')
        self.assertEqual(response.status_code, 403)
        self.assertEqual(Product.objects.count(), 1)  # assert not deleted

Passes!

Stanislavs-iMac:restful_drf stanislavkozlovski$ python3.6 manage.py test restful_example.tests.ProductTests.test_destroy_view_requires_authentication
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.014s

OK
Destroying test database for alias 'default'...
Collapse
 
khorolets profile image
Bohdan Khorolets

I'm so glad I've asked about it! :) Thank you. Please, consider to add the information about permissions to the main article, it's very useful. Thank you!

Thread Thread
 
enether profile image
Stanislav Kozlovski

Done, thanks for the idea!

Collapse
 
darland profile image
Artem Tiumentcev

Thanks, your solution was very helpful to me.

In your example, I received 500 errors if a method does not allow

AssertionError: .accepted_renderer not set on Response
Enter fullscreen mode Exit fullscreen mode

I replaced the Response method on the parent method call, where I got normal behaviour and error 405.

def dispatch(self, request, *args, **kwargs):
    if not hasattr(self, 'VIEWS_BY_METHOD'):
        raise Exception('VIEWS_BY_METHOD static dictionary variable must be defined on a ManageView class!')
    if request.method in self.VIEWS_BY_METHOD:
        return self.VIEWS_BY_METHOD[request.method]()(request, *args, **kwargs)

    return super().dispatch(request, *args, **kwargs)
Enter fullscreen mode Exit fullscreen mode
Collapse
 
loicgasser profile image
Gasser Loïc

This now results in this error

AssertionError: Cannot apply DjangoModelPermissionsOrAnonReadOnly on a view that does not set .queryset or have a .get_queryset() method

This is what I ended up using:

    def dispatch(self, request, *args, **kwargs):
        if not hasattr(self, 'VIEWS_BY_METHOD'):
            raise Exception(
                'VIEWS_BY_METHOD static dictinary must be defined')
        if request.method in self.VIEWS_BY_METHOD:  # pylint: disable=E1101
            return self.VIEWS_BY_METHOD[  # pylint: disable=E1101
                request.method
            ]()(request, *args, **kwargs)

        response = Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
        if not getattr(request, 'accepted_renderer', None):
            neg = self.perform_content_negotiation(request, force=True)
            request.accepted_renderer, request.accepted_media_type = neg

        response.accepted_renderer = request.accepted_renderer
        response.accepted_media_type = request.accepted_media_type
        response.renderer_context = self.get_renderer_context()
        return response
Enter fullscreen mode Exit fullscreen mode
Collapse
 
biskark profile image
Kevin

I had the same issue and found that using a predefined Response type instead of the general Response class fixed it.

E.g.

    from django.http import JsonResponse

    def dispatch(self, request, *args, **kwargs):
        if not hasattr(self, 'VIEWS_BY_METHOD'):
            raise Exception(
                'VIEWS_BY_METHOD static dictinary must be defined')
        if request.method in self.VIEWS_BY_METHOD:  # pylint: disable=E1101
            return self.VIEWS_BY_METHOD[  # pylint: disable=E1101
                request.method
            ]()(request, *args, **kwargs)

        return JsonResponse(data={}, status=status.HTTP_405_METHOD_NOT_ALLOWED)
Enter fullscreen mode Exit fullscreen mode

I haven't tested others, but I assume this would work equally well with an HttpResponse or whichever.

Collapse
 
rajat4665 profile image
Rajat Sharma • Edited

Hi there,
I have one query with HTTP method calling on Url. I have created a class which is inherited from Viewset.
It has different functions to perform different CRUD operations. But when I call two HTTP methods on single URL it doesn't reflect on Options and always go for GET method

Collapse
 
bay007 profile image
egalicia

Thanks a lot, I think this is not Restful, why? you have the verb (delete,update) directly in the url, for Restfull the verb must be performed with http verbs.