How to manually clear/update a cached view in django - django

My goal is to cache a view until an event occurs where the view's cache would need to expire, otherwise cache for 1 hour. This is what I have in urls.py
url(r'^get_some_stuff/$', cache_page(60 * 60, key_prefix='get_some_stuff')(views.StuffView.as_view())),
And this works fine. Now I'm trying to fetch the cached view to verify that there's something there and I tried this:
from django.core.cache import cache
cache.get('get_some_stuff')
But this returns None. I was hoping to do something like this:
from django.core.cache import cache
#relevant event happened
cache.delete('get_some_stuff')
What's the right way to handle cache?
I've tried passing the uri path:
cache.get('/api/get_some_stuff/')
And I still get None returned.
>>> cache.has_key('/api/get_some_stuff/')
False
>>> cache.has_key('/api/get_some_stuff')
False
>>> cache.has_key('get_some_stuff')
False
I've reviewed the suggested answer and it does not solve the underlying issue at all. It does not appear to be as trivial as passing the uri routing path as the key since keys are somewhat abstracted within django.

Here is a code snippet from Relekang about expire #cache_page
from django.core.cache import cache
from django.core.urlresolvers import reverse
from django.http import HttpRequest
from django.utils.cache import get_cache_key
def expire_page_cache(view, args=None):
"""
Removes cache created by cache_page functionality.
Parameters are used as they are in reverse()
"""
if args is None:
path = reverse(view)
else:
path = reverse(view, args=args)
request = HttpRequest()
request.path = path
key = get_cache_key(request)
if cache.has_key(key):
cache.delete(key)

Django's Cache framework only allows to cache data for predefined time and to clear expired cache data you may need to use django-signals to notify some receiver function which clears cache.
And cache.get, cache.has_key, cache.delete requires complete cache_key to be passed not the url or key-prefix. As django takes care of the keys we don't have much control to get or delete data.
If you are using database caching then use raw sql query to delete cache record from the database table when it's stale. write a query which says delete from cache_table with cache_key like ('%1:views.decorators.cache.cache_page%')
I faced same issues with per-view caching and I went with low-level cache api. I cached final result querysets using cache.set() and good part is you can set your own key and play with it.

Related

What does Django use as a Cache Key?

I'm caching Wagtail API endpoints using Django's standard cache_page function which looks something like this:
#method_decorator(cache_page(600), name="detail_view")
#method_decorator(cache_page(600), name="listing_view")
class CashedPagesAPIViewSet(PagesAPIViewSet):
"""Custom API endpoint that can receive a slug."""
pass
Very basic.
I want to be able to clear the API cache on each page save, but I don't know how to get the cache key.
I can clear all the cache with this:
from django.core.cache import cache
from wagtail.core.signals import page_published
def clear_cache(sender, **kwargs):
"""Clear page cache."""
cache.clear()
page_published.connect(clear_cache)
What I want to be able to do is:
def clear_cache(sender, **kwargs):
"""Clear page cache."""
page = kwargs["instance"]
cache.delete(f"/api/v2/pages/{page.id}") # or whatever the key might be

Django admin custom action request

