Extending the default cache tag
If the five settings are not enough for you, or if you want to have a
templatag with a different behavior as the default provided one, you
will be happy to know that django-adv-cache-tag was written with
easily extending in mind.
It provides a class, CacheTag (in adv_cache_tag.tag), which has
a lot of short and simple methods, and even a Meta class (idea
stolen from the django models :D ). So it’s easy to override a simple
part.
Below we will show many ways of extending this class.
Basic override
Imagine you don’t want to change the default settings (all to False,
and using the default backend) but want a templatetag with
versioning activated :
Create a new templatetag file (myapp/templatetags/my_cache_tags.py)
with this:
from adv_cache_tag.tag import CacheTag
class MyCacheTag(CacheTag):
class Meta(CacheTag.Meta):
versioning = True
from django import template
register = template.Library()
MyCacheTag.register(register, 'my_cache')
With these simple lines, you now have a new templatetag to use when you
want versioning:
{% load my_cache_tags %}
{% my_cache 0 myobj_main_template obj.pk obj.date_last_updated %}
obj
{% endmycache %}
As you see, just replace {% load adv_cache %} (or the django default
{% load cache %}) by {% load my_cache_tags %}, your templatetag
module, and the {% cache %} templatetag by your new defined one,
{% my_cache %}. Don’t forget to replace the closing tag too:
{% endmy_cache %}. But the {% nocache %} will stay the same,
except if you want a new one. For this, just add a parameter to the
register method:
MyCacheTag.register(register, 'my_cache', 'my_nocache')
{% my_cache ... %}
cached
{% my_nocache %}not cached{% endmy_nocache %}
{% endmy_cache %}
Note that you can keep the name cache for your tag if you know that
you will not load in your template other templatetag module providing a
cache tag. To do so, two simplest way is:
MyCacheTag.register(register) # 'cache' and 'nocache' are the default values
All settings have matching variables in the Meta class, so you can
override one or many of them in your own classes. See the “Settings”
part to see them
Internal version
When your template file is updated, the only way to invalidate all
cached versions of this template is to update the fragment name or the
arguments passed to the templatetag.
With django-adv-cache-tag you can do this with versioning, by manage
your own version as the last argument to the templetag. But if you want
to use the power of the versioning system of django-adv-cache-tag,
it can be too verbose:
{% load adv_cache %}
{% with template_version=obj.date_last_updated|stringformat:"s"|add:"v1" %}
{% cache 0 myobj_main_template obj.pk obj.date_last_updated %}
...
{% endcache %}
{% endwith %}
django-adv-cache-tag provides a way to do this easily, with the
ADV_CACHE_VERSION settings. But by updating it, all cached
version will be invalidated, not only those you updated.
To do this, simple create your own tag with a specific internal version:
class MyCacheTag(CacheTag):
class Meta(CacheTag.Meta):
internal_version = "v1"
MyCacheTag.register('my_cache')
And then in your template, you can simply do
{% load my_cache_tags %}
{% my_cache 0 myobj_main_template obj.pk obj.date_last_updated %}
...
{% endmy_cache %}
Each time you update the content of your template and want invalidation,
simply change the internal_version in your MyCacheTag class (or
you can use a settings for this)
Change the cache backend
If you want to change the cache backend for one templatetag, it’s easy:
class MyCacheTag(CacheTag):
class Meta:
cache_backend = 'templates'
But you can also to this by overriding a method:
from django.core.cache import get_cache
class MyCacheTag(CacheTag):
def get_cache_object(self):
return get_cache('templates')
And if you want a cache backend for old objects, and another, faster for
recent ones:
from django.core.cache import get_cache
class MyCacheTag(CacheTag):
class Meta:
cache_backend = 'fast_templates'
def get_cache_object(self):
cache_backend = self.options.cache_backend
if self.get_pk() < 1000:
cache_backend = 'slow_templates'
return get_cache(cache_backend)
The value returned by the get_cache_object should be a cache backend
object, but as we only use the set and get methods on this
object, it can be what you want if it provides these two methods. And
even more, you can override the cache_set and cache_get methods
of the CacheTag class if you don’t want to use the default set
and get methods of the cache backend object.
Change the cache key
The CacheTag class provides three class to create the cache key:
get_base_cache_key, which returns a formatable string
(“template.%(nodename)s.%(name)s.%(pk)s.%(hash)s” by default if
include_pk is True or
“template.%(nodename)s.%(name)s.%(hash)s” if False
get_cache_key_args, which returns the arguments to use in the
previous string
get_cache_key, which combine the two
The arguments are:
nodename parameter is the name of the templatetag: it’s
“my_cache” if {% my_cache ... %}
name is the “fragment name” of your templatetag, the value after
the expire-time
pk is used only if self.options.include_pk is True, and
is returned by this.get_pk()
hash is the hash of all arguments after the fragment name,
excluding the last one which is the version number, but only if
self.options.versioning is True
If you want to remove the “template.” part at the start of the cache key
(useless if you have a cache backend dedicated to template caching), you
can do this:
class MyCacheTag(CacheTag):
def get_base_cache_key(self):
cache_key = super(MyCacheTag, self).get_base_cache_key()
return cache.key[9:]
Add an argument to the templatetag
By default, the templatetags provided by CacheTag take the same
arguments as the default django cache templatetag.
If you want to add one, it’s easy as the class provide a
get_template_node_arguments method, which will work as for normal
django templatetags, taking a list of tokens, and returning ones that
will be passed to the real templatetag, a Node class inside the
CacheTag.
Say you want to add a foo argument between the expire time and the
fragment name:
from django import template
class MyCacheTag(CacheTag):
class Node(CacheTag.Node):
def __init__(self, nodename, nodelist, expire_time, foo, fragment_name, vary_on):
""" Save the foo variable in the node (not resolved yet) """
super(Node, self).__init__(self, nodename, nodelist, expire_time, fragment_name, vary_on)
self.foo = foo
def prepare_params(self):
""" Resolve the foo variable to it's real content """
super(CacheTag, self).prepare_params()
self.foo = template.resolve_variable(var, self.context)
@classmethod
def get_template_node_arguments(cls, tokens):
""" Check validity of tokens and return then as ready to be passed to the Node class """
if len(tokens) < 4:
raise template.TemplateSyntaxError(u"'%r' tag requires at least 3 arguments." % tokens[0])
return (tokens[1], tokens[2], tokens[3], tokens[4:])
Prepare caching of templates
This one is not about overriding the class, but it can be useful. When
an object is updated, it can be better to regenerate the cached template
at this time rather than we need to display it.
It’s easy. You can do this by catching the post_save signal of your
model, or just override it’s save method. For this example we will
use this last solution.
The only special thing is to know the path of the template where your
templatetag is. In my case, i have a template just for this (included in
other ones for general use), so it’s easier to find it and regenerate it
as in this example.
As we are not in a request, we have not the Request object here, so
context processors are not working, we must create a context object that
will be used to render the template, with all variables needed.
from django.template import loader, Context
class MyModel(models.Model):
# your fields
def save(self, *args, **kwargs):
super(MyModel, self.save(*args, **kwargs)
template = 'path/to/my_template_file_with_my_cache_block.html'
context = Context({
'obj': self,
# as you have no request, we have to add stuff from context processors manually if we need them
'STATIC_URL': settings.STATIC_URL,
# the line below indicate that we force regenerating the cache, even if it exists
'__regenerate__': True,
# the line below indicate if we only want html without parsing the nocache parts
'__partial__': True,
})
loader.get_template(template).render(context)
Load data from database before rendering
This is a special case. Say that you want to display a list of objects
but you have only ids and versions retrieved from redis (with ZSET,
with as value and updated date (which is used as version) as score , for
example)
If you know you always have a valid version of your template in cache,
because they are regenerated very time they are saved, as seen above,
it’s fine, just add the object’s primary key as the pk in your
templatetag arguments, and the cached template will be loaded.
But if it’s not the case, you will have a problem: when django will
render the template, the only part of the object present in the context
is the primary key, so if you need the name or whatever field to render
the cached template, it won’t work.
With django-adv-cache-tag it’s easy to resolve this, as we can load
the object from the database and adding it to the context.
View
def my_view(request):
objects = [
dict(
pk=val[0],
date_last_updated=val[1]
)
for val in
redis.zrevrange('my_objects', 0, 19, withscores=True)
]
return render(request, "my_results.html", dict(objects=objects))
Template “my_results.html”
{% for obj in objects %}
{% include "my_result.html" %}
{% endfor %}
Template “my_result.html”
{% load my_cache_tags %}
{% my_cache 0 myobj_main_template obj.pk obj.date_last_update %}
{{ obj }}
{% endmy_cache %}
Templatetag
in “myapp/templatetags/my_cache_tags “
from my_app.models import MyModel
class MyCacheTag(CacheTag):
class Meta(CacheTag.Meta):
""" Force options """
include_pk = True
versioning = True
def create_content(self):
""" If the object in context is not a real model, load it from db """
if not isinstance(context['obj'], MyObject):
context['obj'] = MyModel.objects.get(id=self.get_pk())
super(MyCacheTag, self).create_content()
MyCacheTag.register('my_cache')
Careful with this, it generates as database requests as objects to be
loaded.
And more…
If you want to do more, feel free to look at the source code of the
CacheTag class (in tag.py), all methods are documented.