Django rest framework custom 404 error page - django

How to customize Django rest framework error page. Can't find about it. In my rest action code:
from django.http import Http404
class SomeAction(APIView):
def get(self, *args, **kwargs):
raise Http404()
I need to display custom error page in prod.

from django.http import HttpResponse
from django.shortcuts import render_to_response
from django.template import loader, RequestContext, Context
from apps.settings import DEBUG
from libs.requestprovider.middleware import get_current_request
from rest_framework.views import exception_handler
def custom_exception_handler(exc):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc)
# Now add the HTTP status code to the response.
if response is not None:
response.data['status_code'] = response.status_code
if not DEBUG:
#errors can be more generic
if response.data['status_code'] == 404:
t = loader.get_template('404.html')
c = RequestContext(get_current_request(), {})
return HttpResponse(t.render(c), content_type="text/html")
return response

TL;DR
class YourAPIView(APIView):
#staticmethod
def my_exception_handler(exc, context):
response = exception_handler(exc, context)
if if response.status_code >= 400:
return HttpResponse(loader.get_template('your.html').render(context), content_type='text/html')
return response
def get_exception_handler(self):
return self.my_exception_handler
Long story
This link (custom-exception-handling) tells you to set EXCEPTION_HANDLER to yours, and you will notice that the default settings is rest_framework.views.exception_handler
Of course, you can follow the tutorial, but if you don't want to use global settings to control, then see the following.
I suggest you set breakpoints on the function (rest_framework.views.exception_handler) to see what is going on, and then you will know all proceeding.
If you don't want to debug, I list the key points below.
# rest_framework\views.py
class APIView(View):
...
def dispatch(self, request, *args, **kwargs):
...
try:
...
except Exception as exc:
response = self.handle_exception(exc) # <-- focus here
self.response = self.finalize_response(request, response, *args, **kwargs)
return self.response
def handle_exception(self, exc):
...
exception_handler = self.get_exception_handler() # <-- focus here
context = self.get_exception_handler_context()
response = exception_handler(exc, context)
...
return response
def get_exception_handler(self):
return self.settings.EXCEPTION_HANDLER
From the above code, you know you can change get_exception_handler then all down!
here is an example:
# views.py
from rest_framework import generics
from django.http import HttpResponse
from rest_framework import status
from django.template import loader
from rest_framework.views import exception_handler
class YourAPIView(generics.ListAPIView):
...
def get(self, request, *args, **kwargs):
return HttpResponse('ok', content_type='text/html')
#staticmethod
def my_exception_handler(exc, context):
response = exception_handler(exc, context) # <-- this is the default exception_handler
if response.status_code in (status.HTTP_400_BAD_REQUEST, status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN):
my_400_and_403_template = loader.get_template('error.html') # settings.py -> TEMPLATES = [{'DIRS': [ str(Path(BASE_DIR)/Path('templates/your_custom_dir')).replace('\\', '/') ]}] # Where your_custom_dir should contain error.html
return HttpResponse(my_400_and_403_template.render(context), content_type='text/html')
return response
def get_exception_handler(self):
return self.my_exception_handler
# def handle_exception(self, exc):
# return super().handle_exception(exc)

Related

Django-Rest-Framework: No Attribute HelloViewSet

