diff --git a/README.rst b/README.rst index 4ddc5b5..fee2f2e 100644 --- a/README.rst +++ b/README.rst @@ -6,8 +6,7 @@ django-dbtemplates :target: https://jazzband.co/ .. image:: https://github.com/jazzband/django-dbtemplates/workflows/Test/badge.svg - :target: https://github.com/jazzband/django-dbtemplates/actions - :alt: GitHub Actions + :target: https://github.com/jazzband/django-dbtemplates/actions .. image:: https://codecov.io/github/jazzband/django-dbtemplates/coverage.svg?branch=master :alt: Codecov diff --git a/dbtemplates/admin.py b/dbtemplates/admin.py index a878159..1c9095f 100644 --- a/dbtemplates/admin.py +++ b/dbtemplates/admin.py @@ -2,19 +2,27 @@ from django import forms from django.contrib import admin from django.core.exceptions import ImproperlyConfigured -from django.utils.translation import ungettext, ugettext_lazy as _ +try: + # Django 4.0 + from django.utils.translation import gettext_lazy as _ + from django.utils.translation import ngettext +except ImportError: + # Before Django 4.0 + from django.utils.translation import ugettext_lazy as _ + from django.utils.translation import ungettext as ngettext + from django.utils.safestring import mark_safe from dbtemplates.conf import settings -from dbtemplates.models import (Template, remove_cached_template, - add_template_to_cache) +from dbtemplates.models import Template, add_template_to_cache, remove_cached_template from dbtemplates.utils.template import check_template_syntax # Check if either django-reversion-compare or django-reversion is installed and # 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: @@ -30,7 +38,8 @@ class CodeMirrorTextArea(forms.Textarea): class Media: css = dict(screen=[posixpath.join( settings.DBTEMPLATES_MEDIA_PREFIX, 'css/editor.css')]) - js = [posixpath.join(settings.DBTEMPLATES_MEDIA_PREFIX, 'js/codemirror.js')] + js = [posixpath.join(settings.DBTEMPLATES_MEDIA_PREFIX, + 'js/codemirror.js')] def render(self, name, value, attrs=None, renderer=None): result = [] @@ -119,7 +128,7 @@ def invalidate_cache(self, request, queryset): for template in queryset: remove_cached_template(template) count = queryset.count() - message = ungettext( + message = ngettext( "Cache of one template successfully invalidated.", "Cache of %(count)d templates successfully invalidated.", count) @@ -131,7 +140,7 @@ def repopulate_cache(self, request, queryset): for template in queryset: add_template_to_cache(template) count = queryset.count() - message = ungettext( + message = ngettext( "Cache successfully repopulated with one template.", "Cache successfully repopulated with %(count)d templates.", count) @@ -147,15 +156,16 @@ def check_syntax(self, request, queryset): errors.append(f'{template.name}: {error}') if errors: count = len(errors) - message = ungettext( + message = ngettext( "Template syntax check FAILED for %(names)s.", - "Template syntax check FAILED for %(count)d templates: %(names)s.", + "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 = ungettext( + message = ngettext( "Template syntax OK.", "Template syntax OK for %(count)d templates.", count) self.message_user(request, message % {'count': count}) diff --git a/dbtemplates/apps.py b/dbtemplates/apps.py index d913c95..21141b3 100644 --- a/dbtemplates/apps.py +++ b/dbtemplates/apps.py @@ -1,5 +1,8 @@ from django.apps import AppConfig -from django.utils.translation import ugettext_lazy as _ +try: + from django.utils.translation import ugettext_lazy as _ +except ImportError: + from django.utils.translation import gettext_lazy as _ class DBTemplatesConfig(AppConfig): diff --git a/dbtemplates/conf.py b/dbtemplates/conf.py index 8b8809a..010db5b 100644 --- a/dbtemplates/conf.py +++ b/dbtemplates/conf.py @@ -47,8 +47,8 @@ def configure_use_reversion(self, 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 " + raise ImproperlyConfigured("Please add 'reversion_compare' 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 403c460..5a57f27 100644 --- a/dbtemplates/loader.py +++ b/dbtemplates/loader.py @@ -30,7 +30,8 @@ def get_contents(self, origin): 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}' @@ -73,11 +74,11 @@ def _load_template_source(self, template_name, template_dirs=None): try: return self._load_and_store_template(template_name, cache_key, - site, sites__in=[site.id]) + 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) + site, sites__isnull=True) except (Template.MultipleObjectsReturned, Template.DoesNotExist): pass diff --git a/dbtemplates/management/commands/sync_templates.py b/dbtemplates/management/commands/sync_templates.py index b01e61e..7e336e0 100644 --- a/dbtemplates/management/commands/sync_templates.py +++ b/dbtemplates/management/commands/sync_templates.py @@ -1,18 +1,18 @@ import os -from django.contrib.sites.models import Site -from django.core.management.base import CommandError, BaseCommand -from django.template.utils import get_app_template_dirs -from django.template.loader import _engine_list from dbtemplates.models import Template +from django.contrib.sites.models import Site +from django.core.management.base import BaseCommand, CommandError +from django.template.loader import _engine_list +from django.template.utils import get_app_template_dirs -ALWAYS_ASK, FILES_TO_DATABASE, DATABASE_TO_FILES = ('0', '1', '2') +ALWAYS_ASK, FILES_TO_DATABASE, DATABASE_TO_FILES = ("0", "1", "2") DIRS = [] for engine in _engine_list(): DIRS.extend(engine.dirs) DIRS = tuple(DIRS) -app_template_dirs = get_app_template_dirs('templates') +app_template_dirs = get_app_template_dirs("templates") class Command(BaseCommand): @@ -20,36 +20,56 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( - "-e", "--ext", - dest="ext", action="store", default="html", + "-e", + "--ext", + dest="ext", + action="store", + default="html", help="extension of the files you want to " - "sync with the database [default: %(default)s]") + "sync with the database [default: %(default)s]", + ) 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", + ) parser.add_argument( - "-o", "--overwrite", - action="store", dest="overwrite", default='0', + "-o", + "--overwrite", + action="store", + dest="overwrite", + default="0", help="'0' - ask always, '1' - overwrite database " - "templates from template files, '2' - overwrite " - "template files from database templates") + "templates from template files, '2' - overwrite " + "template files from database templates", + ) parser.add_argument( - "-a", "--app-first", - action="store_true", dest="app_first", default=False, + "-a", + "--app-first", + action="store_true", + dest="app_first", + default=False, help="look for templates in applications " - "directories before project templates") + "directories before project templates", + ) parser.add_argument( - "-d", "--delete", - action="store_true", dest="delete", default=False, - help="Delete templates after syncing") + "-d", + "--delete", + action="store_true", + dest="delete", + default=False, + help="Delete templates after syncing", + ) def handle(self, **options): - extension = options.get('ext') - force = options.get('force') - overwrite = options.get('overwrite') - app_first = options.get('app_first') - delete = options.get('delete') + extension = options.get("ext") + force = options.get("force") + overwrite = options.get("overwrite") + app_first = options.get("app_first") + delete = options.get("delete") if not extension.startswith("."): extension = f".{extension}" @@ -57,8 +77,10 @@ def handle(self, **options): try: site = Site.objects.get_current() except Exception: - 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" + ) if app_first: tpl_dirs = app_template_dirs + DIRS @@ -68,11 +90,14 @@ def handle(self, **options): for templatedir in templatedirs: for dirpath, subdirs, filenames in os.walk(templatedir): - for f in [f for f in filenames - if f.endswith(extension) and not f.startswith(".")]: + 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('/'): + if name.startswith("/"): name = name[1:] try: t = Template.on_site.get(name__exact=name) @@ -81,27 +106,35 @@ def handle(self, **options): confirm = input( "\nA '%s' template doesn't exist in the " "database.\nCreate it with '%s'?" - " (y/[n]): """ % (name, path)) - if force or confirm.lower().startswith('y'): - with open(path, encoding='utf-8') as f: + " (y/[n]): " + "" % (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.sites.add(site) else: - while 1: + while True: if overwrite == ALWAYS_ASK: - confirm = input( + _i = ( "\n%(template)s exists in the database.\n" - "(1) Overwrite %(template)s with '%(path)s'\n" - "(2) Overwrite '%(path)s' with %(template)s\n" - "Type 1 or 2 or press to skip: " % - {'template': t.__repr__(), 'path': path}) + "(1) Overwrite %(template)s with '%(path)s'\n" # noqa + "(2) Overwrite '%(path)s' with %(template)s\n" # noqa + "Type 1 or 2 or press to skip: " + % {"template": t.__repr__(), "path": path} + ) + + confirm = input(_i) else: confirm = overwrite - if confirm in ('', FILES_TO_DATABASE, - DATABASE_TO_FILES): + if confirm in ( + "", + FILES_TO_DATABASE, + DATABASE_TO_FILES, + ): if confirm == FILES_TO_DATABASE: - with open(path, encoding='utf-8') as f: + with open(path, encoding="utf-8") as f: t.content = f.read() t.save() t.sites.add(site) @@ -110,9 +143,10 @@ def handle(self, **options): os.remove(path) except OSError: raise CommandError( - f"Couldn't delete {path}") + f"Couldn't delete {path}" + ) elif confirm == DATABASE_TO_FILES: - with open(path, 'w', encoding='utf-8') as f: + with open(path, "w", encoding="utf-8") as f: # noqa f.write(t.content) if delete: t.delete() diff --git a/dbtemplates/migrations/0001_initial.py b/dbtemplates/migrations/0001_initial.py index 5d5b1f4..7ac217f 100644 --- a/dbtemplates/migrations/0001_initial.py +++ b/dbtemplates/migrations/0001_initial.py @@ -1,40 +1,73 @@ import django -from django.db import models, migrations import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('sites', '0001_initial'), + ("sites", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Template', + name="Template", fields=[ - ('id', models.AutoField( - verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField( - help_text="Example: 'flatpages/default.html'", max_length=100, verbose_name='name')), - ('content', models.TextField(verbose_name='content', blank=True)), - ('creation_date', models.DateTimeField( - default=django.utils.timezone.now, verbose_name='creation date')), - ('last_changed', models.DateTimeField( - default=django.utils.timezone.now, verbose_name='last changed')), - ('sites', models.ManyToManyField( - to='sites.Site', verbose_name='sites', blank=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "name", + models.CharField( + help_text="Example: 'flatpages/default.html'", + max_length=100, + verbose_name="name", + ), + ), + ( + "content", + models.TextField(verbose_name="content", blank=True), + ), # noqa + ( + "creation_date", + models.DateTimeField( + default=django.utils.timezone.now, + verbose_name="creation date", # noqa + ), + ), + ( + "last_changed", + models.DateTimeField( + default=django.utils.timezone.now, + verbose_name="last changed", # noqa + ), + ), + ( + "sites", + models.ManyToManyField( + to="sites.Site", verbose_name="sites", blank=True + ), + ), ], options={ - 'ordering': ('name',), - 'db_table': 'django_template', - 'verbose_name': 'template', - 'verbose_name_plural': 'templates', + "ordering": ("name",), + "db_table": "django_template", + "verbose_name": "template", + "verbose_name_plural": "templates", }, bases=(models.Model,), managers=[ - ('objects', django.db.models.manager.Manager()), - ('on_site', django.contrib.sites.managers.CurrentSiteManager('sites')), + ("objects", django.db.models.manager.Manager()), + ( + "on_site", + django.contrib.sites.managers.CurrentSiteManager("sites"), + ), # noqa ], ), ] diff --git a/dbtemplates/models.py b/dbtemplates/models.py index f115cc9..f846b2b 100644 --- a/dbtemplates/models.py +++ b/dbtemplates/models.py @@ -7,7 +7,13 @@ from django.db import models from django.db.models import signals from django.template import TemplateDoesNotExist -from django.utils.translation import ugettext_lazy as _ +try: + # Django >= 4.0 + from django.utils.translation import gettext_lazy as _ +except ImportError: + # Django 3.2 + from django.utils.translation import ugettext_lazy as _ + from django.utils.timezone import now diff --git a/dbtemplates/test_cases.py b/dbtemplates/test_cases.py index 8dc1363..ab553e5 100644 --- a/dbtemplates/test_cases.py +++ b/dbtemplates/test_cases.py @@ -125,7 +125,7 @@ def test_sync_templates(self): verbosity=0, overwrite=DATABASE_TO_FILES) self.assertEqual('temp test modified', open(temp_template_path, - encoding='utf-8').read()) + encoding='utf-8').read()) call_command('sync_templates', force=True, verbosity=0, delete=True, overwrite=DATABASE_TO_FILES) diff --git a/dbtemplates/utils/cache.py b/dbtemplates/utils/cache.py index 0fb2ce5..923ee59 100644 --- a/dbtemplates/utils/cache.py +++ b/dbtemplates/utils/cache.py @@ -1,21 +1,16 @@ -import django -from django.core import signals +from dbtemplates.conf import settings from django.contrib.sites.models import Site +from django.core import signals from django.template.defaultfilters import slugify -from dbtemplates.conf import settings - def get_cache_backend(): """ Compatibilty wrapper for getting Django's cache backend instance """ - if django.VERSION[0] >= 3 and django.VERSION[1] >= 2: - from django.core.cache import caches - cache = caches.create_connection(settings.DBTEMPLATES_CACHE_BACKEND) - else: - from django.core.cache import _create_cache - cache = _create_cache(settings.DBTEMPLATES_CACHE_BACKEND) + 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 # the end of a request cycle. If not implemented in a particular backend # cache.close is a no-op @@ -28,11 +23,11 @@ def get_cache_backend(): def get_cache_key(name): current_site = Site.objects.get_current() - return f'dbtemplates::{slugify(name)}::{current_site.pk}' + return f"dbtemplates::{slugify(name)}::{current_site.pk}" def get_cache_notfound_key(name): - return get_cache_key(name) + '::notfound' + return get_cache_key(name) + "::notfound" def remove_notfound_key(instance): diff --git a/docs/changelog.txt b/docs/changelog.txt index f969846..03a3c16 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -8,14 +8,16 @@ v4.0 (unreleased) This is a backwards-incompatible release! -* Dropped support for Python 2.7. +* Dropped support for Python 2.7 and Django < 3.2. -* Added support for Python 3.8. +* Added support for Python 3.8, 3.9, 3.10. * Moved test runner to GitHub Actions: http://github.com/jazzband/django-dbtemplates/actions +* Django 4.x support + v3.0 (2019-01-27) ----------------- diff --git a/setup.py b/setup.py index 4425f7b..5a43497 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,6 @@ def read(*parts): "static/dbtemplates/js/*.js", ], }, - python_requires=">=3.7", classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", @@ -43,5 +42,7 @@ def read(*parts): "Programming Language :: Python :: 3.10", "Framework :: Django", ], + python_requires=">=3.7", install_requires=["django-appconf >= 0.4"], ) + diff --git a/tox.ini b/tox.ini index 586797b..d0178be 100644 --- a/tox.ini +++ b/tox.ini @@ -4,14 +4,15 @@ usedevelop = True minversion = 1.8 envlist = flake8 - py3{7,8,9}-dj22 py3{7,8,9,10,11}-dj32 + py3{8,9,10,11}-dj{40,41,main} [gh-actions] python = 3.7: py37 3.8: py38 3.9: py39 + 3.10: py310 3.10: py310, flake8 3.11: py311 @@ -27,13 +28,14 @@ setenv = DJANGO_SETTINGS_MODULE = dbtemplates.test_settings deps = -r requirements/tests.txt - dj22: Django<2.3 dj32: Django<3.3 + dj40: Django<4.1 + dj41: Django<4.2 djmain: https://github.com/django/django/archive/main.tar.gz#egg=django commands = python --version - coverage run {envbindir}/django-admin.py test -v2 {posargs:dbtemplates} + coverage run {envbindir}/django-admin test -v2 {posargs:dbtemplates} coverage report coverage xml