Compare commits

..

No commits in common. "master" and "2.0.1" have entirely different histories.

34 changed files with 293 additions and 616 deletions

View file

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

View file

@ -1,40 +0,0 @@
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

View file

@ -1,48 +0,0 @@
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,6 +1,3 @@
.*
!.gitignore
!.coveragerc
*.pyc
.*.swp
MANIFEST
@ -11,4 +8,3 @@ docs/_build
.tox/
*.egg/
.coverage
coverage.xml

View file

@ -1,15 +0,0 @@
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

View file

@ -1,25 +0,0 @@
# .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

30
.travis.yml Normal file
View file

@ -0,0 +1,30 @@
language: python
python:
- '2.7'
- '3.4'
- '3.5'
- '3.6'
# Enable 3.7 without globally enabling sudo and dist: xenial for other build jobs
# See https://github.com/travis-ci/travis-ci/issues/9815
matrix:
include:
- python: 3.7
dist: xenial
sudo: true
sudo: false
cache: pip
install:
- pip install tox-travis
script: tox -v
after_success:
- bash <(curl -s https://codecov.io/bash)
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
python: 3.7

View file

@ -1,46 +0,0 @@
# 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-2019, Jannis Leidel and contributors
Copyright (c) 2007-2016, Jannis Leidel and contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without

View file

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

View file

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

View file

@ -2,21 +2,17 @@ import posixpath
from django import forms
from django.contrib import admin
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext
from django.utils.translation import ungettext, ugettext_lazy as _
from django.utils.safestring import mark_safe
from dbtemplates.conf import settings
from dbtemplates.models import Template, add_template_to_cache, remove_cached_template
from dbtemplates.models import (Template, remove_cached_template,
add_template_to_cache)
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
elif settings.DBTEMPLATES_USE_REVERSION:
# Check if django-reversion is installed and use reversions' VersionAdmin
# as the base admin class if yes
if settings.DBTEMPLATES_USE_REVERSION:
from reversion.admin import VersionAdmin as TemplateModelAdmin
else:
from django.contrib.admin import ModelAdmin as TemplateModelAdmin # noqa
@ -31,14 +27,13 @@ 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 = []
result.append(
super().render(name, value, attrs))
result.append("""
super(CodeMirrorTextArea, self).render(name, value, attrs))
result.append(u"""
<script type="text/javascript">
var editor = CodeMirror.fromTextArea('id_%(name)s', {
path: "%(media_prefix)sjs/",
@ -52,7 +47,7 @@ class CodeMirrorTextArea(forms.Textarea):
});
</script>
""" % dict(media_prefix=settings.DBTEMPLATES_MEDIA_PREFIX, name=name))
return mark_safe("".join(result))
return mark_safe(u"".join(result))
if settings.DBTEMPLATES_USE_CODEMIRROR:
@ -97,7 +92,6 @@ class TemplateAdminForm(forms.ModelForm):
class TemplateAdmin(TemplateModelAdmin):
form = TemplateAdminForm
readonly_fields = ['creation_date', 'last_changed']
fieldsets = (
(None, {
'fields': ('name', 'content'),
@ -122,7 +116,7 @@ class TemplateAdmin(TemplateModelAdmin):
for template in queryset:
remove_cached_template(template)
count = queryset.count()
message = ngettext(
message = ungettext(
"Cache of one template successfully invalidated.",
"Cache of %(count)d templates successfully invalidated.",
count)
@ -134,7 +128,7 @@ class TemplateAdmin(TemplateModelAdmin):
for template in queryset:
add_template_to_cache(template)
count = queryset.count()
message = ngettext(
message = ungettext(
"Cache successfully repopulated with one template.",
"Cache successfully repopulated with %(count)d templates.",
count)
@ -147,19 +141,18 @@ class TemplateAdmin(TemplateModelAdmin):
for template in queryset:
valid, error = check_template_syntax(template)
if not valid:
errors.append(f'{template.name}: {error}')
errors.append('%s: %s' % (template.name, error))
if errors:
count = len(errors)
message = ngettext(
message = ungettext(
"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 = ngettext(
message = ungettext(
"Template syntax OK.",
"Template syntax OK for %(count)d templates.", count)
self.message_user(request, message % {'count': count})

View file

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

View file

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

View file

@ -30,11 +30,10 @@ class Loader(BaseLoader):
content, _ = self._load_template_source(origin.template_name)
return content
def _load_and_store_template(self, template_name, cache_key, site,
**params):
def _load_and_store_template(self, template_name, cache_key, site, **params):
template = Template.objects.get(name__exact=template_name, **params)
db = router.db_for_read(Template, instance=template)
display_name = f'dbtemplates:{db}:{template_name}:{site.domain}'
display_name = 'dbtemplates:%s:%s:%s' % (db, template_name, site.domain)
return set_and_return(cache_key, template.content, display_name)
def _load_template_source(self, template_name, template_dirs=None):
@ -74,11 +73,11 @@ class Loader(BaseLoader):
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

View file

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

View file

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

View file

@ -1,18 +1,22 @@
import io
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 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,121 +24,88 @@ 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]")
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}"
extension = ".%s" % extension
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
else:
tpl_dirs = DIRS + app_template_dirs
templatedirs = [str(d) for d in tpl_dirs if os.path.isdir(d)]
templatedirs = [d for d in tpl_dirs if os.path.isdir(d)]
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("/"):
name = path.split(templatedir)[1]
if name.startswith('/'):
name = name[1:]
try:
t = Template.on_site.get(name__exact=name)
except Template.DoesNotExist:
if not force:
confirm = input(
confirm = raw_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 io.open(path, encoding='utf-8') as f:
t = Template(name=name, content=f.read())
t.save()
t.sites.add(site)
else:
while True:
while 1:
if overwrite == ALWAYS_ASK:
_i = (
confirm = raw_input(
"\n%(template)s exists in the database.\n"
"(1) Overwrite %(template)s with '%(path)s'\n" # noqa
"(2) Overwrite '%(path)s' with %(template)s\n" # noqa
"Type 1 or 2 or press <Enter> to skip: "
% {"template": t.__repr__(), "path": path}
)
confirm = input(_i)
"(1) Overwrite %(template)s with '%(path)s'\n"
"(2) Overwrite '%(path)s' with %(template)s\n"
"Type 1 or 2 or press <Enter> to skip: " %
{'template': t.__repr__(), 'path': path})
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 io.open(path, encoding='utf-8') as f:
t.content = f.read()
t.save()
t.sites.add(site)
@ -143,10 +114,9 @@ class Command(BaseCommand):
os.remove(path)
except OSError:
raise CommandError(
f"Couldn't delete {path}"
)
u"Couldn't delete %s" % path)
elif confirm == DATABASE_TO_FILES:
with open(path, "w", encoding="utf-8") as f: # noqa
with io.open(path, 'w', encoding='utf-8') as f:
f.write(t.content)
if delete:
t.delete()

View file

@ -1,73 +1,44 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
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),
), # 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
),
),
('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)),
],
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"),
), # noqa
('objects', django.db.models.manager.Manager()),
('on_site', django.contrib.sites.managers.CurrentSiteManager(
b'sites')),
],
),
]

View file

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

View file

@ -1,7 +1,7 @@
import io
import os
import shutil
import tempfile
from unittest import mock
from django.conf import settings as django_settings
from django.core.cache.backends.base import BaseCache
@ -22,12 +22,11 @@ from dbtemplates.management.commands.sync_templates import (FILES_TO_DATABASE,
class DbTemplatesTestCase(TestCase):
def setUp(self):
self.old_TEMPLATES = settings.TEMPLATES
if 'dbtemplates.loader.Loader' not in settings.TEMPLATES:
self.old_template_loaders = settings.TEMPLATE_LOADERS
if 'dbtemplates.loader.Loader' not in settings.TEMPLATE_LOADERS:
loader.template_source_loaders = None
settings.TEMPLATES = list(settings.TEMPLATES) + [
'dbtemplates.loader.Loader'
]
settings.TEMPLATE_LOADERS = (list(settings.TEMPLATE_LOADERS) +
['dbtemplates.loader.Loader'])
self.site1, created1 = Site.objects.get_or_create(
domain="example.com", name="example.com")
@ -41,7 +40,7 @@ class DbTemplatesTestCase(TestCase):
def tearDown(self):
loader.template_source_loaders = None
settings.TEMPLATES = self.old_TEMPLATES
settings.TEMPLATE_LOADERS = self.old_template_loaders
def test_basics(self):
self.assertEqual(list(self.t1.sites.all()), [self.site1])
@ -103,9 +102,9 @@ class DbTemplatesTestCase(TestCase):
old_template_dirs = settings.TEMPLATES[0].get('DIRS', [])
temp_template_dir = tempfile.mkdtemp('dbtemplates')
temp_template_path = os.path.join(temp_template_dir, 'temp_test.html')
temp_template = open(temp_template_path, 'w', encoding='utf-8')
temp_template = io.open(temp_template_path, 'w', encoding='utf-8')
try:
temp_template.write('temp test')
temp_template.write(u'temp test')
settings.TEMPLATES[0]['DIRS'] = (temp_template_dir,)
# these works well if is not settings patched at runtime
# for supporting django < 1.7 tests we must patch dirs in runtime
@ -120,13 +119,13 @@ class DbTemplatesTestCase(TestCase):
Template.objects.filter(name='temp_test.html').exists())
t = Template.objects.get(name='temp_test.html')
t.content = 'temp test modified'
t.content = u'temp test modified'
t.save()
call_command('sync_templates', force=True,
verbosity=0, overwrite=DATABASE_TO_FILES)
self.assertEqual('temp test modified',
open(temp_template_path,
encoding='utf-8').read())
self.assertEqual(u'temp test modified',
io.open(temp_template_path,
encoding='utf-8').read())
call_command('sync_templates', force=True, verbosity=0,
delete=True, overwrite=DATABASE_TO_FILES)
@ -152,24 +151,3 @@ class DbTemplatesTestCase(TestCase):
def test_get_cache_name(self):
self.assertEqual(get_cache_key('name with spaces'),
'dbtemplates::name-with-spaces::1')
def test_cache_invalidation(self):
# Add t1 into the cache of site2
self.t1.sites.add(self.site2)
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,15 +18,12 @@ DATABASES = {
INSTALLED_APPS = [
'django.contrib.contenttypes',
'django.contrib.sites',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.admin',
'django.contrib.auth',
'dbtemplates',
]
MIDDLEWARE = (
'django.contrib.sessions.middleware.SessionMiddleware',
MIDDLEWARE_CLASSES = (
'django.contrib.messages.middleware.MessageMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
)
@ -44,7 +41,6 @@ TEMPLATES = [
'loaders': TEMPLATE_LOADERS,
'context_processors': [
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
]
}
},

View file

@ -1,16 +1,16 @@
from dbtemplates.conf import settings
from django.contrib.sites.models import Site
from django.core import signals
from django.contrib.sites.models import Site
from django.template.defaultfilters import slugify
from dbtemplates.conf import settings
def get_cache_backend():
"""
Compatibilty wrapper for getting Django's cache backend instance
"""
from django.core.cache import caches
cache = caches.create_connection(settings.DBTEMPLATES_CACHE_BACKEND)
from django.core.cache import _create_cache
cache = _create_cache(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
@ -21,14 +21,13 @@ def get_cache_backend():
cache = get_cache_backend()
def get_cache_key(name, site=None):
if site is None:
site = Site.objects.get_current()
return f"dbtemplates::{slugify(name)}::{site.pk}"
def get_cache_key(name):
current_site = Site.objects.get_current()
return 'dbtemplates::%s::%s' % (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):
@ -58,5 +57,4 @@ def remove_cached_template(instance, **kwargs):
Called via Django's signals to remove cached templates, if the template
in the database was changed or deleted.
"""
for site in instance.sites.all():
cache.delete(get_cache_key(instance.name, site=site))
cache.delete(get_cache_key(instance.name))

View file

@ -61,17 +61,8 @@ Short installation howto
3. Sync your database with ``python manage.py syncdb``
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's documentation: https://django-reversion.readthedocs.io/en/latest/
.. _django-reversion-compare: https://github.com/jedie/django-reversion-compare
.. _commands:

View file

@ -1,57 +1,6 @@
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)
-----------------

View file

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

View file

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

View file

@ -41,11 +41,6 @@ Set to ``False`` by default.
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``
----------------------------

View file

@ -1,51 +0,0 @@
[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",
]

View file

@ -1 +0,0 @@
django

13
setup.cfg Normal file
View file

@ -0,0 +1,13 @@
[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

60
setup.py Normal file
View file

@ -0,0 +1,60 @@
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'],
)

75
tox.ini
View file

@ -1,58 +1,51 @@
[tox]
minversion = 4.0
skipsdist = True
usedevelop = True
minversion = 1.8
envlist =
flake8
py3{8,9,10,11,12}-dj42
py3{10,11,12}-dj{50}
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
flake8-py27,
flake8-py37,
readme-py27,
readme-py37,
py{27,34,35,36}-dj111
py{34,35,36,37}-dj20
py{34,35,36,37}-dj21
[testenv]
skipsdist = true
package = editable
basepython =
py38: python3.8
py39: python3.9
py310: python3.10
py311: python3.11
py312: python3.12
py313: python3.13
py27: python2.7
py34: python3.4
py35: python3.5
py36: python3.6
py37: python3.7
usedevelop = true
setenv =
DJANGO_SETTINGS_MODULE = dbtemplates.test_settings
deps =
-r requirements/tests.txt
dj42: Django>=4.2,<4.3
dj50: Django>=5.0,<5.1
dj51: Django>=5.1,<5.2
dj52: Django>=5.2,<5.3
djmain: https://github.com/django/django/archive/main.tar.gz#egg=django
dj111: Django<2.0
dj20: Django<2.1
dj21: Django<2.2
djmaster: https://github.com/django/django/archive/master.tar.gz#egg=django
commands =
python --version
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 run {envbindir}/django-admin.py test -v2 {posargs:dbtemplates}
coverage report
coverage xml
[testenv:flake8]
basepython = python3.10
[testenv:readme-py27]
commands = python setup.py check -r -s
deps = readme_renderer
[testenv:readme-py37]
commands = python setup.py check -r -s
deps = readme_renderer
[testenv:flake8-py27]
commands = flake8 dbtemplates
deps = flake8
[testenv:flake8-py37]
commands = flake8 dbtemplates
deps = flake8