cache_page with Class Based Views
Asked Answered
C

9

40

I'm trying to do cache_page with class based views (TemplateView) and i'm not able to. I followed instructions here:

Django--URL Caching Failing for Class Based Views

as well as here:

https://github.com/msgre/hazard/blob/master/hazard/urls.py

But I get this error:

cache_page has a single mandatory positional argument: timeout

I read the code for cache_page and it has the following:

if len(args) != 1 or callable(args[0]):
    raise TypeError("cache_page has a single mandatory positional argument: timeout")
cache_timeout = args[0]

which means it wont allow more than 1 argument. Is there any other way to get cache_page to work?? I have been digging into this for sometime...

It seems like the previous solutions wont work any longer

Cragsman answered 22/1, 2014 at 20:42 Comment(1)
If you don't append your urls.py we wont be able to help...Metralgia
S
64

According to the caching docs, the correct way to cache a CBV in the URLs is:

from django.views.decorators.cache import cache_page

url(r'^my_url/?$', cache_page(60*60)(MyView.as_view())),

Note that the answer you linked to is out of date. The old way of using the decorator has been removed (changeset).

Steamer answered 22/1, 2014 at 20:59 Comment(5)
It doesn't feel right hiding the caching in the urls.py, but well.Corrie
@J.C.Leitão in this answer, I was showing the correct way to cache a CBV in the urls, because the linked answer was out of date at that time. You don't have to do the caching in urls.py if you don't want to. You could override the dispatch method of your view if you prefer (you might find method_decorator useful).Steamer
@JorgeLeitão - Great solution, but I agree this does not "feel" right. Any updates on this in 2019 would be appreciated! Any thoughts Alasdair?Lotus
@ScottSkiles you can use method_decorator as I suggested before. You don't need to override dispatch to use it any more, see Dayson's answer. You might be able to find or write a mixin that works (I see madjardi has one in their answer), but I haven't used any so don't have any recommendations.Steamer
Re: putting the cache decorator in urls.py feels weird, but the reason is provided in the Django docs: This approach couples your view to the cache system, which is not ideal for several reasons. For instance, you might want to reuse the view functions on another, cache-less site, or you might want to distribute the views to people who might want to use them without being cached.Mannerly
S
36

You can simply decorate the class itself instead of overriding the dispatch method or using a mixin.

For example

from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator

@method_decorator(cache_page(60 * 5), name='dispatch')
class ListView(ListView):
...

Django docs on decorating a method within a class based view.

Saxecoburggotha answered 3/2, 2018 at 12:4 Comment(2)
How do you test and invalidate cache page? I am having issues when testing it manually. It still serves cache from disk even though my redis keys are empty.Mikaela
Be careful not to decorate the dispatch() method with cache_page() if using LoginRequiredMixin. If a logged-in user populates the cache, the page will become publicly available.Secondhand
I
32

yet another good example CacheMixin from cyberdelia github

class CacheMixin(object):
    cache_timeout = 60

    def get_cache_timeout(self):
        return self.cache_timeout

    def dispatch(self, *args, **kwargs):
        return cache_page(self.get_cache_timeout())(super(CacheMixin, self).dispatch)(*args, **kwargs)

usecase:

from django.views.generic.detail import DetailView


class ArticleView(CacheMixin, DetailView):
    cache_timeout = 90
    template_name = "article_detail.html"
    queryset = Article.objects.articles()
    context_object_name = "article"
Inconsecutive answered 11/11, 2014 at 6:6 Comment(1)
Brilliant! Feels much better doing this that wrapping the url in a functionKirkcudbright
I
13

You can add it as a class decorator and even add multiple using a list:

@method_decorator([vary_on_cookie, cache_page(900)], name='dispatch')
class SomeClass(View):
   ...
Inaccurate answered 18/5, 2018 at 9:49 Comment(3)
Can we set infinite ttl for cache_page?Assiduity
@Assiduity there is no such way, but you probably never wanna cache something for longer than 1 yearGoth
I know what you mean, but I wanna cache the response for anytime that model has changed so It must be longer than 1 yearAssiduity
F
3

