diff --git a/dbtemplates/admin.py b/dbtemplates/admin.py index 34e3865..3109a41 100644 --- a/dbtemplates/admin.py +++ b/dbtemplates/admin.py @@ -4,13 +4,13 @@ from django.contrib import admin from django.utils.translation import ungettext, ugettext_lazy as _ from django.utils.safestring import mark_safe -from dbtemplates import settings +from dbtemplates.conf import settings from dbtemplates.models import (Template, remove_cached_template, add_template_to_cache) # Check if django-reversion is installed and use reversions' VersionAdmin # as the base admin class if yes -if settings.USE_REVERSION: +if settings.DBTEMPLATES_USE_REVERSION: from reversion.admin import VersionAdmin as TemplateModelAdmin else: from django.contrib.admin import ModelAdmin as TemplateModelAdmin @@ -23,8 +23,8 @@ class CodeMirrorTextArea(forms.Textarea): """ class Media: css = dict(screen=[ - posixpath.join(settings.MEDIA_PREFIX, 'css/editor.css')]) - js = [posixpath.join(settings.MEDIA_PREFIX, 'js/codemirror.js')] + posixpath.join(settings.DBTEMPLATES_MEDIA_PREFIX, 'css/editor.css')]) + js = [posixpath.join(settings.DBTEMPLATES_MEDIA_PREFIX, 'js/codemirror.js')] def render(self, name, value, attrs=None): result = [] @@ -43,15 +43,15 @@ class CodeMirrorTextArea(forms.Textarea): lineNumbers: true }); -""" % dict(media_prefix=settings.MEDIA_PREFIX, name=name)) +""" % dict(media_prefix=settings.DBTEMPLATES_MEDIA_PREFIX, name=name)) return mark_safe(u"".join(result)) -if settings.USE_CODEMIRROR: +if settings.DBTEMPLATES_USE_CODEMIRROR: TemplateContentTextArea = CodeMirrorTextArea else: TemplateContentTextArea = forms.Textarea -if settings.AUTO_POPULATE_CONTENT: +if settings.DBTEMPLATES_AUTO_POPULATE_CONTENT: content_help_text = _("Leaving this empty causes Django to look for a " "template with the given name and populate this field with its " "content.") diff --git a/dbtemplates/conf.py b/dbtemplates/conf.py new file mode 100644 index 0000000..34357d0 --- /dev/null +++ b/dbtemplates/conf.py @@ -0,0 +1,42 @@ +import posixpath + +from django.core.exceptions import ImproperlyConfigured + +from dbtemplates.utils.settings import AppSettings + + +class DbTemplatesSettings(AppSettings): + USE_CODEMIRROR = False + USE_REVERSION = False + ADD_DEFAULT_SITE = True + AUTO_POPULATE_CONTENT = True + MEDIA_PREFIX = None + CACHE_BACKEND = None + + def configure_media_prefix(self, value): + if value is None: + base_url = getattr(self, "STATIC_URL", None) + if base_url is None: + base_url = self.MEDIA_URL + value = posixpath.join(base_url, "dbtemplates/") + return value + + def configure_cache_backend(self, value): + # If we are on Django 1.3 AND using the new CACHES setting.. + if hasattr(self, "CACHES"): + if "dbtemplates" in self.CACHES: + return "dbtemplates" + else: + return "default" + if isinstance(value, basestring) and value.startswith("dbtemplates."): + raise ImproperlyConfigured("Please upgrade to one of the supported " + "backends as defined in the Django docs.") + return value + + def configure_use_reversion(self, value): + if value and 'reversion' not in self.INSTALLED_APPS: + raise ImproperlyConfigured("Please add 'reversion' to your " + "INSTALLED_APPS setting to make use of it in dbtemplates.") + return value + +settings = DbTemplatesSettings("DBTEMPLATES") diff --git a/dbtemplates/management/commands/sync_templates.py b/dbtemplates/management/commands/sync_templates.py index c4330e4..6288a7e 100644 --- a/dbtemplates/management/commands/sync_templates.py +++ b/dbtemplates/management/commands/sync_templates.py @@ -1,11 +1,11 @@ import os from optparse import make_option -from django.conf import settings from django.contrib.sites.models import Site from django.core.management.base import CommandError, NoArgsCommand from django.template.loaders.app_directories import app_template_dirs +from dbtemplates.conf import settings from dbtemplates.models import Template ALWAYS_ASK, FILES_TO_DATABASE, DATABASE_TO_FILES = ('0', '1', '2') diff --git a/dbtemplates/models.py b/dbtemplates/models.py index ef36677..2cc4cf2 100644 --- a/dbtemplates/models.py +++ b/dbtemplates/models.py @@ -9,9 +9,10 @@ from django.utils.translation import gettext_lazy as _ from django.contrib.sites.models import Site from django.contrib.sites.managers import CurrentSiteManager -from dbtemplates import settings -from dbtemplates.utils import (add_default_site, add_template_to_cache, - remove_cached_template, get_template_source) +from dbtemplates.conf import settings +from dbtemplates.utils.cache import add_template_to_cache, remove_cached_template +from dbtemplates.utils.template import get_template_source + class Template(models.Model): @@ -22,8 +23,8 @@ class Template(models.Model): name = models.CharField(_('name'), max_length=100, help_text=_("Example: 'flatpages/default.html'")) content = models.TextField(_('content'), blank=True) - sites = models.ManyToManyField(Site, verbose_name=_('sites'), blank=True, - null=True) + sites = models.ManyToManyField(Site, verbose_name=_('sites'), + blank=True, null=True) creation_date = models.DateTimeField(_('creation date'), default=datetime.now) last_changed = models.DateTimeField(_('last changed'), @@ -59,11 +60,24 @@ class Template(models.Model): 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 settings.AUTO_POPULATE_CONTENT and not self.content: + if settings.DBTEMPLATES_AUTO_POPULATE_CONTENT and not self.content: self.populate() super(Template, self).save(*args, **kwargs) +def add_default_site(instance, **kwargs): + """ + Called via Django's signals to cache the templates, if the template + in the database was added or changed, only if + DBTEMPLATES_ADD_DEFAULT_SITE setting is set. + """ + if not settings.DBTEMPLATES_ADD_DEFAULT_SITE: + return + current_site = Site.objects.get_current() + if current_site not in instance.sites.all(): + instance.sites.add(current_site) + + signals.post_save.connect(add_default_site, sender=Template) signals.post_save.connect(add_template_to_cache, sender=Template) signals.pre_delete.connect(remove_cached_template, sender=Template) diff --git a/dbtemplates/settings.py b/dbtemplates/settings.py deleted file mode 100644 index c1bef09..0000000 --- a/dbtemplates/settings.py +++ /dev/null @@ -1,33 +0,0 @@ -import posixpath -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured - -if "dbtemplates" in getattr(settings, "CACHES", {}): - # If we are on Django 1.3 AND using the new CACHES setting.. - cache = "dbtemplates" -else: - # ..or fall back to the old CACHE_BACKEND setting - cache = getattr(settings, "DBTEMPLATES_CACHE_BACKEND", None) -if isinstance(cache, basestring) and cache.startswith("dbtemplates."): - raise ImproperlyConfigured("Please upgrade to one of the supported " - "backends as defined in the Django docs.") -CACHE_BACKEND = cache - -ADD_DEFAULT_SITE = getattr(settings, 'DBTEMPLATES_ADD_DEFAULT_SITE', True) - -AUTO_POPULATE_CONTENT = getattr( - settings, 'DBTEMPLATES_AUTO_POPULATE_CONTENT', True) - -base_url = getattr(settings, "STATIC_URL", None) -if base_url is None: - base_url = settings.MEDIA_URL -MEDIA_PREFIX = getattr(settings, 'DBTEMPLATES_MEDIA_PREFIX', - posixpath.join(base_url, "dbtemplates/")) - -USE_REVERSION = getattr(settings, 'DBTEMPLATES_USE_REVERSION', False) - -if USE_REVERSION and 'reversion' not in settings.INSTALLED_APPS: - raise ImproperlyConfigured("Please add reversion to your " - "INSTALLED_APPS setting to make use of it in dbtemplates.") - -USE_CODEMIRROR = getattr(settings, 'DBTEMPLATES_USE_CODEMIRROR', False) diff --git a/dbtemplates/tests.py b/dbtemplates/tests.py index 0ddd6d0..df95e54 100644 --- a/dbtemplates/tests.py +++ b/dbtemplates/tests.py @@ -1,65 +1,49 @@ -from django import VERSION +from __future__ import with_statement + from django.core.management import call_command from django.template import loader, Context from django.test import TestCase from django.contrib.sites.models import Site -from dbtemplates import settings -from dbtemplates.loader import load_template_source +from dbtemplates.conf import settings from dbtemplates.models import Template -from dbtemplates.utils import get_template_source +from dbtemplates.utils.template import get_template_source + class DbTemplatesTestCase(TestCase): def setUp(self): self.site1, created1 = Site.objects.get_or_create(domain="example.com", name="example.com") self.site2, created2 = Site.objects.get_or_create(domain="example.org", name="example.org") + self.t1, _ = Template.objects.get_or_create(name='base.html', content='base') + self.t2, _ = Template.objects.get_or_create(name='sub.html', content='sub') + self.t2.sites.add(self.site2) def test_basiscs(self): - t1 = Template(name='base.html', content='{% block content %}Welcome at {{ title }}{% endblock %}') - t1.save() - self.assertEqual(Site.objects.get_current(), t1.sites.all()[0]) - self.assertTrue("Welcome at" in t1.content) + self.assertEqual(list(self.t1.sites.all()), [self.site1]) + self.assertTrue("base" in self.t1.content) + self.assertEqual(list(Template.objects.filter(sites=self.site1)), [self.t1, self.t2]) + self.assertEqual(list(self.t2.sites.all()), [self.site1, self.site2]) - t2 = Template(name='sub.html', content='{% extends "base.html" %}{% block content %}This is {{ title }}{% endblock %}') - t2.save() - t2.sites.add(self.site2) - - self.assertEqual(list(Template.objects.filter(sites=self.site1)), [t1, t2]) - self.assertEqual(list(t2.sites.all()), [self.site1, self.site2]) + def test_empty_sites(self): + old_add_default_site = settings.DBTEMPLATES_ADD_DEFAULT_SITE + try: + settings.DBTEMPLATES_ADD_DEFAULT_SITE = False + self.t3 = Template.objects.create(name='footer.html', content='footer') + self.assertEqual(list(self.t3.sites.all()), []) + finally: + settings.DBTEMPLATES_ADD_DEFAULT_SITE = old_add_default_site def test_load_templates(self): - self.test_basiscs() - original_template_source_loaders = loader.template_source_loaders - loader.template_source_loaders = [load_template_source] - try: - result1 = loader.get_template("base.html").render(Context({'title':'MainPage'})) - self.assertEqual(result1, u'Welcome at MainPage') - result2 = loader.get_template("sub.html").render(Context({'title':'SubPage'})) - self.assertEqual(result2, u'This is SubPage') - - if VERSION[:2] >= (1, 2): - from dbtemplates.loader import Loader - dbloader = Loader() - loader.template_source_loaders = [dbloader.load_template_source] - result = loader.get_template("base.html").render(Context({'title':'MainPage'})) - self.assertEqual(result, u'Welcome at MainPage') - result2 = loader.get_template("sub.html").render(Context({'title':'SubPage'})) - self.assertEqual(result2, u'This is SubPage') - finally: - loader.template_source_loaders = original_template_source_loaders + result = loader.get_template("base.html").render(Context({})) + self.assertEqual(result, 'base') + result2 = loader.get_template("sub.html").render(Context({})) + self.assertEqual(result2, 'sub') def test_error_templates_creation(self): call_command('create_error_templates', force=True, verbosity=0) self.assertEqual(list(Template.objects.filter(sites=self.site1)), - list(Template.objects.filter())) - - def test_disabling_default_site(self): - old_add_default_site = settings.ADD_DEFAULT_SITE - settings.ADD_DEFAULT_SITE = False - t3 = Template.objects.create(name='footer.html', content='ohai') - self.assertEqual(list(t3.sites.all()), []) - settings.ADD_DEFAULT_SITE = old_add_default_site + list(Template.objects.filter())) def test_automatic_sync(self): admin_base_template = get_template_source('admin/base.html') diff --git a/dbtemplates/utils/cache.py b/dbtemplates/utils/cache.py index 0dbe9d2..f93410f 100644 --- a/dbtemplates/utils/cache.py +++ b/dbtemplates/utils/cache.py @@ -16,7 +16,7 @@ def get_cache_key(name): return 'dbtemplates::%s::%s' % (name, current_site.pk) -def set_and_return(content): +def set_and_return(content, display_name): # Save in cache backend explicitly if manually deleted or invalidated if cache: cache.set(cache_key, content) diff --git a/dbtemplates/utils/settings.py b/dbtemplates/utils/settings.py new file mode 100644 index 0000000..221e58d --- /dev/null +++ b/dbtemplates/utils/settings.py @@ -0,0 +1,105 @@ +from inspect import getmembers +from django.conf import settings + + +class AppSettings(object): + """ + An app setting object to be used for handling app setting defaults + gracefully and providing a nice API for them. Say you have an app + called ``myapp`` and want to define a few defaults, and refer to the + defaults easily in the apps code. Add a ``settings.py`` to your app:: + + from path.to.utils import AppSettings + + class MyAppSettings(AppSettings): + SETTING_1 = "one" + SETTING_2 = ( + "two", + ) + + Then initialize the setting with the correct prefix in the location of + of your choice, e.g. ``conf.py`` of the app module:: + + settings = MyAppSettings(prefix="MYAPP") + + The ``MyAppSettings`` instance will automatically look at Django's + global setting to determine each of the settings and respect the + provided ``prefix``. E.g. adding this to your site's ``settings.py`` + will set the ``SETTING_1`` setting accordingly:: + + MYAPP_SETTING_1 = "uno" + + Usage + ----- + + Instead of using ``from django.conf import settings`` as you would + usually do, you can switch to using your apps own settings module + to access the app settings:: + + from myapp.conf import settings + + print myapp_settings.MYAPP_SETTING_1 + + ``AppSettings`` instances also work as pass-throughs for other + global settings that aren't related to the app. For example the + following code is perfectly valid:: + + from myapp.conf import settings + + if "myapp" in settings.INSTALLED_APPS: + print "yay, myapp is installed!" + + Custom handling + --------------- + + Each of the settings can be individually configured with callbacks. + For example, in case a value of a setting depends on other settings + or other dependencies. The following example sets one setting to a + different value depending on a global setting:: + + from django.conf import settings + + class MyCustomAppSettings(AppSettings): + ENABLED = True + + def configure_enabled(self, value): + return value and not self.DEBUG + + custom_settings = MyCustomAppSettings("MYAPP") + + The value of ``custom_settings.MYAPP_ENABLED`` will vary depending on the + value of the global ``DEBUG`` setting. + + Each of the app settings can be customized by providing + a method ``configure_`` that takes the default + value as defined in the class attributes as the only parameter. + The method needs to return the value to be use for the setting in + question. + """ + def __dir__(self): + return sorted(list(set(self.__dict__.keys() + dir(settings)))) + + __members__ = lambda self: self.__dir__() + + def __getattr__(self, name): + if name.startswith(self._prefix): + raise AttributeError("%r object has no attribute %r" % + (self.__class__.__name__, name)) + return getattr(settings, name) + + def __setattr__(self, name, value): + super(AppSettings, self).__setattr__(name, value) + if name in dir(settings): + setattr(settings, name, value) + + def __init__(self, prefix): + super(AppSettings, self).__setattr__('_prefix', prefix) + for setting, class_value in getmembers(self.__class__): + if setting == setting.upper(): + prefixed = "%s_%s" % (prefix.upper(), setting.upper()) + configured_value = getattr(settings, prefixed, class_value) + callback = getattr(self, "configure_%s" % setting.lower(), None) + if callable(callback): + configured_value = callback(configured_value) + delattr(self.__class__, setting) + setattr(self, prefixed, configured_value) diff --git a/runtests.py b/runtests.py index a9b1115..e0d6ef6 100644 --- a/runtests.py +++ b/runtests.py @@ -15,6 +15,11 @@ if not settings.configured: 'django.contrib.admin', 'dbtemplates', ], + TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.load_template_source', + 'django.template.loaders.app_directories.load_template_source', + 'dbtemplates.loader.Loader', + ) ) from django.test.simple import run_tests