From adac76f533c68f7ad54717c86cd886f34965c813 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 10 Jun 2025 02:14:14 -0600 Subject: [PATCH 1/6] Optionally connect signal and modernize tests --- dbtemplates/models.py | 27 +++---- dbtemplates/test_cases.py | 158 +++++++++++++++++++------------------- 2 files changed, 95 insertions(+), 90 deletions(-) diff --git a/dbtemplates/models.py b/dbtemplates/models.py index aa87dcb..6a9d35d 100644 --- a/dbtemplates/models.py +++ b/dbtemplates/models.py @@ -54,13 +54,6 @@ class Template(models.Model): except TemplateDoesNotExist: pass - def save(self, *args, **kwargs): - # If content is empty look for a template with the given name and - # populate the template instance with its content. - if settings.DBTEMPLATES_AUTO_POPULATE_CONTENT and not self.content: - self.populate() - super().save(*args, **kwargs) - def add_default_site(instance, **kwargs): """ @@ -68,13 +61,21 @@ def add_default_site(instance, **kwargs): 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) + instance.sites.add(Site.objects.get_current()) -signals.post_save.connect(add_default_site, sender=Template) +def populate_empty_content(instance, **kwargs): + # If content is empty look for a template with the given name and + # populate the template instance with its content. + if not instance.content: + instance.populate() + + +if settings.DBTEMPLATES_ADD_DEFAULT_SITE: + signals.post_save.connect(add_default_site, sender=Template) + +if settings.DBTEMPLATES_AUTO_POPULATE_CONTENT: + signals.pre_save.connect(populate_empty_content, 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/test_cases.py b/dbtemplates/test_cases.py index 062e2e5..d29f376 100644 --- a/dbtemplates/test_cases.py +++ b/dbtemplates/test_cases.py @@ -1,18 +1,22 @@ import os import shutil import tempfile +from contextlib import contextmanager +from pathlib import Path from unittest import mock from django.conf import settings as django_settings from django.core.cache.backends.base import BaseCache from django.core.management import call_command +from django.db.models.signals import post_save from django.template import loader, TemplateDoesNotExist -from django.test import TestCase +from django.test import TestCase, modify_settings, override_settings +from django.test.signals import receiver, setting_changed from django.contrib.sites.models import Site from dbtemplates.conf import settings -from dbtemplates.models import Template +from dbtemplates.models import Template, add_default_site from dbtemplates.utils.cache import get_cache_backend, get_cache_key from dbtemplates.utils.template import (get_template_source, check_template_syntax) @@ -20,15 +24,32 @@ from dbtemplates.management.commands.sync_templates import (FILES_TO_DATABASE, DATABASE_TO_FILES) -class DbTemplatesTestCase(TestCase): - def setUp(self): - self.old_TEMPLATES = settings.TEMPLATES - if 'dbtemplates.loader.Loader' not in settings.TEMPLATES: - loader.template_source_loaders = None - settings.TEMPLATES = list(settings.TEMPLATES) + [ - 'dbtemplates.loader.Loader' - ] +@receiver(setting_changed) +def handle_add_default_site(sender, setting, value, **kwargs): + if setting == "DBTEMPLATES_ADD_DEFAULT_SITE": + if value: + post_save.connect(add_default_site, sender=Template) + else: + post_save.disconnect(add_default_site, sender=Template) + +@contextmanager +def temptemplate(name: str, cleanup: bool = True): + temp_template_dir = Path(tempfile.mkdtemp('dbtemplates')) + temp_template_path = temp_template_dir / name + try: + yield temp_template_path + finally: + shutil.rmtree(temp_template_dir) + + +class DbTemplatesTestCase(TestCase): + @modify_settings( + TEMPLATES={ + "append": "dbtemplates.loader.Loader", + }, + ) + 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( @@ -39,48 +60,35 @@ class DbTemplatesTestCase(TestCase): name='sub.html', content='sub') self.t2.sites.add(self.site2) - def tearDown(self): - loader.template_source_loaders = None - settings.TEMPLATES = self.old_TEMPLATES - def test_basics(self): - 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]) + self.assertQuerySetEqual(self.t1.sites.all(), Site.objects.filter(id=self.site1.id)) + self.assertIn("base", self.t1.content) + self.assertQuerySetEqual(Template.objects.filter(sites=self.site1), + Template.objects.filter(id__in=[self.t1.id, self.t2.id])) + self.assertQuerySetEqual(self.t2.sites.all(), Site.objects.filter(id__in=[self.site1.id, self.site2.id])) + @override_settings(DBTEMPLATES_ADD_DEFAULT_SITE=False) 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 + self.t3 = Template.objects.create( + name='footer.html', content='footer') + self.assertQuerySetEqual(self.t3.sites.all(), self.t3.sites.none()) + @override_settings(DBTEMPLATES_ADD_DEFAULT_SITE=False) def test_load_templates_sites(self): - old_add_default_site = settings.DBTEMPLATES_ADD_DEFAULT_SITE - old_site_id = django_settings.SITE_ID - try: - settings.DBTEMPLATES_ADD_DEFAULT_SITE = False - t_site1 = Template.objects.create( - name='copyright.html', content='(c) example.com') - t_site1.sites.add(self.site1) - t_site2 = Template.objects.create( - name='copyright.html', content='(c) example.org') - t_site2.sites.add(self.site2) + t_site1 = Template.objects.create( + name='copyright.html', content='(c) example.com') + t_site1.sites.add(self.site1) + t_site2 = Template.objects.create( + name='copyright.html', content='(c) example.org') + t_site2.sites.add(self.site2) - django_settings.SITE_ID = Site.objects.create( - domain="example.net", name="example.net").id + new_site = Site.objects.create( + domain="example.net", name="example.net") + with self.settings(SITE_ID=new_site.id): Site.objects.clear_cache() self.assertRaises(TemplateDoesNotExist, loader.get_template, "copyright.html") - finally: - django_settings.SITE_ID = old_site_id - settings.DBTEMPLATES_ADD_DEFAULT_SITE = old_add_default_site def test_load_templates(self): result = loader.get_template("base.html").render() @@ -90,8 +98,8 @@ class DbTemplatesTestCase(TestCase): 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())) + self.assertQuerySetEqual(Template.objects.filter(sites=self.site1), + Template.objects.filter()) self.assertTrue(Template.objects.filter(name='404.html').exists()) def test_automatic_sync(self): @@ -101,42 +109,38 @@ class DbTemplatesTestCase(TestCase): def test_sync_templates(self): old_template_dirs = settings.TEMPLATES[0].get('DIRS', []) - temp_template_dir = tempfile.mkdtemp('dbtemplates') - temp_template_path = os.path.join(temp_template_dir, 'temp_test.html') - temp_template = open(temp_template_path, 'w', encoding='utf-8') - try: - temp_template.write('temp test') - settings.TEMPLATES[0]['DIRS'] = (temp_template_dir,) - # these works well if is not settings patched at runtime - # for supporting django < 1.7 tests we must patch dirs in runtime - from dbtemplates.management.commands import sync_templates - sync_templates.DIRS = settings.TEMPLATES[0]['DIRS'] + with temptemplate('temp_test.html') as temp_template_path: + with open(temp_template_path, 'w', encoding='utf-8') as temp_template: + temp_template.write('temp test') + try: + settings.TEMPLATES[0]['DIRS'] = (temp_template_path.parent,) + # these works well if is not settings patched at runtime + # for supporting django < 1.7 tests we must patch dirs in runtime + from dbtemplates.management.commands import sync_templates + sync_templates.DIRS = settings.TEMPLATES[0]['DIRS'] - self.assertFalse( - Template.objects.filter(name='temp_test.html').exists()) - call_command('sync_templates', force=True, - verbosity=0, overwrite=FILES_TO_DATABASE) - self.assertTrue( - Template.objects.filter(name='temp_test.html').exists()) + self.assertFalse( + Template.objects.filter(name='temp_test.html').exists()) + call_command('sync_templates', force=True, + verbosity=0, overwrite=FILES_TO_DATABASE) + self.assertTrue( + Template.objects.filter(name='temp_test.html').exists()) - t = Template.objects.get(name='temp_test.html') - t.content = 'temp test modified' - t.save() - call_command('sync_templates', force=True, - verbosity=0, overwrite=DATABASE_TO_FILES) - self.assertEqual('temp test modified', - open(temp_template_path, - encoding='utf-8').read()) + t = Template.objects.get(name='temp_test.html') + t.content = 'temp test modified' + t.save() + call_command('sync_templates', force=True, + verbosity=0, overwrite=DATABASE_TO_FILES) + with open(temp_template_path, encoding='utf-8') as f: + self.assertEqual('temp test modified', f.read()) - call_command('sync_templates', force=True, verbosity=0, - delete=True, overwrite=DATABASE_TO_FILES) - self.assertTrue(os.path.exists(temp_template_path)) - self.assertFalse( - Template.objects.filter(name='temp_test.html').exists()) - finally: - temp_template.close() - settings.TEMPLATES[0]['DIRS'] = old_template_dirs - shutil.rmtree(temp_template_dir) + call_command('sync_templates', force=True, verbosity=0, + delete=True, overwrite=DATABASE_TO_FILES) + self.assertTrue(os.path.exists(temp_template_path)) + self.assertFalse( + Template.objects.filter(name='temp_test.html').exists()) + finally: + settings.TEMPLATES[0]['DIRS'] = old_template_dirs def test_get_cache(self): self.assertTrue(isinstance(get_cache_backend(), BaseCache)) From 2c8e80bc6e720292b6eb27431e42449486375b58 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 10 Jun 2025 22:16:03 -0600 Subject: [PATCH 2/6] Use new-ish Python features and update tests --- dbtemplates/loader.py | 21 +- .../management/commands/sync_templates.py | 28 +-- dbtemplates/models.py | 4 +- dbtemplates/test_cases.py | 192 +++++++++++------- dbtemplates/test_settings.py | 8 +- dbtemplates/utils/template.py | 8 +- 6 files changed, 150 insertions(+), 111 deletions(-) diff --git a/dbtemplates/loader.py b/dbtemplates/loader.py index 5a57f27..243da31 100644 --- a/dbtemplates/loader.py +++ b/dbtemplates/loader.py @@ -52,23 +52,24 @@ class Loader(BaseLoader): # timestamp. site = Site.objects.get_current() cache_key = get_cache_key(template_name) - if cache: - try: - backend_template = cache.get(cache_key) - if backend_template: - return backend_template, template_name - except Exception: - pass - # Not found in cache, move on. cache_notfound_key = get_cache_notfound_key(template_name) if cache: + try: + backend_template = cache.get(cache_key) + except Exception: + pass + else: + if backend_template: + return (backend_template, template_name) + try: notfound = cache.get(cache_notfound_key) - if notfound: - raise TemplateDoesNotExist(template_name) except Exception: raise TemplateDoesNotExist(template_name) + else: + if notfound: + raise TemplateDoesNotExist(template_name) # Not marked as not-found, move on... diff --git a/dbtemplates/management/commands/sync_templates.py b/dbtemplates/management/commands/sync_templates.py index 7e336e0..6ce7fdc 100644 --- a/dbtemplates/management/commands/sync_templates.py +++ b/dbtemplates/management/commands/sync_templates.py @@ -1,4 +1,4 @@ -import os +from pathlib import Path from dbtemplates.models import Template from django.contrib.sites.models import Site @@ -86,19 +86,17 @@ class Command(BaseCommand): tpl_dirs = app_template_dirs + DIRS else: tpl_dirs = DIRS + app_template_dirs - templatedirs = [str(d) for d in tpl_dirs if os.path.isdir(d)] + templatedirs = [Path(d) for d in tpl_dirs if Path(d).is_dir()] for templatedir in templatedirs: - for dirpath, subdirs, filenames in os.walk(templatedir): + for dirpath, subdirs, filenames in templatedir.walk(follow_symlinks=True): for f in [ f for f in filenames if f.endswith(extension) and not f.startswith(".") ]: - path = os.path.join(dirpath, f) - name = path.split(str(templatedir))[1] - if name.startswith("/"): - name = name[1:] + path = dirpath / f + name = path.relative_to(templatedir) try: t = Template.on_site.get(name__exact=name) except Template.DoesNotExist: @@ -110,9 +108,7 @@ class Command(BaseCommand): "" % (name, path) ) if force or confirm.lower().startswith("y"): - with open(path, encoding="utf-8") as f: - t = Template(name=name, content=f.read()) - t.save() + t = Template.objects.create(name=name, content=path.read_text(encoding="utf-8")) t.sites.add(site) else: while True: @@ -134,20 +130,18 @@ class Command(BaseCommand): DATABASE_TO_FILES, ): if confirm == FILES_TO_DATABASE: - with open(path, encoding="utf-8") as f: - t.content = f.read() - t.save() - t.sites.add(site) + t.content = path.read_text(encoding="utf-8") + t.save() + t.sites.add(site) if delete: try: - os.remove(path) + path.unlink(missing_ok=True) except OSError: raise CommandError( f"Couldn't delete {path}" ) elif confirm == DATABASE_TO_FILES: - with open(path, "w", encoding="utf-8") as f: # noqa - f.write(t.content) + path.write_text(t.content, encoding="utf-8") if delete: t.delete() break diff --git a/dbtemplates/models.py b/dbtemplates/models.py index 6a9d35d..e84ecd3 100644 --- a/dbtemplates/models.py +++ b/dbtemplates/models.py @@ -49,10 +49,10 @@ class Template(models.Model): name = self.name try: source = get_template_source(name) - if source: - self.content = source except TemplateDoesNotExist: pass + else: + self.content = source def add_default_site(instance, **kwargs): diff --git a/dbtemplates/test_cases.py b/dbtemplates/test_cases.py index d29f376..acd0cf1 100644 --- a/dbtemplates/test_cases.py +++ b/dbtemplates/test_cases.py @@ -1,11 +1,9 @@ -import os import shutil import tempfile from contextlib import contextmanager from pathlib import Path from unittest import mock -from django.conf import settings as django_settings from django.core.cache.backends.base import BaseCache from django.core.management import call_command from django.db.models.signals import post_save @@ -16,12 +14,12 @@ from django.test.signals import receiver, setting_changed from django.contrib.sites.models import Site from dbtemplates.conf import settings +from dbtemplates.loader import Loader from dbtemplates.models import Template, add_default_site -from dbtemplates.utils.cache import get_cache_backend, get_cache_key +from dbtemplates.utils.cache import cache, get_cache_backend, get_cache_key, set_and_return from dbtemplates.utils.template import (get_template_source, check_template_syntax) -from dbtemplates.management.commands.sync_templates import (FILES_TO_DATABASE, - DATABASE_TO_FILES) +from dbtemplates.management.commands import sync_templates @receiver(setting_changed) @@ -43,7 +41,15 @@ def temptemplate(name: str, cleanup: bool = True): shutil.rmtree(temp_template_dir) -class DbTemplatesTestCase(TestCase): +class DbTemplatesCacheTestCase(TestCase): + def test_set_and_return(self): + self.assertTrue(bool(cache)) + rtn = set_and_return("this_is_the_cache_key", "cache test content", "cache display name") + self.assertEqual(rtn, ("cache test content", "cache display name")) + self.assertEqual(cache.get("this_is_the_cache_key"), "cache test content") + + +class BaseDbTemplatesTestCase(TestCase): @modify_settings( TEMPLATES={ "append": "dbtemplates.loader.Loader", @@ -60,18 +66,15 @@ class DbTemplatesTestCase(TestCase): name='sub.html', content='sub') self.t2.sites.add(self.site2) - def test_basics(self): - self.assertQuerySetEqual(self.t1.sites.all(), Site.objects.filter(id=self.site1.id)) - self.assertIn("base", self.t1.content) - self.assertQuerySetEqual(Template.objects.filter(sites=self.site1), - Template.objects.filter(id__in=[self.t1.id, self.t2.id])) - self.assertQuerySetEqual(self.t2.sites.all(), Site.objects.filter(id__in=[self.site1.id, self.site2.id])) - @override_settings(DBTEMPLATES_ADD_DEFAULT_SITE=False) - def test_empty_sites(self): - self.t3 = Template.objects.create( - name='footer.html', content='footer') - self.assertQuerySetEqual(self.t3.sites.all(), self.t3.sites.none()) +class DbTemplatesLoaderTestCase(BaseDbTemplatesTestCase): + def test_load_and_store_template(self): + from django.template.loader import _engine_list + from django.core.cache import CacheKeyWarning + loader = Loader(_engine_list()[0]) + with self.assertWarns(CacheKeyWarning): + rtn = loader._load_and_store_template('base.html', 'base template cache key', self.site1) + self.assertEqual(rtn, ('base', f'dbtemplates:default:base.html:example.com')) @override_settings(DBTEMPLATES_ADD_DEFAULT_SITE=False) def test_load_templates_sites(self): @@ -96,67 +99,6 @@ class DbTemplatesTestCase(TestCase): result2 = loader.get_template("sub.html").render() self.assertEqual(result2, 'sub') - def test_error_templates_creation(self): - call_command('create_error_templates', force=True, verbosity=0) - self.assertQuerySetEqual(Template.objects.filter(sites=self.site1), - Template.objects.filter()) - self.assertTrue(Template.objects.filter(name='404.html').exists()) - - def test_automatic_sync(self): - admin_base_template = get_template_source('admin/base.html') - template = Template.objects.create(name='admin/base.html') - self.assertEqual(admin_base_template, template.content) - - def test_sync_templates(self): - old_template_dirs = settings.TEMPLATES[0].get('DIRS', []) - with temptemplate('temp_test.html') as temp_template_path: - with open(temp_template_path, 'w', encoding='utf-8') as temp_template: - temp_template.write('temp test') - try: - settings.TEMPLATES[0]['DIRS'] = (temp_template_path.parent,) - # these works well if is not settings patched at runtime - # for supporting django < 1.7 tests we must patch dirs in runtime - from dbtemplates.management.commands import sync_templates - sync_templates.DIRS = settings.TEMPLATES[0]['DIRS'] - - self.assertFalse( - Template.objects.filter(name='temp_test.html').exists()) - call_command('sync_templates', force=True, - verbosity=0, overwrite=FILES_TO_DATABASE) - self.assertTrue( - Template.objects.filter(name='temp_test.html').exists()) - - t = Template.objects.get(name='temp_test.html') - t.content = 'temp test modified' - t.save() - call_command('sync_templates', force=True, - verbosity=0, overwrite=DATABASE_TO_FILES) - with open(temp_template_path, encoding='utf-8') as f: - self.assertEqual('temp test modified', f.read()) - - call_command('sync_templates', force=True, verbosity=0, - delete=True, overwrite=DATABASE_TO_FILES) - self.assertTrue(os.path.exists(temp_template_path)) - self.assertFalse( - Template.objects.filter(name='temp_test.html').exists()) - finally: - settings.TEMPLATES[0]['DIRS'] = old_template_dirs - - def test_get_cache(self): - self.assertTrue(isinstance(get_cache_backend(), BaseCache)) - - def test_check_template_syntax(self): - bad_template, _ = Template.objects.get_or_create( - name='bad.html', content='{% if foo %}Bar') - good_template, _ = Template.objects.get_or_create( - name='good.html', content='{% if foo %}Bar{% endif %}') - self.assertFalse(check_template_syntax(bad_template)[0]) - self.assertTrue(check_template_syntax(good_template)[0]) - - def test_get_cache_name(self): - self.assertEqual(get_cache_key('name with spaces'), - 'dbtemplates::name-with-spaces::1') - def test_cache_invalidation(self): # Add t1 into the cache of site2 self.t1.sites.add(self.site2) @@ -177,3 +119,97 @@ class DbTemplatesTestCase(TestCase): return_value=self.site2): result = loader.get_template("base.html").render() self.assertEqual(result, 'new content') + + +class DbTemplatesModelsTestCase(BaseDbTemplatesTestCase): + def test_basics(self): + self.assertQuerySetEqual(self.t1.sites.all(), Site.objects.filter(id=self.site1.id)) + self.assertIn("base", self.t1.content) + self.assertEqual(str(self.t1), self.t1.name) + self.assertEqual(str(self.t2), self.t2.name) + self.assertQuerySetEqual(Template.objects.filter(sites=self.site1), + Template.objects.filter(id__in=[self.t1.id, self.t2.id])) + self.assertQuerySetEqual(self.t2.sites.all(), Site.objects.filter(id__in=[self.site1.id, self.site2.id])) + + def test_populate(self): + t0 = Template.objects.create(name='header.html', content='