I am trying to build a simple REST API.
I tried adding a viewset, somehow I get an error that there is no such attribute.
If I remove the viewset and just run using the APIView, it loads just fine. I am stuck. What could be the problem? What should I do to make it work?
Here's the rest_profiles.views.py FILE:
from django.shortcuts import render
from rest_framework import viewsets
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .serializers import HelloSerializer
# Create your views here.
class HelloApiView(APIView):
'''Test API View'''
serializer_class = HelloSerializer
def get(self, request, format=None):
'''Returns a list of API features'''
an_apiview = [
'Uses HTTP methods as functions (get, psot, put, patch, delete)',
'Similar to Django View',
'Mapped manually to URLs'
]
return Response({'message': 'Hello from HelloAPIVIew', 'an_apiview': an_apiview})
def post(self, request):
'''Create Hello Message'''
serializer = HelloSerializer(data=request.data)
if serializer.is_valid():
name = serializer.data.get('name')
message = 'Hello {0}'.format(name)
return Response({'message': message})
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def put(self, request):
'''Handles Updates'''
serializer = HelloSerializer(data=request.data)
return Response({'message': 'put'})
def patch(self, request, pk=None):
'''Handles partial Updates'''
serializer = HelloSerializer(data=request.data)
return Response({'message': 'patch'})
def delete(self, request, pk=None):
'''Handles deleting items'''
serializer = HelloSerializer(data=request.data)
return Response({'message': 'delete'})
class HelloViewSet(viewsets.ViewSet):
'''Test API Viewset'''
def list(self, request):
'''Return Hello Message'''
a_viewset = [
'Uses Actions (list, create, retrieve, update, partial_update)',
'automatically maps to URLs using Router',
'More functionality with less code'
]
return Response({'message': 'Hello', 'a_viewset': a_viewset})
and Here is the urls.py file:
from django.conf.urls import url
from django.conf.urls import include
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register('hello-viewset', views.HelloViewSet,
base_name='hello-viewset')
urlpatterns = [
url('hello-view/', views.HelloApiView.as_view()),
url('', include(router.urls))
]
What could possibly be the problem? And the possible work-around?
If your code is pasted correctly, it looks like your HelloViewSet is inside your HelloApiView.
Indentation is important in Python. You need to unindent this code:
class HelloViewSet(viewsets.ViewSet):
'''Test API Viewset'''
def list(self, request):
'''Return Hello Message'''
a_viewset = [
'Uses Actions (list, create, retrieve, update, partial_update)',
'automatically maps to URLs using Router',
'More functionality with less code'
]
return Response({'message': 'Hello', 'a_viewset': a_viewset})
This will put it directly in your views module so it can be imported.

SynchronousOnlyOperation Error in with django 3 and django channels