I've written a custom action and through model permissions have granted these to 2 users.
But now I only want one of them to run it at any one time. So was thinking whenever they select the actionbox and press the action button, it checks if a request.POST is already being made.
So my question is can I interrogate if there any other HTTP requests made before it takes the user to the intermediary page and display a message? But without having to mine the server logs.
To take a step back, I think what you're really asking is how do you share data across entrypoints to your application. e.g. if you only wanted 1 person to be able to trigger an action on a button at a time.
One strategy for doing this is to take some deployment-wide accessible datastore (like a cache or a message queue that ALL instances of your deployment have access to) and put in a message there that acts like a lock. This would rely on that datastore to support atomic reads and writes. Within Django, something like redis or memcached work well for this purpose (especially if you're using it as your cache backend.)
You might have something that looks like this (Example taken from the celery docs):
from datetime import datetime, timedelta
from django.core.cache import cache
from contextlib import contextmanager
LOCK_EXPIRE = 600 # Let the lock timeout in case your code crashes.
#contextmanager
def memcache_lock(lock_id):
timeout_at = datetime.now() + timedelta(seconds=LOCK_EXPIRE)
# cache.add fails if the key already exists
status = cache.add(lock_id, 'locked', LOCK_EXPIRE)
try:
yield status
finally:
if datetime.now() < timeout_at and status:
# don't release the lock if we exceeded the timeout
# to lessen the chance of releasing an expired lock
# owned by someone else
# also don't release the lock if we didn't acquire it
cache.delete(lock_id)
def my_custom_action(self, *args, **kwargs):
lock_id = "my-custom-action-lock"
with memcache_lock(lock_id) as acquired:
if acquired:
return do_stuff()
else:
do_something_else_if_someone_is_already_doing_stuff()
return

Django cache_page with keys [duplicate]

The #cache_page decorator is awesome. But for my blog I would like to keep a page in cache until someone comments on a post. This sounds like a great idea as people rarely comment so keeping the pages in memcached while nobody comments would be great. I'm thinking that someone must have had this problem before? And this is different than caching per url.
So a solution I'm thinking of is:
#cache_page( 60 * 15, "blog" );
def blog( request ) ...
And then I'd keep a list of all cache keys used for the blog view and then have way of expire the "blog" cache space. But I'm not super experienced with Django so I'm wondering if someone knows a better way of doing this?
This solution works for django versions before 1.7
Here's a solution I wrote to do just what you're talking about on some of my own projects:
def expire_view_cache(view_name, args=[], namespace=None, key_prefix=None):
"""
This function allows you to invalidate any view-level cache.
view_name: view function you wish to invalidate or it's named url pattern
args: any arguments passed to the view function
namepace: optioal, if an application namespace is needed
key prefix: for the #cache_page decorator for the function (if any)
"""
from django.core.urlresolvers import reverse
from django.http import HttpRequest
from django.utils.cache import get_cache_key
from django.core.cache import cache
# create a fake request object
request = HttpRequest()
# Loookup the request path:
if namespace:
view_name = namespace + ":" + view_name
request.path = reverse(view_name, args=args)
# get cache key, expire if the cached item exists:
key = get_cache_key(request, key_prefix=key_prefix)
if key:
if cache.get(key):
# Delete the cache entry.
#
# Note that there is a possible race condition here, as another
# process / thread may have refreshed the cache between
# the call to cache.get() above, and the cache.set(key, None)
# below. This may lead to unexpected performance problems under
# severe load.
cache.set(key, None, 0)
return True
return False
Django keys these caches of the view request, so what this does is creates a fake request object for the cached view, uses that to fetch the cache key, then expires it.
To use it in the way you're talking about, try something like:
from django.db.models.signals import post_save
from blog.models import Entry
def invalidate_blog_index(sender, **kwargs):
expire_view_cache("blog")
post_save.connect(invalidate_portfolio_index, sender=Entry)
So basically, when ever a blog Entry object is saved, invalidate_blog_index is called and the cached view is expired. NB: haven't tested this extensively, but it's worked fine for me so far.
The cache_page decorator will use CacheMiddleware in the end which will generate a cache key based on the request (look at django.utils.cache.get_cache_key) and the key_prefix ("blog" in your case). Note that "blog" is only a prefix, not the whole cache key.
You can get notified via django's post_save signal when a comment is saved, then you can try to build the cache key for the appropriate page(s) and finally say cache.delete(key).
However this requires the cache_key, which is constructed with the request for the previously cached view. This request object is not available when a comment is saved. You could construct the cache key without the proper request object, but this construction happens in a function marked as private (_generate_cache_header_key), so you are not supposed to use this function directly. However, you could build an object that has a path attribute that is the same as for the original cached view and Django wouldn't notice, but I don't recommend that.
The cache_page decorator abstracts caching quite a bit for you and makes it hard to delete a certain cache object directly. You could make up your own keys and handle them in the same way, but this requires some more programming and is not as abstract as the cache_page decorator.
You will also have to delete multiple cache objects when your comments are displayed in multiple views (i.e. index page with comment counts and individual blog entry pages).
To sum up: Django does time based expiration of cache keys for you, but custom deletion of cache keys at the right time is more tricky.
I wrote Django-groupcache for this kind of situations (you can download the code here). In your case, you could write:
from groupcache.decorators import cache_tagged_page
#cache_tagged_page("blog", 60 * 15)
def blog(request):
...
From there, you could simply do later on:
from groupcache.utils import uncache_from_tag
# Uncache all view responses tagged as "blog"
uncache_from_tag("blog")
Have a look at cache_page_against_model() as well: it's slightly more involved, but it will allow you to uncache responses automatically based on model entity changes.
With the latest version of Django(>=2.0) what you are looking for is very easy to implement:
from django.utils.cache import learn_cache_key
from django.core.cache import cache
from django.views.decorators.cache import cache_page
keys = set()
#cache_page( 60 * 15, "blog" );
def blog( request ):
response = render(request, 'template')
keys.add(learn_cache_key(request, response)
return response
def invalidate_cache()
cache.delete_many(keys)
You can register the invalidate_cache as a callback when someone updates a post in the blog via a pre_save signal.
This won't work on django 1.7; as you can see here https://docs.djangoproject.com/en/dev/releases/1.7/#cache-keys-are-now-generated-from-the-request-s-absolute-url the new cache keys are generated with the full URL, so a path-only fake request won't work. You must setup properly request host value.
fake_meta = {'HTTP_HOST':'myhost',}
request.META = fake_meta
If you have multiple domains working with the same views, you should cycle them in the HTTP_HOST, get proper key and do the clean for each one.
Django view cache invalidation for v1.7 and above. Tested on Django 1.9.
def invalidate_cache(path=''):
''' this function uses Django's caching function get_cache_key(). Since 1.7,
Django has used more variables from the request object (scheme, host,
path, and query string) in order to create the MD5 hashed part of the
cache_key. Additionally, Django will use your server's timezone and
language as properties as well. If internationalization is important to
your application, you will most likely need to adapt this function to
handle that appropriately.
'''
from django.core.cache import cache
from django.http import HttpRequest
from django.utils.cache import get_cache_key
# Bootstrap request:
# request.path should point to the view endpoint you want to invalidate
# request.META must include the correct SERVER_NAME and SERVER_PORT as django uses these in order
# to build a MD5 hashed value for the cache_key. Similarly, we need to artificially set the
# language code on the request to 'en-us' to match the initial creation of the cache_key.
# YMMV regarding the language code.
request = HttpRequest()
request.META = {'SERVER_NAME':'localhost','SERVER_PORT':8000}
request.LANGUAGE_CODE = 'en-us'
request.path = path
try:
cache_key = get_cache_key(request)
if cache_key :
if cache.has_key(cache_key):
cache.delete(cache_key)
return (True, 'successfully invalidated')
else:
return (False, 'cache_key does not exist in cache')
else:
raise ValueError('failed to create cache_key')
except (ValueError, Exception) as e:
return (False, e)
Usage:
status, message = invalidate_cache(path='/api/v1/blog/')
I had same problem and I didn't want to mess with HTTP_HOST, so I created my own cache_page decorator:
from django.core.cache import cache
def simple_cache_page(cache_timeout):
"""
Decorator for views that tries getting the page from the cache and
populates the cache if the page isn't in the cache yet.
The cache is keyed by view name and arguments.
"""
def _dec(func):
def _new_func(*args, **kwargs):
key = func.__name__
if kwargs:
key += ':' + ':'.join([kwargs[key] for key in kwargs])
response = cache.get(key)
if not response:
response = func(*args, **kwargs)
cache.set(key, response, cache_timeout)
return response
return _new_func
return _dec
To expired page cache just need to call:
cache.set('map_view:' + self.slug, None, 0)
where self.slug - param from urls.py
url(r'^map/(?P<slug>.+)$', simple_cache_page(60 * 60 * 24)(map_view), name='map'),
Django 1.11, Python 3.4.3
FWIW I had to modify mazelife's solution to get it working:
def expire_view_cache(view_name, args=[], namespace=None, key_prefix=None, method="GET"):
"""
This function allows you to invalidate any view-level cache.
view_name: view function you wish to invalidate or it's named url pattern
args: any arguments passed to the view function
namepace: optioal, if an application namespace is needed
key prefix: for the #cache_page decorator for the function (if any)
from: http://stackoverflow.com/questions/2268417/expire-a-view-cache-in-django
added: method to request to get the key generating properly
"""
from django.core.urlresolvers import reverse
from django.http import HttpRequest
from django.utils.cache import get_cache_key
from django.core.cache import cache
# create a fake request object
request = HttpRequest()
request.method = method
# Loookup the request path:
if namespace:
view_name = namespace + ":" + view_name
request.path = reverse(view_name, args=args)
# get cache key, expire if the cached item exists:
key = get_cache_key(request, key_prefix=key_prefix)
if key:
if cache.get(key):
cache.set(key, None, 0)
return True
return False
Instead of using the cache page decorator, you could manually cache the blog post object (or similar) if there are no comments, and then when there's a first comment, re-cache the blog post object so that it's up to date (assuming the object has attributes that reference any comments), but then just let that cached data for the commented blog post expire and then no bother re-cacheing...
Instead of explicit cache expiration you could probably use new "key_prefix" every time somebody comment the post. E.g. it might be datetime of the last post's comment (you could even combine this value with the Last-Modified header).
Unfortunately Django (including cache_page()) does not support dynamic "key_prefix"es (checked on Django 1.9) but there is workaround exists. You can implement your own cache_page() which may use extended CacheMiddleware with dynamic "key_prefix" support included. For example:
from django.middleware.cache import CacheMiddleware
from django.utils.decorators import decorator_from_middleware_with_args
def extended_cache_page(cache_timeout, key_prefix=None, cache=None):
return decorator_from_middleware_with_args(ExtendedCacheMiddleware)(
cache_timeout=cache_timeout,
cache_alias=cache,
key_prefix=key_prefix,
)
class ExtendedCacheMiddleware(CacheMiddleware):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if callable(self.key_prefix):
self.key_function = self.key_prefix
def key_function(self, request, *args, **kwargs):
return self.key_prefix
def get_key_prefix(self, request):
return self.key_function(
request,
*request.resolver_match.args,
**request.resolver_match.kwargs
)
def process_request(self, request):
self.key_prefix = self.get_key_prefix(request)
return super().process_request(request)
def process_response(self, request, response):
self.key_prefix = self.get_key_prefix(request)
return super().process_response(request, response)
Then in your code:
from django.utils.lru_cache import lru_cache
#lru_cache()
def last_modified(request, blog_id):
"""return fresh key_prefix"""
#extended_cache_page(60 * 15, key_prefix=last_modified)
def view_blog(request, blog_id):
"""view blog page with comments"""
Most of the solutions above didn't work in our case because we use https. The source code for get_cache_key reveals that it uses request.get_absolute_uri() to generate the cache key.
The default HttpRequest class sets the scheme as http. Thus we need to override it to use https for our dummy request object.
This is the code that works fine for us :)
from django.core.cache import cache
from django.http import HttpRequest
from django.utils.cache import get_cache_key
class HttpsRequest(HttpRequest):
#property
def scheme(self):
return "https"
def invalidate_cache_page(
path,
query_params=None,
method="GET",
):
request = HttpsRequest()
# meta information can be checked from error logs
request.META = {
"SERVER_NAME": "www.yourwebsite.com",
"SERVER_PORT": "443",
"QUERY_STRING": query_params,
}
request.path = path
key = get_cache_key(request, method=method)
if cache.has_key(key):
cache.delete(key)
Now I can use this utility function to invalidate the cache from any of our views:
page = reverse('url_name', kwargs={'id': obj.id})
invalidate_cache_page(path)
Duncan's answer works well with Django 1.9. But if we need invalidate url with GET-parameter we have to make a little changes in request.
Eg for .../?mykey=myvalue
request.META = {'SERVER_NAME':'127.0.0.1','SERVER_PORT':8000, 'REQUEST_METHOD':'GET', 'QUERY_STRING': 'mykey=myvalue'}
request.GET.__setitem__(key='mykey', value='myvalue')
I struggled with a similar situation and here is the solution I came up with, I started it on an earlier version of Django but it is currently in use on version 2.0.3.
First issue: when you set things to be cached in Django, it sets headers so that downstream caches -- including the browser cache -- cache your page.
To override that, you need to set middleware. I cribbed this from elsewhere on StackOverflow, but can't find it at the moment. In appname/middleware.py:
from django.utils.cache import add_never_cache_headers
class Disable(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
add_never_cache_headers(response)
return response
Then in settings.py, to MIDDLEWARE, add:
'appname.middleware.downstream_caching.Disable',
Keep in mind that this approach completely disables downstream caching, which may not be what you want.
Finally, I added to my views.py:
def expire_page(request, path=None, query_string=None, method='GET'):
"""
:param request: "real" request, or at least one providing the same scheme, host, and port as what you want to expire
:param path: The path you want to expire, if not the path on the request
:param query_string: The query string you want to expire, as opposed to the path on the request
:param method: the HTTP method for the page, if not GET
:return: None
"""
if query_string is not None:
request.META['QUERY_STRING'] = query_string
if path is not None:
request.path = path
request.method = method
# get_raw_uri and method show, as of this writing, everything used in the cache key
# print('req uri: {} method: {}'.format(request.get_raw_uri(), request.method))
key = get_cache_key(request)
if key in cache:
cache.delete(key)
I didn't like having to pass in a request object, but as of this writing, it provides the scheme/protocol, host, and port for the request, pretty much any request object for your site/app will do, as long as you pass in the path and query string.
One more updated version of Duncan's answer: had to figure out correct meta fields: (tested on Django 1.9.8)
def invalidate_cache(path=''):
import socket
from django.core.cache import cache
from django.http import HttpRequest
from django.utils.cache import get_cache_key
request = HttpRequest()
domain = 'www.yourdomain.com'
request.META = {'SERVER_NAME': socket.gethostname(), 'SERVER_PORT':8000, "HTTP_HOST": domain, 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br'}
request.LANGUAGE_CODE = 'en-us'
request.path = path
try:
cache_key = get_cache_key(request)
if cache_key :
if cache.has_key(cache_key):
cache.delete(cache_key)
return (True, 'successfully invalidated')
else:
return (False, 'cache_key does not exist in cache')
else:
raise ValueError('failed to create cache_key')
except (ValueError, Exception) as e:
return (False, e)
The solution is simple, and do not require any additional work.
Example
#cache_page(60 * 10)
def our_team(request, sorting=None):
...
This will set the response to the cache with the default key.
Expire a view cache
from django.utils.cache import get_cache_key
from django.core.cache import cache
def our_team(request, sorting=None):
# This will remove the cache value and set it to None
cache.set(get_cache_key(request), None)
Simple, Clean, Fast.

Django: how to handle caching in views that differ for Ajax requests?

I have a Django application and a postgres backend. It's essentially a search site with a large database, and the data typically changes once a day. I would like to start caching, to reduce load on the database.
I've set up memcached, but I have the following architecture in my views, designed to let my app use Ajax in the front-end:
#cache_page(60 * 60 * 12)
def items(request, pattern=None, specialurl=None):
if request.is_ajax():
template = "result_ajax.html"
else:
template = "index.html"
.. and unfortunately, the combination of caching plus special handling of Ajax calls does not work well.
This is beacuse memcached doesn't distinguish between the Ajax results and the non-Ajax results - so Ajax calls from the front-end are given cached non-Ajax results, and vice versa.
So what I need to do is to figure out how else to cache. I can think of the following options:
Cache only the database queries, for up to a day at a time. Is this possible?
Cache the fragment of the template within result_ajax.html that actually displays the results. (index.html actually includes result_ajax.html.)
Which of these is likely to be the best way to do things?
I would try something like this tell the cache decorator to use a different cache key for Ajax and non-Ajax requests:
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers
#cache_page(60 * 60 * 12)
#vary_on_headers('X-Requested-With')
def items(request, pattern=None, specialurl=None):
if request.is_ajax():
template = "result_ajax.html"
else:
template = "index.html"

Caching query results in django

I'm trying to find a way to cache the results of a query that won't change with frequency. For example, categories of products from an e-commerce (cellphones, TV, etc).
I'm thinking of using the template fragment caching, but in this fragment, I will iterate over a list of these categories. This list is avaliable in any part of the site, so it's in my base.html file. Do I have always to send the list of categories when rendering the templates? Or is there a more dynamic way to do this, making the list always available in the template?
Pop your cached query into Django's cache:
from django.core.cache import cache
cache.set('key', queryset)
Then create a context processor to add the value of the cache to all templates:
# myproject/myapp/context_processors.py
from django.core.cache import cache
def cached_queries():
return {'cache', cache.get('key')}
Then add your context processor in your Django settings file:
TEMPLATE_CONTEXT_PROCESSORS += (
'myproject.myapp.context_processors.cached_queries'
)
Now you will be able to access the cache variable in all generic templates and all templates which have a requests context, which a template is given if this is done in the view:
return render_to_response('my_template.html',
my_data_dictionary,
context_instance=RequestContext(request))
When to Set the Cache
It depends on what is contained in the cache. However a common problem is that Django only really gets to execute Python whenever a page request is sent, and this is often not where you want to do this kind of work.
An alternative is to create a custom management command for a particular app. You can then either run this manually when necessary, or more commonly set this to run as a cron job.
To create a management command you must create a class decended from Command inside of a management/commands directory located inside of an app:
# myproject/myapp/management/commands/update_cache.py
from django.core.management.base import NoArgsCommand
from django.core.cache import cache
class Command(NoArgsCommand):
help = 'Refreshes my cache'
def handle_noargs(self, **options):
cache.set('key', queryset)
The name of this file is important as this will be the name of the command. In this case you can now call this on the command line:
python manage.py update_cache
You can also use johnny-cache for automatic caching of querysets. It will (by default) cache all querysets, but you can force it not to cache some.