Compare commits

...

106 commits
2.0 ... master

Author SHA1 Message Date
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
Michał Pasternak
13bacacef8
Merge pull request #126 from mpasternak/master
Django 4.x
2022-08-11 07:28:21 +02:00
Michał Pasternak
fa72b2771b Flake8 2022-08-10 22:32:23 +02:00
Michał Pasternak
fac9f6a807 Mention dropping Django below 3.2 2022-08-10 22:29:46 +02:00
Michał Pasternak
f53244ce54 Merge branch 'master' of github.com:mpasternak/django-dbtemplates-iplweb 2022-08-10 22:29:10 +02:00
Michał Pasternak
6ca53981d3 Test all the supported versions 2022-08-10 22:28:36 +02:00
Michał Pasternak
45e216b8ac Remove compatibility wrapper 2022-08-10 22:28:21 +02:00
Michał Pasternak
5a363dbe34
Update setup.py
Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
2022-08-10 10:47:46 +02:00
Michał Pasternak
2c8dc82721
Update tox.ini
Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
2022-08-09 17:50:42 +02:00
Michał Pasternak
98bc9921b0 Remove trailing whitespace 2022-08-09 17:41:44 +02:00
Michał Pasternak
b6b0bf48ba Django 4.0 i18n fixes 2022-08-09 17:38:47 +02:00
Michał Pasternak
36fbb80fc0 Update to Django 4.0 i18n style 2022-08-09 17:38:39 +02:00
Michał Pasternak
e08a8d3950 Document changes in 4.0 (unreleased) 2022-08-09 17:35:13 +02:00
Michał Pasternak
6dd23d2325 Proper links to CI badge 2022-08-09 17:34:29 +02:00
Michał Pasternak
a68caedc2d Revert unintentional typo 2022-08-09 17:34:14 +02:00
Michał Pasternak
2b5034951f Remove unsupported Django versions 2022-08-09 17:34:06 +02:00
Michał Pasternak
b138cafcfa Passing flake8 2022-08-08 00:49:40 +02:00
Michał Pasternak
f98fb8ca24 Min. supported Python version 2022-08-08 00:40:17 +02:00
Michał Pasternak
2e3f009426
Merge branch 'master' into master 2022-08-08 00:36:05 +02:00
Michał Pasternak
8e8b76fc0a
Merge pull request #125 from jazzband/add-3.10
Add support for Python 3.10
2022-08-08 00:25:12 +02:00
Jazzband Bot
49dd3be520
Jazzband: Created local 'CODE_OF_CONDUCT.md' from remote 'CODE_OF_CONDUCT.md' (#120) 2022-08-05 14:12:18 +02:00
Hugo van Kemenade
74ee8a2fe3 Add pre-commit config 2022-06-15 16:02:33 +03:00
Michał Pasternak
1a20742b71 Fix flake8 target on GitHub 2022-06-15 14:58:13 +02:00
Hugo van Kemenade
9455b36281 Upgrade to f-strings with 'flynt .' 2022-06-15 15:54:51 +03:00
Hugo van Kemenade
165ffd0b15 Test Python 3.11 beta 2022-06-15 15:54:51 +03:00
Hugo van Kemenade
a3054c7724 Fix Flake8 2022-06-15 15:47:52 +03:00
Hugo van Kemenade
e5e4e55d22 Upgrade Python syntax with pyupgrade --py37-plus 2022-06-15 15:47:52 +03:00
Hugo van Kemenade
9a27f4938c Drop support for EOL Python 3.6 2022-06-15 15:47:52 +03:00
Michał Pasternak
013057425f Min. Python version 2022-06-15 14:43:43 +02:00
Michał Pasternak
7ffc0fa33f Don't build unversal wheel 2022-06-15 14:43:32 +02:00
Michał Pasternak
de4babcad4 Describe changes 2022-06-15 14:39:44 +02:00
Hugo van Kemenade
6b18f19ffd Add support for Python 3.10 2022-06-15 15:39:30 +03:00
Michał Pasternak
5281b74ea7 Don't change basepython for flake8 2022-06-15 14:31:46 +02:00
Michał Pasternak
9dbdb95227 Rename for PyPI release 2022-06-15 14:29:06 +02:00
Michał Pasternak
e1e11c42cb Django 4.x fixes 2022-06-15 14:25:32 +02:00
Michał Pasternak
9f664ea43c Django 4.x fix 2022-06-15 14:22:48 +02:00
Michał Pasternak
e477561810 Proper image path 2022-06-15 14:22:42 +02:00
Michał Pasternak
2d622ee28c Python 3.10 2022-06-15 14:22:32 +02:00
Michał Pasternak
8901551893 Merge branch 'master' of github.com:mpasternak/django-dbtemplates-iplweb 2022-06-15 14:22:06 +02:00
Michał Pasternak
9e4a6f7c78 Django 4.x 2022-06-15 14:20:06 +02:00
Michał Pasternak
9486f0c78b
Merge pull request #118 from wamberg/cache-compat-#116
Django 3.2 compatibility
2022-06-15 12:17:42 +02:00
Nikolaus Schlemm
376f33916f focus on currently supported releases of python and django 2022-02-08 09:20:18 -05:00
Bill Amberg
8cd8a17bc1 Use strings explicitly for template directories 2022-02-08 09:15:46 -05:00
Bill Amberg
f45cd228f9 Get cache for Django>=3.2 2022-02-08 09:15:46 -05:00
Bill Amberg
24b5f469a2 Add failing test environment for Django==3.2 2022-02-08 09:15:33 -05:00
Giovanni B
2b747bc4af
Fixes #113 (#115)
* Fixes #113

* Update tox.ini

Co-authored-by: Jannis Leidel <jannis@leidel.info>

Co-authored-by: Jannis Leidel <jannis@leidel.info>
2021-10-21 17:35:56 +02:00
Jannis Leidel
ac86ca5ec9
Rename Django's dev branch to main. (#114)
More information: https://groups.google.com/g/django-developers/c/tctDuKUGosc/
Refs: https://github.com/django/django/pull/14048
2021-03-09 13:34:27 +01:00
Jannis Leidel
012fe061a8
Migrate to GitHub Actions. (#112)
* Add GitHub Actions test workflow.

* Update version map.

* Fix flake8

* Write coverage file.

* Fix typo.

* Add release workflow.

* Removed Travis cruft and updated other files.

* Update trove classifiers.

* Black setup.

* Remove unneeded twine check.

* Extend changelog.

* Fix six issue.

* Update tox.ini

Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>

* Add 3.9.

* Add base python

Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
2020-12-09 09:55:35 +01:00
Kaustubh Bhalerao
68ac54a72b
Django 3.0.4 (#107)
Co-authored-by: Kaustubh Bhalerao <bhalerao@soildiagnostics.com>
2020-06-15 14:17:18 +07:00
Nikolaus Schlemm
7ea4655b57 integrate with django-reversion-compare to offer a history compare view in admin 2020-06-15 13:08:47 +07:00
John Vandenberg
1d53b25cc2
Merge pull request #102 from nschlemm/nschlemm-patch-help_text 2020-05-06 00:31:10 +07:00
John Vandenberg
81ba1364f8
Merge pull request #103 from nschlemm/update-tox 2020-05-05 20:47:13 +07:00
John Vandenberg
ae97ad7e24
Merge pull request #100 from nschlemm/nschlemm-patch-raw_input 2020-05-05 20:36:54 +07:00
John Vandenberg
5c6c35bf3d
Merge pull request #98 from nschlemm/patch-2 2020-05-05 20:35:41 +07:00
Nikolaus Schlemm
50f02a50bc added django 2.2, dropped django 2.0 and python 3.4
keeping in sync with https://www.djangoproject.com/download/#supported-versions
2019-06-19 13:37:44 +02:00
Nikolaus Schlemm
230157640c let's keep flake8 silent until we can throw this backwards-compatibility hack out completely 2019-06-19 12:50:02 +02:00
Nikolaus Schlemm
f443f29047
Fixes #101 by expanding the help text default value 2019-06-19 12:34:03 +02:00
Nikolaus Schlemm
79d1d3986f
Fixes #99 by replacing raw_input with input
added (propably obsolete) backwards compatability as django already threw out six
2019-06-19 12:15:59 +02:00
Nikolaus Schlemm
73a33361d3
Fixes #82 TypeError: can only concatenate list (not "tuple") to list 2019-01-28 02:07:52 +01:00
Jannis Leidel
a04933e5ce
Fix changelog date. 2019-01-27 22:54:59 +01:00
Jannis Leidel
78ca3f48dd
Bump version to 3.0. 2019-01-27 22:26:41 +01:00
Jannis Leidel
727746add1
12 years! 2019-01-27 22:26:28 +01:00
Jannis Leidel
07d62d8d26
Updated changelog. 2019-01-27 22:25:42 +01:00
Jannis Leidel
4ece48c7b2
Remove Python 3.4 from Django 2.1 tests since it’s not supported. 2019-01-27 22:24:20 +01:00
Jannis Leidel
966b68ee5c
Added some Python versions to trove classifiers. 2019-01-27 22:23:54 +01:00
Jannis Leidel
039f6f419f
Build universal wheel files. 2019-01-27 22:08:06 +01:00
Jannis Leidel
24c8d2005a
Use twine instead of readme_renderer for readme check. 2019-01-27 22:07:58 +01:00
Jannis Leidel
df358a53dd
Merge pull request #88 from m-vdb/mvdb/fix-migration
Fix bytes string migration (python3)
2019-01-27 22:02:31 +01:00
Jannis Leidel
f5b9e36fc7
Merge pull request #89 from m-vdb/mvdb/fix-str-representation
Fix Template string representation for python3
2019-01-27 21:58:28 +01:00
Jannis Leidel
16afa49831
Use django.utils.six instead of six directly. 2019-01-27 21:54:57 +01:00
Jannis Leidel
cef1ec49c0
Merge pull request #97 from nschlemm/patch-1
Fix flake8 W504 line break after binary operator
2019-01-27 21:46:24 +01:00
Jannis Leidel
745e64f3e0
Re-enable project releases via Jazzband.co. Fix #95. 2019-01-27 21:43:29 +01:00
Nikolaus Schlemm
28bde8c32d
Fix flake8 W504 line break after binary operator
and thereby hopefully allow for closing https://github.com/jazzband/django-dbtemplates/issues/95 ?
2019-01-26 01:18:32 +01:00
Waldecir Santos
16a80d4635
fix PyPi upload 2018-10-01 20:14:42 +01:00
Waldecir Santos
6522a39250
added server tag to deploy 2018-10-01 19:45:48 +01:00
Waldecir Santos
149a16e308
bump version 2018-10-01 18:53:22 +01:00
Waldecir Santos
92e0e1ca0c
Merge pull request #87 from eprikazc/master
Add support for Django2.0, drop 1.8
2018-10-01 18:52:35 +01:00
Eugene Prikazchikov
c2009596ef Add workaround to make Python3.7 in Travis work properly 2018-10-01 11:20:33 +03:00
Eugene Prikazchikov
9cce7cd03b Update tox.ini to support Python3.7 2018-10-01 10:59:47 +03:00
Eugene Prikazchikov
34e34b7259 Add renderer parameter to CodeMirrorTextArea.render
According to django2.1
2018-09-13 23:37:41 +03:00
m-vdb
db5e4ec4b6 fix Template string representation for python3 2018-02-12 20:12:38 +01:00
m-vdb
3ba01f425c remove byte string from migration file 2018-02-12 20:02:23 +01:00
Jannis Leidel
77d8fc4e33
Merge pull request #83 from paulgueltekin/master
fixed method call with named argument
2017-12-09 23:49:08 +01:00
Jannis Leidel
3939947a0e
Merge pull request #86 from kammato/russian_locale_fix
Recompiled russian locale correctly
2017-12-09 23:46:11 +01:00
Eugene Prikazchikov
1c80410a5b Update get_template_source utility function to use up-to-date template API 2017-12-09 21:38:42 +03:00
Eugene Prikazchikov
18afb50582 Remove django 1.8 from tox.ini 2017-12-09 20:43:44 +03:00
Eugene Prikazchikov
e3318658a5 Update Loader class to up-to-date template loader API
load_template_sources is deprecated in Django 1.9 ad removed in Django2.0
see https://docs.djangoproject.com/en/1.9/releases/1.9/#template-loader-apis-have-changed
2017-12-09 20:40:57 +03:00
Jannis Leidel
3ee69a58ce
Drop testing pypy. 2017-11-22 15:47:11 +01:00
Jannis Leidel
9a84b1af86
Fix test matrix. 2017-11-22 15:43:19 +01:00
Jannis Leidel
bc206765fa
Appease flake8. 2017-11-22 11:01:02 +01:00
Khamid Tomov
8176a137dd Recompiled russian locale correctly 2017-05-18 18:00:03 +03:00
Paul Gueltekin
1c9467fa37 fixed method call with named argument 2017-01-17 16:51:06 +01:00
37 changed files with 650 additions and 320 deletions

View file

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

40
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,40 @@
name: Release
on:
push:
tags:
- '*'
jobs:
build:
if: github.repository == 'jazzband/django-dbtemplates'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U setuptools twine wheel
- name: Build package
run: |
python setup.py --version
python setup.py sdist --format=gztar bdist_wheel
twine check dist/*
- name: Upload packages to Jazzband
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: jazzband
password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
repository_url: https://jazzband.co/projects/django-dbtemplates/upload

48
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,48 @@
name: Test
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 5
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11', '3.13']
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache
uses: actions/cache@v3
with:
path: ${{ steps.pip-cache.outputs.dir }}
key:
${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}-${{ hashFiles('**/tox.ini') }}
restore-keys: |
${{ matrix.python-version }}-v1-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade tox tox-gh-actions
- name: Tox tests
run: |
tox -v
- name: Upload coverage
uses: codecov/codecov-action@v2
with:
name: Python ${{ matrix.python-version }}

4
.gitignore vendored
View file

@ -1,3 +1,6 @@
.*
!.gitignore
!.coveragerc
*.pyc *.pyc
.*.swp .*.swp
MANIFEST MANIFEST
@ -8,3 +11,4 @@ docs/_build
.tox/ .tox/
*.egg/ *.egg/
.coverage .coverage
coverage.xml

15
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,15 @@
repos:
- repo: https://github.com/asottile/pyupgrade
rev: v2.34.0
hooks:
- id: pyupgrade
args: [--py37-plus]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: check-merge-conflict
- id: check-yaml
ci:
autoupdate_schedule: quarterly

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,39 +0,0 @@
language: python
python: 3.5
sudo: false
cache: pip
env:
- TOXENV=flake8-py27
- TOXENV=flake8-py35
- TOXENV=readme-py27
- TOXENV=py27-dj18
- TOXENV=py27-dj19
- TOXENV=py27-dj110
- TOXENV=py27-djmaster
- TOXENV=py34-dj18
- TOXENV=py34-dj19
- TOXENV=py34-dj110
- TOXENV=py34-djmaster
- TOXENV=py35-dj18
- TOXENV=py35-dj19
- TOXENV=py35-dj110
- TOXENV=py35-djmaster
- TOXENV=pypy-dj18
- TOXENV=pypy-dj19
- TOXENV=pypy-dj110
- TOXENV=pypy-djmaster
install:
- pip install tox codecov
script: tox -v
after_success:
- codecov
deploy:
provider: pypi
user: jazzband
distributions: "sdist bdist_wheel"
password:
secure: B7imNYNndd2HEr79+3/jXLjFo/MdzzHUkH1NJ7G+YFtYjEkRFEaVuPXhuX1LYO9/qzYDcqHVrnsg65ZUXCftAg0/zFZ7zhEn/WEYZA2nkPosoSrYNV+s3XH/DMmpXdG6mKRN4D0mLQXaCwGeuQ2wHFOg7HCsz+tojFwYnEyN9ag=
on:
tags: true
repo: jazzband/django-dbtemplates
condition: "$TOXENV = py27-dj110"

46
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,46 @@
# Code of Conduct
As contributors and maintainers of the Jazzband projects, and in the interest of
fostering an open and welcoming community, we pledge to respect all people who
contribute through reporting issues, posting feature requests, updating documentation,
submitting pull requests or patches, and other activities.
We are committed to making participation in the Jazzband a harassment-free experience
for everyone, regardless of the level of experience, gender, gender identity and
expression, sexual orientation, disability, personal appearance, body size, race,
ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery
- Personal attacks
- Trolling or insulting/derogatory comments
- Public or private harassment
- Publishing other's private information, such as physical or electronic addresses,
without explicit permission
- Other unethical or unprofessional conduct
The Jazzband roadies have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are not
aligned to this Code of Conduct, or to ban temporarily or permanently any contributor
for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
By adopting this Code of Conduct, the roadies commit themselves to fairly and
consistently applying these principles to every aspect of managing the jazzband
projects. Roadies who do not follow or enforce the Code of Conduct may be permanently
removed from the Jazzband roadies.
This code of conduct applies both within project spaces and in public spaces when an
individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and
investigated and will result in a response that is deemed necessary and appropriate to
the circumstances. Roadies are obligated to maintain confidentiality with regard to the
reporter of an incident.
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version
1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version]
[homepage]: https://contributor-covenant.org
[version]: https://contributor-covenant.org/version/1/3/0/

View file

@ -1,4 +1,4 @@
Copyright (c) 2007-2016, Jannis Leidel and contributors Copyright (c) 2007-2019, Jannis Leidel and contributors
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without

View file

@ -5,9 +5,8 @@ django-dbtemplates
:alt: Jazzband :alt: Jazzband
:target: https://jazzband.co/ :target: https://jazzband.co/
.. image:: https://travis-ci.org/jazzband/django-dbtemplates.svg?branch=master .. image:: https://github.com/jazzband/django-dbtemplates/workflows/Test/badge.svg
:alt: Build Status :target: https://github.com/jazzband/django-dbtemplates/actions
:target: http://travis-ci.org/jazzband/django-dbtemplates
.. image:: https://codecov.io/github/jazzband/django-dbtemplates/coverage.svg?branch=master .. image:: https://codecov.io/github/jazzband/django-dbtemplates/coverage.svg?branch=master
:alt: Codecov :alt: Codecov

View file

@ -1,3 +1,3 @@
__version__ = "2.0" import importlib.metadata
default_app_config = 'dbtemplates.apps.DBTemplatesConfig' __version__ = importlib.metadata.version("django-dbtemplates")

View file

@ -2,17 +2,21 @@ 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
from django.utils.translation import ungettext, ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.translation import 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
from dbtemplates.models import (Template, remove_cached_template, from dbtemplates.models import Template, add_template_to_cache, remove_cached_template
add_template_to_cache)
from dbtemplates.utils.template import check_template_syntax from dbtemplates.utils.template import check_template_syntax
# Check if django-reversion is installed and use reversions' VersionAdmin # Check if either django-reversion-compare or django-reversion is installed and
# as the base admin class if yes # use reversion_compare's CompareVersionAdmin or reversion's VersionAdmin as
if settings.DBTEMPLATES_USE_REVERSION: # the base admin class if yes
if settings.DBTEMPLATES_USE_REVERSION_COMPARE:
from reversion_compare.admin import CompareVersionAdmin \
as TemplateModelAdmin
elif settings.DBTEMPLATES_USE_REVERSION:
from reversion.admin import VersionAdmin as TemplateModelAdmin from reversion.admin import VersionAdmin as TemplateModelAdmin
else: else:
from django.contrib.admin import ModelAdmin as TemplateModelAdmin # noqa from django.contrib.admin import ModelAdmin as TemplateModelAdmin # noqa
@ -27,13 +31,14 @@ class CodeMirrorTextArea(forms.Textarea):
class Media: class Media:
css = dict(screen=[posixpath.join( css = dict(screen=[posixpath.join(
settings.DBTEMPLATES_MEDIA_PREFIX, 'css/editor.css')]) 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): def render(self, name, value, attrs=None, renderer=None):
result = [] result = []
result.append( result.append(
super(CodeMirrorTextArea, self).render(name, value, attrs)) super().render(name, value, attrs))
result.append(u""" result.append("""
<script type="text/javascript"> <script type="text/javascript">
var editor = CodeMirror.fromTextArea('id_%(name)s', { var editor = CodeMirror.fromTextArea('id_%(name)s', {
path: "%(media_prefix)sjs/", path: "%(media_prefix)sjs/",
@ -47,7 +52,8 @@ class CodeMirrorTextArea(forms.Textarea):
}); });
</script> </script>
""" % dict(media_prefix=settings.DBTEMPLATES_MEDIA_PREFIX, name=name)) """ % dict(media_prefix=settings.DBTEMPLATES_MEDIA_PREFIX, name=name))
return mark_safe(u"".join(result)) return mark_safe("".join(result))
if settings.DBTEMPLATES_USE_CODEMIRROR: if settings.DBTEMPLATES_USE_CODEMIRROR:
TemplateContentTextArea = CodeMirrorTextArea TemplateContentTextArea = CodeMirrorTextArea
@ -80,7 +86,7 @@ class TemplateAdminForm(forms.ModelForm):
Custom AdminForm to make the content textarea wider. Custom AdminForm to make the content textarea wider.
""" """
content = forms.CharField( content = forms.CharField(
widget=TemplateContentTextArea({'rows': '24'}), widget=TemplateContentTextArea(attrs={'rows': '24'}),
help_text=content_help_text, required=False) help_text=content_help_text, required=False)
class Meta: class Meta:
@ -91,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'),
@ -115,7 +122,7 @@ class TemplateAdmin(TemplateModelAdmin):
for template in queryset: for template in queryset:
remove_cached_template(template) remove_cached_template(template)
count = queryset.count() count = queryset.count()
message = ungettext( message = ngettext(
"Cache of one template successfully invalidated.", "Cache of one template successfully invalidated.",
"Cache of %(count)d templates successfully invalidated.", "Cache of %(count)d templates successfully invalidated.",
count) count)
@ -127,7 +134,7 @@ class TemplateAdmin(TemplateModelAdmin):
for template in queryset: for template in queryset:
add_template_to_cache(template) add_template_to_cache(template)
count = queryset.count() count = queryset.count()
message = ungettext( message = ngettext(
"Cache successfully repopulated with one template.", "Cache successfully repopulated with one template.",
"Cache successfully repopulated with %(count)d templates.", "Cache successfully repopulated with %(count)d templates.",
count) count)
@ -140,18 +147,19 @@ class TemplateAdmin(TemplateModelAdmin):
for template in queryset: for template in queryset:
valid, error = check_template_syntax(template) valid, error = check_template_syntax(template)
if not valid: if not valid:
errors.append('%s: %s' % (template.name, error)) errors.append(f'{template.name}: {error}')
if errors: if errors:
count = len(errors) count = len(errors)
message = ungettext( message = ngettext(
"Template syntax check FAILED for %(names)s.", "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) count)
self.message_user(request, message % self.message_user(request, message %
{'count': count, 'names': ', '.join(errors)}) {'count': count, 'names': ', '.join(errors)})
else: else:
count = queryset.count() count = queryset.count()
message = ungettext( message = ngettext(
"Template syntax OK.", "Template syntax OK.",
"Template syntax OK for %(count)d templates.", count) "Template syntax OK for %(count)d templates.", count)
self.message_user(request, message % {'count': count}) self.message_user(request, message % {'count': count})
@ -161,4 +169,5 @@ class TemplateAdmin(TemplateModelAdmin):
return ", ".join([site.name for site in template.sites.all()]) 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) admin.site.register(Template, TemplateAdmin)

View file

@ -1,7 +1,9 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _ 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

@ -2,7 +2,6 @@ import posixpath
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.conf import settings from django.conf import settings
from django.utils.six import string_types
from appconf import AppConf from appconf import AppConf
@ -10,6 +9,7 @@ from appconf import AppConf
class DbTemplatesConf(AppConf): class DbTemplatesConf(AppConf):
USE_CODEMIRROR = False USE_CODEMIRROR = False
USE_REVERSION = False USE_REVERSION = False
USE_REVERSION_COMPARE = False
USE_TINYMCE = False USE_TINYMCE = False
USE_REDACTOR = False USE_REDACTOR = False
ADD_DEFAULT_SITE = True ADD_DEFAULT_SITE = True
@ -32,7 +32,7 @@ class DbTemplatesConf(AppConf):
return "dbtemplates" return "dbtemplates"
else: else:
return "default" return "default"
if isinstance(value, string_types) and value.startswith("dbtemplates."): if isinstance(value, str) and value.startswith("dbtemplates."):
raise ImproperlyConfigured("Please upgrade to one of the " raise ImproperlyConfigured("Please upgrade to one of the "
"supported backends as defined " "supported backends as defined "
"in the Django docs.") "in the Django docs.")
@ -45,6 +45,13 @@ class DbTemplatesConf(AppConf):
"use of it in dbtemplates.") "use of it in dbtemplates.")
return value 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.")
return value
def configure_use_tinymce(self, value): def configure_use_tinymce(self, value):
if value and 'tinymce' not in settings.INSTALLED_APPS: if value and 'tinymce' not in settings.INSTALLED_APPS:
raise ImproperlyConfigured("Please add 'tinymce' to your " raise ImproperlyConfigured("Please add 'tinymce' to your "

View file

@ -1,6 +1,6 @@
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.db import router from django.db import router
from django.template import TemplateDoesNotExist from django.template import Origin, TemplateDoesNotExist
from django.template.loaders.base import Loader as BaseLoader from django.template.loaders.base import Loader as BaseLoader
from dbtemplates.models import Template from dbtemplates.models import Template
@ -19,13 +19,25 @@ class Loader(BaseLoader):
""" """
is_usable = True is_usable = True
def load_and_store_template(self, template_name, cache_key, site, **params): def get_template_sources(self, template_name, template_dirs=None):
yield Origin(
name=template_name,
template_name=template_name,
loader=self,
)
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):
template = Template.objects.get(name__exact=template_name, **params) template = Template.objects.get(name__exact=template_name, **params)
db = router.db_for_read(Template, instance=template) db = router.db_for_read(Template, instance=template)
display_name = 'dbtemplates:%s:%s:%s' % (db, template_name, site.domain) display_name = f'dbtemplates:{db}:{template_name}:{site.domain}'
return set_and_return(cache_key, template.content, display_name) 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, template_dirs=None):
# The logic should work like this: # The logic should work like this:
# * Try to find the template in the cache. If found, return it. # * Try to find the template in the cache. If found, return it.
# * Now check the cache if a lookup for the given template # * Now check the cache if a lookup for the given template
@ -45,7 +57,7 @@ class Loader(BaseLoader):
backend_template = cache.get(cache_key) backend_template = cache.get(cache_key)
if backend_template: if backend_template:
return backend_template, template_name return backend_template, template_name
except: except Exception:
pass pass
# Not found in cache, move on. # Not found in cache, move on.
@ -55,18 +67,18 @@ class Loader(BaseLoader):
notfound = cache.get(cache_notfound_key) notfound = cache.get(cache_notfound_key)
if notfound: if notfound:
raise TemplateDoesNotExist(template_name) raise TemplateDoesNotExist(template_name)
except: except Exception:
raise TemplateDoesNotExist(template_name) raise TemplateDoesNotExist(template_name)
# Not marked as not-found, move on... # Not marked as not-found, move on...
try: try:
return self.load_and_store_template(template_name, cache_key, 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): except (Template.MultipleObjectsReturned, Template.DoesNotExist):
try: try:
return self.load_and_store_template(template_name, cache_key, return self._load_and_store_template(template_name, cache_key,
site, sites__isnull=True) site, sites__isnull=True)
except (Template.MultipleObjectsReturned, Template.DoesNotExist): except (Template.MultipleObjectsReturned, Template.DoesNotExist):
pass pass

View file

@ -16,7 +16,6 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"

View file

@ -12,7 +12,7 @@ class Command(BaseCommand):
for template in Template.objects.all(): for template in Template.objects.all():
valid, error = check_template_syntax(template) valid, error = check_template_syntax(template)
if not valid: if not valid:
errors.append('%s: %s' % (template.name, error)) errors.append(f'{template.name}: {error}')
if errors: if errors:
raise CommandError( raise CommandError(
'Some templates contained errors\n%s' % '\n'.join(errors)) 'Some templates contained errors\n%s' % '\n'.join(errors))

View file

@ -43,7 +43,7 @@ class Command(BaseCommand):
verbosity = int(options.get('verbosity', 1)) verbosity = int(options.get('verbosity', 1))
for error_code in (404, 500): for error_code in (404, 500):
template, created = Template.objects.get_or_create( template, created = Template.objects.get_or_create(
name="%s.html" % error_code) name=f"{error_code}.html")
if created or (not created and force): if created or (not created and force):
template.content = TEMPLATES.get(error_code, '') template.content = TEMPLATES.get(error_code, '')
template.save() template.save()

View file

@ -1,22 +1,18 @@
import io
import os 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
try:
from django.utils.six import input as raw_input
except ImportError:
pass
from dbtemplates.models import Template 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 = [] DIRS = []
for engine in _engine_list(): for engine in _engine_list():
DIRS.extend(engine.dirs) DIRS.extend(engine.dirs)
app_template_dirs = get_app_template_dirs('templates') DIRS = tuple(DIRS)
app_template_dirs = get_app_template_dirs("templates")
class Command(BaseCommand): class Command(BaseCommand):
@ -24,88 +20,121 @@ class Command(BaseCommand):
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
"-e", "--ext", "-e",
dest="ext", action="store", default="html", "--ext",
dest="ext",
action="store",
default="html",
help="extension of the files you want to " help="extension of the files you want to "
"sync with the database [default: %default]") "sync with the database [default: %(default)s]",
)
parser.add_argument( parser.add_argument(
"-f", "--force", "-f",
action="store_true", dest="force", default=False, "--force",
help="overwrite existing database templates") action="store_true",
dest="force",
default=False,
help="overwrite existing database templates",
)
parser.add_argument( parser.add_argument(
"-o", "--overwrite", "-o",
action="store", dest="overwrite", default='0', "--overwrite",
action="store",
dest="overwrite",
default="0",
help="'0' - ask always, '1' - overwrite database " help="'0' - ask always, '1' - overwrite database "
"templates from template files, '2' - overwrite " "templates from template files, '2' - overwrite "
"template files from database templates") "template files from database templates",
)
parser.add_argument( parser.add_argument(
"-a", "--app-first", "-a",
action="store_true", dest="app_first", default=False, "--app-first",
action="store_true",
dest="app_first",
default=False,
help="look for templates in applications " help="look for templates in applications "
"directories before project templates") "directories before project templates",
)
parser.add_argument( parser.add_argument(
"-d", "--delete", "-d",
action="store_true", dest="delete", default=False, "--delete",
help="Delete templates after syncing") action="store_true",
dest="delete",
default=False,
help="Delete templates after syncing",
)
def handle(self, **options): def handle(self, **options):
extension = options.get('ext') extension = options.get("ext")
force = options.get('force') force = options.get("force")
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")
if not extension.startswith("."): if not extension.startswith("."):
extension = ".%s" % extension extension = f".{extension}"
try: try:
site = Site.objects.get_current() site = Site.objects.get_current()
except: except Exception:
raise CommandError("Please make sure to have the sites contrib " raise CommandError(
"app installed and setup with a site object") "Please make sure to have the sites contrib "
"app installed and setup with a site object"
)
if app_first: if app_first:
tpl_dirs = app_template_dirs + DIRS tpl_dirs = app_template_dirs + DIRS
else: else:
tpl_dirs = DIRS + app_template_dirs tpl_dirs = DIRS + app_template_dirs
templatedirs = [d for d in tpl_dirs if os.path.isdir(d)] templatedirs = [str(d) for d in tpl_dirs if os.path.isdir(d)]
for templatedir in templatedirs: for templatedir in templatedirs:
for dirpath, subdirs, filenames in os.walk(templatedir): for dirpath, subdirs, filenames in os.walk(templatedir):
for f in [f for f in filenames for f in [
if f.endswith(extension) and not f.startswith(".")]: f
for f in filenames
if f.endswith(extension) and not f.startswith(".")
]:
path = os.path.join(dirpath, f) path = os.path.join(dirpath, f)
name = path.split(templatedir)[1] name = path.split(str(templatedir))[1]
if name.startswith('/'): if name.startswith("/"):
name = name[1:] name = name[1:]
try: try:
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 = raw_input( confirm = input(
"\nA '%s' template doesn't exist in the " "\nA '%s' template doesn't exist in the "
"database.\nCreate it with '%s'?" "database.\nCreate it with '%s'?"
" (y/[n]): """ % (name, path)) " (y/[n]): "
if force or confirm.lower().startswith('y'): "" % (name, path)
with io.open(path, encoding='utf-8') as f: )
if force or confirm.lower().startswith("y"):
with open(path, encoding="utf-8") as f:
t = Template(name=name, content=f.read()) t = Template(name=name, content=f.read())
t.save() t.save()
t.sites.add(site) t.sites.add(site)
else: else:
while 1: while True:
if overwrite == ALWAYS_ASK: if overwrite == ALWAYS_ASK:
confirm = raw_input( _i = (
"\n%(template)s exists in the database.\n" "\n%(template)s exists in the database.\n"
"(1) Overwrite %(template)s with '%(path)s'\n" "(1) Overwrite %(template)s with '%(path)s'\n" # noqa
"(2) Overwrite '%(path)s' with %(template)s\n" "(2) Overwrite '%(path)s' with %(template)s\n" # noqa
"Type 1 or 2 or press <Enter> to skip: " % "Type 1 or 2 or press <Enter> to skip: "
{'template': t.__repr__(), 'path': path}) % {"template": t.__repr__(), "path": path}
)
confirm = input(_i)
else: else:
confirm = overwrite confirm = overwrite
if confirm in ('', FILES_TO_DATABASE, if confirm in (
DATABASE_TO_FILES): "",
FILES_TO_DATABASE,
DATABASE_TO_FILES,
):
if confirm == FILES_TO_DATABASE: if confirm == FILES_TO_DATABASE:
with io.open(path, encoding='utf-8') as f: with open(path, encoding="utf-8") as f:
t.content = f.read() t.content = f.read()
t.save() t.save()
t.sites.add(site) t.sites.add(site)
@ -114,9 +143,10 @@ class Command(BaseCommand):
os.remove(path) os.remove(path)
except OSError: except OSError:
raise CommandError( raise CommandError(
u"Couldn't delete %s" % path) f"Couldn't delete {path}"
)
elif confirm == DATABASE_TO_FILES: elif confirm == DATABASE_TO_FILES:
with io.open(path, 'w', encoding='utf-8') as f: with open(path, "w", encoding="utf-8") as f: # noqa
f.write(t.content) f.write(t.content)
if delete: if delete:
t.delete() t.delete()

View file

@ -1,44 +1,73 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import django import django
from django.db import models, migrations
import django.utils.timezone import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('sites', '0001_initial'), ("sites", "0001_initial"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Template', name="Template",
fields=[ fields=[
('id', models.AutoField( (
verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), "id",
('name', models.CharField( models.AutoField(
help_text="Example: 'flatpages/default.html'", max_length=100, verbose_name='name')), verbose_name="ID",
('content', models.TextField(verbose_name='content', blank=True)), serialize=False,
('creation_date', models.DateTimeField( auto_created=True,
default=django.utils.timezone.now, verbose_name='creation date')), primary_key=True,
('last_changed', models.DateTimeField( ),
default=django.utils.timezone.now, verbose_name='last changed')), ),
('sites', models.ManyToManyField( (
to='sites.Site', verbose_name='sites', blank=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={ options={
'ordering': ('name',), "ordering": ("name",),
'db_table': 'django_template', "db_table": "django_template",
'verbose_name': 'template', "verbose_name": "template",
'verbose_name_plural': 'templates', "verbose_name_plural": "templates",
}, },
bases=(models.Model,), bases=(models.Model,),
managers=[ managers=[
('objects', django.db.models.manager.Manager()), ("objects", django.db.models.manager.Manager()),
('on_site', django.contrib.sites.managers.CurrentSiteManager( (
b'sites')), "on_site",
django.contrib.sites.managers.CurrentSiteManager("sites"),
), # noqa
], ],
), ),
] ]

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

@ -1,15 +1,16 @@
# -*- coding: utf-8 -*-
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
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.timezone import now
class Template(models.Model): class Template(models.Model):
@ -17,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.AutoField(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=_(u'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')
@ -36,7 +37,7 @@ class Template(models.Model):
verbose_name_plural = _('templates') verbose_name_plural = _('templates')
ordering = ('name',) ordering = ('name',)
def __unicode__(self): def __str__(self):
return self.name return self.name
def populate(self, name=None): def populate(self, name=None):
@ -54,12 +55,11 @@ 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:
self.populate() self.populate()
super(Template, self).save(*args, **kwargs) super().save(*args, **kwargs)
def add_default_site(instance, **kwargs): def add_default_site(instance, **kwargs):

View file

@ -1,7 +1,7 @@
import io
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
@ -22,11 +22,12 @@ from dbtemplates.management.commands.sync_templates import (FILES_TO_DATABASE,
class DbTemplatesTestCase(TestCase): class DbTemplatesTestCase(TestCase):
def setUp(self): def setUp(self):
self.old_template_loaders = settings.TEMPLATE_LOADERS self.old_TEMPLATES = settings.TEMPLATES
if 'dbtemplates.loader.Loader' not in settings.TEMPLATE_LOADERS: if 'dbtemplates.loader.Loader' not in settings.TEMPLATES:
loader.template_source_loaders = None loader.template_source_loaders = None
settings.TEMPLATE_LOADERS = (list(settings.TEMPLATE_LOADERS) + settings.TEMPLATES = list(settings.TEMPLATES) + [
['dbtemplates.loader.Loader']) 'dbtemplates.loader.Loader'
]
self.site1, created1 = Site.objects.get_or_create( self.site1, created1 = Site.objects.get_or_create(
domain="example.com", name="example.com") domain="example.com", name="example.com")
@ -40,9 +41,9 @@ class DbTemplatesTestCase(TestCase):
def tearDown(self): def tearDown(self):
loader.template_source_loaders = None loader.template_source_loaders = None
settings.TEMPLATE_LOADERS = self.old_template_loaders settings.TEMPLATES = self.old_TEMPLATES
def test_basiscs(self): def test_basics(self):
self.assertEqual(list(self.t1.sites.all()), [self.site1]) self.assertEqual(list(self.t1.sites.all()), [self.site1])
self.assertTrue("base" in self.t1.content) self.assertTrue("base" in self.t1.content)
self.assertEqual(list(Template.objects.filter(sites=self.site1)), self.assertEqual(list(Template.objects.filter(sites=self.site1)),
@ -102,9 +103,9 @@ class DbTemplatesTestCase(TestCase):
old_template_dirs = settings.TEMPLATES[0].get('DIRS', []) old_template_dirs = settings.TEMPLATES[0].get('DIRS', [])
temp_template_dir = tempfile.mkdtemp('dbtemplates') temp_template_dir = tempfile.mkdtemp('dbtemplates')
temp_template_path = os.path.join(temp_template_dir, 'temp_test.html') temp_template_path = os.path.join(temp_template_dir, 'temp_test.html')
temp_template = io.open(temp_template_path, 'w', encoding='utf-8') temp_template = open(temp_template_path, 'w', encoding='utf-8')
try: try:
temp_template.write(u'temp test') temp_template.write('temp test')
settings.TEMPLATES[0]['DIRS'] = (temp_template_dir,) settings.TEMPLATES[0]['DIRS'] = (temp_template_dir,)
# these works well if is not settings patched at runtime # these works well if is not settings patched at runtime
# for supporting django < 1.7 tests we must patch dirs in runtime # for supporting django < 1.7 tests we must patch dirs in runtime
@ -119,13 +120,13 @@ class DbTemplatesTestCase(TestCase):
Template.objects.filter(name='temp_test.html').exists()) Template.objects.filter(name='temp_test.html').exists())
t = Template.objects.get(name='temp_test.html') t = Template.objects.get(name='temp_test.html')
t.content = u'temp test modified' t.content = 'temp test modified'
t.save() t.save()
call_command('sync_templates', force=True, call_command('sync_templates', force=True,
verbosity=0, overwrite=DATABASE_TO_FILES) verbosity=0, overwrite=DATABASE_TO_FILES)
self.assertEqual(u'temp test modified', self.assertEqual('temp test modified',
io.open(temp_template_path, open(temp_template_path,
encoding='utf-8').read()) encoding='utf-8').read())
call_command('sync_templates', force=True, verbosity=0, call_command('sync_templates', force=True, verbosity=0,
delete=True, overwrite=DATABASE_TO_FILES) delete=True, overwrite=DATABASE_TO_FILES)
@ -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

@ -18,12 +18,15 @@ DATABASES = {
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'dbtemplates', 'dbtemplates',
] ]
MIDDLEWARE_CLASSES = ( MIDDLEWARE = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
) )
@ -39,6 +42,10 @@ TEMPLATES = [
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'OPTIONS': { 'OPTIONS': {
'loaders': TEMPLATE_LOADERS, 'loaders': TEMPLATE_LOADERS,
'context_processors': [
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
]
} }
}, },
] ]

View file

@ -1,32 +1,34 @@
from django.core import signals
from django.contrib.sites.models import Site
from django.template.defaultfilters import slugify
from dbtemplates.conf import settings from dbtemplates.conf import settings
from django.contrib.sites.models import Site
from django.core import signals
from django.template.defaultfilters import slugify
def get_cache_backend(): def get_cache_backend():
""" """
Compatibilty wrapper for getting Django's cache backend instance Compatibilty wrapper for getting Django's cache backend instance
""" """
from django.core.cache import _create_cache from django.core.cache import caches
cache = _create_cache(settings.DBTEMPLATES_CACHE_BACKEND) cache = caches.create_connection(settings.DBTEMPLATES_CACHE_BACKEND)
# Some caches -- python-memcached in particular -- need to do a cleanup at # 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 # the end of a request cycle. If not implemented in a particular backend
# cache.close is a no-op # cache.close is a no-op
signals.request_finished.connect(cache.close) signals.request_finished.connect(cache.close)
return cache return cache
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 'dbtemplates::%s::%s' % (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):
return get_cache_key(name) + '::notfound' return get_cache_key(name) + "::notfound"
def remove_notfound_key(instance): def remove_notfound_key(instance):
@ -56,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,6 +1,5 @@
from django.template import (Template, TemplateDoesNotExist, from django.template import (Template, TemplateDoesNotExist,
TemplateSyntaxError) TemplateSyntaxError)
from importlib import import_module
def get_loaders(): def get_loaders():
@ -17,20 +16,13 @@ def get_template_source(name):
if loader.__module__.startswith('dbtemplates.'): if loader.__module__.startswith('dbtemplates.'):
# Don't give a damn about dbtemplates' own loader. # Don't give a damn about dbtemplates' own loader.
continue continue
module = import_module(loader.__module__) for origin in loader.get_template_sources(name):
load_template_source = getattr( try:
module, 'load_template_source', None) source = loader.get_contents(origin)
if load_template_source is None: except (NotImplementedError, TemplateDoesNotExist):
load_template_source = loader.load_template_source continue
try:
source, origin = load_template_source(name)
if source: if source:
return source return source
except NotImplementedError:
pass
except TemplateDoesNotExist:
pass
return None
def check_template_syntax(template): def check_template_syntax(template):

View file

@ -61,8 +61,17 @@ Short installation howto
3. Sync your database with ``python manage.py syncdb`` 3. Sync your database with ``python manage.py syncdb``
4. Set ``DBTEMPLATES_USE_REVERSION`` setting to ``True`` 4. Set ``DBTEMPLATES_USE_REVERSION`` setting to ``True``
History compare view
--------------------
You can also use ``dbtemplates`` together with `django-reversion-compare`_ which
provides a history compare view to compare two versions of a model which is under
reversion.
.. _django-reversion: https://github.com/etianen/django-reversion .. _django-reversion: https://github.com/etianen/django-reversion
.. _django-reversion's documentation: https://django-reversion.readthedocs.io/en/latest/ .. _django-reversion's documentation: https://django-reversion.readthedocs.io/en/latest/
.. _django-reversion-compare: https://github.com/jedie/django-reversion-compare
.. _commands: .. _commands:

View file

@ -1,6 +1,57 @@
Changelog Changelog
========= =========
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.
* Django 5.x support
v4.0 (2022-09-3)
-----------------
.. warning::
This is a backwards-incompatible release!
* Dropped support for Python 2.7 and Django < 3.2.
* 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)
-----------------
.. warning::
This is a backwards-incompatible release!
* Dropped support for Django < 1.11.
* Added support for Django 2.0 and 2.1.
* Added support for Python 3.7.
* Recompiled Russian locale.
* Fixed byte string in migration file that caused the migration
system to falsely think that there are new changes.
* Fixed string representation of template model, e.g. to improve
readability in choice fields.
v2.0 (2016-09-29) v2.0 (2016-09-29)
----------------- -----------------

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# django-dbtemplates documentation build configuration file, created by # django-dbtemplates documentation build configuration file, created by
# sphinx-quickstart on Fri Oct 9 14:52:11 2009. # sphinx-quickstart on Fri Oct 9 14:52:11 2009.
@ -37,8 +36,8 @@ source_suffix = '.txt'
master_doc = 'index' master_doc = 'index'
# General information about the project. # General information about the project.
project = u'django-dbtemplates' project = 'django-dbtemplates'
copyright = u'2007-2012, Jannis Leidel and contributors' copyright = '2007-2019, Jannis Leidel and contributors'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
@ -177,8 +176,8 @@ htmlhelp_basename = 'django-dbtemplatesdoc'
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]). # (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ latex_documents = [
('index', 'django-dbtemplates.tex', u'django-dbtemplates Documentation', ('index', 'django-dbtemplates.tex', 'django-dbtemplates Documentation',
u'Jannis Leidel and contributors', 'manual'), 'Jannis Leidel and contributors', 'manual'),
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of

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/

