Are POST requests cached in Django? - django

I want to get some view cached depending on the POST data sent to it.
Does the django.views.decorators.cache.cache_page decorator do that automatically or do I need to tweak it somehow? In the latter case, how should I do that?
I'm trying to cache GraphQL POST requests.

No, POST responses are never cached:
if request.method not in ('GET', 'HEAD'):
request._cache_update_cache = False
return None # Don't bother checking the cache.
(from FetchFromCacheMiddleware in django.middleware.cache).
You'll have to implement something yourself using the low-level cache API. It's most unusual to cache a response to a POST request, since a POST request is meant to change things in the database and the result is always unique to the particular request. You'll have to think about what exactly you want to cache.

I ended up creating a custom decorator which caches responses based on request path, query parameters, and posted data:
# myproject/apps/core/caching.py
import hashlib
import base64
from functools import wraps
from django.core.cache import cache
from django.conf import settings
def make_hash_sha256(o):
hasher = hashlib.sha256()
hasher.update(repr(make_hashable(o)).encode())
return base64.b64encode(hasher.digest()).decode()
def make_hashable(o):
if isinstance(o, (tuple, list)):
return tuple((make_hashable(e) for e in o))
if isinstance(o, dict):
return tuple(sorted((k,make_hashable(v)) for k,v in o.items()))
if isinstance(o, (set, frozenset)):
return tuple(sorted(make_hashable(e) for e in o))
return o
def cache_get_and_post_requests(duration=600):
def view_decorator(view):
#wraps(view)
def view_wrapper(request, *args, **kwargs):
# TODO: make the key also dependable on the user or cookies if necessary
cache_key = "-".join((
settings.CACHE_MIDDLEWARE_KEY_PREFIX,
make_hash_sha256((
request.path,
list(request.GET.items()),
list(request.POST.items()),
request.body,
)),
))
cached_response = cache.get(cache_key)
if cached_response:
return cached_response
response = view(request, *args, **kwargs)
cache.set(cache_key, response, duration)
return response
return view_wrapper
return view_decorator
Then I can use it in the URL configuration like so:
# myproject/urls.py
from django.urls import path
from django.conf.urls.i18n import i18n_patterns
from graphene_django.views import GraphQLView
from myproject.apps.core.caching import cache_get_and_post_requests
urlpatterns = i18n_patterns(
# …
path("graphql/", cache_get_and_post_requests(60*5)(GraphQLView.as_view(graphiql=True))),
)

Related

Django REST Framework: COUNT query generated by PageNumberPagination is slow

I don't want to run count queries on views where count is not needed.
How can I turn it off?
I found the following workaround in another post on stackoverflow.
The count query is not fired, but I can see pages that have no data.
(I can see up to ?page=10000, even though there are only about 10 pages.)
#settings.py
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
...
}
import sys
from django.core.paginator import Paginator
from django.utils.functional import cached_property
from rest_framework.pagination import PageNumberPagination
class CustomPaginatorClass(Paginator):
#cached_property
def count(self):
return sys.maxsize
class CustomPagination(PageNumberPagination):
django_paginator_class = CustomPaginatorClass
You can raise a HTTP 404 in case the page does not contain any elements with:
from rest_framework.exceptions import NotFound
class CustomPagination(PageNumberPagination):
django_paginator_class = CustomPaginatorClass
def paginate_queryset(self, queryset, request, view=None):
data = super().paginate_queryset(queryset, request, view=view)
if not data:
raise NotFound('No data found for this page')
return data
This will fetch the paginated data with one query, and then we check if there is at least one element. If that is not the case, we know that the page we are using should not exist.

Django, all views require login except `#login_not_required` decoratored views

Django is centered towards allow access to all the majority of the time, and restricting acces on the exception.
I am making a site whereby I would like access restricted the most of the time, and the rare case is open to all. Ensuring that one has added a #login_required decorate to all views is error prone.
I acheived this by creating a custom middleware like this. To keep track of open URLs, I defined a list, and then whenever I wished to open up a URL, I added it to the list, and the middleware checked the request path against this list, and allowed exceptions accordingly.
This method above works, but I often mess it up with changing urls, and other issues to do with code reuse on different sites.
Ideally I would like to create my own #login_not_requied decorator. How to get the class based view or function the request is going to call within the middleware, so I can check whether the view does not require login?
First create the decorator:
from functools import wraps
def login_not_required(obj):
"""Adds the attrbiute login_not_required = True to the object (func/class).
Use it as follows:
#login_not_required
class FooView(generic.View):
...
#login_not_required
def bar_view(request):
...
"""
#wraps(obj)
def decorator():
obj.login_not_required = True
return obj
return decorator()
Then create your own custom middleware:
from contextlib import suppress
from django.conf import settings
from django.shortcuts import redirect
NONE_AUTH_ACCOUNT_PATHS = [
settings.STATIC_URL,
'/accounts/login/',
'/accounts/password_reset/',
'/accounts/reset/',
'/favicon.ico',
'/terminal/login/',
'/terminal/login_failed/',
]
class RequireLoginCheck:
"""Middleware to require authentication on all views by default, except when allowed.
URLS can be opened by adding them to NONE_AUTH_ACCOUNT_PATHS, or by adding
the #login_not_required decorator.
Must appear below the sessions middleware because the sessions middleware
adds the user to the request, which is used by this middleware.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
return self.get_response(request)
def _is_none_auth_path(self, path):
for none_auth_path in NONE_AUTH_ACCOUNT_PATHS:
if path.startswith(none_auth_path):
return True
return False
def _is_login_not_required(self, view_func):
with suppress(AttributeError):
# If a class with the #login_not_required decorator, will return True
return view_func.view_class.login_not_required
with suppress(AttributeError):
# If a function with the #login_not_required decorator, will return True
return view_func.login_not_required
return False
def process_view(self, request, view_func, view_args, view_kwargs):
"""https://docs.djangoproject.com/en/stable/topics/http/middleware/#other-middleware-hooks"""
if not (
request.user.is_authenticated
or self._is_login_not_required(view_func)
or self._is_none_auth_path(request.path)
):
if settings.LOGIN_URL != request.path:
# if next URL after login is the same login URL, then cyclic loop
return redirect('%s?next=%s' % (settings.LOGIN_URL, request.path))
else:
return redirect('%s?next=%s' % (settings.LOGIN_URL, '/'))
return None
Ensure you add your middleware to MIDDLEWARE bellow the sessions middleware, in settings.py!
If you have ways it can be improved, please comment below and we make it better for all.

