from django.conf import settings
from django.core.cache import cache
from django.db import models

CACHE_PREFIX = settings.CACHE_MIDDLEWARE_KEY_PREFIX
CACHE_EXPIRE = 30

def cache_key(model, id):
    return ('%s:%s:%s' % (CACHE_PREFIX, model._meta.db_table, id)).replace(' ', '')

class RowCacheManager(models.Manager):
    """Manager for caching single-row queries. To make invalidation easy,
    we use an extra layer of indirection. The query arguments are used as a
    cache key, whose stored value is the unique cache key pointing to the
    object. When a model using RowCacheManager is saved, this unique cache
    should be invalidated. Doing two memcached queries is still much faster
    than fetching from the database."""
    def get(self, *args, **kwargs):
        # TODO: skip the layer of indirection if 'kwargs' contains id or pk?
        id = repr(kwargs)        
        pointer_key = cache_key(self.model, id)
        model_key = cache.get(pointer_key)
        if model_key is not None: 
            model = cache.get(model_key)
            if model is not None:
                return model
        # One of the cache queries missed, so we have to get the object from the database:
        model = super(RowCacheManager, self).get(*args, **kwargs)
        if not model_key:
            model_key = cache_key(model, model.pk)
            cache.set(pointer_key, model_key, CACHE_EXPIRE)
        cache.set(model_key, model, CACHE_EXPIRE) # you can use 'add' instead of 'set' here
        return model
 
class ModelWithCaching(models.Model):
    def save(self, *args, **kwargs):
        super(ModelWithCaching, self).save()
        if kwargs.pop('invalidate_cache', True):
            cache.delete(cache_key(self, self.id))
    class Meta:
        abstract = True
############################# Cross this line at your own risk
    __metaclass__ = MetaCaching
        
ModelBase = type(models.Model)

class MetaCaching(ModelBase):
    """Sets ``objects'' on any model that inherits from ModelWithCaching to
    be a RowCacheManager. This is tightly coupled to Django internals, so it
    could break if you upgrade Django. This was done partially as a proof-of-
    concept. It is advised to only use code above the comment line."""
    def __new__(*args, **kwargs):
        new_class = ModelBase.__new__(*args, **kwargs)
        new_manager = RowCacheManager()
        new_manager.contribute_to_class(new_class, 'objects')
        new_class._default_manager = new_manager
        return new_class