View file

@ -41,6 +41,11 @@ Set to ``False`` by default.
A boolean, if enabled triggers the use of ``django-reversion``. A boolean, if enabled triggers the use of ``django-reversion``.
``DBTEMPLATES_USE_REVERSION_COMPARE``
-----------------------------
A boolean, if enabled triggers the use of ``django-reversion-compare``.
``DBTEMPLATES_MEDIA_PREFIX`` ``DBTEMPLATES_MEDIA_PREFIX``
---------------------------- ----------------------------

51
pyproject.toml Normal file
View file

@ -0,0 +1,51 @@
[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",
"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,13 +0,0 @@
[egg_info]
#tag_build = a1
[build_sphinx]
source-dir = docs/
build-dir = docs/_build
all_files = 1
[upload_docs]
upload-dir = docs/_build/html
[upload_sphinx]
upload-dir = docs/_build/html

View file

@ -1,60 +0,0 @@
import ast
import os
import io
from setuptools import setup, find_packages
class VersionFinder(ast.NodeVisitor):
def __init__(self):
self.version = None
def visit_Assign(self, node):
if node.targets[0].id == '__version__':
self.version = node.value.s
def read(*parts):
filename = os.path.join(os.path.dirname(__file__), *parts)
with io.open(filename, encoding='utf-8') as fp:
return fp.read()
def find_version(*parts):
finder = VersionFinder()
finder.visit(ast.parse(read(*parts)))
return finder.version
setup(
name='django-dbtemplates',
version=find_version('dbtemplates', '__init__.py'),
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 :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Framework :: Django',
],
install_requires=['django-appconf >= 0.4'],
)