Custom Middleware in Django with Exclusions

I require to check authentication via token during execution of certain views, while some views can be accessed without authentication.
So, how do i make a middleware and exclude some views from it.
Any other idea to solve this is appreciated.
I would suggest taking inspiration from the csrf middleware that Django provides
from django.utils.deprecation import MiddlewareMixin
class MyAuthenticationMiddleware(MiddlewareMixin):
def process_view(self, request, callback, callback_args, callback_kwargs):
if getattr(callback, 'my_exempt_flag', False):
return None
# Authentication goes here
# Return None if authentication was successful
# Return a HttpResponse with some error status if not successful
And create a decorator to wrap your views
from functools import wraps
def exempt_from_my_authentication_middleware(view_func):
def wrapped_view(*args, **kwargs):
return view_func(*args, **kwargs)
wrapped_view.my_exempt_flag = True
return wraps(view_func)(wrapped_view)
Can be used like so
#exempt_from_my_authentication_middleware
def my_view(request):
# TODO

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;

#login_required for multiple views

I am going to have probably over 20 views. All of them require the user to authenticate first. Do I have to put #login_required over each one or is there a better way?
https://docs.djangoproject.com/en/1.6/topics/auth/default/#django.contrib.auth.decorators.login_required
I ended up making a new file in my npage app directory called lockdown.py and pasted the code from this solution:
import re
from django.conf import settings
from django.contrib.auth.decorators import login_required
class RequireLoginMiddleware(object):
"""
Middleware component that wraps the login_required decorator around
matching URL patterns. To use, add the class to MIDDLEWARE_CLASSES and
define LOGIN_REQUIRED_URLS and LOGIN_REQUIRED_URLS_EXCEPTIONS in your
settings.py. For example:
------
LOGIN_REQUIRED_URLS = (
r'/topsecret/(.*)$',
)
LOGIN_REQUIRED_URLS_EXCEPTIONS = (
r'/topsecret/login(.*)$',
r'/topsecret/logout(.*)$',
)
------
LOGIN_REQUIRED_URLS is where you define URL patterns; each pattern must
be a valid regex.
LOGIN_REQUIRED_URLS_EXCEPTIONS is, conversely, where you explicitly
define any exceptions (like login and logout URLs).
"""
def __init__(self):
self.required = tuple(re.compile(url) for url in settings.LOGIN_REQUIRED_URLS)
self.exceptions = tuple(re.compile(url) for url in settings.LOGIN_REQUIRED_URLS_EXCEPTIONS)
def process_view(self, request, view_func, view_args, view_kwargs):
# No need to process URLs if user already logged in
if request.user.is_authenticated():
return None
# An exception match should immediately return None
for url in self.exceptions:
if url.match(request.path):
return None
# Requests matching a restricted URL pattern are returned
# wrapped with the login_required decorator
for url in self.required:
if url.match(request.path):
return login_required(view_func)(request, *view_args, **view_kwargs)
# Explicitly return None for all non-matching requests
return None
After that in settings.py I added this to MIDDLEWARE_CLASSES...
MIDDLEWARE_CLASSES = (
# ...
'npage.lockdown.RequireLoginMiddleware',
)
And of course, these lines to lock the whole site down:
LOGIN_REQUIRED_URLS = (
r'/(.*)$',
)
LOGIN_REQUIRED_URLS_EXCEPTIONS = (
r'/login(.*)$',
r'/logout(.*)$',
)
As of Django 3+, you have to do like followings:
Step 1: Create a new file anything.py in your yourapp directory and write the following:
import re
from django.conf import settings
from django.contrib.auth.decorators import login_required
//for registering a class as middleware you at least __init__() and __call__()
//for this case we additionally need process_view() which will be automatically called by Django before rendering a view/template
class ClassName(object):
//need for one time initialization, here response is a function which will be called to get response from view/template
def __init__(self, response):
self.get_response = response
self.required = tuple(re.compile(url) for url in settings.AUTH_URLS)
self.exceptions = tuple(re.compile(url)for url in settings.NO_AUTH_URLS)
def __call__(self, request):
//any code written here will be called before requesting response
response = self.get_response(request)
//any code written here will be called after response
return response
//this is called before requesting response
def process_view(self, request, view_func, view_args, view_kwargs):
//if authenticated return no exception
if request.user.is_authenticated:
return None
//if found in allowed exceptional urls return no exception
for url in self.exceptions:
if url.match(request.path):
return None
//return login_required()
for url in self.required:
if url.match(request.path):
return login_required(view_func)(request, *view_args, **view_kwargs)
//default case, no exception
return None
Step 2: Add this anything.py to Middleware[] in project/settings.py like followings
MIDDLEWARE = [
// your previous middleware
'yourapp.anything.ClassName',
]
Step 3: Also add the following snippet into project/settings.py
AUTH_URLS = (
//i am disallowing all url
r'(.*)',
)
NO_AUTH_URLS = (
r'/admin(.*)$',
)