From 41ff3506280bb6c4bda1a5622c74a2999dd563c4 Mon Sep 17 00:00:00 2001 From: leidel Date: Sun, 2 Nov 2008 00:05:38 +0000 Subject: [PATCH] Refactored template loader to use new backend architecture, that populates the cache by using signals. Adds a BaseCacheBackend class with a simple API (load, save, remove) to be subclassed and specified in the DBTEMPLATES_CACHE_BACKEND settings. Adds DjangoCacheBackend (uses Django's own caching mechanism) and FileSystemBackend (simple approach to filesystem saving). git-svn-id: https://django-dbtemplates.googlecode.com/svn/trunk@58 cfb8ba98-e953-0410-9cff-959ffddf5974 committer: leidel --HG-- extra : convert_revision : fde60d8ab2e834d898d9f662cf013d5546370eeb --- dbtemplates/cache.py | 97 +++++++++++++++++++++++++++++++++++++++++++ dbtemplates/loader.py | 87 ++++++++++---------------------------- dbtemplates/models.py | 53 ++++++++++++++++++++++- 3 files changed, 170 insertions(+), 67 deletions(-) create mode 100644 dbtemplates/cache.py diff --git a/dbtemplates/cache.py b/dbtemplates/cache.py new file mode 100644 index 0000000..5f5c709 --- /dev/null +++ b/dbtemplates/cache.py @@ -0,0 +1,97 @@ +import os +from django.conf import settings +from django.core.cache import cache +from django.contrib.sites.models import Site +from django.template import TemplateDoesNotExist +from django.core.exceptions import ImproperlyConfigured +from django.utils.encoding import smart_unicode, force_unicode + +class BaseCacheBackend(object): + """ + Base class for custom cache backend of dbtemplates to be used while + subclassing. + + Set DBTEMPLATES_CACHE_BACKEND setting to the Python path to that subclass. + """ + def __init__(self): + self.site = Site.objects.get_current() + + def load(self, name): + """ + Loads a template from the cache with the given name. + """ + raise NotImplemented + + def save(self, name, content): + """ + Saves the given template content with the given name in the cache. + """ + raise NotImplemented + + def remove(self, name): + """ + Removes the template with the given name from the cache. + """ + raise NotImplemented + +class DjangoCacheBackend(BaseCacheBackend): + """ + A cache backend that uses Django's cache mechanism. + """ + def _cache_key(self, name): + return 'dbtemplates::%s' % name + + def load(self, name): + cache_key = self._cache_key(name) + return cache.get(cache_key) + + def save(self, name, content): + cache_key = self._cache_key(name) + cache.set(cache_key, content) + + def remove(self, name): + cache_key = self._cache_key(name) + cache.delete(cache_key) + +class FileSystemBackend(BaseCacheBackend): + """ + A cache backend that uses simple files to hold the template cache. + """ + def __init__(self): + try: + self.cache_dir = getattr(settings, 'DBTEMPLATES_CACHE_DIR', None) + self.cache_dir = os.path.normpath(self.cache_dir) + if not os.path.isdir(self.cache_dir): + raise Exception + except: + raise ImproperlyConfigured('You\'re using the dbtemplates\' file system cache backend without having set the DBTEMPLATES_CACHE_DIR setting to a valid value. Make sure the directory exists and is writeable for the user your Django instance is running with.') + super(FileSystemBackend, self).__init__() + + def _filepath(self, name): + return os.path.join(self.cache_dir, name) + + def load(self, name): + try: + filepath = self._filepath(name) + return open(filepath).read().decode('utf-8') + except: + raise None + + def save(self, name, content, retry=False): + try: + filepath = self._filepath(name) + dirname = os.path.dirname(filepath) + if not os.path.exists(dirname): + os.makedirs(dirname) + cache_file = open(filepath, 'w') + cache_file.write(force_unicode(content).encode('utf-8')) + cache_file.close() + except Exception: + raise + + def remove(self, name): + try: + filepath = self._filepath(name) + os.remove(filepath) + except: + pass diff --git a/dbtemplates/loader.py b/dbtemplates/loader.py index f9b7284..a5421ab 100644 --- a/dbtemplates/loader.py +++ b/dbtemplates/loader.py @@ -1,75 +1,32 @@ import os from django.conf import settings -from django.db.models import signals from django.template import TemplateDoesNotExist -from django.contrib.sites.models import Site +from django.core.exceptions import ImproperlyConfigured -from dbtemplates.models import Template +from dbtemplates.models import Template, backend -try: - site = Site.objects.get_current() -except: - site = None - -try: - cache_dir = os.path.normpath(getattr(settings, 'DBTEMPLATES_CACHE_DIR', None)) - if not os.path.isdir(cache_dir): - raise -except: - cache_dir = None - def load_template_source(template_name, template_dirs=None): """ - Tries to load the template from DBTEMPLATES_CACHE_DIR. If it does not exist - loads templates from the database by querying the database field ``name`` - with a template path and ``sites`` with the current site, - and tries to save the template as DBTEMPLATES_CACHE_DIR/``name`` for subsequent - requests. - If DBTEMPLATES_CACHE_DIR is not configured falls back to database-only operation. + Tries to load the template from DBTEMPLATES_CACHE_DIR. If it does not + exist loads templates from the database by querying the database field + ``name`` with a template path and ``sites`` with the current site, + and tries to save the template as DBTEMPLATES_CACHE_DIR/``name`` for + subsequent requests. If DBTEMPLATES_CACHE_DIR is not configured falls + back to database-only operation. """ - if site is not None: - if cache_dir is not None: - filepath = os.path.join(cache_dir, template_name) - try: - return (open(filepath).read(), filepath) - except IOError: - try: - t = Template.objects.get(name__exact=template_name, sites__pk=site.id) - try: - f = open(filepath, 'w') - f.write(t.content) - f.close() - except IOError: - try: - head, tail = os.path.split(filepath) - if head and not os.path.isdir(head): - os.makedirs(head) - except Exception: - pass - - return (t.content, 'db:%s:%s' % (settings.DATABASE_ENGINE, template_name)) - except: - pass - else: - try: - t = Template.objects.get(name__exact=template_name, sites__pk=site.id) - return (t.content, 'db:%s:%s' % (settings.DATABASE_ENGINE, template_name)) - except: - pass + display_name = 'db:%s:%s' % (settings.DATABASE_ENGINE, template_name) + if backend: + try: + backend_template = backend.load(template_name) + if backend_template is not None: + return backend_template, template_name + except: + pass + try: + template = Template.objects.get(name__exact=template_name, + sites__pk=settings.SITE_ID) + return (template.content, display_name) + except: + pass raise TemplateDoesNotExist, template_name load_template_source.is_usable = True - -def remove_cached_template(instance, **kwargs): - """ - Called via django's signals to remove cached templates, if the template in the - database was changed or deleted. - """ - if cache_dir is not None: - try: - filepath = os.path.join(cache_dir, instance.name) - os.remove(filepath) - except OSError: - pass - -signals.post_save.connect(remove_cached_template, sender=Template) -signals.pre_delete.connect(remove_cached_template, sender=Template) diff --git a/dbtemplates/models.py b/dbtemplates/models.py index 08f9711..eb115ca 100644 --- a/dbtemplates/models.py +++ b/dbtemplates/models.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- from datetime import datetime from django.db import models +from django.conf import settings +from django.db.models import signals from django.contrib.sites.models import Site from django.utils.translation import gettext_lazy as _ from django.template import TemplateDoesNotExist from django.template.loader import find_template_source - +from django.core.exceptions import ImproperlyConfigured class Template(models.Model): """ @@ -14,7 +16,7 @@ class Template(models.Model): """ name = models.CharField(_('name'), unique=True, max_length=100, help_text=_("Example: 'flatpages/default.html'")) content = models.TextField(_('content'), blank=True) - sites = models.ManyToManyField(Site) + sites = models.ManyToManyField(Site, default=[settings.SITE_ID]) creation_date = models.DateTimeField(_('creation date'), default=datetime.now) last_changed = models.DateTimeField(_('last changed'), default=datetime.now) @@ -29,6 +31,8 @@ class Template(models.Model): def save(self, *args, **kwargs): self.last_changed = datetime.now() + # If content is empty look for a template with the given name and + # populate the template instance with its content. if not self.content: try: source, origin = find_template_source(self.name) @@ -38,6 +42,51 @@ class Template(models.Model): pass super(Template, self).save(*args, **kwargs) +# Check if django-reversion is installed and register the model with it if yes +try: + models.get_app('reversion') +except ImproperlyConfigured: + pass +else: + import reversion + reversion.register(Template) + +def get_cache_backend(): + path = getattr(settings, 'DBTEMPLATES_CACHE_BACKEND', False) + if path: + i = path.rfind('.') + module, attr = path[:i], path[i+1:] + try: + mod = __import__(module, {}, {}, [attr]) + except ImportError, e: + raise ImproperlyConfigured, 'Error importing dbtemplates cache backend %s: "%s"' % (module, e) + try: + cls = getattr(mod, attr) + except AttributeError: + raise ImproperlyConfigured, 'Module "%s" does not define a "%s" cache backend' % (module, attr) + return cls() + return False + +backend = get_cache_backend() + +def add_template_to_cache(instance, **kwargs): + """ + Called via Django's signals to cache the templates, if the template + in the database was added or changed. + """ + backend.save(instance.name, instance.content) + +def remove_cached_template(instance, **kwargs): + """ + Called via Django's signals to remove cached templates, if the template + in the database was changed or deleted. + """ + backend.remove(instance.name) + +if backend: + signals.post_save.connect(remove_cached_template, sender=Template) + signals.post_save.connect(add_template_to_cache, sender=Template) + signals.pre_delete.connect(remove_cached_template, sender=Template) __test__ = {'API_TESTS':""" >>> from django.template import loader, Context