This is a header

') + t0.populate() + self.assertEqual(t0.content, "

This is a header

") + t0.populate(name='header.html') + self.assertEqual(t0.content, "

This is a header

") + + with temptemplate('temp_test.html') as temp_template_path: + temp_template_path.write_text('temp test') + (temp_template_path.parent / 'temp_test_2.html').write_text('temp test 2') + NEW_TEMPLATES = settings.TEMPLATES.copy() + NEW_TEMPLATES[0]['DIRS'] = (temp_template_path.parent,) + with self.settings(TEMPLATES=NEW_TEMPLATES): + t1 = Template.objects.create(name='temp_test.html') + t1.populate() + self.assertEqual(t1.content, "temp test") + t2 = Template.objects.create(name='temp_test.html') + t2.populate(name='temp_test_2.html') + self.assertEqual(t2.content, "temp test 2") + t3 = Template.objects.create(name='temp_test_3.html') + self.assertIsNone(t3.populate(name='temp_test_doesnt_exist.html')) + self.assertEqual(t3.content, "") + + @override_settings(DBTEMPLATES_ADD_DEFAULT_SITE=False) + def test_empty_sites(self): + self.t3 = Template.objects.create( + name='footer.html', content='footer') + self.assertQuerySetEqual(self.t3.sites.all(), self.t3.sites.none()) + + def test_error_templates_creation(self): + call_command('create_error_templates', force=True, verbosity=0) + self.assertQuerySetEqual(Template.objects.filter(sites=self.site1), + Template.objects.filter()) + self.assertTrue(Template.objects.filter(name='404.html').exists()) + + def test_automatic_sync(self): + admin_base_template = get_template_source('admin/base.html') + template = Template.objects.create(name='admin/base.html') + self.assertEqual(admin_base_template, template.content) + + def test_get_cache(self): + self.assertTrue(isinstance(get_cache_backend(), BaseCache)) + + def test_check_template_syntax(self): + bad_template, _ = Template.objects.get_or_create( + name='bad.html', content='{% if foo %}Bar') + good_template, _ = Template.objects.get_or_create( + name='good.html', content='{% if foo %}Bar{% endif %}') + self.assertFalse(check_template_syntax(bad_template)[0]) + self.assertTrue(check_template_syntax(good_template)[0]) + + def test_get_cache_name(self): + self.assertEqual(get_cache_key('name with spaces'), + 'dbtemplates::name-with-spaces::1') + + +class DbTemplatesSyncTemplatesCommandTestCase(TestCase): + def test_sync_templates(self): + with temptemplate('temp_test.html') as temp_template_path: + temp_template_path.write_text('temp test', encoding='utf-8') + NEW_TEMPLATES = settings.TEMPLATES.copy() + NEW_TEMPLATES[0]['DIRS'] = sync_templates.DIRS = (temp_template_path.parent,) + with self.settings(TEMPLATES=NEW_TEMPLATES): + self.assertFalse( + Template.objects.filter(name='temp_test.html').exists()) + call_command('sync_templates', force=True, + verbosity=0, overwrite=sync_templates.FILES_TO_DATABASE) + self.assertTrue( + Template.objects.filter(name='temp_test.html').exists()) + + t = Template.objects.get(name='temp_test.html') + t.content = 'temp test modified' + t.save() + call_command('sync_templates', force=True, + verbosity=0, overwrite=sync_templates.DATABASE_TO_FILES) + self.assertEqual('temp test modified', temp_template_path.read_text(encoding='utf-8')) + + call_command('sync_templates', ext='.html', app_first=True, force=True, verbosity=0, + delete=True, overwrite=sync_templates.DATABASE_TO_FILES) + self.assertTrue(temp_template_path.exists()) + self.assertFalse( + Template.objects.filter(name='temp_test.html').exists()) diff --git a/dbtemplates/test_settings.py b/dbtemplates/test_settings.py index 198fdde..275435d 100644 --- a/dbtemplates/test_settings.py +++ b/dbtemplates/test_settings.py @@ -1,5 +1,3 @@ -DBTEMPLATES_CACHE_BACKEND = 'dummy://' - DATABASE_ENGINE = 'sqlite3' # SQLite does not support removing unique constraints (see #28) SOUTH_TESTS_MIGRATE = False @@ -8,6 +6,12 @@ SITE_ID = 1 SECRET_KEY = 'something-something' +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + }, +} + DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', diff --git a/dbtemplates/utils/template.py b/dbtemplates/utils/template.py index 662b4f6..cd82c36 100644 --- a/dbtemplates/utils/template.py +++ b/dbtemplates/utils/template.py @@ -12,6 +12,7 @@ def get_loaders(): def get_template_source(name): source = None + not_found = [] for loader in get_loaders(): if loader.__module__.startswith('dbtemplates.'): # Don't give a damn about dbtemplates' own loader. @@ -19,10 +20,13 @@ def get_template_source(name): for origin in loader.get_template_sources(name): try: source = loader.get_contents(origin) - except (NotImplementedError, TemplateDoesNotExist): + except (NotImplementedError, TemplateDoesNotExist) as exc: + if exc.args[0] not in not_found: + not_found.append(exc.args[0]) continue - if source: + else: return source + raise TemplateDoesNotExist(name, chain=not_found) def check_template_syntax(template): From 58e88b1f18b7ed6d38e25152cb753b2fed3d944f Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 10 Jun 2025 22:17:05 -0600 Subject: [PATCH 3/6] Format with ruff --- dbtemplates/admin.py | 133 ++++++++------ dbtemplates/apps.py | 6 +- dbtemplates/conf.py | 48 +++-- dbtemplates/loader.py | 26 ++- .../commands/check_template_syntax.py | 7 +- .../commands/create_error_templates.py | 34 ++-- .../management/commands/sync_templates.py | 4 +- dbtemplates/migrations/0001_initial.py | 1 - ...2_alter_template_creation_date_and_more.py | 15 +- dbtemplates/models.py | 30 +-- dbtemplates/test_cases.py | 171 +++++++++++------- dbtemplates/test_settings.py | 52 +++--- dbtemplates/utils/cache.py | 1 + dbtemplates/utils/template.py | 6 +- docs/conf.py | 96 +++++----- 15 files changed, 367 insertions(+), 263 deletions(-) diff --git a/dbtemplates/admin.py b/dbtemplates/admin.py index f6d65af..9b782d0 100644 --- a/dbtemplates/admin.py +++ b/dbtemplates/admin.py @@ -14,8 +14,7 @@ from dbtemplates.utils.template import check_template_syntax # use reversion_compare's CompareVersionAdmin or reversion's VersionAdmin as # the base admin class if yes if settings.DBTEMPLATES_USE_REVERSION_COMPARE: - from reversion_compare.admin import CompareVersionAdmin \ - as TemplateModelAdmin + from reversion_compare.admin import CompareVersionAdmin as TemplateModelAdmin elif settings.DBTEMPLATES_USE_REVERSION: from reversion.admin import VersionAdmin as TemplateModelAdmin else: @@ -23,22 +22,22 @@ else: class CodeMirrorTextArea(forms.Textarea): - """ A custom widget for the CodeMirror browser editor to be used with the content field of the Template model. """ + class Media: - css = dict(screen=[posixpath.join( - settings.DBTEMPLATES_MEDIA_PREFIX, 'css/editor.css')]) - js = [posixpath.join(settings.DBTEMPLATES_MEDIA_PREFIX, - 'js/codemirror.js')] + css = dict( + screen=[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, renderer=None): result = [] + result.append(super().render(name, value, attrs)) result.append( - super().render(name, value, attrs)) - result.append(""" + """ -""" % dict(media_prefix=settings.DBTEMPLATES_MEDIA_PREFIX, name=name)) +""" + % dict(media_prefix=settings.DBTEMPLATES_MEDIA_PREFIX, name=name) + ) return mark_safe("".join(result)) @@ -61,62 +62,79 @@ else: TemplateContentTextArea = forms.Textarea 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.") + content_help_text = _( + "Leaving this empty causes Django to look for a " + "template with the given name and populate this " + "field with its content." + ) else: content_help_text = "" if settings.DBTEMPLATES_USE_CODEMIRROR and settings.DBTEMPLATES_USE_TINYMCE: - raise ImproperlyConfigured("You may use either CodeMirror or TinyMCE " - "with dbtemplates, not both. Please disable " - "one of them.") + raise ImproperlyConfigured( + "You may use either CodeMirror or TinyMCE " + "with dbtemplates, not both. Please disable " + "one of them." + ) if settings.DBTEMPLATES_USE_TINYMCE: from tinymce.widgets import AdminTinyMCE + TemplateContentTextArea = AdminTinyMCE elif settings.DBTEMPLATES_USE_REDACTOR: from redactor.widgets import RedactorEditor + TemplateContentTextArea = RedactorEditor class TemplateAdminForm(forms.ModelForm): - """ Custom AdminForm to make the content textarea wider. """ + content = forms.CharField( - widget=TemplateContentTextArea(attrs={'rows': '24'}), - help_text=content_help_text, required=False) + widget=TemplateContentTextArea(attrs={"rows": "24"}), + help_text=content_help_text, + required=False, + ) class Meta: model = Template - fields = ('name', 'content', 'sites', 'creation_date', 'last_changed') + fields = ("name", "content", "sites", "creation_date", "last_changed") fields = "__all__" class TemplateAdmin(TemplateModelAdmin): form = TemplateAdminForm - readonly_fields = ['creation_date', 'last_changed'] + readonly_fields = ["creation_date", "last_changed"] fieldsets = ( - (None, { - 'fields': ('name', 'content'), - 'classes': ('monospace',), - }), - (_('Advanced'), { - 'fields': (('sites'),), - }), - (_('Date/time'), { - 'fields': (('creation_date', 'last_changed'),), - 'classes': ('collapse',), - }), + ( + None, + { + "fields": ("name", "content"), + "classes": ("monospace",), + }, + ), + ( + _("Advanced"), + { + "fields": (("sites"),), + }, + ), + ( + _("Date/time"), + { + "fields": (("creation_date", "last_changed"),), + "classes": ("collapse",), + }, + ), ) - filter_horizontal = ('sites',) - list_display = ('name', 'creation_date', 'last_changed', 'site_list') - list_filter = ('sites',) + filter_horizontal = ("sites",) + list_display = ("name", "creation_date", "last_changed", "site_list") + list_filter = ("sites",) save_as = True - search_fields = ('name', 'content') - actions = ['invalidate_cache', 'repopulate_cache', 'check_syntax'] + search_fields = ("name", "content") + actions = ["invalidate_cache", "repopulate_cache", "check_syntax"] def invalidate_cache(self, request, queryset): for template in queryset: @@ -125,10 +143,11 @@ class TemplateAdmin(TemplateModelAdmin): message = ngettext( "Cache of one template successfully invalidated.", "Cache of %(count)d templates successfully invalidated.", - count) - self.message_user(request, message % {'count': count}) - invalidate_cache.short_description = _("Invalidate cache of " - "selected templates") + count, + ) + self.message_user(request, message % {"count": count}) + + invalidate_cache.short_description = _("Invalidate cache of selected templates") def repopulate_cache(self, request, queryset): for template in queryset: @@ -137,37 +156,43 @@ class TemplateAdmin(TemplateModelAdmin): message = ngettext( "Cache successfully repopulated with one template.", "Cache successfully repopulated with %(count)d templates.", - count) - self.message_user(request, message % {'count': count}) - repopulate_cache.short_description = _("Repopulate cache with " - "selected templates") + count, + ) + self.message_user(request, message % {"count": count}) + + repopulate_cache.short_description = _("Repopulate cache with selected templates") def check_syntax(self, request, queryset): errors = [] for template in queryset: valid, error = check_template_syntax(template) if not valid: - errors.append(f'{template.name}: {error}') + errors.append(f"{template.name}: {error}") if errors: count = len(errors) message = ngettext( "Template syntax check FAILED for %(names)s.", - "Template syntax check FAILED for " - "%(count)d templates: %(names)s.", - count) - self.message_user(request, message % - {'count': count, 'names': ', '.join(errors)}) + "Template syntax check FAILED for %(count)d templates: %(names)s.", + count, + ) + self.message_user( + request, message % {"count": count, "names": ", ".join(errors)} + ) else: count = queryset.count() message = ngettext( "Template syntax OK.", - "Template syntax OK for %(count)d templates.", count) - self.message_user(request, message % {'count': count}) + "Template syntax OK for %(count)d templates.", + count, + ) + self.message_user(request, message % {"count": count}) + check_syntax.short_description = _("Check template syntax") def site_list(self, template): return ", ".join([site.name for site in template.sites.all()]) - site_list.short_description = _('sites') + + site_list.short_description = _("sites") admin.site.register(Template, TemplateAdmin) diff --git a/dbtemplates/apps.py b/dbtemplates/apps.py index 9f75273..a98ba83 100644 --- a/dbtemplates/apps.py +++ b/dbtemplates/apps.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _ class DBTemplatesConfig(AppConfig): - name = 'dbtemplates' - verbose_name = _('Database templates') + name = "dbtemplates" + verbose_name = _("Database templates") - default_auto_field = 'django.db.models.AutoField' + default_auto_field = "django.db.models.AutoField" diff --git a/dbtemplates/conf.py b/dbtemplates/conf.py index 010db5b..77b5d08 100644 --- a/dbtemplates/conf.py +++ b/dbtemplates/conf.py @@ -33,35 +33,45 @@ class DbTemplatesConf(AppConf): else: return "default" if isinstance(value, str) and value.startswith("dbtemplates."): - raise ImproperlyConfigured("Please upgrade to one of the " - "supported backends as defined " - "in the Django docs.") + 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 settings.INSTALLED_APPS: - raise ImproperlyConfigured("Please add 'reversion' to your " - "INSTALLED_APPS setting to make " - "use of it in dbtemplates.") + if value and "reversion" not in settings.INSTALLED_APPS: + raise ImproperlyConfigured( + "Please add 'reversion' to your " + "INSTALLED_APPS setting to make " + "use of it in dbtemplates." + ) return value def configure_use_reversion_compare(self, value): - if value and 'reversion_compare' not in settings.INSTALLED_APPS: - raise ImproperlyConfigured("Please add 'reversion_compare' to your" - " INSTALLED_APPS setting to make " - "use of it in dbtemplates.") + if value and "reversion_compare" not in settings.INSTALLED_APPS: + raise ImproperlyConfigured( + "Please add 'reversion_compare' to your" + " INSTALLED_APPS setting to make " + "use of it in dbtemplates." + ) return value def configure_use_tinymce(self, value): - if value and 'tinymce' not in settings.INSTALLED_APPS: - raise ImproperlyConfigured("Please add 'tinymce' to your " - "INSTALLED_APPS setting to make " - "use of it in dbtemplates.") + if value and "tinymce" not in settings.INSTALLED_APPS: + raise ImproperlyConfigured( + "Please add 'tinymce' to your " + "INSTALLED_APPS setting to make " + "use of it in dbtemplates." + ) return value def configure_use_redactor(self, value): - if value and 'redactor' not in settings.INSTALLED_APPS: - raise ImproperlyConfigured("Please add 'redactor' to your " - "INSTALLED_APPS setting to make " - "use of it in dbtemplates.") + if value and "redactor" not in settings.INSTALLED_APPS: + raise ImproperlyConfigured( + "Please add 'redactor' to your " + "INSTALLED_APPS setting to make " + "use of it in dbtemplates." + ) return value diff --git a/dbtemplates/loader.py b/dbtemplates/loader.py index 243da31..30e9dbb 100644 --- a/dbtemplates/loader.py +++ b/dbtemplates/loader.py @@ -4,8 +4,12 @@ from django.template import Origin, TemplateDoesNotExist from django.template.loaders.base import Loader as BaseLoader from dbtemplates.models import Template -from dbtemplates.utils.cache import (cache, get_cache_key, - set_and_return, get_cache_notfound_key) +from dbtemplates.utils.cache import ( + cache, + get_cache_key, + set_and_return, + get_cache_notfound_key, +) class Loader(BaseLoader): @@ -17,6 +21,7 @@ class Loader(BaseLoader): it falls back to query the database field ``name`` with the template path and ``sites`` with the current site. """ + is_usable = True def get_template_sources(self, template_name, template_dirs=None): @@ -30,11 +35,10 @@ class Loader(BaseLoader): content, _ = self._load_template_source(origin.template_name) return content - def _load_and_store_template(self, template_name, cache_key, site, - **params): + def _load_and_store_template(self, template_name, cache_key, site, **params): template = Template.objects.get(name__exact=template_name, **params) db = router.db_for_read(Template, instance=template) - display_name = f'dbtemplates:{db}:{template_name}:{site.domain}' + display_name = f"dbtemplates:{db}:{template_name}:{site.domain}" return set_and_return(cache_key, template.content, display_name) def _load_template_source(self, template_name, template_dirs=None): @@ -74,15 +78,17 @@ class Loader(BaseLoader): # Not marked as not-found, move on... try: - return self._load_and_store_template(template_name, cache_key, - site, sites__in=[site.id]) + return self._load_and_store_template( + template_name, cache_key, site, sites__in=[site.id] + ) except (Template.MultipleObjectsReturned, Template.DoesNotExist): try: - return self._load_and_store_template(template_name, cache_key, - site, sites__isnull=True) + return self._load_and_store_template( + template_name, cache_key, site, sites__isnull=True + ) except (Template.MultipleObjectsReturned, Template.DoesNotExist): pass # Mark as not-found in cache. - cache.set(cache_notfound_key, '1') + cache.set(cache_notfound_key, "1") raise TemplateDoesNotExist(template_name) diff --git a/dbtemplates/management/commands/check_template_syntax.py b/dbtemplates/management/commands/check_template_syntax.py index 3837e65..c315ad2 100644 --- a/dbtemplates/management/commands/check_template_syntax.py +++ b/dbtemplates/management/commands/check_template_syntax.py @@ -12,8 +12,9 @@ class Command(BaseCommand): for template in Template.objects.all(): valid, error = check_template_syntax(template) if not valid: - errors.append(f'{template.name}: {error}') + errors.append(f"{template.name}: {error}") if errors: raise CommandError( - 'Some templates contained errors\n%s' % '\n'.join(errors)) - self.stdout.write('OK') + "Some templates contained errors\n%s" % "\n".join(errors) + ) + self.stdout.write("OK") diff --git a/dbtemplates/management/commands/create_error_templates.py b/dbtemplates/management/commands/create_error_templates.py index 3c53d24..f7a78c9 100644 --- a/dbtemplates/management/commands/create_error_templates.py +++ b/dbtemplates/management/commands/create_error_templates.py @@ -29,29 +29,39 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( - "-f", "--force", action="store_true", dest="force", - default=False, help="overwrite existing database templates") + "-f", + "--force", + action="store_true", + dest="force", + default=False, + help="overwrite existing database templates", + ) def handle(self, **options): - force = options.get('force') + force = options.get("force") try: site = Site.objects.get_current() except Site.DoesNotExist: - raise CommandError("Please make sure to have the sites contrib " - "app installed and setup with a site object") + raise CommandError( + "Please make sure to have the sites contrib " + "app installed and setup with a site object" + ) - verbosity = int(options.get('verbosity', 1)) + verbosity = int(options.get("verbosity", 1)) for error_code in (404, 500): template, created = Template.objects.get_or_create( - name=f"{error_code}.html") + name=f"{error_code}.html" + ) if created or (not created and force): - template.content = TEMPLATES.get(error_code, '') + template.content = TEMPLATES.get(error_code, "") template.save() template.sites.add(site) if verbosity >= 1: - sys.stdout.write("Created database template " - "for %s errors.\n" % error_code) + sys.stdout.write( + "Created database template for %s errors.\n" % error_code + ) else: if verbosity >= 1: - sys.stderr.write("A template for %s errors " - "already exists.\n" % error_code) + sys.stderr.write( + "A template for %s errors already exists.\n" % error_code + ) diff --git a/dbtemplates/management/commands/sync_templates.py b/dbtemplates/management/commands/sync_templates.py index 6ce7fdc..047fedb 100644 --- a/dbtemplates/management/commands/sync_templates.py +++ b/dbtemplates/management/commands/sync_templates.py @@ -108,7 +108,9 @@ class Command(BaseCommand): "" % (name, path) ) if force or confirm.lower().startswith("y"): - t = Template.objects.create(name=name, content=path.read_text(encoding="utf-8")) + t = Template.objects.create( + name=name, content=path.read_text(encoding="utf-8") + ) t.sites.add(site) else: while True: diff --git a/dbtemplates/migrations/0001_initial.py b/dbtemplates/migrations/0001_initial.py index 7ac217f..b0e5dab 100644 --- a/dbtemplates/migrations/0001_initial.py +++ b/dbtemplates/migrations/0001_initial.py @@ -4,7 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("sites", "0001_initial"), ] diff --git a/dbtemplates/migrations/0002_alter_template_creation_date_and_more.py b/dbtemplates/migrations/0002_alter_template_creation_date_and_more.py index 61cb561..73d4a0d 100644 --- a/dbtemplates/migrations/0002_alter_template_creation_date_and_more.py +++ b/dbtemplates/migrations/0002_alter_template_creation_date_and_more.py @@ -4,20 +4,19 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('dbtemplates', '0001_initial'), + ("dbtemplates", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='template', - name='creation_date', - field=models.DateTimeField(auto_now_add=True, verbose_name='creation date'), + model_name="template", + name="creation_date", + field=models.DateTimeField(auto_now_add=True, verbose_name="creation date"), ), migrations.AlterField( - model_name='template', - name='last_changed', - field=models.DateTimeField(auto_now=True, verbose_name='last changed'), + model_name="template", + name="last_changed", + field=models.DateTimeField(auto_now=True, verbose_name="last changed"), ), ] diff --git a/dbtemplates/models.py b/dbtemplates/models.py index e84ecd3..62f4a1c 100644 --- a/dbtemplates/models.py +++ b/dbtemplates/models.py @@ -18,24 +18,26 @@ class Template(models.Model): Defines a template model for use with the database template loader. The field ``name`` is the equivalent to the filename of a static template. """ - id = models.AutoField(primary_key=True, verbose_name=_('ID'), - serialize=False, auto_created=True) - 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) - creation_date = models.DateTimeField(_('creation date'), auto_now_add=True) - last_changed = models.DateTimeField(_('last changed'), auto_now=True) + + id = models.AutoField( + primary_key=True, verbose_name=_("ID"), serialize=False, auto_created=True + ) + 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) + creation_date = models.DateTimeField(_("creation date"), auto_now_add=True) + last_changed = models.DateTimeField(_("last changed"), auto_now=True) objects = models.Manager() - on_site = CurrentSiteManager('sites') + on_site = CurrentSiteManager("sites") class Meta: - db_table = 'django_template' - verbose_name = _('template') - verbose_name_plural = _('templates') - ordering = ('name',) + db_table = "django_template" + verbose_name = _("template") + verbose_name_plural = _("templates") + ordering = ("name",) def __str__(self): return self.name diff --git a/dbtemplates/test_cases.py b/dbtemplates/test_cases.py index acd0cf1..0a2109d 100644 --- a/dbtemplates/test_cases.py +++ b/dbtemplates/test_cases.py @@ -16,9 +16,13 @@ from django.contrib.sites.models import Site from dbtemplates.conf import settings from dbtemplates.loader import Loader from dbtemplates.models import Template, add_default_site -from dbtemplates.utils.cache import cache, get_cache_backend, get_cache_key, set_and_return -from dbtemplates.utils.template import (get_template_source, - check_template_syntax) +from dbtemplates.utils.cache import ( + cache, + get_cache_backend, + get_cache_key, + set_and_return, +) +from dbtemplates.utils.template import get_template_source, check_template_syntax from dbtemplates.management.commands import sync_templates @@ -33,7 +37,7 @@ def handle_add_default_site(sender, setting, value, **kwargs): @contextmanager def temptemplate(name: str, cleanup: bool = True): - temp_template_dir = Path(tempfile.mkdtemp('dbtemplates')) + temp_template_dir = Path(tempfile.mkdtemp("dbtemplates")) temp_template_path = temp_template_dir / name try: yield temp_template_path @@ -44,7 +48,9 @@ def temptemplate(name: str, cleanup: bool = True): class DbTemplatesCacheTestCase(TestCase): def test_set_and_return(self): self.assertTrue(bool(cache)) - rtn = set_and_return("this_is_the_cache_key", "cache test content", "cache display name") + rtn = set_and_return( + "this_is_the_cache_key", "cache test content", "cache display name" + ) self.assertEqual(rtn, ("cache test content", "cache display name")) self.assertEqual(cache.get("this_is_the_cache_key"), "cache test content") @@ -57,13 +63,13 @@ class BaseDbTemplatesTestCase(TestCase): ) def setUp(self): self.site1, created1 = Site.objects.get_or_create( - domain="example.com", name="example.com") + 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') + 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) @@ -71,33 +77,38 @@ class DbTemplatesLoaderTestCase(BaseDbTemplatesTestCase): def test_load_and_store_template(self): from django.template.loader import _engine_list from django.core.cache import CacheKeyWarning + loader = Loader(_engine_list()[0]) with self.assertWarns(CacheKeyWarning): - rtn = loader._load_and_store_template('base.html', 'base template cache key', self.site1) - self.assertEqual(rtn, ('base', f'dbtemplates:default:base.html:example.com')) + rtn = loader._load_and_store_template( + "base.html", "base template cache key", self.site1 + ) + self.assertEqual(rtn, ("base", "dbtemplates:default:base.html:example.com")) @override_settings(DBTEMPLATES_ADD_DEFAULT_SITE=False) def test_load_templates_sites(self): t_site1 = Template.objects.create( - name='copyright.html', content='(c) example.com') + name="copyright.html", content="(c) example.com" + ) t_site1.sites.add(self.site1) t_site2 = Template.objects.create( - name='copyright.html', content='(c) example.org') + name="copyright.html", content="(c) example.org" + ) t_site2.sites.add(self.site2) - new_site = Site.objects.create( - domain="example.net", name="example.net") + new_site = Site.objects.create(domain="example.net", name="example.net") with self.settings(SITE_ID=new_site.id): Site.objects.clear_cache() - self.assertRaises(TemplateDoesNotExist, - loader.get_template, "copyright.html") + self.assertRaises( + TemplateDoesNotExist, loader.get_template, "copyright.html" + ) def test_load_templates(self): result = loader.get_template("base.html").render() - self.assertEqual(result, 'base') + self.assertEqual(result, "base") result2 = loader.get_template("sub.html").render() - self.assertEqual(result2, 'sub') + self.assertEqual(result2, "sub") def test_cache_invalidation(self): # Add t1 into the cache of site2 @@ -123,52 +134,61 @@ class DbTemplatesLoaderTestCase(BaseDbTemplatesTestCase): class DbTemplatesModelsTestCase(BaseDbTemplatesTestCase): def test_basics(self): - self.assertQuerySetEqual(self.t1.sites.all(), Site.objects.filter(id=self.site1.id)) + self.assertQuerySetEqual( + self.t1.sites.all(), Site.objects.filter(id=self.site1.id) + ) self.assertIn("base", self.t1.content) self.assertEqual(str(self.t1), self.t1.name) self.assertEqual(str(self.t2), self.t2.name) - self.assertQuerySetEqual(Template.objects.filter(sites=self.site1), - Template.objects.filter(id__in=[self.t1.id, self.t2.id])) - self.assertQuerySetEqual(self.t2.sites.all(), Site.objects.filter(id__in=[self.site1.id, self.site2.id])) + self.assertQuerySetEqual( + Template.objects.filter(sites=self.site1), + Template.objects.filter(id__in=[self.t1.id, self.t2.id]), + ) + self.assertQuerySetEqual( + self.t2.sites.all(), + Site.objects.filter(id__in=[self.site1.id, self.site2.id]), + ) def test_populate(self): - t0 = Template.objects.create(name='header.html', content='