69
tox.ini
View file

@ -1,43 +1,58 @@
[tox] [tox]
skipsdist = True minversion = 4.0
usedevelop = True
minversion = 1.8
envlist = envlist =
flake8-py27, flake8
flake8-py35, py3{8,9,10,11,12}-dj42
readme-py27, py3{10,11,12}-dj{50}
py{27,34,35,py}-dj{18,19,110,master} py3{10,11,12,13}-dj{51,52}
py3{12,13}-dj{main}
coverage
[gh-actions]
python =
3.8: py38
3.9: py39
3.10: py310
3.10: py310, flake8
3.11: py311
3.12: py312
3.13: py313
[testenv] [testenv]
skipsdist = true
package = editable
basepython = basepython =
py27: python2.7 py38: python3.8
py34: python3.4 py39: python3.9
py35: python3.5 py310: python3.10
pypy: pypy py311: python3.11
usedevelop = true py312: python3.12
py313: python3.13
setenv = setenv =
DJANGO_SETTINGS_MODULE = dbtemplates.test_settings DJANGO_SETTINGS_MODULE = dbtemplates.test_settings
deps = deps =
-rrequirements/tests.txt -r requirements/tests.txt
dj18: https://github.com/django/django/archive/stable/1.8.x.tar.gz#egg=django dj42: Django>=4.2,<4.3
dj19: https://github.com/django/django/archive/stable/1.9.x.tar.gz#egg=django dj50: Django>=5.0,<5.1
dj110: https://github.com/django/django/archive/stable/1.10.x.tar.gz#egg=django dj51: Django>=5.1,<5.2
djmaster: https://github.com/django/django/archive/master.tar.gz#egg=django dj52: Django>=5.2,<5.3
djmain: https://github.com/django/django/archive/main.tar.gz#egg=django
commands = commands =
python --version python --version
coverage run {envbindir}/django-admin.py 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
[testenv:readme-py27] [testenv:flake8]
commands = python setup.py check -r -s basepython = python3.10
deps = readme_renderer
[testenv:flake8-py27]
commands = flake8 dbtemplates
deps = flake8
[testenv:flake8-py35]
commands = flake8 dbtemplates commands = flake8 dbtemplates
deps = flake8 deps = flake8