I had a django 2 app and i used django channels for socket connection.
i just update django to version 3. and now daphne show this error when i try to make a socket connection. i had not any problem with django 2.
[Failure instance: Traceback: <class 'django.core.exceptions.SynchronousOnlyOperation'>: You cannot call this from an async context - use a thread or sync_to_async.
/home/ubuntu/pl_env/lib/python3.6/site-packages/autobahn/websocket/protocol.py:2844:processHandshake
/home/ubuntu/pl_env/lib/python3.6/site-packages/txaio/tx.py:429:as_future
/home/ubuntu/pl_env/lib/python3.6/site-packages/twisted/internet/defer.py:151:maybeDeferred
/home/ubuntu/pl_env/lib/python3.6/site-packages/daphne/ws_protocol.py:83:onConnect
--- <exception caught here> ---
/home/ubuntu/pl_env/lib/python3.6/site-packages/twisted/internet/defer.py:151:maybeDeferred
/home/ubuntu/pl_env/lib/python3.6/site-packages/daphne/server.py:201:create_application
/home/ubuntu/pl_env/lib/python3.6/site-packages/channels/routing.py:54:__call__
/home/ubuntu/pl_env/lib/python3.6/site-packages/channels/security/websocket.py:37:__call__
/home/ubuntu/petroline_django/orders/token_auth.py:25:__call__
/home/ubuntu/pl_env/lib/python3.6/site-packages/django/db/models/manager.py:82:manager_method
/home/ubuntu/pl_env/lib/python3.6/site-packages/django/db/models/query.py:411:get
/home/ubuntu/pl_env/lib/python3.6/site-packages/django/db/models/query.py:258:__len__
/home/ubuntu/pl_env/lib/python3.6/site-packages/django/db/models/query.py:1261:_fetch_all
/home/ubuntu/pl_env/lib/python3.6/site-packages/django/db/models/query.py:57:__iter__
/home/ubuntu/pl_env/lib/python3.6/site-packages/django/db/models/sql/compiler.py:1142:execute_sql
/home/ubuntu/pl_env/lib/python3.6/site-packages/django/utils/asyncio.py:24:inner
it says the problem is in token_auth.py, line 25. this line is token = Token.objects.get(key=token_key)
this is my token_auth.py that handles token authentication.
from channels.auth import AuthMiddlewareStack
from django.contrib.auth.models import AnonymousUser
from django.db import close_old_connections
from rest_framework.authtoken.models import Token
class TokenAuthMiddleware:
"""
Token authorization middleware for Django Channels 2
see:
https://channels.readthedocs.io/en/latest/topics/authentication.html#custom-authentication
"""
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
headers = dict(scope['headers'])
if b'authorization' in headers:
try:
token_name, token_key = headers[b'authorization'].decode().split()
if token_name == 'Token':
# Close old database connections to prevent usage of timed out connections
close_old_connections()
token = Token.objects.get(key=token_key)
scope['user'] = token.user
except Token.DoesNotExist:
scope['user'] = AnonymousUser()
return self.inner(scope)
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
Thanks to #ivissani answers, i fixed my TokenAuthMiddleware with some of SessionMiddleware codes.
I have opened an issue for django channels about updating docs.
#database_sync_to_async
def get_user(token_key):
try:
return Token.objects.get(key=token_key).user
except Token.DoesNotExist:
return AnonymousUser()
class TokenAuthMiddleware:
"""
Token authorization middleware for Django Channels 2
see:
https://channels.readthedocs.io/en/latest/topics/authentication.html#custom-authentication
"""
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
return TokenAuthMiddlewareInstance(scope, self)
class TokenAuthMiddlewareInstance:
def __init__(self, scope, middleware):
self.middleware = middleware
self.scope = dict(scope)
self.inner = self.middleware.inner
async def __call__(self, receive, send):
headers = dict(self.scope['headers'])
if b'authorization' in headers:
token_name, token_key = headers[b'authorization'].decode().split()
if token_name == 'Token':
self.scope['user'] = await get_user(token_key)
inner = self.inner(self.scope)
return await inner(receive, send)
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
Fixed by using #database_sync_to_async decorator:
(see https://github.com/MathieuB1/KOREK-backend/commit/ff6a4b542cda583a1d5abbf200a5d57ef328cae0#diff-95e545fb374a9ed7e8af8c31087a3f29)
import jwt, re
import traceback
from channels.auth import AuthMiddlewareStack
from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser
from django.conf import LazySettings
from jwt import InvalidSignatureError, ExpiredSignatureError, DecodeError
from django.contrib.auth.models import User
from django.contrib.sessions.models import Session
settings = LazySettings()
from django.db import close_old_connections
#database_sync_to_async
def close_connections():
close_old_connections()
#database_sync_to_async
def get_user(user_jwt):
try:
return User.objects.get(id=user_jwt)
except User.DoesNotExist:
return AnonymousUser()
class TokenAuthMiddleware:
"""
Token authorization middleware for Django Channels 2
"""
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
# Close old database connections to prevent usage of timed out connections
close_connections()
# Login with JWT
try:
if scope['subprotocols'][0] != 'None':
token = scope['subprotocols'][0]
try:
user_jwt = jwt.decode(
token,
settings.SECRET_KEY,
)
scope['user'] = get_user(user_jwt['user_id'])
return self.inner(scope)
except (InvalidSignatureError, KeyError, ExpiredSignatureError, DecodeError):
traceback.print_exc()
pass
except Exception as e:
traceback.print_exc()
else:
raise
Refer to this part of the documentation. There it is explained that Django 3 will raise such exception if you try to use the ORM from within an async context (which seems to be the case).
As Django Channels documentation explains solution would be to use sync_to_async as follows:
from channels.db import database_sync_to_async
class TokenAuthMiddleware:
# more code here
async def __call__(self, scope):
# and some more code here
token = await database_sync_to_async(Token.objects.get(key=token_key))()
Although please bear in mind that I haven't used this in my life, so it may fail.
Note that in the Django channels documentation it says that you need to write your query in a separate method. So if this fails try doing that.

Using Django UserPassesTestMixin with LoginRequiredMixin to go to an authorized URL

I'm trying to write a mixin that will protect views by first checking if someone is logged in and then if they have been onboarded. It seems to work, by blocking views it's attached to, but it the URLjust goes to a 403 forbidden. Any ideas on how to get it to go to the named url?
from django.contrib.auth.mixins import UserPassesTestMixin
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.contrib.auth.mixins import LoginRequiredMixin
class OnboardedMixin(LoginRequiredMixin, UserPassesTestMixin):
"""
a custom mixin that checks to see if the user has been onboarded yet
"""
def test_func(self):
if self.request.user.onboarded and self.request.user.is_active:
return True
def get_login_url(self):
return redirect('onboarding',)
Rather than taking this approach, maybe its best to use a decorator instead. For example:
from django.contrib.auth.decorators import login_required
def my_login_required(function):
def wrapper(obj, request, *args, **kw):
decorated_view_func = login_required(request)
if not decorated_view_func.user.is_authenticated:
return decorated_view_func(request) # restricts without login and sends to signin view
if request.user.onboarded and request.user.is_active:
return function(obj, request, *args, **kw)
return HttpResponseRedirect("/onboarding/")
return wrapper
And use this decorator in desired views:
class SomeView(DetailView):
...
#my_login_requried
def dispatch(self, *args, **kwargs):
return super(SomeView, self).dispatch(*args, **kwargs)

Django, extract kwargs from url

Suppose I have a url pattern
url(r'^/my_app/class/(?P<class_id>\d+)/$', my_class.my_class, name='my_class')
For a given url
http://example.com/my_app/class/3/, I'd like to get class_id.
I can do this with regex myself.
I am wondering if there's a utility function for this since Django is already doing this to resolve url to a view.
There is an example of using resolve() function in Django docs. Value of next variable has HTTP url to be parsed with urlparse() / resolve():
https://docs.djangoproject.com/en/1.11/ref/urlresolvers/#resolve
from django.urls import resolve
from django.http import HttpResponseRedirect, Http404
from django.utils.six.moves.urllib.parse import urlparse
def myview(request):
next = request.META.get('HTTP_REFERER', None) or '/'
response = HttpResponseRedirect(next)
# modify the request and response as required, e.g. change locale
# and set corresponding locale cookie
view, args, kwargs = resolve(urlparse(next)[2])
kwargs['request'] = request
try:
view(*args, **kwargs)
except Http404:
return HttpResponseRedirect('/')
return response
If you are using generic views or just views, you can do something like this:
class myView(View): # or UpdateView, CreateView, DeleteView
template_name = 'mytemplate.html'
def get(self, request, *args, **kwargs):
context = {}
class_id = self.kwargs['class_id']
# do something with your class_id
return render(request, self.template_name, context)
# same with the post method.
Yes, you can do like this
def my_class(request, class_id):
# this class_id is the class_id in url
# do something;

How disable return of HTML error page with django rest framework?

If I have a error outside the libs of DRF, django send back the HTML of the error instead of the proper error response use by DRF.
For example:
#api_view(['POST'])
#permission_classes((IsAuthenticated,))
def downloadData(request):
print request.POST['tables']
Return the exception MultiValueDictKeyError: "'tables'". And get back the full HTML. How get only the error a JSON?
P.D:
This is the final code:
#api_view(['GET', 'POST'])
def process_exception(request, exception):
# response = json.dumps({'status': status.HTTP_500_INTERNAL_SERVER_ERROR,
# 'message': str(exception)})
# return HttpResponse(response,
# content_type='application/json; charset=utf-8')
return Response({
'error': True,
'content': unicode(exception)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
class ExceptionMiddleware(object):
def process_exception(self, request, exception):
# response = json.dumps({'status': status.HTTP_500_INTERNAL_SERVER_ERROR,
# 'message': str(exception)})
# return HttpResponse(response,
# content_type='application/json; charset=utf-8')
print exception
return process_exception(request, exception)
One way of returning json would be to catch the exceptions and return proper response (assuming you're using JSONParser as default parser):
from rest_framework.response import Response
from rest_framework import status
#api_view(['POST'])
#permission_classes((IsAuthenticated,))
def downloadData(request):
try:
print request.POST['tables']
except:
return Response({'error': True, 'content': 'Exception!'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response({'error': False})
UPDATE
For global wise use-case the correct idea would be to put the json response in exception middleware.
You can find example in this blog post.
In your case you need to return DRF response, so if any exception gets raised it will end up in the process_exception:
from rest_framework.response import Response
class ExceptionMiddleware(object):
def process_exception(self, request, exception):
return Response({'error': True, 'content': exception}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
You can replace the default error handlers by specifying a custom handler in your URLConf as documented here
Something like this:
# In urls.py
handler500 = 'my_app.views.api_500'
and:
# In my_app.views
def api_500(request):
response = HttpResponse('{"detail":"An Error Occurred"}', content_type="application/json", status=500)
return response
I hope that helps.
As you can see in the documentation.
All you need to do is configure settings.
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
'rest_framework.parsers.JSONParser',
),
'EXCEPTION_HANDLER': 'core.views.api_500_handler',
}
And pointing to a view that will recieve (exception, context)
Like this:
from rest_framework.views import exception_handler
...
def api_500_handler(exception, context):
response = exception_handler(exception, context)
try:
detail = response.data['detail']
except AttributeError:
detail = exception.message
response = HttpResponse(
json.dumps({'detail': detail}),
content_type="application/json", status=500
)
return response
My implementation is like this because if a expected rest framework exception is raised, like a 'exceptions.NotFound', exception.message will be empty. Thats why Im first calling to exception_handler of rest framework. If is an expected exception, I will get its message.