Django Rest Framework: Use DELETE on GenericAPIView without implementing Retrieve - django

I'm using Django Rest Framework and want to be able to delete a Content instance via DELETE to /api/content/<int:pk>/. I don't want to implement any method to respond to GET requests.
When I include a .retrieve() method as follows, the DELETE request works:
class ContentViewSet(GenericViewSet):
def get_queryset(self):
return Content.objects.filter(user=self.request.user)
def retrieve(self, request, pk=None):
pass #this works, but I don't want .retrieve() at all
def delete(self, request, pk=None):
content = self.get_object()
#look up some info info here
content.delete()
return Response('return some info')
If I replace .retrieve() with RetrieveModelMixin it also works. However, if I remove both of these, which is what want to do, I get the following error.
django.urls.exceptions.NoReverseMatch: Reverse for 'content-detail' not found. 'content-detail' is not a valid view function or pattern name.
I haven't tested, but I assume the same thing would happen with PUT and PATCH.
My questions are:
How can I allow DELETE without implementing a .retrieve() method, and
Why can't DRF create the urlconf without .retrieve() implemented?
UPDATE: Failing test and complete error traceback caused by removing .retrieve() method
from rest_framework.test import APITestCase, APIClient
from myapp.models import Content
class ContentTestCase(APITestCase):
def setUp(self):
self.content = Content.objects.create(title='New content')
self.client = APIClient()
def test_DELETE_content(self):
url = reverse('content-detail', kwargs={'pk':self.content.pk})
response = self.client.delete(url)
self.assertEqual(response.status_code, 200)
Results in:
Traceback (most recent call last):
File "myproject/myapp/tests.py", line 548, in test_DELETE_content
url = reverse('content-detail', kwargs={'pk':self.content})
File "python3.6/site-packages/rest_framework/reverse.py", line 50, in reverse
url = _reverse(viewname, args, kwargs, request, format, **extra)
File "python3.6/site-packages/rest_framework/reverse.py", line 63, in _reverse
url = django_reverse(viewname, args=args, kwargs=kwargs, **extra)
File "python3.6/site-packages/django/urls/base.py", line 90, in reverse
return iri_to_uri(resolver._reverse_with_prefix(view, prefix, *args, **kwargs))
File "python3.6/site-packages/django/urls/resolvers.py", line 636, in _reverse_with_prefix
raise NoReverseMatch(msg)
django.urls.exceptions.NoReverseMatch: Reverse for 'content-detail' not found. 'content-detail' is not a valid view function or pattern name.

How can I allow DELETE without implementing a .retrieve() method?
Just remove the retrieve() method from the view class. Which means, the GenericViewSet doesn't provide any HTTP Actions unless it's defined in your class. So, the following will be your code snippet,
class ContentViewSet(GenericViewSet):
def get_queryset(self):
return Content.objects.filter(user=self.request.user)
def delete(self, request, pk=None):
content = self.get_object()
# look up some info info here
content.delete()
return Response('return some info')
or you could use mixin classes here,
from rest_framework.mixins import DestroyModelMixin
class ContentViewSet(DestroyModelMixin, GenericViewSet):
def get_queryset(self):
return Content.objects.filter(user=self.request.user)
Why can't DRF create the urlconf without .retrieve() implemented?
I'm not sure how you've defined your URLs. When I tried with DRF Router, it's only creating the URL conf for defined actions.
You've got GET and DELETE actions on your end-point because of you'd defined the retrieve() method in your view class.
Hope this help :)

Wild guess here but did you use a SimpleRouter or a DefaultRouter to build your urlpatterns?
If so, that's your problem. The router uses a viewset and expects to have all methods implemented. More info here
What you can do is just add your url to urlpatterns like you would normally do on django using the .as_view() method.

My solution for part 1. is to include the mixin but restrict the http_method_names:
class ContentViewSet(RetrieveModelMixin, GenericViewSet):
http_method_names = ['delete']
...
However, I still don't know why I have to include RetrieveModelMixin at all.

Related

Django Rest - Use #action with custom decorator

I have a Rest API in Django and I have the following method in a class that extends ModelViewSet:
#custom_decorator
#action(methods=['get'], detail=False, url_name="byname", url_path="byname")
def get_by_name(self, request):
# get query params from get request
username = request.query_params["username"]
experiment = request.query_params["experiment"]
If I remove the first annotator everything works fine. But when I am trying to call this function with both decorators, it does not even find the specific url path.
Is it possible to use multiple decorators along with the #action decorator?
I was having the same issue and fixed it the following way:
from functools import wraps
def custom_decorator(func):
# ADD THIS LINE TO YOUR CUSTOM DECORATOR
#wraps(func)
def func_wrapper(*args, **kwargs):
return func(*args, **kwargs)
return func_wrapper
#action(methods=['get'], detail=False, url_name="byname", url_path="byname")
#custom_decorator
def get_by_name(self, request):
# other code
I believe the issue is that the action decorator does not recognize the function after adding custom_decorator because the name is changed, so by adding #wraps(func) the function name stays the same. (#wraps docs)

Django rest framework- calling another class-based view