This is a header

') + t0 = Template.objects.create( + name="header.html", content="

This is a header

" + ) t0.populate() self.assertEqual(t0.content, "

This is a header

") - t0.populate(name='header.html') + t0.populate(name="header.html") self.assertEqual(t0.content, "

This is a header

") - with temptemplate('temp_test.html') as temp_template_path: - temp_template_path.write_text('temp test') - (temp_template_path.parent / 'temp_test_2.html').write_text('temp test 2') + with temptemplate("temp_test.html") as temp_template_path: + temp_template_path.write_text("temp test") + (temp_template_path.parent / "temp_test_2.html").write_text("temp test 2") NEW_TEMPLATES = settings.TEMPLATES.copy() - NEW_TEMPLATES[0]['DIRS'] = (temp_template_path.parent,) + NEW_TEMPLATES[0]["DIRS"] = (temp_template_path.parent,) with self.settings(TEMPLATES=NEW_TEMPLATES): - t1 = Template.objects.create(name='temp_test.html') + t1 = Template.objects.create(name="temp_test.html") t1.populate() self.assertEqual(t1.content, "temp test") - t2 = Template.objects.create(name='temp_test.html') - t2.populate(name='temp_test_2.html') + t2 = Template.objects.create(name="temp_test.html") + t2.populate(name="temp_test_2.html") self.assertEqual(t2.content, "temp test 2") - t3 = Template.objects.create(name='temp_test_3.html') - self.assertIsNone(t3.populate(name='temp_test_doesnt_exist.html')) + t3 = Template.objects.create(name="temp_test_3.html") + self.assertIsNone(t3.populate(name="temp_test_doesnt_exist.html")) self.assertEqual(t3.content, "") @override_settings(DBTEMPLATES_ADD_DEFAULT_SITE=False) def test_empty_sites(self): - self.t3 = Template.objects.create( - name='footer.html', content='footer') + self.t3 = Template.objects.create(name="footer.html", content="footer") self.assertQuerySetEqual(self.t3.sites.all(), self.t3.sites.none()) def test_error_templates_creation(self): - call_command('create_error_templates', force=True, verbosity=0) - self.assertQuerySetEqual(Template.objects.filter(sites=self.site1), - Template.objects.filter()) - self.assertTrue(Template.objects.filter(name='404.html').exists()) + call_command("create_error_templates", force=True, verbosity=0) + self.assertQuerySetEqual( + Template.objects.filter(sites=self.site1), Template.objects.filter() + ) + self.assertTrue(Template.objects.filter(name="404.html").exists()) def test_automatic_sync(self): - admin_base_template = get_template_source('admin/base.html') - template = Template.objects.create(name='admin/base.html') + admin_base_template = get_template_source("admin/base.html") + template = Template.objects.create(name="admin/base.html") self.assertEqual(admin_base_template, template.content) def test_get_cache(self): @@ -176,40 +196,63 @@ class DbTemplatesModelsTestCase(BaseDbTemplatesTestCase): def test_check_template_syntax(self): bad_template, _ = Template.objects.get_or_create( - name='bad.html', content='{% if foo %}Bar') + name="bad.html", content="{% if foo %}Bar" + ) good_template, _ = Template.objects.get_or_create( - name='good.html', content='{% if foo %}Bar{% endif %}') + name="good.html", content="{% if foo %}Bar{% endif %}" + ) self.assertFalse(check_template_syntax(bad_template)[0]) self.assertTrue(check_template_syntax(good_template)[0]) def test_get_cache_name(self): - self.assertEqual(get_cache_key('name with spaces'), - 'dbtemplates::name-with-spaces::1') + self.assertEqual( + get_cache_key("name with spaces"), "dbtemplates::name-with-spaces::1" + ) class DbTemplatesSyncTemplatesCommandTestCase(TestCase): def test_sync_templates(self): - with temptemplate('temp_test.html') as temp_template_path: - temp_template_path.write_text('temp test', encoding='utf-8') + with temptemplate("temp_test.html") as temp_template_path: + temp_template_path.write_text("temp test", encoding="utf-8") NEW_TEMPLATES = settings.TEMPLATES.copy() - NEW_TEMPLATES[0]['DIRS'] = sync_templates.DIRS = (temp_template_path.parent,) + NEW_TEMPLATES[0]["DIRS"] = sync_templates.DIRS = ( + temp_template_path.parent, + ) with self.settings(TEMPLATES=NEW_TEMPLATES): self.assertFalse( - Template.objects.filter(name='temp_test.html').exists()) - call_command('sync_templates', force=True, - verbosity=0, overwrite=sync_templates.FILES_TO_DATABASE) - self.assertTrue( - Template.objects.filter(name='temp_test.html').exists()) + Template.objects.filter(name="temp_test.html").exists() + ) + call_command( + "sync_templates", + force=True, + verbosity=0, + overwrite=sync_templates.FILES_TO_DATABASE, + ) + self.assertTrue(Template.objects.filter(name="temp_test.html").exists()) - t = Template.objects.get(name='temp_test.html') - t.content = 'temp test modified' + t = Template.objects.get(name="temp_test.html") + t.content = "temp test modified" t.save() - call_command('sync_templates', force=True, - verbosity=0, overwrite=sync_templates.DATABASE_TO_FILES) - self.assertEqual('temp test modified', temp_template_path.read_text(encoding='utf-8')) + call_command( + "sync_templates", + force=True, + verbosity=0, + overwrite=sync_templates.DATABASE_TO_FILES, + ) + self.assertEqual( + "temp test modified", temp_template_path.read_text(encoding="utf-8") + ) - call_command('sync_templates', ext='.html', app_first=True, force=True, verbosity=0, - delete=True, overwrite=sync_templates.DATABASE_TO_FILES) + call_command( + "sync_templates", + ext=".html", + app_first=True, + force=True, + verbosity=0, + delete=True, + overwrite=sync_templates.DATABASE_TO_FILES, + ) self.assertTrue(temp_template_path.exists()) self.assertFalse( - Template.objects.filter(name='temp_test.html').exists()) + Template.objects.filter(name="temp_test.html").exists() + ) diff --git a/dbtemplates/test_settings.py b/dbtemplates/test_settings.py index 275435d..d39673d 100644 --- a/dbtemplates/test_settings.py +++ b/dbtemplates/test_settings.py @@ -1,10 +1,10 @@ -DATABASE_ENGINE = 'sqlite3' +DATABASE_ENGINE = "sqlite3" # SQLite does not support removing unique constraints (see #28) SOUTH_TESTS_MIGRATE = False SITE_ID = 1 -SECRET_KEY = 'something-something' +SECRET_KEY = "something-something" CACHES = { "default": { @@ -13,43 +13,43 @@ CACHES = { } DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", } } INSTALLED_APPS = [ - 'django.contrib.contenttypes', - 'django.contrib.sites', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.admin', - 'django.contrib.auth', - 'dbtemplates', + "django.contrib.contenttypes", + "django.contrib.sites", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.admin", + "django.contrib.auth", + "dbtemplates", ] MIDDLEWARE = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", ) TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - 'dbtemplates.loader.Loader', + "django.template.loaders.filesystem.Loader", + "django.template.loaders.app_directories.Loader", + "dbtemplates.loader.Loader", ) TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'OPTIONS': { - 'loaders': TEMPLATE_LOADERS, - 'context_processors': [ - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ] - } + "BACKEND": "django.template.backends.django.DjangoTemplates", + "OPTIONS": { + "loaders": TEMPLATE_LOADERS, + "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, }, ] diff --git a/dbtemplates/utils/cache.py b/dbtemplates/utils/cache.py index 89039ab..11bd6ca 100644 --- a/dbtemplates/utils/cache.py +++ b/dbtemplates/utils/cache.py @@ -9,6 +9,7 @@ def get_cache_backend(): Compatibilty wrapper for getting Django's cache backend instance """ from django.core.cache import caches + cache = caches.create_connection(settings.DBTEMPLATES_CACHE_BACKEND) # Some caches -- python-memcached in particular -- need to do a cleanup at diff --git a/dbtemplates/utils/template.py b/dbtemplates/utils/template.py index cd82c36..e8d3be4 100644 --- a/dbtemplates/utils/template.py +++ b/dbtemplates/utils/template.py @@ -1,9 +1,9 @@ -from django.template import (Template, TemplateDoesNotExist, - TemplateSyntaxError) +from django.template import Template, TemplateDoesNotExist, TemplateSyntaxError def get_loaders(): from django.template.loader import _engine_list + loaders = [] for engine in _engine_list(): loaders.extend(engine.engine.template_loaders) @@ -14,7 +14,7 @@ def get_template_source(name): source = None not_found = [] for loader in get_loaders(): - if loader.__module__.startswith('dbtemplates.'): + if loader.__module__.startswith("dbtemplates."): # Don't give a damn about dbtemplates' own loader. continue for origin in loader.get_template_sources(name): diff --git a/docs/conf.py b/docs/conf.py index 2055241..6b2422f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,29 +15,29 @@ import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.append(os.path.abspath('.')) +sys.path.append(os.path.abspath(".")) # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage'] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.coverage"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.txt' +source_suffix = ".txt" # The encoding of source files. -#source_encoding = 'utf-8' +# source_encoding = 'utf-8' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'django-dbtemplates' -copyright = '2007-2019, Jannis Leidel and contributors' +project = "django-dbtemplates" +copyright = "2007-2019, Jannis Leidel and contributors" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -46,61 +46,62 @@ copyright = '2007-2019, Jannis Leidel and contributors' # The short X.Y version. try: from dbtemplates import __version__ + # The short X.Y version. - version = '.'.join(__version__.split('.')[:2]) + version = ".".join(__version__.split(".")[:2]) # The full version, including alpha/beta/rc tags. release = __version__ except ImportError: - version = release = 'dev' + version = release = "dev" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. -#unused_docs = [] +# unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. -exclude_trees = ['_build'] +exclude_trees = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'default' +html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = ['_theme'] @@ -114,12 +115,12 @@ html_short_title = "django-dbtemplates" # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -128,71 +129,76 @@ html_short_title = "django-dbtemplates" # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_use_modindex = True +# html_use_modindex = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' +# html_file_suffix = '' # Output file base name for HTML help builder. -htmlhelp_basename = 'django-dbtemplatesdoc' +htmlhelp_basename = "django-dbtemplatesdoc" # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'django-dbtemplates.tex', 'django-dbtemplates Documentation', - 'Jannis Leidel and contributors', 'manual'), + ( + "index", + "django-dbtemplates.tex", + "django-dbtemplates Documentation", + "Jannis Leidel and contributors", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_use_modindex = True +# latex_use_modindex = True From 347731929824b32b4b58239ffd3ee84ae775b9f8 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 10 Jun 2025 22:44:06 -0600 Subject: [PATCH 4/6] Use os.walk instead of Path.walk until we support only Python 3.12+ --- dbtemplates/management/commands/sync_templates.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dbtemplates/management/commands/sync_templates.py b/dbtemplates/management/commands/sync_templates.py index 047fedb..1cad812 100644 --- a/dbtemplates/management/commands/sync_templates.py +++ b/dbtemplates/management/commands/sync_templates.py @@ -1,3 +1,4 @@ +import os from pathlib import Path from dbtemplates.models import Template @@ -89,13 +90,15 @@ class Command(BaseCommand): templatedirs = [Path(d) for d in tpl_dirs if Path(d).is_dir()] for templatedir in templatedirs: - for dirpath, subdirs, filenames in templatedir.walk(follow_symlinks=True): + # TODO: Replace os.walk(templatedir) with templatedir.walk(follow_symlinks=True) + # once we only support Python 3.12+ + for dirpath, subdirs, filenames in os.walk(templatedir): for f in [ f for f in filenames if f.endswith(extension) and not f.startswith(".") ]: - path = dirpath / f + path = Path(dirpath) / f name = path.relative_to(templatedir) try: t = Template.on_site.get(name__exact=name) From f57ca2f7698efb452ac6665d3ea02e22fa6392b6 Mon Sep 17 00:00:00 2001 From: blag Date: Wed, 11 Jun 2025 21:35:10 -0600 Subject: [PATCH 5/6] Save a few more database queries --- dbtemplates/loader.py | 4 ++-- dbtemplates/utils/cache.py | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/dbtemplates/loader.py b/dbtemplates/loader.py index 30e9dbb..32aad54 100644 --- a/dbtemplates/loader.py +++ b/dbtemplates/loader.py @@ -55,9 +55,9 @@ class Loader(BaseLoader): # in the cache indicating that queries failed, with the current # timestamp. site = Site.objects.get_current() - cache_key = get_cache_key(template_name) + cache_key = get_cache_key(template_name, current_site=site) # Not found in cache, move on. - cache_notfound_key = get_cache_notfound_key(template_name) + cache_notfound_key = get_cache_notfound_key(template_name, current_site=site) if cache: try: backend_template = cache.get(cache_key) diff --git a/dbtemplates/utils/cache.py b/dbtemplates/utils/cache.py index 11bd6ca..96076b9 100644 --- a/dbtemplates/utils/cache.py +++ b/dbtemplates/utils/cache.py @@ -22,14 +22,13 @@ def get_cache_backend(): cache = get_cache_backend() -def get_cache_key(name, site=None): - if site is None: - site = Site.objects.get_current() - return f"dbtemplates::{slugify(name)}::{site.pk}" +def get_cache_key(name, current_site=None): + current_site = current_site or Site.objects.get_current() + return f"dbtemplates::{slugify(name)}::{current_site.pk}" -def get_cache_notfound_key(name): - return get_cache_key(name) + "::notfound" +def get_cache_notfound_key(name, current_site=None): + return get_cache_key(name, current_site=current_site) + "::notfound" def remove_notfound_key(instance): @@ -60,4 +59,4 @@ def remove_cached_template(instance, **kwargs): in the database was changed or deleted. """ for site in instance.sites.all(): - cache.delete(get_cache_key(instance.name, site=site)) + cache.delete(get_cache_key(instance.name, current_site=site)) From 4e51b34b5314a01cf710eeee1ac351455247891e Mon Sep 17 00:00:00 2001 From: blag Date: Wed, 11 Jun 2025 21:41:52 -0600 Subject: [PATCH 6/6] Add type hints to loader --- dbtemplates/loader.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dbtemplates/loader.py b/dbtemplates/loader.py index 32aad54..a1a9942 100644 --- a/dbtemplates/loader.py +++ b/dbtemplates/loader.py @@ -1,3 +1,5 @@ +from typing import Optional, Tuple + from django.contrib.sites.models import Site from django.db import router from django.template import Origin, TemplateDoesNotExist @@ -31,17 +33,17 @@ class Loader(BaseLoader): loader=self, ) - def get_contents(self, origin): + def get_contents(self, origin: Origin) -> str: content, _ = self._load_template_source(origin.template_name) return content - def _load_and_store_template(self, template_name, cache_key, site, **params): + def _load_and_store_template(self, template_name: str, cache_key: str, site: Site, **params) -> Tuple[str, str]: template = Template.objects.get(name__exact=template_name, **params) db = router.db_for_read(Template, instance=template) display_name = f"dbtemplates:{db}:{template_name}:{site.domain}" return set_and_return(cache_key, template.content, display_name) - def _load_template_source(self, template_name, template_dirs=None): + def _load_template_source(self, template_name: str, template_dirs: Optional[str] = None) -> Tuple[str, str]: # The logic should work like this: # * Try to find the template in the cache. If found, return it. # * Now check the cache if a lookup for the given template