I created this little mixin generator to do the caching in the views file, instead of in the URL conf:

def CachedView(cache_time=60 * 60):
    """
    Mixing generator for caching class-based views.

    Example usage:

    class MyView(CachedView(60), TemplateView):
        ....

    :param cache_time: time to cache the page, in seconds
    :return: a mixin for caching a view for a particular number of seconds
    """
    class CacheMixin(object):
        @classmethod
        def as_view(cls, **initkwargs):
            return cache_page(cache_time)(
                super(CacheMixin, cls).as_view(**initkwargs)
            )
    return CacheMixin
Fulminant answered 15/9, 2016 at 8:43 Comment(0)
G
3

Yet another answer, we found this to be simplest and is specific to template views.

class CachedTemplateView(TemplateView):
    @classonlymethod
    def as_view(cls, **initkwargs): #@NoSelf
        return cache_page(15 * 60)(super(CachedTemplateView, cls).as_view(**initkwargs))
Giannini answered 15/12, 2016 at 21:40 Comment(0)
F
3

Would like to add this: If you need to use multiple decorators for cache like vary_on_headers and cache_page together, here is one way I did:

class CacheHeaderMixin(object):
    cache_timeout = int(config('CACHE_TIMEOUT', default=300))
    # cache_timeout = 60 * 5

    def get_cache_timeout(self):
       return self.cache_timeout

    def dispatch(self, *args, **kwargs):
       return vary_on_headers('Authorization')(cache_page(self.get_cache_timeout())(super(CacheHeaderMixin, self).dispatch))(*args, **kwargs)

This way cache is stored and it varies for different Authorization header (JWT). You may use like this for a class based view.

class UserListAPIView(CacheHeaderMixin, ListAPIView):
    serializer_class = UserSerializer
    def get_queryset(self):
        return CustomUser.objects.all()
Forethought answered 25/9, 2021 at 4:22 Comment(1)
needs some indent formatting. but great answerSpleen
G
2

I didn't found a good cache solution for class based views and created my own: https://gist.github.com/svetlyak40wt/11126018

It is a mixin for a class. Add it before the main base class and implement method get_cache_params like that:

def get_cache_params(self, *args, **kwargs):
   return ('some-prefix-{username}'.format(
       username=self.request.user.username),
            3600)
Genseric answered 20/4, 2014 at 21:46 Comment(0)
R
0

Here's my variation of the CachedView() mixin - I don't want to cache the view if the user is authenticated, because their view of pages will be unique to them (e.g. include their username, log-out link, etc).

class CacheMixin(object):
    """
    Add this mixin to a view to cache it.

    Disables caching for logged-in users.
    """
    cache_timeout = 60 * 5 # seconds

    def get_cache_timeout(self):
        return self.cache_timeout

    def dispatch(self, *args, **kwargs):
        if hasattr(self.request, 'user') and self.request.user.is_authenticated:
            # Logged-in, return the page without caching.
            return super().dispatch(*args, **kwargs)
        else:
            # Unauthenticated user; use caching.
            return cache_page(self.get_cache_timeout())(super().dispatch)(*args, **kwargs)
Rau answered 15/5, 2018 at 16:18 Comment(4)
Wondering how you got the request.user working in this case -- no matter what I do the CacheMixin will always return an AnonymousUser whileas the underlying (Model)Viewset will in fact return the actual user... What's the magic here?Reenter
@Liam If I do print(self.request.user) as the first line in dispatch() then I see AnonymousUser if I'm not logged in and phil (my username) if I'm logged in. Do you see something else, or am I misunderstanding the problem?Rau
The print(self.request.user) keeps printing AnonymousUser for me, even when logged in. It feels like dispatch() is called before the authentication happens behind the screens, but then you should have the problem as well.. Gist for referenceReenter
Strange. I'm using my example code above with a pretty new install of Django that hasn't had much done to it, and if I log in and then view a page, that debug line will print my username. I'm stumped I'm afraid.Rau

© 2022 - 2024 — McMap. All rights reserved.