Compare commits

..

19 commits

Author SHA1 Message Date
Jared Whiklo
eab2ce0a7e
Add yes/no options (#137)
Some checks failed
Test / build (3.10) (push) Has been cancelled
Test / build (3.11) (push) Has been cancelled
Test / build (3.13) (push) Has been cancelled
Test / build (3.14) (push) Has been cancelled
Test / build (3.8) (push) Has been cancelled
Test / build (3.9) (push) Has been cancelled
Co-authored-by: Michał Pasternak <michal.dtz@gmail.com>
2026-04-24 09:46:33 +02:00
Edouard
15329d25e9
fix: use BigAutoField as id (#159)
* Add migration to alter template ID field to BigAutoField (#157)

* Update Template model to use BigAutoField for ID and remove obsolete migration (#157)

* Add migration to alter template ID field to BigAutoField
2026-04-24 09:45:38 +02:00
Viktor Kálmán
54a2662325
fix pre commit to work with Python 3.14 (#163) 2026-04-24 09:44:52 +02:00
Viktor Kálmán
dd9cbef340
confirm Python 3.14 support (#161)
Some checks failed
Test / build (3.10) (push) Has been cancelled
Test / build (3.11) (push) Has been cancelled
Test / build (3.13) (push) Has been cancelled
Test / build (3.14) (push) Has been cancelled
Test / build (3.8) (push) Has been cancelled
Test / build (3.9) (push) Has been cancelled
2026-04-19 22:51:48 +02:00
Thomas Güttler
10e7de2eda
Fix cache invalidation (#131) 2025-06-17 16:38:05 -07:00
Viktor Kálmán
930ce5c65c
add project urls (#156) 2025-06-09 11:47:43 -07:00
blag
8e284b54d8
Revert "Split out an AbstractTemplate model for easier reuse" (#154)
This reverts commit 46be8fc748.
2025-05-29 23:31:49 +02:00
blag
05f1ee1193
Split out an AbstractTemplate model for easier reuse (#150) 2025-05-26 22:09:25 +02:00
blag
303bd0cabe
Add missing migration for creation_date and last_changed changes (#151) 2025-05-26 22:08:21 +02:00
Łukasz Chojnacki
64d112cc4f
Add explicit id field to avoid creating migration with DEFAULT_AUTO_FIELD set to BigAutoField (#142)
Co-authored-by: blag <blag@users.noreply.github.com>
2025-05-26 20:13:03 +02:00
blag
7f1c6701c1
Properly add Django 5.2 and tweak coverage collection (#148) 2025-05-26 20:12:55 +02:00
blag
873c90b777
Convert setup.py and setup.cfg to pyproject.toml (#149) 2025-05-26 20:12:37 +02:00
Thomas Güttler
e64a457281
There is no INSTALL file. (#128)
removed "Follow the instructions in the INSTALL file", since there is no INSTALL file.

Co-authored-by: blag <blag@users.noreply.github.com>
2025-05-26 20:00:16 +02:00
blag
a7f4e0bbe8
Make creation_date and last_updated fields readonly in admin (#144) 2025-05-26 19:59:50 +02:00
blag
218b28b7aa
Let Django handle creation_date and last_changed (#145) 2025-05-26 19:59:18 +02:00
blag
602717af95
Add default_auto_field to AppConfig (#146) 2025-05-26 19:53:42 +02:00
Jannis Leidel
8769e29057
Use correct release branch of pypa/publish action. (#138) 2025-05-20 21:40:09 +02:00
Viktor Kálmán
ac740e06f3
Support for Python 3.12 and up (#143)
* support for Python 3.12 and up

* removed unused deprecated ugettext imports

* fix django main being Python 3.12+

* missed some copypaste
2025-02-16 21:08:16 +01:00
Jannis Leidel
233a401e75
Fix docs rendering (#127)
* Create .readthedocs.yaml

* Add docs requirements
2022-08-29 12:09:54 +02:00
21 changed files with 241 additions and 121 deletions

View file

@ -3,4 +3,4 @@ source = dbtemplates
branch = 1 branch = 1
[report] [report]
omit = *tests*,*migrations* omit = *tests*,*/migrations/*,test_*

View file

@ -33,7 +33,7 @@ jobs:
- name: Upload packages to Jazzband - name: Upload packages to Jazzband
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@master uses: pypa/gh-action-pypi-publish@release/v1
with: with:
user: jazzband user: jazzband
password: ${{ secrets.JAZZBAND_RELEASE_KEY }} password: ${{ secrets.JAZZBAND_RELEASE_KEY }}

View file

@ -7,9 +7,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 5 max-parallel: 6
matrix: matrix:
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11-dev'] python-version: ['3.8', '3.9', '3.10', '3.11', '3.13', '3.14']
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View file

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.34.0 rev: v3.21.2
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py37-plus] args: [--py37-plus]

25
.readthedocs.yaml Normal file
View file

@ -0,0 +1,25 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-20.04
tools:
python: "3.9"
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# If using Sphinx, optionally build your docs in additional formats such as PDF
formats:
- pdf
# Optionally declare the Python requirements required to build your docs
python:
install:
- requirements: requirements/docs.txt

View file

@ -1,10 +1,3 @@
from pkg_resources import get_distribution, DistributionNotFound import importlib.metadata
try: __version__ = importlib.metadata.version("django-dbtemplates")
__version__ = get_distribution("django-dbtemplates").version
except DistributionNotFound:
# package is not installed
__version__ = None
default_app_config = 'dbtemplates.apps.DBTemplatesConfig'

View file

@ -2,15 +2,8 @@ import posixpath
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
try: from django.utils.translation import gettext_lazy as _
# Django 4.0 from django.utils.translation import ngettext
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 django.utils.safestring import mark_safe
from dbtemplates.conf import settings from dbtemplates.conf import settings
@ -104,6 +97,7 @@ class TemplateAdminForm(forms.ModelForm):
class TemplateAdmin(TemplateModelAdmin): class TemplateAdmin(TemplateModelAdmin):
form = TemplateAdminForm form = TemplateAdminForm
readonly_fields = ['creation_date', 'last_changed']
fieldsets = ( fieldsets = (
(None, { (None, {
'fields': ('name', 'content'), 'fields': ('name', 'content'),

View file

@ -1,10 +1,9 @@
from django.apps import AppConfig from django.apps import AppConfig
try: from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
except ImportError:
from django.utils.translation import gettext_lazy as _
class DBTemplatesConfig(AppConfig): class DBTemplatesConfig(AppConfig):
name = 'dbtemplates' name = 'dbtemplates'
verbose_name = _('Database templates') verbose_name = _('Database templates')
default_auto_field = 'django.db.models.AutoField'

View file

@ -63,6 +63,23 @@ class Command(BaseCommand):
default=False, default=False,
help="Delete templates after syncing", help="Delete templates after syncing",
) )
group = parser.add_mutually_exclusive_group()
group.add_argument(
"-y",
"--yes",
action="store_true",
dest="auto_answer",
default=None,
help="Answer yes to all template creation questions."
)
group.add_argument(
"-n",
"--no",
action="store_false",
dest="auto_answer",
default=None,
help="Answer no to all template creation questions."
)
def handle(self, **options): def handle(self, **options):
extension = options.get("ext") extension = options.get("ext")
@ -70,6 +87,7 @@ class Command(BaseCommand):
overwrite = options.get("overwrite") overwrite = options.get("overwrite")
app_first = options.get("app_first") app_first = options.get("app_first")
delete = options.get("delete") delete = options.get("delete")
auto_answer = options.get('auto_answer')
if not extension.startswith("."): if not extension.startswith("."):
extension = f".{extension}" extension = f".{extension}"
@ -103,12 +121,15 @@ class Command(BaseCommand):
t = Template.on_site.get(name__exact=name) t = Template.on_site.get(name__exact=name)
except Template.DoesNotExist: except Template.DoesNotExist:
if not force: if not force:
confirm = input( if auto_answer is not None:
"\nA '%s' template doesn't exist in the " confirm = "y" if auto_answer else "n"
"database.\nCreate it with '%s'?" else:
" (y/[n]): " confirm = input(
"" % (name, path) "\nA '%s' template doesn't exist in the "
) "database.\nCreate it with '%s'?"
" (y/[n]): "
"" % (name, path)
)
if force or confirm.lower().startswith("y"): if force or confirm.lower().startswith("y"):
with open(path, encoding="utf-8") as f: with open(path, encoding="utf-8") as f:
t = Template(name=name, content=f.read()) t = Template(name=name, content=f.read())

View file

@ -0,0 +1,23 @@
# Generated by Django 5.1 on 2025-05-26 19:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dbtemplates', '0001_initial'),
]
operations = [
migrations.AlterField(
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'),
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 4.2.23 on 2025-07-28 13:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("dbtemplates", "0002_alter_template_creation_date_and_more"),
]
operations = [
migrations.AlterField(
model_name="template",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
]

View file

@ -1,20 +1,16 @@
from dbtemplates.conf import settings from dbtemplates.conf import settings
from dbtemplates.utils.cache import (add_template_to_cache, from dbtemplates.utils.cache import (
remove_cached_template) add_template_to_cache,
remove_cached_template,
)
from dbtemplates.utils.template import get_template_source from dbtemplates.utils.template import get_template_source
from django.contrib.sites.managers import CurrentSiteManager from django.contrib.sites.managers import CurrentSiteManager
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.db import models from django.db import models
from django.db.models import signals from django.db.models import signals
from django.template import TemplateDoesNotExist from django.template import TemplateDoesNotExist
try: from django.utils.translation import gettext_lazy as _
# 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
class Template(models.Model): class Template(models.Model):
@ -22,15 +18,15 @@ class Template(models.Model):
Defines a template model for use with the database template loader. Defines a template model for use with the database template loader.
The field ``name`` is the equivalent to the filename of a static template. The field ``name`` is the equivalent to the filename of a static template.
""" """
id = models.BigAutoField(primary_key=True, verbose_name=_('ID'),
serialize=False, auto_created=True)
name = models.CharField(_('name'), max_length=100, name = models.CharField(_('name'), max_length=100,
help_text=_("Example: 'flatpages/default.html'")) help_text=_("Example: 'flatpages/default.html'"))
content = models.TextField(_('content'), blank=True) content = models.TextField(_('content'), blank=True)
sites = models.ManyToManyField(Site, verbose_name=_('sites'), sites = models.ManyToManyField(Site, verbose_name=_('sites'),
blank=True) blank=True)
creation_date = models.DateTimeField(_('creation date'), creation_date = models.DateTimeField(_('creation date'), auto_now_add=True)
default=now) last_changed = models.DateTimeField(_('last changed'), auto_now=True)
last_changed = models.DateTimeField(_('last changed'),
default=now)
objects = models.Manager() objects = models.Manager()
on_site = CurrentSiteManager('sites') on_site = CurrentSiteManager('sites')
@ -59,7 +55,6 @@ class Template(models.Model):
pass pass
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.last_changed = now()
# If content is empty look for a template with the given name and # If content is empty look for a template with the given name and
# populate the template instance with its content. # populate the template instance with its content.
if settings.DBTEMPLATES_AUTO_POPULATE_CONTENT and not self.content: if settings.DBTEMPLATES_AUTO_POPULATE_CONTENT and not self.content:

View file

@ -1,6 +1,7 @@
import os import os
import shutil import shutil
import tempfile import tempfile
from unittest import mock
from django.conf import settings as django_settings from django.conf import settings as django_settings
from django.core.cache.backends.base import BaseCache from django.core.cache.backends.base import BaseCache
@ -151,3 +152,24 @@ class DbTemplatesTestCase(TestCase):
def test_get_cache_name(self): def test_get_cache_name(self):
self.assertEqual(get_cache_key('name with spaces'), self.assertEqual(get_cache_key('name with spaces'),
'dbtemplates::name-with-spaces::1') 'dbtemplates::name-with-spaces::1')
def test_cache_invalidation(self):
# Add t1 into the cache of site2
self.t1.sites.add(self.site2)
with mock.patch('django.contrib.sites.models.SiteManager.get_current',
return_value=self.site2):
result = loader.get_template("base.html").render()
self.assertEqual(result, 'base')
# Update content
self.t1.content = 'new content'
self.t1.save()
result = loader.get_template("base.html").render()
self.assertEqual(result, 'new content')
# Cache invalidation should work across sites.
# Site2 should see the new content.
with mock.patch('django.contrib.sites.models.SiteManager.get_current',
return_value=self.site2):
result = loader.get_template("base.html").render()
self.assertEqual(result, 'new content')

View file

@ -21,9 +21,10 @@ def get_cache_backend():
cache = get_cache_backend() cache = get_cache_backend()
def get_cache_key(name): def get_cache_key(name, site=None):
current_site = Site.objects.get_current() if site is None:
return f"dbtemplates::{slugify(name)}::{current_site.pk}" site = Site.objects.get_current()
return f"dbtemplates::{slugify(name)}::{site.pk}"
def get_cache_notfound_key(name): def get_cache_notfound_key(name):
@ -57,4 +58,5 @@ def remove_cached_template(instance, **kwargs):
Called via Django's signals to remove cached templates, if the template Called via Django's signals to remove cached templates, if the template
in the database was changed or deleted. in the database was changed or deleted.
""" """
cache.delete(get_cache_key(instance.name)) for site in instance.sites.all():
cache.delete(get_cache_key(instance.name, site=site))

View file

@ -1,7 +1,20 @@
Changelog Changelog
========= =========
v4.0 (2022-08-29) v5.0 (unreleased)
-----------------
.. warning::
This is a backwards-incompatible release!
* Dropped support for Python 3.7 and Django < 4.2.
* Added support for Python 3.11, 3.12, 3.13, 3.14.
* Django 5.x and 6.0 support
v4.0 (2022-09-3)
----------------- -----------------
.. warning:: .. warning::

View file

@ -3,8 +3,7 @@ Setup
1. Get the source from the `Git repository`_ or install it from the 1. Get the source from the `Git repository`_ or install it from the
Python Package Index by running ``pip install django-dbtemplates``. Python Package Index by running ``pip install django-dbtemplates``.
2. Follow the instructions in the INSTALL file 2. Edit the settings.py of your Django site:
3. Edit the settings.py of your Django site:
* Add ``dbtemplates`` to the ``INSTALLED_APPS`` setting * Add ``dbtemplates`` to the ``INSTALLED_APPS`` setting
@ -61,8 +60,8 @@ Setup
from the database to be used to override templates in other locations, from the database to be used to override templates in other locations,
put ``dbtemplates.loader.Loader`` at the beginning of ``loaders``. put ``dbtemplates.loader.Loader`` at the beginning of ``loaders``.
4. Sync your database ``python manage.py migrate`` 3. Sync your database ``python manage.py migrate``
5. Restart your Django server 4. Restart your Django server
.. _Git repository: https://github.com/jazzband/django-dbtemplates/ .. _Git repository: https://github.com/jazzband/django-dbtemplates/

52
pyproject.toml Normal file
View file

@ -0,0 +1,52 @@
[build-system]
requires = [
"setuptools>=61.2",
"setuptools_scm",
]
build-backend = "setuptools.build_meta"
[project]
name = "django-dbtemplates"
authors = [{name = "Jannis Leidel", email = "jannis@leidel.info"}]
description = "Template loader for templates stored in the database"
readme = "README.rst"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Framework :: Django",
]
requires-python = ">=3.8"
dependencies = ["django-appconf >= 0.4"]
dynamic = ["version"]
[project.urls]
Documentation = "https://django-dbtemplates.readthedocs.io/"
Changelog = "https://django-dbtemplates.readthedocs.io/en/latest/changelog.html"
Source = "https://github.com/jazzband/django-dbtemplates"
[tool.setuptools]
zip-safe = false
include-package-data = false
[tool.setuptools.packages]
find = {namespaces = false}
[tool.setuptools.package-data]
dbtemplates = [
"locale/*/LC_MESSAGES/*",
"static/dbtemplates/css/*.css",
"static/dbtemplates/js/*.js",
]

1
requirements/docs.txt Normal file
View file

@ -0,0 +1 @@
django

View file

@ -1,4 +0,0 @@
[build_sphinx]
source-dir = docs/
build-dir = docs/_build
all_files = 1

View file

@ -1,48 +0,0 @@
import os
import io
from setuptools import setup, find_packages
def read(*parts):
filename = os.path.join(os.path.dirname(__file__), *parts)
with open(filename, encoding="utf-8") as fp:
return fp.read()
setup(
name="django-dbtemplates",
use_scm_version={"version_scheme": "post-release"},
setup_requires=["setuptools_scm"],
description="Template loader for templates stored in the database",
long_description=read("README.rst"),
author="Jannis Leidel",
author_email="jannis@leidel.info",
url="https://django-dbtemplates.readthedocs.io/",
packages=find_packages(),
zip_safe=False,
package_data={
"dbtemplates": [
"locale/*/LC_MESSAGES/*",
"static/dbtemplates/css/*.css",
"static/dbtemplates/js/*.js",
],
},
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Framework :: Django",
],
python_requires=">=3.7",
install_requires=["django-appconf >= 0.4"],
)

37
tox.ini
View file

@ -1,41 +1,54 @@
[tox] [tox]
skipsdist = True minversion = 4.0
usedevelop = True
minversion = 1.8
envlist = envlist =
flake8 flake8
py3{7,8,9,10,11}-dj32 py3{8,9,10,11,12}-dj42
py3{8,9,10,11}-dj{40,41,main} py3{10,11,12,13,14}-dj52
py3{12,13,14}-dj60
py3{12,13,14}-djmain
coverage
[gh-actions] [gh-actions]
python = python =
3.7: py37
3.8: py38 3.8: py38
3.9: py39 3.9: py39
3.10: py310 3.10: py310
3.10: py310, flake8 3.10: py310, flake8
3.11: py311 3.11: py311
3.12: py312
3.13: py313
3.14: py314
[testenv] [testenv]
skipsdist = true
package = editable
basepython = basepython =
py37: python3.7
py38: python3.8 py38: python3.8
py39: python3.9 py39: python3.9
py310: python3.10 py310: python3.10
py311: python3.11 py311: python3.11
usedevelop = true py312: python3.12
py313: python3.13
py314: python3.14
setenv = setenv =
DJANGO_SETTINGS_MODULE = dbtemplates.test_settings DJANGO_SETTINGS_MODULE = dbtemplates.test_settings
deps = deps =
-r requirements/tests.txt -r requirements/tests.txt
dj32: Django<3.3 dj42: Django>=4.2,<4.3
dj40: Django<4.1 dj52: Django>=5.2,<5.3
dj41: Django<4.2 dj60: Django>=6.0,<6.1
djmain: https://github.com/django/django/archive/main.tar.gz#egg=django djmain: https://github.com/django/django/archive/main.tar.gz#egg=django
commands = commands =
python --version python --version
coverage run {envbindir}/django-admin test -v2 {posargs:dbtemplates} python -c "import django ; print(django.VERSION)"
coverage run --branch --parallel-mode {envbindir}/django-admin test -v2 {posargs:dbtemplates}
[testenv:coverage]
basepython = python3.10
deps = coverage
commands =
coverage combine
coverage report coverage report
coverage xml coverage xml