I have pored over several similar posts (and Calling a class-based view of an app from another app in same project seemed promising, but does not work), but some are older and none quite work for me. Here's my setup (using Django==2.0.6, djangorestframework==3.8.2)
I have a basic model (simplified here):
from django.db import models
class Resource(models.Model):
name = models.CharField(max_length=100, null=False)
I have a basic endpoint where I can list and create Resource instances:
from rest_framework import generics, permissions
from myapp.models import Resource
from myapp.serializers import ResourceSerializer
class ListAndCreateResource(generics.ListCreateAPIView):
queryset = Resource.objects.all()
serializer_class = ResourceSerializer
permission_classes = (permissions.IsAuthenticated,)
(afaik, the details of the serializer are not relevant, so that is left out).
Anyway, in addition to that basic endpoint, I have another API endpoint which performs some actions, but also creates some Resource objects in the process. Of course, I would like to make use of the functionality encapsulated in the ListAndCreateResource class so I only have to maintain one place where Resources are created.
I have tried:
Attempt 1:
class SomeOtherView(generics.CreateAPIView):
def post(self, request, *args, **kwargs):
# ... some other functionality...
# ...
response = ListAndCreateResource().post(request, *args, **kwargs)
# ... more functionality...
return Response({'message': 'ok'})
Unfortunately, that does not work for me. In my trace, I get:
File "/home/projects/venv/lib/python3.5/site-packages/rest_framework/generics.py", line 111, in get_serializer
kwargs['context'] = self.get_serializer_context()
File "/home/projects/venv/lib/python3.5/site-packages/rest_framework/generics.py", line 137, in get_serializer_context
'request': self.request,
AttributeError: 'ListAndCreateResource' object has no attribute 'request'
Attempt 2:
This attempt tries to use the as_view method which is part of all Django class-based views:
class SomeOtherView(generics.CreateAPIView):
def post(self, request, *args, **kwargs):
# ... some other functionality...
# ...
response = ListAndCreateResource.as_view()(request, *args, **kwargs)
# ... more functionality...
return Response({'message': 'ok'})
But that gives up with:
AssertionError: The `request` argument must be an instance of `django.http.HttpRequest`, not `rest_framework.request.Request`
So my question is...is there a straightforward way to do this? I can access the _request attribute of the rest_framework.request.Request object (which is of type django.http.HttpRequest, but then I do not have any of the authentication details that are contained in the DRF Request object (indeed, my ListAndCreateResource returns a 403 if I use response = ListAndCreateResource().as_view()(request._request, *args, **kwargs) in attempt #2 above).
Thanks in advance!
This seems a bit late, but in case anyone is wondering.
class SomeOtherView(generics.CreateAPIView):
def post(self, request, *args, **kwargs):
# ... some other functionality...
# ...
response = ListAndCreateResource.as_view()(request, *args, **kwargs)
# ... more functionality...
return Response({'message': 'ok'})
The as_view() is a function that when called, returns a function that takes a request, *args, **kwargs. So basically, a class view is an encapsulated function view.
I think you can use request._request. The DRF keeps a protected member _request, as is, received from the API call.
You can access the request with self.request in class based views.

Django : when client request http methods that are not defined in the view class

I am now learning django rest framework library. And when I read the tutorial I suddenly curious about that what will happen if client request http methods that are not defined in the view class. For example If I write the code like below
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from django.http import Http404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
class SnippetList(APIView):
"""
List all snippets, or create a new snippet.
"""
def get(self, request, format=None):
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return Response(serializer.data)
def post(self, request, format=None):
serializer = SnippetSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
In this case I define the "get" and "post" methods in the View class. what if client request the "put" or "delete" methods than what happens? I read the django docs and it saids View class dispatch the http method by using dispatch(). But what is happening? Thanks in advance!
You can take a look at the source on Github. The dispatch method checks which HTTP verb was used, and calls the appropriate function, or returns 405 - Method not allowed status code when the verb is not valid/expected (http_method_not_allowed is a django built-in method in the base View class that just returns the 405 status code).
The relevant portion is pasted below:
# Get the appropriate handler method
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(),
self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
response = handler(request, *args, **kwargs)
Essentially the same thing is done in django's own views (dispatch in django.views.generic.View):
def dispatch(self, request, *args, **kwargs):
# Try to dispatch to the right method; if a method doesn't exist,
# defer to the error handler. Also defer to the error handler if the
# request method isn't on the approved list.
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
return handler(request, *args, **kwargs)
If you're ever developing with django's own view classes, "Classy Class-Based Views" is a very helpful resource.

Tastypie-django custom error handling

I would like to return some JSON responses back instead of just returning a header with an error code. Is there a way in tastypie to handle errors like that?
Figured it out eventually. Here's a good resource to look at, if anyone else needs it. http://gist.github.com/1116962
class YourResource(ModelResource):
def wrap_view(self, view):
"""
Wraps views to return custom error codes instead of generic 500's
"""
#csrf_exempt
def wrapper(request, *args, **kwargs):
try:
callback = getattr(self, view)
response = callback(request, *args, **kwargs)
if request.is_ajax():
patch_cache_control(response, no_cache=True)
# response is a HttpResponse object, so follow Django's instructions
# to change it to your needs before you return it.
# https://docs.djangoproject.com/en/dev/ref/request-response/
return response
except (BadRequest, ApiFieldError), e:
return HttpBadRequest({'code': 666, 'message':e.args[0]})
except ValidationError, e:
# Or do some JSON wrapping around the standard 500
return HttpBadRequest({'code': 777, 'message':', '.join(e.messages)})
except Exception, e:
# Rather than re-raising, we're going to things similar to
# what Django does. The difference is returning a serialized
# error message.
return self._handle_500(request, e)
return wrapper
You could overwrite tastypie's Resource method _handle_500(). The fact that it starts with an underscore indeed indicates that this is a "private" method and shouldn't be overwritten, but I find it a cleaner way than having to overwrite wrap_view() and replicate a lot of logic.
This is the way I use it:
from tastypie import http
from tastypie.resources import ModelResource
from tastypie.exceptions import TastypieError
class MyResource(ModelResource):
class Meta:
queryset = MyModel.objects.all()
fields = ('my', 'fields')
def _handle_500(self, request, exception):
if isinstance(exception, TastypieError):
data = {
'error_message': getattr(
settings,
'TASTYPIE_CANNED_ERROR',
'Sorry, this request could not be processed.'
),
}
return self.error_response(
request,
data,
response_class=http.HttpApplicationError
)
else:
return super(MyResource, self)._handle_500(request, exception)
In this case I catch all Tastypie errors by checking if exception is an instance of TastypieError and return a JSON reponse with the message "Sorry, this request could not be processed.". If it's a different exception, I call the parent _handle_500 using super(), which will create a django error page in development mode or send_admins() in production mode.
If you want to have a specific JSON response for a specific exception, just do the isinstance() check on a specific exception. Here are all the Tastypie exceptions:
https://github.com/toastdriven/django-tastypie/blob/master/tastypie/exceptions.py
Actually I think there should be a better/cleaner way to do this in Tastypie, so I opened a ticket on their github.

How to install a custom upload handler in a Django generic view?

In order to provide progress feedback of file uploads I need to install a custom upload handler for a specific view. This is documented for "classical" Django views at
https://docs.djangoproject.com/en/dev/topics/http/file-uploads/#modifying-upload-handlers-on-the-fly
For generic views, however, I could not find any instructions and I came up with the following:
from django.utils import importlib
from django.core.exceptions import ImproperlyConfigured
from django.views.decorators.csrf import csrf_protect
class UploadHandlerMixin(object):
'''
A mixin for Django generic views that installs a custom upload handler in front of
the current chain of upload handlers.
You specify the handler to install by overriding the 'upload_handler' attribute of
the class, specifying the module and class name in the form 'path.to.module.class':
class MyView(UploadHandlerMixin, View):
upload_handler = 'path.to.module.MyUploadHandler'
If you do not override 'upload_handler', no additional upload handler will be
installed.
If the CsrfViewMiddleware is installed (which is the default) then you must use
your view as follows in your urls.py:
from django.views.decorators.csrf import csrf_exempt
url(r'^.../$', csrf_exempt(MyView.as_view()), ...),
Internally, the UploadHandlerMixin mixin will install the upload handler and then
perform the CSRF check. (This is necessary because the CSRF check inspects
request.POST, and afterwards upload handlers cannot be changed, see documentation
link given below.)
The handler is installed as described in the Django documentation "Modifying upload handlers
on the fly", see https://docs.djangoproject.com/en/dev/topics/http/file-uploads/#modifying-upload-handlers-on-the-fly
'''
upload_handler = None
def dispatch(self, request, *args, **kwargs):
if not self.upload_handler is None:
request.upload_handlers.insert(0, UploadHandlerMixin._instantiate_upload_handler(self.upload_handler, request))
return _uploadhandler_dispatch(request, self, *args, **kwargs)
#staticmethod
def _instantiate_upload_handler(path, *args, **kwargs):
i = path.rfind('.')
module, attr = path[:i], path[i+1:]
try:
mod = importlib.import_module(module)
except ImportError, e:
raise ImproperlyConfigured('Error importing upload handler module %s: "%s"' % (module, e))
except ValueError, e:
raise ImproperlyConfigured('Error importing upload handler module. Is FILE_UPLOAD_HANDLERS a correctly defined list or tuple?')
try:
cls = getattr(mod, attr)
except AttributeError:
raise ImproperlyConfigured('Module "%s" does not define a "%s" upload handler backend' % (module, attr))
return cls(*args, **kwargs)
#csrf_protect
def _uploadhandler_dispatch(request, view, *args, **kwargs):
return super(UploadHandlerMixin, view).dispatch(request, *args, **kwargs)
Is this the "recommended way" to accomplish the task? Is it okay security-wise?
Answering myself, an alternative to a service-side solution (like Django with a custom upload handler) is a client-side solution like JQuery File Upload. It uses the fact that more recent browsers allow the upload progress to be read on the client. In my case, this was much simpler to implement and causes no additional server load.