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... [Read Full]
markdown guide
 

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 :)

 

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'...
 

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!

 

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

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)
 

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
 

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.

code of conduct - report abuse