Login

Binding signals to abstract models

Author:
andreterra
Posted:
May 15, 2012
Language:
Python
Version:
1.4
Score:
2 (after 2 ratings)

Intro

I found a question on SO for which Justin Lilly's answer was correct but not as thorough as I'd like, so I ended up working on a simple snippet that shows how to bind signals at runtime, which is nifty when you want to bind signals to an abstract class.

Bonus: simple cache invalidation!

Question

How do I use Django signals with an abstract model?

I have an abstract model that keeps an on-disk cache. When I delete the model, I need it to delete the cache. I want this to happen for every derived model as well.

If I connect the signal specifying the abstract model, this does not propagate to the derived models:

pre_delete.connect(clear_cache, sender=MyAbstractModel, weak=False)

If I try to connect the signal in an init, where I can get the derived class name, it works, but I'm afraid it will attempt to clear the cache as many times as I've initialized a derived model, not just once.

Where should I connect the signal?

Answer

I've created a custom manager that binds a post_save signal to every child of a class, be it abstract or not.

This is a one-off, poorly tested code, so beware! It works so far, though.

In this example, we allow an abstract model to define CachedModelManager as a manager, which then extends basic caching functionality to the model and its children. It allows you to define a list of volatile keys that should be deleted upon every save (hence the post_save signal) and adds a couple of helper functions to generate cache keys, as well as retrieving, setting and deleting keys.

This of course assumes you have a cache backend setup and working properly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# helperapp\models.py
# -*- coding: UTF-8
from django.db import models
from django.core.cache import cache

class CachedModelManager(models.Manager):
    def contribute_to_class(self, model, name):
        super(CachedModelManager, self).contribute_to_class(model, name)

        setattr(model, 'volatile_cache_keys',
                getattr(model, 'volatile_cache_keys', []))

        setattr(model, 'cache_key', getattr(model, 'cache_key', cache_key))
        setattr(model, 'get_cache', getattr(model, 'get_cache', get_cache))
        setattr(model, 'set_cache', getattr(model, 'set_cache', set_cache))
        setattr(model, 'del_cache', getattr(model, 'del_cache', del_cache))

        self._bind_flush_signal(model)

    def _bind_flush_signal(self, model):
        models.signals.post_save.connect(flush_volatile_keys, model)

def flush_volatile_keys(sender, **kwargs):
    instance = kwargs.pop('instance', False)

    for key in instance.volatile_cache_keys:
        instance.del_cache(key)

def cache_key(instance, key):
    if not instance.pk:
        name = "%s.%s" % (instance._meta.app_label, instance._meta.module_name)
        raise models.ObjectDoesNotExist("Can't generate a cache key for " +
                                        "this instance of '%s' " % name +
                                        "before defining a primary key.")
    else:
        return "%s.%s.%s.%s" % (instance._meta.app_label,
                                instance._meta.module_name,
                                instance.pk, key)

def get_cache(instance, key):
    result = cache.get(instance.cache_key(key))
    return result

def set_cache(instance, key, value, timeout=60*60*24*3):
    result = cache.set(instance.cache_key(key), value, timeout)
    return result

def del_cache(instance, key):
    result = cache.delete(instance.cache_key(key))
    return result



# myapp\models.py
# -*- coding: UTF-8
from django.contrib.auth.models import User
from django.db import models

from helperapp.models import CachedModelManager

class Abstract(models.Model):
    creator = models.ForeignKey(User)

    cache = CachedModelManager()

    class Meta:
        abstract = True


class Community(Abstract):
    members = models.ManyToManyField(User)

    volatile_cache_keys = ['members_list',]

    @property
    def members_list(self):
        result = self.get_cache('members_list')

        if not result:
            result = self.members.all()
            self.set_cache('members_list', result)

        return result

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 9 months, 3 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 4 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 5 months ago
  5. Help text hyperlinks by sa2812 1 year, 6 months ago

Comments

tjurewicz (on October 25, 2012):

Django==1.4.1, Python 2.6

I've tried implementing this snippet as is. The first issue I'm having is that I get the following AttributeError:

AttributeError: type object 'Community' has no attribute 'objects'

I think this is happening because you are adding a manager to a model, but not setting the default manager. Can you confirm this is the behavior you see?

#

est (on February 24, 2014):

Could you please make more generic example of connecting signals to abstract model leaving things as simple as possible?

#

Please login first before commenting.