Django Caching for Authenticated Users Only
Question:
Question
In Django, how can create a single cached version of a page (same for all users) that’s only visible to authenticated users?
Setup
The pages I wish to cache are only available to authenticated users (they use @login_required
on the view). These pages are the same for all authenticated users (e.g. no need to setup vary_on_headers
based on unique users).
However, I don’t want these cached pages to be visible to non-authenticated users.
What I’ve tried so far
- Page level cache (shows pages intended for logged in users to non-logged in users)
- Looked into using
vary_on_headers
, but I don’t need individually cached pages for each user
- I checked out template fragment caching, but unless I’m confused, this won’t meet my needs
- Substantial searching (seems that everyone wants to do the reverse)
Thanks!
Example View
@login_required
@cache_page(60 * 60)
def index(request):
'''Display the home page'''
return render(request, 'index.html')
settings.py (relevant portion)
# Add the below for memcache
MIDDLEWARE_CLASSES += (
'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware',
)
# Enable memcache
# https://devcenter.heroku.com/articles/memcache#using_memcache_from_python
CACHES = {
'default': {
'BACKEND': 'django_pylibmc.memcached.PyLibMCCache'
}
}
Solution
Based on the answer by @Tisho I solved this problem by
- Creating a
decorators.py
file in my app
- Adding the below code to it
- Importing the function in
views.py
- Applying it as a decorator to the views I wanted to cache for logged in users only
decorators.py
from functools import wraps
from django.views.decorators.cache import cache_page
from django.utils.decorators import available_attrs
def cache_on_auth(timeout):
def decorator(view_func):
@wraps(view_func, assigned=available_attrs(view_func))
def _wrapped_view(request, *args, **kwargs):
if request.user.is_authenticated():
return cache_page(timeout)(view_func)(request, *args, **kwargs)
else:
return view_func(request, *args, **kwargs)
return _wrapped_view
return decorator
For logged in users, it would cache the page (or serve them the cached page) for non-logged in users, it would just give them the regular view, which was decorated with @login_required
and would require them to login.
Answers:
The default cache_page
decorator accepts a variable called key_prefix
. However, it can be passed as a string parameter only. So you can write your own decorator, that will dynamically modify this prefix_key
based on the is_authenticated
value. Here is an example:
from django.views.decorators.cache import cache_page
def cache_on_auth(timeout):
def decorator(view_func):
@wraps(view_func, assigned=available_attrs(view_func))
def _wrapped_view(request, *args, **kwargs):
return cache_page(timeout, key_prefix="_auth_%s_" % request.user.is_authenticated())(view_func)(request, *args, **kwargs)
return _wrapped_view
return decorator
and then use it on the view:
@cache_on_auth(60*60)
def myview(request)
Then, the generated cache_key will look like:
cache key:
views.decorators.cache.cache_page._auth_False_.GET.123456.123456
if the user is authenticated, and
cache key:
views.decorators.cache.cache_page._auth_True_.GET.789012.789012
if the user is not authenticated.
I’d advice against using the cache middleware if you want fine tuning of your caching abilities.
However, if you do want to persist keeping it, you could try something like (not saying it would work as is, but something similar to it):
@never_cache
def dynamic_index(request):
# do dynamic stuff
def cached_index(request):
return dynamic_index(request)
@never_cache
def index(request):
if request.user.is_authenticaded():
return cached_index(request)
return dynamic_index(request)
Worst case scenario, you can use cache.set('view_name', template_rendering_result)
, and cache.get
, to just cache the HTML manually.
If the @wrap decorator in the @Tisho answer makes your brain hurt, or if an explicit solution is better than an implicit one, here’s a simple procedural way to serve different cache results:
from django.views.decorators.cache import cache_page
def index(request):
"""
:type request: HttpRequest
"""
is_authenticated = request.user.is_authenticated()
if is_authenticated:
return render_user(request)
else:
return render_visitor(request)
@cache_page(5, key_prefix='user_cache')
def render_user(request):
print 'refreshing user_cache'
return render(request, 'home-user.html', {})
@cache_page(10, key_prefix='visitor_cache')
def render_visitor(request):
print 'refreshing visitor_cache'
return render(request, 'home-visitor.html', {})
I know this is a very old question, but we have a new alternative to solve this.
You can use the decorator cache_control passing private
as True
to protect your data.
Here’s an updated version of @Tisho’s answer that works for Django 3.0 (the old version no longer works since Python 2 support was dropped in Django 3.0):
from django.views.decorators.cache import cache_page
from functools import wraps, WRAPPER_ASSIGNMENTS
def cache_on_auth(timeout):
"""
Caches views up to two times: Once for authenticated users, and
once for unauthenticated users.
"""
def decorator(view_func):
@wraps(view_func, assigned=WRAPPER_ASSIGNMENTS)
def _wrapped_view(request, *args, **kwargs):
result = cache_page(
timeout,
key_prefix=(f"_auth_{request.user.is_authenticated}_"))
return result(view_func)(request, *args, **kwargs)
return _wrapped_view
return decorator
Question
In Django, how can create a single cached version of a page (same for all users) that’s only visible to authenticated users?
Setup
The pages I wish to cache are only available to authenticated users (they use @login_required
on the view). These pages are the same for all authenticated users (e.g. no need to setup vary_on_headers
based on unique users).
However, I don’t want these cached pages to be visible to non-authenticated users.
What I’ve tried so far
- Page level cache (shows pages intended for logged in users to non-logged in users)
- Looked into using
vary_on_headers
, but I don’t need individually cached pages for each user - I checked out template fragment caching, but unless I’m confused, this won’t meet my needs
- Substantial searching (seems that everyone wants to do the reverse)
Thanks!
Example View
@login_required
@cache_page(60 * 60)
def index(request):
'''Display the home page'''
return render(request, 'index.html')
settings.py (relevant portion)
# Add the below for memcache
MIDDLEWARE_CLASSES += (
'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware',
)
# Enable memcache
# https://devcenter.heroku.com/articles/memcache#using_memcache_from_python
CACHES = {
'default': {
'BACKEND': 'django_pylibmc.memcached.PyLibMCCache'
}
}
Solution
Based on the answer by @Tisho I solved this problem by
- Creating a
decorators.py
file in my app - Adding the below code to it
- Importing the function in
views.py
- Applying it as a decorator to the views I wanted to cache for logged in users only
decorators.py
from functools import wraps
from django.views.decorators.cache import cache_page
from django.utils.decorators import available_attrs
def cache_on_auth(timeout):
def decorator(view_func):
@wraps(view_func, assigned=available_attrs(view_func))
def _wrapped_view(request, *args, **kwargs):
if request.user.is_authenticated():
return cache_page(timeout)(view_func)(request, *args, **kwargs)
else:
return view_func(request, *args, **kwargs)
return _wrapped_view
return decorator
For logged in users, it would cache the page (or serve them the cached page) for non-logged in users, it would just give them the regular view, which was decorated with @login_required
and would require them to login.
The default cache_page
decorator accepts a variable called key_prefix
. However, it can be passed as a string parameter only. So you can write your own decorator, that will dynamically modify this prefix_key
based on the is_authenticated
value. Here is an example:
from django.views.decorators.cache import cache_page
def cache_on_auth(timeout):
def decorator(view_func):
@wraps(view_func, assigned=available_attrs(view_func))
def _wrapped_view(request, *args, **kwargs):
return cache_page(timeout, key_prefix="_auth_%s_" % request.user.is_authenticated())(view_func)(request, *args, **kwargs)
return _wrapped_view
return decorator
and then use it on the view:
@cache_on_auth(60*60)
def myview(request)
Then, the generated cache_key will look like:
cache key:
views.decorators.cache.cache_page._auth_False_.GET.123456.123456
if the user is authenticated, and
cache key:
views.decorators.cache.cache_page._auth_True_.GET.789012.789012
if the user is not authenticated.
I’d advice against using the cache middleware if you want fine tuning of your caching abilities.
However, if you do want to persist keeping it, you could try something like (not saying it would work as is, but something similar to it):
@never_cache
def dynamic_index(request):
# do dynamic stuff
def cached_index(request):
return dynamic_index(request)
@never_cache
def index(request):
if request.user.is_authenticaded():
return cached_index(request)
return dynamic_index(request)
Worst case scenario, you can use cache.set('view_name', template_rendering_result)
, and cache.get
, to just cache the HTML manually.
If the @wrap decorator in the @Tisho answer makes your brain hurt, or if an explicit solution is better than an implicit one, here’s a simple procedural way to serve different cache results:
from django.views.decorators.cache import cache_page
def index(request):
"""
:type request: HttpRequest
"""
is_authenticated = request.user.is_authenticated()
if is_authenticated:
return render_user(request)
else:
return render_visitor(request)
@cache_page(5, key_prefix='user_cache')
def render_user(request):
print 'refreshing user_cache'
return render(request, 'home-user.html', {})
@cache_page(10, key_prefix='visitor_cache')
def render_visitor(request):
print 'refreshing visitor_cache'
return render(request, 'home-visitor.html', {})
I know this is a very old question, but we have a new alternative to solve this.
You can use the decorator cache_control passing private
as True
to protect your data.
Here’s an updated version of @Tisho’s answer that works for Django 3.0 (the old version no longer works since Python 2 support was dropped in Django 3.0):
from django.views.decorators.cache import cache_page
from functools import wraps, WRAPPER_ASSIGNMENTS
def cache_on_auth(timeout):
"""
Caches views up to two times: Once for authenticated users, and
once for unauthenticated users.
"""
def decorator(view_func):
@wraps(view_func, assigned=WRAPPER_ASSIGNMENTS)
def _wrapped_view(request, *args, **kwargs):
result = cache_page(
timeout,
key_prefix=(f"_auth_{request.user.is_authenticated}_"))
return result(view_func)(request, *args, **kwargs)
return _wrapped_view
return decorator