Django Caching for Authenticated Users Only
Asked Answered
W

5

23

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

  1. Creating a decorators.py file in my app
  2. Adding the below code to it
  3. Importing the function in views.py
  4. 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.

Wellspring answered 26/7, 2012 at 2:34 Comment(1)
No middleware solution below.Titograd
E
32

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.

Epifocal answered 28/7, 2012 at 19:22 Comment(5)
Thanks for the answer. I'll admit, I was surprised that Django doesn't have a "built in" way to accomplish this. Perhaps they'll add it in the future.Wellspring
Yep, might worth adding it. Actually Django provides only the basic features, while extending them is quite easy indeed. I just saw something missing in the answer - to disable caching for non-authenticated users - just return view_func(request, *args, **kwargs) without applying the cache_page decorator. I think it is easy to modify according to the actual needs.Epifocal
5 years later and so helpful. Thank you Tisho!Indigestion
I don't know if I made something wrong, but I had to add the never_cache before this decorator to work properly in Django 2.2.Beggarweed
Note that in Django 1.10+ the is_authenticated is an attribute, not a function. So you need to change the request.user.is_authenticated() part to request.user.is_authenticated stackoverflow.com/a/3644910Preciosa
T
4

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', {})
Titograd answered 10/1, 2014 at 2:35 Comment(0)
S
0

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.

Schadenfreude answered 28/7, 2012 at 19:31 Comment(0)
L
0

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
Lumbago answered 1/9, 2022 at 23:50 Comment(0)
S
-2

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.

Sendai answered 20/5, 2020 at 14:7 Comment(1)
This wouldn't share the same cached value for all authenticated users, which is what is requested here.Health

© 2022 - 2024 — McMap. All rights reserved.