├── dbtemplates ├── utils │ ├── __init__.py │ ├── template.py │ └── cache.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── check_template_syntax.py │ │ ├── create_error_templates.py │ │ └── sync_templates.py ├── migrations │ ├── __init__.py │ ├── 0002_alter_template_creation_date_and_more.py │ └── 0001_initial.py ├── __init__.py ├── locale │ ├── da │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fi │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── he │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── it │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ru │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── pt_BR │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── zh_CN │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── apps.py ├── static │ └── dbtemplates │ │ ├── css │ │ ├── editor.css │ │ └── django.css │ │ └── js │ │ ├── tokenize.js │ │ ├── highlight.js │ │ ├── mirrorframe.js │ │ ├── util.js │ │ ├── stringstream.js │ │ ├── parsedjango.js │ │ ├── undo.js │ │ ├── codemirror.js │ │ └── select.js ├── test_settings.py ├── conf.py ├── models.py ├── loader.py ├── admin.py └── test_cases.py ├── requirements ├── docs.txt └── tests.txt ├── .coveragerc ├── .gitignore ├── MANIFEST.in ├── .tx └── config ├── CONTRIBUTING.md ├── .pre-commit-config.yaml ├── AUTHORS ├── .readthedocs.yaml ├── docs ├── index.txt ├── settings.txt ├── overview.txt ├── advanced.txt ├── Makefile ├── make.bat ├── conf.py └── changelog.txt ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── README.rst ├── tox.ini ├── pyproject.toml ├── CODE_OF_CONDUCT.md └── LICENSE /dbtemplates/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | django 2 | -------------------------------------------------------------------------------- /dbtemplates/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dbtemplates/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dbtemplates/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/tests.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | coverage 3 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = dbtemplates 3 | branch = 1 4 | 5 | [report] 6 | omit = *tests*,*/migrations/*,test_* 7 | -------------------------------------------------------------------------------- /dbtemplates/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | 3 | __version__ = importlib.metadata.version("django-dbtemplates") 4 | -------------------------------------------------------------------------------- /dbtemplates/locale/da/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-dbtemplates/master/dbtemplates/locale/da/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /dbtemplates/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-dbtemplates/master/dbtemplates/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /dbtemplates/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-dbtemplates/master/dbtemplates/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /dbtemplates/locale/fi/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-dbtemplates/master/dbtemplates/locale/fi/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /dbtemplates/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-dbtemplates/master/dbtemplates/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /dbtemplates/locale/he/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-dbtemplates/master/dbtemplates/locale/he/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /dbtemplates/locale/it/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-dbtemplates/master/dbtemplates/locale/it/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /dbtemplates/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-dbtemplates/master/dbtemplates/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /dbtemplates/locale/pt_BR/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-dbtemplates/master/dbtemplates/locale/pt_BR/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /dbtemplates/locale/zh_CN/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-dbtemplates/master/dbtemplates/locale/zh_CN/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.gitignore 3 | !.coveragerc 4 | *.pyc 5 | .*.swp 6 | MANIFEST 7 | build 8 | dist 9 | *.egg-info 10 | docs/_build 11 | .tox/ 12 | *.egg/ 13 | .coverage 14 | coverage.xml 15 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE AUTHORS README.rst MANIFEST.in tox.ini .coveragerc CONTRIBUTING.md 2 | recursive-include docs *.txt 3 | recursive-include dbtemplates/locale * 4 | recursive-include dbtemplates/static/dbtemplates *.css *.js 5 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [django-dbtemplates.main] 2 | file_filter = dbtemplates/locale//LC_MESSAGES/django.po 3 | source_file = dbtemplates/locale/en/LC_MESSAGES/django.po 4 | source_lang = en 5 | 6 | [main] 7 | host = https://www.transifex.com 8 | -------------------------------------------------------------------------------- /dbtemplates/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class DBTemplatesConfig(AppConfig): 6 | name = 'dbtemplates' 7 | verbose_name = _('Database templates') 8 | 9 | default_auto_field = 'django.db.models.AutoField' 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) 2 | 3 | This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/docs/conduct) and follow the [guidelines](https://jazzband.co/docs/guidelines). 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/asottile/pyupgrade 3 | rev: v2.34.0 4 | hooks: 5 | - id: pyupgrade 6 | args: [--py37-plus] 7 | 8 | - repo: https://github.com/pre-commit/pre-commit-hooks 9 | rev: v4.3.0 10 | hooks: 11 | - id: check-merge-conflict 12 | - id: check-yaml 13 | 14 | ci: 15 | autoupdate_schedule: quarterly 16 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Alen Mujezinovic 2 | Alex Gaynor 3 | Alex Kamedov 4 | Alexander Artemenko 5 | Arne Brodowski 6 | David Paccoud 7 | Diego Búrigo Zacarão 8 | Dmitry Falk 9 | Jannis Leidel 10 | Jure Cuhalev 11 | Jason Mayfield 12 | Kevin Mooney 13 | Mark Stahler 14 | Matt Dorn 15 | Oliver George 16 | Selwin Ong 17 | Stephan Peijnik , ANEXIA Internetdienstleistungs GmbH, http://www.anexia.at/ 18 | Zhang Kun 19 | -------------------------------------------------------------------------------- /dbtemplates/static/dbtemplates/css/editor.css: -------------------------------------------------------------------------------- 1 | .dbtemplates-template div.content { 2 | white-space: nowrap; 3 | margin-right: 0.6em; 4 | } 5 | .dbtemplates-template #id_content { 6 | height: 40.2em; 7 | width: 85%; 8 | } 9 | .CodeMirror-line-numbers { 10 | width: 2.2em; 11 | text-align: right; 12 | margin: .4em; 13 | margin-left: 6em; 14 | padding: 0; 15 | font-family: monospace; 16 | font-size: 10pt; 17 | color: #999; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | tools: 12 | python: "3.9" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # If using Sphinx, optionally build your docs in additional formats such as PDF 19 | formats: 20 | - pdf 21 | 22 | # Optionally declare the Python requirements required to build your docs 23 | python: 24 | install: 25 | - requirements: requirements/docs.txt 26 | -------------------------------------------------------------------------------- /dbtemplates/migrations/0002_alter_template_creation_date_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-05-26 19:59 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('dbtemplates', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='template', 15 | name='creation_date', 16 | field=models.DateTimeField(auto_now_add=True, verbose_name='creation date'), 17 | ), 18 | migrations.AlterField( 19 | model_name='template', 20 | name='last_changed', 21 | field=models.DateTimeField(auto_now=True, verbose_name='last changed'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /dbtemplates/management/commands/check_template_syntax.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import CommandError, BaseCommand 2 | 3 | from dbtemplates.models import Template 4 | from dbtemplates.utils.template import check_template_syntax 5 | 6 | 7 | class Command(BaseCommand): 8 | help = "Ensures templates stored in the database don't have syntax errors." 9 | 10 | def handle(self, **options): 11 | errors = [] 12 | for template in Template.objects.all(): 13 | valid, error = check_template_syntax(template) 14 | if not valid: 15 | errors.append(f'{template.name}: {error}') 16 | if errors: 17 | raise CommandError( 18 | 'Some templates contained errors\n%s' % '\n'.join(errors)) 19 | self.stdout.write('OK') 20 | -------------------------------------------------------------------------------- /docs/index.txt: -------------------------------------------------------------------------------- 1 | django-dbtemplates 2 | ================== 3 | 4 | ``dbtemplates`` is a Django app that consists of two parts: 5 | 6 | 1. It allows you to store templates in your database 7 | 2. It provides `template loader`_ that enables Django to load the 8 | templates from the database 9 | 10 | It also features optional support for :ref:`versioned storage ` 11 | and :ref:`django-admin command `, integrates with Django's 12 | :ref:`caching system ` and the :ref:`admin actions `. 13 | 14 | Please see https://django-dbtemplates.readthedocs.io/ for more details. 15 | 16 | The source code and issue tracker can be found on Github: https://github.com/jazzband/django-dbtemplates 17 | 18 | .. _template loader: http://docs.djangoproject.com/en/dev/ref/templates/api/#loading-templates 19 | 20 | Contents: 21 | 22 | .. toctree:: 23 | :maxdepth: 2 24 | 25 | overview 26 | advanced 27 | settings 28 | changelog -------------------------------------------------------------------------------- /dbtemplates/static/dbtemplates/css/django.css: -------------------------------------------------------------------------------- 1 | html { 2 | cursor: text; 3 | } 4 | 5 | .editbox { 6 | margin: .4em; 7 | padding: 0; 8 | font-family: monospace; 9 | font-size: 10pt; 10 | color: black; 11 | } 12 | 13 | .editbox p { 14 | margin: 0; 15 | } 16 | 17 | span.django { 18 | color: #999; 19 | } 20 | 21 | span.django-quote { 22 | color: #281; 23 | } 24 | 25 | span.django-tag-name { 26 | color: #000; 27 | font-weight: bold; 28 | } 29 | 30 | span.xml-tagname { 31 | color: #A0B; 32 | } 33 | 34 | span.xml-attribute { 35 | color: #281; 36 | } 37 | 38 | span.xml-punctuation { 39 | color: black; 40 | } 41 | 42 | span.xml-attname { 43 | color: #00F; 44 | } 45 | 46 | span.xml-comment { 47 | color: #A70; 48 | } 49 | 50 | span.xml-cdata { 51 | color: #48A; 52 | } 53 | 54 | span.xml-processing { 55 | color: #999; 56 | } 57 | 58 | span.xml-entity { 59 | color: #A22; 60 | } 61 | 62 | span.xml-error { 63 | color: #F00 !important; 64 | } 65 | 66 | span.xml-text { 67 | color: black; 68 | } 69 | -------------------------------------------------------------------------------- /dbtemplates/utils/template.py: -------------------------------------------------------------------------------- 1 | from django.template import (Template, TemplateDoesNotExist, 2 | TemplateSyntaxError) 3 | 4 | 5 | def get_loaders(): 6 | from django.template.loader import _engine_list 7 | loaders = [] 8 | for engine in _engine_list(): 9 | loaders.extend(engine.engine.template_loaders) 10 | return loaders 11 | 12 | 13 | def get_template_source(name): 14 | source = None 15 | for loader in get_loaders(): 16 | if loader.__module__.startswith('dbtemplates.'): 17 | # Don't give a damn about dbtemplates' own loader. 18 | continue 19 | for origin in loader.get_template_sources(name): 20 | try: 21 | source = loader.get_contents(origin) 22 | except (NotImplementedError, TemplateDoesNotExist): 23 | continue 24 | if source: 25 | return source 26 | 27 | 28 | def check_template_syntax(template): 29 | try: 30 | Template(template.content) 31 | except TemplateSyntaxError as e: 32 | return (False, e) 33 | return (True, None) 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | if: github.repository == 'jazzband/django-dbtemplates' 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: "3.10" 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install -U pip 26 | python -m pip install -U setuptools twine wheel 27 | 28 | - name: Build package 29 | run: | 30 | python setup.py --version 31 | python setup.py sdist --format=gztar bdist_wheel 32 | twine check dist/* 33 | 34 | - name: Upload packages to Jazzband 35 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 36 | uses: pypa/gh-action-pypi-publish@release/v1 37 | with: 38 | user: jazzband 39 | password: ${{ secrets.JAZZBAND_RELEASE_KEY }} 40 | repository_url: https://jazzband.co/projects/django-dbtemplates/upload 41 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-dbtemplates 2 | ================== 3 | 4 | .. image:: https://jazzband.co/static/img/badge.svg 5 | :alt: Jazzband 6 | :target: https://jazzband.co/ 7 | 8 | .. image:: https://github.com/jazzband/django-dbtemplates/workflows/Test/badge.svg 9 | :target: https://github.com/jazzband/django-dbtemplates/actions 10 | 11 | .. image:: https://codecov.io/github/jazzband/django-dbtemplates/coverage.svg?branch=master 12 | :alt: Codecov 13 | :target: https://codecov.io/github/jazzband/django-dbtemplates?branch=master 14 | 15 | ``dbtemplates`` is a Django app that consists of two parts: 16 | 17 | 1. It allows you to store templates in your database 18 | 2. It provides `template loader`_ that enables Django to load the 19 | templates from the database 20 | 21 | It also features optional support for versioned storage and django-admin 22 | command, integrates with Django's caching system and the admin actions. 23 | 24 | Please see https://django-dbtemplates.readthedocs.io/ for more details. 25 | 26 | The source code and issue tracker can be found on Github: 27 | 28 | https://github.com/jazzband/django-dbtemplates 29 | 30 | .. _template loader: http://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | max-parallel: 5 11 | matrix: 12 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.13'] 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | 22 | - name: Get pip cache dir 23 | id: pip-cache 24 | run: | 25 | echo "::set-output name=dir::$(pip cache dir)" 26 | 27 | - name: Cache 28 | uses: actions/cache@v3 29 | with: 30 | path: ${{ steps.pip-cache.outputs.dir }} 31 | key: 32 | ${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}-${{ hashFiles('**/tox.ini') }} 33 | restore-keys: | 34 | ${{ matrix.python-version }}-v1- 35 | 36 | - name: Install dependencies 37 | run: | 38 | python -m pip install --upgrade pip 39 | python -m pip install --upgrade tox tox-gh-actions 40 | 41 | - name: Tox tests 42 | run: | 43 | tox -v 44 | 45 | - name: Upload coverage 46 | uses: codecov/codecov-action@v2 47 | with: 48 | name: Python ${{ matrix.python-version }} 49 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 4.0 3 | envlist = 4 | flake8 5 | py3{8,9,10,11,12}-dj42 6 | py3{10,11,12}-dj{50} 7 | py3{10,11,12,13}-dj{51,52} 8 | py3{12,13}-dj{main} 9 | coverage 10 | 11 | [gh-actions] 12 | python = 13 | 3.8: py38 14 | 3.9: py39 15 | 3.10: py310 16 | 3.10: py310, flake8 17 | 3.11: py311 18 | 3.12: py312 19 | 3.13: py313 20 | 21 | [testenv] 22 | skipsdist = true 23 | package = editable 24 | basepython = 25 | py38: python3.8 26 | py39: python3.9 27 | py310: python3.10 28 | py311: python3.11 29 | py312: python3.12 30 | py313: python3.13 31 | setenv = 32 | DJANGO_SETTINGS_MODULE = dbtemplates.test_settings 33 | deps = 34 | -r requirements/tests.txt 35 | dj42: Django>=4.2,<4.3 36 | dj50: Django>=5.0,<5.1 37 | dj51: Django>=5.1,<5.2 38 | dj52: Django>=5.2,<5.3 39 | djmain: https://github.com/django/django/archive/main.tar.gz#egg=django 40 | 41 | commands = 42 | python --version 43 | python -c "import django ; print(django.VERSION)" 44 | coverage run --branch --parallel-mode {envbindir}/django-admin test -v2 {posargs:dbtemplates} 45 | 46 | [testenv:coverage] 47 | basepython = python3.10 48 | deps = coverage 49 | commands = 50 | coverage combine 51 | coverage report 52 | coverage xml 53 | 54 | [testenv:flake8] 55 | basepython = python3.10 56 | commands = flake8 dbtemplates 57 | deps = flake8 58 | 59 | [flake8] 60 | exclude=.tox 61 | ignore=E501,E127,E128,E124 62 | -------------------------------------------------------------------------------- /dbtemplates/test_settings.py: -------------------------------------------------------------------------------- 1 | DBTEMPLATES_CACHE_BACKEND = 'dummy://' 2 | 3 | DATABASE_ENGINE = 'sqlite3' 4 | # SQLite does not support removing unique constraints (see #28) 5 | SOUTH_TESTS_MIGRATE = False 6 | 7 | SITE_ID = 1 8 | 9 | SECRET_KEY = 'something-something' 10 | 11 | DATABASES = { 12 | 'default': { 13 | 'ENGINE': 'django.db.backends.sqlite3', 14 | 'NAME': ':memory:', 15 | } 16 | } 17 | 18 | INSTALLED_APPS = [ 19 | 'django.contrib.contenttypes', 20 | 'django.contrib.sites', 21 | 'django.contrib.sessions', 22 | 'django.contrib.messages', 23 | 'django.contrib.admin', 24 | 'django.contrib.auth', 25 | 'dbtemplates', 26 | ] 27 | 28 | MIDDLEWARE = ( 29 | 'django.contrib.sessions.middleware.SessionMiddleware', 30 | 'django.contrib.messages.middleware.MessageMiddleware', 31 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 32 | ) 33 | 34 | TEMPLATE_LOADERS = ( 35 | 'django.template.loaders.filesystem.Loader', 36 | 'django.template.loaders.app_directories.Loader', 37 | 'dbtemplates.loader.Loader', 38 | ) 39 | 40 | TEMPLATES = [ 41 | { 42 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 43 | 'OPTIONS': { 44 | 'loaders': TEMPLATE_LOADERS, 45 | 'context_processors': [ 46 | 'django.contrib.auth.context_processors.auth', 47 | 'django.contrib.messages.context_processors.messages', 48 | ] 49 | } 50 | }, 51 | ] 52 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=61.2", 4 | "setuptools_scm", 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [project] 9 | name = "django-dbtemplates" 10 | authors = [{name = "Jannis Leidel", email = "jannis@leidel.info"}] 11 | description = "Template loader for templates stored in the database" 12 | readme = "README.rst" 13 | classifiers = [ 14 | "Development Status :: 5 - Production/Stable", 15 | "Environment :: Web Environment", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: BSD License", 18 | "Operating System :: OS Independent", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3 :: Only", 22 | "Programming Language :: Python :: 3.8", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | "Programming Language :: Python :: 3.13", 28 | "Framework :: Django", 29 | ] 30 | requires-python = ">=3.8" 31 | dependencies = ["django-appconf >= 0.4"] 32 | dynamic = ["version"] 33 | 34 | [project.urls] 35 | Documentation = "https://django-dbtemplates.readthedocs.io/" 36 | Changelog = "https://django-dbtemplates.readthedocs.io/en/latest/changelog.html" 37 | Source = "https://github.com/jazzband/django-dbtemplates" 38 | 39 | [tool.setuptools] 40 | zip-safe = false 41 | include-package-data = false 42 | 43 | [tool.setuptools.packages] 44 | find = {namespaces = false} 45 | 46 | [tool.setuptools.package-data] 47 | dbtemplates = [ 48 | "locale/*/LC_MESSAGES/*", 49 | "static/dbtemplates/css/*.css", 50 | "static/dbtemplates/js/*.js", 51 | ] 52 | -------------------------------------------------------------------------------- /dbtemplates/utils/cache.py: -------------------------------------------------------------------------------- 1 | from dbtemplates.conf import settings 2 | from django.contrib.sites.models import Site 3 | from django.core import signals 4 | from django.template.defaultfilters import slugify 5 | 6 | 7 | def get_cache_backend(): 8 | """ 9 | Compatibilty wrapper for getting Django's cache backend instance 10 | """ 11 | from django.core.cache import caches 12 | cache = caches.create_connection(settings.DBTEMPLATES_CACHE_BACKEND) 13 | 14 | # Some caches -- python-memcached in particular -- need to do a cleanup at 15 | # the end of a request cycle. If not implemented in a particular backend 16 | # cache.close is a no-op 17 | signals.request_finished.connect(cache.close) 18 | return cache 19 | 20 | 21 | cache = get_cache_backend() 22 | 23 | 24 | def get_cache_key(name, site=None): 25 | if site is None: 26 | site = Site.objects.get_current() 27 | return f"dbtemplates::{slugify(name)}::{site.pk}" 28 | 29 | 30 | def get_cache_notfound_key(name): 31 | return get_cache_key(name) + "::notfound" 32 | 33 | 34 | def remove_notfound_key(instance): 35 | # Remove notfound key as soon as we save the template. 36 | cache.delete(get_cache_notfound_key(instance.name)) 37 | 38 | 39 | def set_and_return(cache_key, content, display_name): 40 | # Save in cache backend explicitly if manually deleted or invalidated 41 | if cache: 42 | cache.set(cache_key, content) 43 | return (content, display_name) 44 | 45 | 46 | def add_template_to_cache(instance, **kwargs): 47 | """ 48 | Called via Django's signals to cache the templates, if the template 49 | in the database was added or changed. 50 | """ 51 | remove_cached_template(instance) 52 | remove_notfound_key(instance) 53 | cache.set(get_cache_key(instance.name), instance.content) 54 | 55 | 56 | def remove_cached_template(instance, **kwargs): 57 | """ 58 | Called via Django's signals to remove cached templates, if the template 59 | in the database was changed or deleted. 60 | """ 61 | for site in instance.sites.all(): 62 | cache.delete(get_cache_key(instance.name, site=site)) 63 | -------------------------------------------------------------------------------- /dbtemplates/management/commands/create_error_templates.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from django.core.management.base import CommandError, BaseCommand 3 | from django.contrib.sites.models import Site 4 | 5 | from dbtemplates.models import Template 6 | 7 | TEMPLATES = { 8 | 404: """ 9 | {% extends "base.html" %} 10 | {% load i18n %} 11 | {% block content %} 12 |

{% trans 'Page not found' %}

13 |

{% trans "We're sorry, but the requested page could not be found." %}

14 | {% endblock %} 15 | """, 16 | 500: """ 17 | {% extends "base.html" %} 18 | {% load i18n %} 19 | {% block content %} 20 |

{% trans 'Server Error (500)' %}

21 |

{% trans "There's been an error." %}

22 | {% endblock %} 23 | """, 24 | } 25 | 26 | 27 | class Command(BaseCommand): 28 | help = "Creates the default error templates as database template objects." 29 | 30 | def add_arguments(self, parser): 31 | parser.add_argument( 32 | "-f", "--force", action="store_true", dest="force", 33 | default=False, help="overwrite existing database templates") 34 | 35 | def handle(self, **options): 36 | force = options.get('force') 37 | try: 38 | site = Site.objects.get_current() 39 | except Site.DoesNotExist: 40 | raise CommandError("Please make sure to have the sites contrib " 41 | "app installed and setup with a site object") 42 | 43 | verbosity = int(options.get('verbosity', 1)) 44 | for error_code in (404, 500): 45 | template, created = Template.objects.get_or_create( 46 | name=f"{error_code}.html") 47 | if created or (not created and force): 48 | template.content = TEMPLATES.get(error_code, '') 49 | template.save() 50 | template.sites.add(site) 51 | if verbosity >= 1: 52 | sys.stdout.write("Created database template " 53 | "for %s errors.\n" % error_code) 54 | else: 55 | if verbosity >= 1: 56 | sys.stderr.write("A template for %s errors " 57 | "already exists.\n" % error_code) 58 | -------------------------------------------------------------------------------- /dbtemplates/static/dbtemplates/js/tokenize.js: -------------------------------------------------------------------------------- 1 | // A framework for simple tokenizers. Takes care of newlines and 2 | // white-space, and of getting the text from the source stream into 3 | // the token object. A state is a function of two arguments -- a 4 | // string stream and a setState function. The second can be used to 5 | // change the tokenizer's state, and can be ignored for stateless 6 | // tokenizers. This function should advance the stream over a token 7 | // and return a string or object containing information about the next 8 | // token, or null to pass and have the (new) state be called to finish 9 | // the token. When a string is given, it is wrapped in a {style, type} 10 | // object. In the resulting object, the characters consumed are stored 11 | // under the content property. Any whitespace following them is also 12 | // automatically consumed, and added to the value property. (Thus, 13 | // content is the actual meaningful part of the token, while value 14 | // contains all the text it spans.) 15 | 16 | function tokenizer(source, state) { 17 | // Newlines are always a separate token. 18 | function isWhiteSpace(ch) { 19 | // The messy regexp is because IE's regexp matcher is of the 20 | // opinion that non-breaking spaces are no whitespace. 21 | return ch != "\n" && /^[\s\u00a0]*$/.test(ch); 22 | } 23 | 24 | var tokenizer = { 25 | state: state, 26 | 27 | take: function(type) { 28 | if (typeof(type) == "string") 29 | type = {style: type, type: type}; 30 | 31 | type.content = (type.content || "") + source.get(); 32 | if (!/\n$/.test(type.content)) 33 | source.nextWhile(isWhiteSpace); 34 | type.value = type.content + source.get(); 35 | return type; 36 | }, 37 | 38 | next: function () { 39 | if (!source.more()) throw StopIteration; 40 | 41 | var type; 42 | if (source.equals("\n")) { 43 | source.next(); 44 | return this.take("whitespace"); 45 | } 46 | 47 | if (source.applies(isWhiteSpace)) 48 | type = "whitespace"; 49 | else 50 | while (!type) 51 | type = this.state(source, function(s) {tokenizer.state = s;}); 52 | 53 | return this.take(type); 54 | } 55 | }; 56 | return tokenizer; 57 | } 58 | -------------------------------------------------------------------------------- /dbtemplates/static/dbtemplates/js/highlight.js: -------------------------------------------------------------------------------- 1 | // Minimal framing needed to use CodeMirror-style parsers to highlight 2 | // code. Load this along with tokenize.js, stringstream.js, and your 3 | // parser. Then call highlightText, passing a string as the first 4 | // argument, and as the second argument either a callback function 5 | // that will be called with an array of SPAN nodes for every line in 6 | // the code, or a DOM node to which to append these spans, and 7 | // optionally (not needed if you only loaded one parser) a parser 8 | // object. 9 | 10 | // Stuff from util.js that the parsers are using. 11 | var StopIteration = {toString: function() {return "StopIteration"}}; 12 | 13 | var Editor = {}; 14 | var indentUnit = 2; 15 | 16 | (function(){ 17 | function normaliseString(string) { 18 | var tab = ""; 19 | for (var i = 0; i < indentUnit; i++) tab += " "; 20 | 21 | string = string.replace(/\t/g, tab).replace(/\u00a0/g, " ").replace(/\r\n?/g, "\n"); 22 | var pos = 0, parts = [], lines = string.split("\n"); 23 | for (var line = 0; line < lines.length; line++) { 24 | if (line != 0) parts.push("\n"); 25 | parts.push(lines[line]); 26 | } 27 | 28 | return { 29 | next: function() { 30 | if (pos < parts.length) return parts[pos++]; 31 | else throw StopIteration; 32 | } 33 | }; 34 | } 35 | 36 | window.highlightText = function(string, callback, parser) { 37 | parser = (parser || Editor.Parser).make(stringStream(normaliseString(string))); 38 | var line = []; 39 | if (callback.nodeType == 1) { 40 | var node = callback; 41 | callback = function(line) { 42 | for (var i = 0; i < line.length; i++) 43 | node.appendChild(line[i]); 44 | node.appendChild(document.createElement("BR")); 45 | }; 46 | } 47 | 48 | try { 49 | while (true) { 50 | var token = parser.next(); 51 | if (token.value == "\n") { 52 | callback(line); 53 | line = []; 54 | } 55 | else { 56 | var span = document.createElement("SPAN"); 57 | span.className = token.style; 58 | span.appendChild(document.createTextNode(token.value)); 59 | line.push(span); 60 | } 61 | } 62 | } 63 | catch (e) { 64 | if (e != StopIteration) throw e; 65 | } 66 | if (line.length) callback(line); 67 | } 68 | })(); 69 | -------------------------------------------------------------------------------- /docs/settings.txt: -------------------------------------------------------------------------------- 1 | Settings 2 | ======== 3 | 4 | ``DBTEMPLATES_ADD_DEFAULT_SITE`` 5 | -------------------------------- 6 | 7 | ``dbtemplates`` adds the current site (``settings.SITE_ID``) to the database 8 | template when it is created by default. You can disable this feature by 9 | setting ``DBTEMPLATES_ADD_DEFAULT_SITE`` to ``False``. 10 | 11 | ``DBTEMPLATES_AUTO_POPULATE_CONTENT`` 12 | ------------------------------------- 13 | 14 | ``dbtemplates`` auto-populates the content of a newly created template with 15 | the content of a template with the same name the other template loader. 16 | To disable this feature set ``DBTEMPLATES_AUTO_POPULATE_CONTENT`` to 17 | ``False``. 18 | 19 | ``DBTEMPLATES_CACHE_BACKEND`` 20 | ----------------------------- 21 | 22 | The dotted Python path to the cache backend class. See 23 | :ref:`Caching ` for details. 24 | 25 | ``DBTEMPLATES_USE_CODEMIRROR`` 26 | ------------------------------ 27 | 28 | A boolean, if enabled triggers the use of the CodeMirror based editor. 29 | Set to ``False`` by default. 30 | 31 | ``DBTEMPLATES_USE_TINYMCE`` 32 | --------------------------- 33 | 34 | .. versionadded:: 1.3 35 | 36 | A boolean, if enabled triggers the use of the TinyMCE based editor. 37 | Set to ``False`` by default. 38 | 39 | ``DBTEMPLATES_USE_REVERSION`` 40 | ----------------------------- 41 | 42 | A boolean, if enabled triggers the use of ``django-reversion``. 43 | 44 | ``DBTEMPLATES_USE_REVERSION_COMPARE`` 45 | ----------------------------- 46 | 47 | A boolean, if enabled triggers the use of ``django-reversion-compare``. 48 | 49 | ``DBTEMPLATES_MEDIA_PREFIX`` 50 | ---------------------------- 51 | 52 | The URL prefix for ``dbtemplates``' media -- CSS and JavaScript used by 53 | the CodeMirror based editor. Make sure to use a trailing 54 | slash, and to have this be different from the ``STATIC_URL`` setting 55 | (since the same URL cannot be mapped onto two different sets of 56 | files). 57 | 58 | .. warning:: 59 | Starting in version 1.0, ``dbtemplates`` uses the ``STATIC_URL`` setting, 60 | originally introduced by the django-staticfiles_ app. The app has since 61 | been added to Django itself and isn't needed if you use Django 1.3 or 62 | higher. Please refer to the `contrib docs`_ in that case. 63 | 64 | .. _django-staticfiles: http://pypi.python.org/pypi/django-staticfiles 65 | .. _contrib docs: http://docs.djangoproject.com/en/dev/ref/staticfiles/ 66 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | As contributors and maintainers of the Jazzband projects, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating documentation, 6 | submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in the Jazzband a harassment-free experience 9 | for everyone, regardless of the level of experience, gender, gender identity and 10 | expression, sexual orientation, disability, personal appearance, body size, race, 11 | ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | - The use of sexualized language or imagery 16 | - Personal attacks 17 | - Trolling or insulting/derogatory comments 18 | - Public or private harassment 19 | - Publishing other's private information, such as physical or electronic addresses, 20 | without explicit permission 21 | - Other unethical or unprofessional conduct 22 | 23 | The Jazzband roadies have the right and responsibility to remove, edit, or reject 24 | comments, commits, code, wiki edits, issues, and other contributions that are not 25 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor 26 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | By adopting this Code of Conduct, the roadies commit themselves to fairly and 29 | consistently applying these principles to every aspect of managing the jazzband 30 | projects. Roadies who do not follow or enforce the Code of Conduct may be permanently 31 | removed from the Jazzband roadies. 32 | 33 | This code of conduct applies both within project spaces and in public spaces when an 34 | individual is representing the project or its community. 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 37 | contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and 38 | investigated and will result in a response that is deemed necessary and appropriate to 39 | the circumstances. Roadies are obligated to maintain confidentiality with regard to the 40 | reporter of an incident. 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 43 | 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/3/0/ 47 | -------------------------------------------------------------------------------- /dbtemplates/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import django 2 | import django.utils.timezone 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("sites", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Template", 15 | fields=[ 16 | ( 17 | "id", 18 | models.AutoField( 19 | verbose_name="ID", 20 | serialize=False, 21 | auto_created=True, 22 | primary_key=True, 23 | ), 24 | ), 25 | ( 26 | "name", 27 | models.CharField( 28 | help_text="Example: 'flatpages/default.html'", 29 | max_length=100, 30 | verbose_name="name", 31 | ), 32 | ), 33 | ( 34 | "content", 35 | models.TextField(verbose_name="content", blank=True), 36 | ), # noqa 37 | ( 38 | "creation_date", 39 | models.DateTimeField( 40 | default=django.utils.timezone.now, 41 | verbose_name="creation date", # noqa 42 | ), 43 | ), 44 | ( 45 | "last_changed", 46 | models.DateTimeField( 47 | default=django.utils.timezone.now, 48 | verbose_name="last changed", # noqa 49 | ), 50 | ), 51 | ( 52 | "sites", 53 | models.ManyToManyField( 54 | to="sites.Site", verbose_name="sites", blank=True 55 | ), 56 | ), 57 | ], 58 | options={ 59 | "ordering": ("name",), 60 | "db_table": "django_template", 61 | "verbose_name": "template", 62 | "verbose_name_plural": "templates", 63 | }, 64 | bases=(models.Model,), 65 | managers=[ 66 | ("objects", django.db.models.manager.Manager()), 67 | ( 68 | "on_site", 69 | django.contrib.sites.managers.CurrentSiteManager("sites"), 70 | ), # noqa 71 | ], 72 | ), 73 | ] 74 | -------------------------------------------------------------------------------- /dbtemplates/static/dbtemplates/js/mirrorframe.js: -------------------------------------------------------------------------------- 1 | /* Demonstration of embedding CodeMirror in a bigger application. The 2 | * interface defined here is a mess of prompts and confirms, and 3 | * should probably not be used in a real project. 4 | */ 5 | 6 | function MirrorFrame(place, options) { 7 | this.home = document.createElement("DIV"); 8 | if (place.appendChild) 9 | place.appendChild(this.home); 10 | else 11 | place(this.home); 12 | 13 | var self = this; 14 | function makeButton(name, action) { 15 | var button = document.createElement("INPUT"); 16 | button.type = "button"; 17 | button.value = name; 18 | self.home.appendChild(button); 19 | button.onclick = function(){self[action].call(self);}; 20 | } 21 | 22 | makeButton("Search", "search"); 23 | makeButton("Replace", "replace"); 24 | makeButton("Current line", "line"); 25 | makeButton("Jump to line", "jump"); 26 | makeButton("Insert constructor", "macro"); 27 | makeButton("Indent all", "reindent"); 28 | 29 | this.mirror = new CodeMirror(this.home, options); 30 | } 31 | 32 | MirrorFrame.prototype = { 33 | search: function() { 34 | var text = prompt("Enter search term:", ""); 35 | if (!text) return; 36 | 37 | var first = true; 38 | do { 39 | var cursor = this.mirror.getSearchCursor(text, first); 40 | first = false; 41 | while (cursor.findNext()) { 42 | cursor.select(); 43 | if (!confirm("Search again?")) 44 | return; 45 | } 46 | } while (confirm("End of document reached. Start over?")); 47 | }, 48 | 49 | replace: function() { 50 | // This is a replace-all, but it is possible to implement a 51 | // prompting replace. 52 | var from = prompt("Enter search string:", ""), to; 53 | if (from) to = prompt("What should it be replaced with?", ""); 54 | if (to == null) return; 55 | 56 | var cursor = this.mirror.getSearchCursor(from, false); 57 | while (cursor.findNext()) 58 | cursor.replace(to); 59 | }, 60 | 61 | jump: function() { 62 | var line = prompt("Jump to line:", ""); 63 | if (line && !isNaN(Number(line))) 64 | this.mirror.jumpToLine(Number(line)); 65 | }, 66 | 67 | line: function() { 68 | alert("The cursor is currently at line " + this.mirror.currentLine()); 69 | this.mirror.focus(); 70 | }, 71 | 72 | macro: function() { 73 | var name = prompt("Name your constructor:", ""); 74 | if (name) 75 | this.mirror.replaceSelection("function " + name + "() {\n \n}\n\n" + name + ".prototype = {\n \n};\n"); 76 | }, 77 | 78 | reindent: function() { 79 | this.mirror.reindent(); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007-2019, Jannis Leidel and contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | * Neither the name of the author nor the names of other 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | 31 | 32 | This software includes CodeMirror released under the following license: 33 | 34 | Copyright (c) 2007-2009 Marijn Haverbeke 35 | 36 | This software is provided 'as-is', without any express or implied 37 | warranty. In no event will the authors be held liable for any 38 | damages arising from the use of this software. 39 | 40 | Permission is granted to anyone to use this software for any 41 | purpose, including commercial applications, and to alter it and 42 | redistribute it freely, subject to the following restrictions: 43 | 44 | 1. The origin of this software must not be misrepresented; you must 45 | not claim that you wrote the original software. If you use this 46 | software in a product, an acknowledgment in the product 47 | documentation would be appreciated but is not required. 48 | 49 | 2. Altered source versions must be plainly marked as such, and must 50 | not be misrepresented as being the original software. 51 | 52 | 3. This notice may not be removed or altered from any source 53 | distribution. 54 | 55 | Marijn Haverbeke 56 | marijnh at gmail 57 | -------------------------------------------------------------------------------- /dbtemplates/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2011-08-15 13:13+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: admin.py:56 21 | msgid "" 22 | "Leaving this empty causes Django to look for a template with the given name " 23 | "and populate this field with its content." 24 | msgstr "" 25 | 26 | #: admin.py:82 27 | msgid "Advanced" 28 | msgstr "" 29 | 30 | #: admin.py:85 31 | msgid "Date/time" 32 | msgstr "" 33 | 34 | #: admin.py:102 35 | #, python-format 36 | msgid "Cache of one template successfully invalidated." 37 | msgid_plural "Cache of %(count)d templates successfully invalidated." 38 | msgstr[0] "" 39 | msgstr[1] "" 40 | 41 | #: admin.py:106 42 | msgid "Invalidate cache of selected templates" 43 | msgstr "" 44 | 45 | #: admin.py:114 46 | #, python-format 47 | msgid "Cache successfully repopulated with one template." 48 | msgid_plural "Cache successfully repopulated with %(count)d templates." 49 | msgstr[0] "" 50 | msgstr[1] "" 51 | 52 | #: admin.py:118 53 | msgid "Repopulate cache with selected templates" 54 | msgstr "" 55 | 56 | #: admin.py:130 57 | #, python-format 58 | msgid "Template syntax check FAILED for %(names)s." 59 | msgid_plural "Template syntax check FAILED for %(count)d templates: %(names)s." 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | #: admin.py:138 64 | #, python-format 65 | msgid "Template syntax OK." 66 | msgid_plural "Template syntax OK for %(count)d templates." 67 | msgstr[0] "" 68 | msgstr[1] "" 69 | 70 | #: admin.py:141 71 | msgid "Check template syntax" 72 | msgstr "" 73 | 74 | #: admin.py:145 models.py:25 75 | msgid "sites" 76 | msgstr "" 77 | 78 | #: models.py:22 79 | msgid "name" 80 | msgstr "" 81 | 82 | #: models.py:23 83 | msgid "Example: 'flatpages/default.html'" 84 | msgstr "" 85 | 86 | #: models.py:24 87 | msgid "content" 88 | msgstr "" 89 | 90 | #: models.py:27 91 | msgid "creation date" 92 | msgstr "" 93 | 94 | #: models.py:29 95 | msgid "last changed" 96 | msgstr "" 97 | 98 | #: models.py:37 99 | msgid "template" 100 | msgstr "" 101 | 102 | #: models.py:38 103 | msgid "templates" 104 | msgstr "" 105 | -------------------------------------------------------------------------------- /dbtemplates/locale/zh_CN/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: django-dbtemplates\n" 8 | "Report-Msgid-Bugs-To: https://github.com/jezdez/django-dbtemplates/issues\n" 9 | "POT-Creation-Date: 2011-08-15 13:13+0200\n" 10 | "PO-Revision-Date: 2011-08-15 11:14+0000\n" 11 | "Last-Translator: Jannis \n" 12 | "Language-Team: LANGUAGE \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Language: zh_CN\n" 17 | "Plural-Forms: nplurals=1; plural=0\n" 18 | 19 | #: admin.py:56 20 | msgid "" 21 | "Leaving this empty causes Django to look for a template with the given name " 22 | "and populate this field with its content." 23 | msgstr "此项目留空将使系统用指定的名称寻找模板并应用到该项目。" 24 | 25 | #: admin.py:82 26 | msgid "Advanced" 27 | msgstr "" 28 | 29 | #: admin.py:85 30 | msgid "Date/time" 31 | msgstr "日期/时间" 32 | 33 | #: admin.py:102 34 | #, fuzzy, python-format 35 | msgid "Cache of one template successfully invalidated." 36 | msgid_plural "Cache of %(count)d templates successfully invalidated." 37 | msgstr[0] "该模板的缓存已经成功撤销。" 38 | 39 | #: admin.py:106 40 | msgid "Invalidate cache of selected templates" 41 | msgstr "撤销选中模板的缓存" 42 | 43 | #: admin.py:114 44 | #, fuzzy, python-format 45 | msgid "Cache successfully repopulated with one template." 46 | msgid_plural "Cache successfully repopulated with %(count)d templates." 47 | msgstr[0] "该模板的缓存已经成功启用。" 48 | 49 | #: admin.py:118 50 | msgid "Repopulate cache with selected templates" 51 | msgstr "重新启用选中模板的缓存" 52 | 53 | #: admin.py:130 54 | #, python-format 55 | msgid "Template syntax check FAILED for %(names)s." 56 | msgid_plural "" 57 | "Template syntax check FAILED for %(count)d templates: %(names)s." 58 | msgstr[0] "" 59 | 60 | #: admin.py:138 61 | #, python-format 62 | msgid "Template syntax OK." 63 | msgid_plural "Template syntax OK for %(count)d templates." 64 | msgstr[0] "" 65 | 66 | #: admin.py:141 67 | msgid "Check template syntax" 68 | msgstr "" 69 | 70 | #: admin.py:145 models.py:25 71 | msgid "sites" 72 | msgstr "站点" 73 | 74 | #: models.py:22 75 | msgid "name" 76 | msgstr "名称" 77 | 78 | #: models.py:23 79 | msgid "Example: 'flatpages/default.html'" 80 | msgstr "例如: 'flatpages/default.html'" 81 | 82 | #: models.py:24 83 | msgid "content" 84 | msgstr "内容" 85 | 86 | #: models.py:27 87 | msgid "creation date" 88 | msgstr "创建日期" 89 | 90 | #: models.py:29 91 | msgid "last changed" 92 | msgstr "最新变更" 93 | 94 | #: models.py:37 95 | msgid "template" 96 | msgstr "模板" 97 | 98 | #: models.py:38 99 | msgid "templates" 100 | msgstr "模板" 101 | 102 | 103 | -------------------------------------------------------------------------------- /dbtemplates/locale/he/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: django-dbtemplates\n" 8 | "Report-Msgid-Bugs-To: https://github.com/jezdez/django-dbtemplates/issues\n" 9 | "POT-Creation-Date: 2011-08-15 13:13+0200\n" 10 | "PO-Revision-Date: 2011-08-15 11:14+0000\n" 11 | "Last-Translator: Jannis \n" 12 | "Language-Team: LANGUAGE \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Language: he\n" 17 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 18 | 19 | #: admin.py:56 20 | msgid "" 21 | "Leaving this empty causes Django to look for a template with the given name " 22 | "and populate this field with its content." 23 | msgstr "אם זה ריק אז ג'נגו מחפש תבנית עם שם סיפק וממלא את השדה עם תוכנו" 24 | 25 | #: admin.py:82 26 | msgid "Advanced" 27 | msgstr "" 28 | 29 | #: admin.py:85 30 | msgid "Date/time" 31 | msgstr "תאריך / זמן" 32 | 33 | #: admin.py:102 34 | #, python-format 35 | msgid "Cache of one template successfully invalidated." 36 | msgid_plural "Cache of %(count)d templates successfully invalidated." 37 | msgstr[0] "" 38 | msgstr[1] "" 39 | 40 | #: admin.py:106 41 | msgid "Invalidate cache of selected templates" 42 | msgstr "" 43 | 44 | #: admin.py:114 45 | #, python-format 46 | msgid "Cache successfully repopulated with one template." 47 | msgid_plural "Cache successfully repopulated with %(count)d templates." 48 | msgstr[0] "" 49 | msgstr[1] "" 50 | 51 | #: admin.py:118 52 | msgid "Repopulate cache with selected templates" 53 | msgstr "" 54 | 55 | #: admin.py:130 56 | #, python-format 57 | msgid "Template syntax check FAILED for %(names)s." 58 | msgid_plural "" 59 | "Template syntax check FAILED for %(count)d templates: %(names)s." 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | #: admin.py:138 64 | #, python-format 65 | msgid "Template syntax OK." 66 | msgid_plural "Template syntax OK for %(count)d templates." 67 | msgstr[0] "" 68 | msgstr[1] "" 69 | 70 | #: admin.py:141 71 | msgid "Check template syntax" 72 | msgstr "" 73 | 74 | #: admin.py:145 models.py:25 75 | msgid "sites" 76 | msgstr "אתרים" 77 | 78 | #: models.py:22 79 | msgid "name" 80 | msgstr "שם" 81 | 82 | #: models.py:23 83 | msgid "Example: 'flatpages/default.html'" 84 | msgstr "דוגמא: 'flatpages/default.html'" 85 | 86 | #: models.py:24 87 | msgid "content" 88 | msgstr "תוכן" 89 | 90 | #: models.py:27 91 | msgid "creation date" 92 | msgstr "נוצר ב" 93 | 94 | #: models.py:29 95 | msgid "last changed" 96 | msgstr "שונה ב" 97 | 98 | #: models.py:37 99 | msgid "template" 100 | msgstr "תבנית" 101 | 102 | #: models.py:38 103 | msgid "templates" 104 | msgstr "תבניות" 105 | 106 | 107 | -------------------------------------------------------------------------------- /dbtemplates/conf.py: -------------------------------------------------------------------------------- 1 | import posixpath 2 | 3 | from django.core.exceptions import ImproperlyConfigured 4 | from django.conf import settings 5 | 6 | from appconf import AppConf 7 | 8 | 9 | class DbTemplatesConf(AppConf): 10 | USE_CODEMIRROR = False 11 | USE_REVERSION = False 12 | USE_REVERSION_COMPARE = False 13 | USE_TINYMCE = False 14 | USE_REDACTOR = False 15 | ADD_DEFAULT_SITE = True 16 | AUTO_POPULATE_CONTENT = True 17 | MEDIA_PREFIX = None 18 | CACHE_BACKEND = None 19 | 20 | def configure_media_prefix(self, value): 21 | if value is None: 22 | base_url = getattr(settings, "STATIC_URL", None) 23 | if base_url is None: 24 | base_url = settings.MEDIA_URL 25 | value = posixpath.join(base_url, "dbtemplates/") 26 | return value 27 | 28 | def configure_cache_backend(self, value): 29 | # If we are on Django 1.3 AND using the new CACHES setting.. 30 | if hasattr(settings, "CACHES"): 31 | if "dbtemplates" in settings.CACHES: 32 | return "dbtemplates" 33 | else: 34 | return "default" 35 | if isinstance(value, str) and value.startswith("dbtemplates."): 36 | raise ImproperlyConfigured("Please upgrade to one of the " 37 | "supported backends as defined " 38 | "in the Django docs.") 39 | return value 40 | 41 | def configure_use_reversion(self, value): 42 | if value and 'reversion' not in settings.INSTALLED_APPS: 43 | raise ImproperlyConfigured("Please add 'reversion' to your " 44 | "INSTALLED_APPS setting to make " 45 | "use of it in dbtemplates.") 46 | return value 47 | 48 | def configure_use_reversion_compare(self, value): 49 | if value and 'reversion_compare' not in settings.INSTALLED_APPS: 50 | raise ImproperlyConfigured("Please add 'reversion_compare' to your" 51 | " INSTALLED_APPS setting to make " 52 | "use of it in dbtemplates.") 53 | return value 54 | 55 | def configure_use_tinymce(self, value): 56 | if value and 'tinymce' not in settings.INSTALLED_APPS: 57 | raise ImproperlyConfigured("Please add 'tinymce' to your " 58 | "INSTALLED_APPS setting to make " 59 | "use of it in dbtemplates.") 60 | return value 61 | 62 | def configure_use_redactor(self, value): 63 | if value and 'redactor' not in settings.INSTALLED_APPS: 64 | raise ImproperlyConfigured("Please add 'redactor' to your " 65 | "INSTALLED_APPS setting to make " 66 | "use of it in dbtemplates.") 67 | return value 68 | -------------------------------------------------------------------------------- /dbtemplates/locale/da/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: django-dbtemplates\n" 8 | "Report-Msgid-Bugs-To: https://github.com/jezdez/django-dbtemplates/issues\n" 9 | "POT-Creation-Date: 2011-08-15 13:13+0200\n" 10 | "PO-Revision-Date: 2011-08-15 11:14+0000\n" 11 | "Last-Translator: Jannis \n" 12 | "Language-Team: LANGUAGE \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Language: da\n" 17 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 18 | 19 | #: admin.py:56 20 | msgid "" 21 | "Leaving this empty causes Django to look for a template with the given name " 22 | "and populate this field with its content." 23 | msgstr "" 24 | "Hvis du efterlader dette felt tomt, så vil Django søge efter en template med" 25 | " det givne navn og udfylde dette felt med dets indhold." 26 | 27 | #: admin.py:82 28 | msgid "Advanced" 29 | msgstr "" 30 | 31 | #: admin.py:85 32 | msgid "Date/time" 33 | msgstr "Dato/tid" 34 | 35 | #: admin.py:102 36 | #, python-format 37 | msgid "Cache of one template successfully invalidated." 38 | msgid_plural "Cache of %(count)d templates successfully invalidated." 39 | msgstr[0] "" 40 | msgstr[1] "" 41 | 42 | #: admin.py:106 43 | msgid "Invalidate cache of selected templates" 44 | msgstr "" 45 | 46 | #: admin.py:114 47 | #, python-format 48 | msgid "Cache successfully repopulated with one template." 49 | msgid_plural "Cache successfully repopulated with %(count)d templates." 50 | msgstr[0] "" 51 | msgstr[1] "" 52 | 53 | #: admin.py:118 54 | msgid "Repopulate cache with selected templates" 55 | msgstr "" 56 | 57 | #: admin.py:130 58 | #, python-format 59 | msgid "Template syntax check FAILED for %(names)s." 60 | msgid_plural "" 61 | "Template syntax check FAILED for %(count)d templates: %(names)s." 62 | msgstr[0] "" 63 | msgstr[1] "" 64 | 65 | #: admin.py:138 66 | #, python-format 67 | msgid "Template syntax OK." 68 | msgid_plural "Template syntax OK for %(count)d templates." 69 | msgstr[0] "" 70 | msgstr[1] "" 71 | 72 | #: admin.py:141 73 | msgid "Check template syntax" 74 | msgstr "" 75 | 76 | #: admin.py:145 models.py:25 77 | msgid "sites" 78 | msgstr "websider" 79 | 80 | #: models.py:22 81 | msgid "name" 82 | msgstr "navn" 83 | 84 | #: models.py:23 85 | msgid "Example: 'flatpages/default.html'" 86 | msgstr "Eksempel: 'flatpages/default.html'" 87 | 88 | #: models.py:24 89 | msgid "content" 90 | msgstr "indhold" 91 | 92 | #: models.py:27 93 | msgid "creation date" 94 | msgstr "oprettelsesdato" 95 | 96 | #: models.py:29 97 | msgid "last changed" 98 | msgstr "sidst ændret" 99 | 100 | #: models.py:37 101 | msgid "template" 102 | msgstr "skabelon" 103 | 104 | #: models.py:38 105 | msgid "templates" 106 | msgstr "skabeloner" 107 | 108 | 109 | -------------------------------------------------------------------------------- /dbtemplates/locale/it/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: django-dbtemplates\n" 8 | "Report-Msgid-Bugs-To: https://github.com/jezdez/django-dbtemplates/issues\n" 9 | "POT-Creation-Date: 2011-08-15 13:13+0200\n" 10 | "PO-Revision-Date: 2011-08-15 11:14+0000\n" 11 | "Last-Translator: Jannis \n" 12 | "Language-Team: LANGUAGE \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Language: it\n" 17 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 18 | 19 | #: admin.py:56 20 | msgid "" 21 | "Leaving this empty causes Django to look for a template with the given name " 22 | "and populate this field with its content." 23 | msgstr "" 24 | "Lasciandolo vuoto, Django cercherà un template con lo stesso nome che avete " 25 | "indicato sopra e userà il suo contenuto per riempire questo campo." 26 | 27 | #: admin.py:82 28 | msgid "Advanced" 29 | msgstr "" 30 | 31 | #: admin.py:85 32 | msgid "Date/time" 33 | msgstr "" 34 | 35 | #: admin.py:102 36 | #, python-format 37 | msgid "Cache of one template successfully invalidated." 38 | msgid_plural "Cache of %(count)d templates successfully invalidated." 39 | msgstr[0] "" 40 | msgstr[1] "" 41 | 42 | #: admin.py:106 43 | msgid "Invalidate cache of selected templates" 44 | msgstr "" 45 | 46 | #: admin.py:114 47 | #, python-format 48 | msgid "Cache successfully repopulated with one template." 49 | msgid_plural "Cache successfully repopulated with %(count)d templates." 50 | msgstr[0] "" 51 | msgstr[1] "" 52 | 53 | #: admin.py:118 54 | msgid "Repopulate cache with selected templates" 55 | msgstr "" 56 | 57 | #: admin.py:130 58 | #, python-format 59 | msgid "Template syntax check FAILED for %(names)s." 60 | msgid_plural "" 61 | "Template syntax check FAILED for %(count)d templates: %(names)s." 62 | msgstr[0] "" 63 | msgstr[1] "" 64 | 65 | #: admin.py:138 66 | #, python-format 67 | msgid "Template syntax OK." 68 | msgid_plural "Template syntax OK for %(count)d templates." 69 | msgstr[0] "" 70 | msgstr[1] "" 71 | 72 | #: admin.py:141 73 | msgid "Check template syntax" 74 | msgstr "" 75 | 76 | #: admin.py:145 models.py:25 77 | msgid "sites" 78 | msgstr "" 79 | 80 | #: models.py:22 81 | msgid "name" 82 | msgstr "nome" 83 | 84 | #: models.py:23 85 | msgid "Example: 'flatpages/default.html'" 86 | msgstr "Esempio: 'flatpages/default.html'" 87 | 88 | #: models.py:24 89 | msgid "content" 90 | msgstr "contenuto" 91 | 92 | #: models.py:27 93 | msgid "creation date" 94 | msgstr "data di creazione" 95 | 96 | #: models.py:29 97 | msgid "last changed" 98 | msgstr "ultimo cambiamento" 99 | 100 | #: models.py:37 101 | msgid "template" 102 | msgstr "template" 103 | 104 | #: models.py:38 105 | msgid "templates" 106 | msgstr "template" 107 | 108 | 109 | -------------------------------------------------------------------------------- /dbtemplates/locale/pt_BR/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Herson Hersonls , 2011. 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-dbtemplates\n" 9 | "Report-Msgid-Bugs-To: https://github.com/jezdez/django-dbtemplates/issues\n" 10 | "POT-Creation-Date: 2011-08-15 13:13+0200\n" 11 | "PO-Revision-Date: 2011-08-15 11:14+0000\n" 12 | "Last-Translator: Jannis \n" 13 | "Language-Team: Portuguese (Brazilian) (http://www.transifex.net/projects/p/django-dbtemplates/team/pt_BR/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: pt_BR\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1)\n" 19 | 20 | #: admin.py:56 21 | msgid "" 22 | "Leaving this empty causes Django to look for a template with the given name " 23 | "and populate this field with its content." 24 | msgstr "" 25 | "Manter isto vazio faz com que o Django procure por um modelo (template) com " 26 | "o dado nome e preencha este campo com o seu conteúdo" 27 | 28 | #: admin.py:82 29 | msgid "Advanced" 30 | msgstr "Avançado" 31 | 32 | #: admin.py:85 33 | msgid "Date/time" 34 | msgstr "Data/hora" 35 | 36 | #: admin.py:102 37 | #, python-format 38 | msgid "Cache of one template successfully invalidated." 39 | msgid_plural "Cache of %(count)d templates successfully invalidated." 40 | msgstr[0] "" 41 | msgstr[1] "" 42 | 43 | #: admin.py:106 44 | msgid "Invalidate cache of selected templates" 45 | msgstr "" 46 | 47 | #: admin.py:114 48 | #, python-format 49 | msgid "Cache successfully repopulated with one template." 50 | msgid_plural "Cache successfully repopulated with %(count)d templates." 51 | msgstr[0] "" 52 | msgstr[1] "" 53 | 54 | #: admin.py:118 55 | msgid "Repopulate cache with selected templates" 56 | msgstr "" 57 | 58 | #: admin.py:130 59 | #, python-format 60 | msgid "Template syntax check FAILED for %(names)s." 61 | msgid_plural "" 62 | "Template syntax check FAILED for %(count)d templates: %(names)s." 63 | msgstr[0] "" 64 | msgstr[1] "" 65 | 66 | #: admin.py:138 67 | #, python-format 68 | msgid "Template syntax OK." 69 | msgid_plural "Template syntax OK for %(count)d templates." 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | #: admin.py:141 74 | msgid "Check template syntax" 75 | msgstr "" 76 | 77 | #: admin.py:145 models.py:25 78 | msgid "sites" 79 | msgstr "sites" 80 | 81 | #: models.py:22 82 | msgid "name" 83 | msgstr "Name" 84 | 85 | #: models.py:23 86 | msgid "Example: 'flatpages/default.html'" 87 | msgstr "Exemplo: 'flatpages/default.html'" 88 | 89 | #: models.py:24 90 | msgid "content" 91 | msgstr "conteúdo" 92 | 93 | #: models.py:27 94 | msgid "creation date" 95 | msgstr "Data de criação" 96 | 97 | #: models.py:29 98 | msgid "last changed" 99 | msgstr "ultima modificação" 100 | 101 | #: models.py:37 102 | msgid "template" 103 | msgstr "modelo" 104 | 105 | #: models.py:38 106 | msgid "templates" 107 | msgstr "modelos" 108 | 109 | 110 | -------------------------------------------------------------------------------- /dbtemplates/models.py: -------------------------------------------------------------------------------- 1 | from dbtemplates.conf import settings 2 | from dbtemplates.utils.cache import ( 3 | add_template_to_cache, 4 | remove_cached_template, 5 | ) 6 | from dbtemplates.utils.template import get_template_source 7 | 8 | from django.contrib.sites.managers import CurrentSiteManager 9 | from django.contrib.sites.models import Site 10 | from django.db import models 11 | from django.db.models import signals 12 | from django.template import TemplateDoesNotExist 13 | from django.utils.translation import gettext_lazy as _ 14 | 15 | 16 | class Template(models.Model): 17 | """ 18 | Defines a template model for use with the database template loader. 19 | The field ``name`` is the equivalent to the filename of a static template. 20 | """ 21 | id = models.AutoField(primary_key=True, verbose_name=_('ID'), 22 | serialize=False, auto_created=True) 23 | name = models.CharField(_('name'), max_length=100, 24 | help_text=_("Example: 'flatpages/default.html'")) 25 | content = models.TextField(_('content'), blank=True) 26 | sites = models.ManyToManyField(Site, verbose_name=_('sites'), 27 | blank=True) 28 | creation_date = models.DateTimeField(_('creation date'), auto_now_add=True) 29 | last_changed = models.DateTimeField(_('last changed'), auto_now=True) 30 | 31 | objects = models.Manager() 32 | on_site = CurrentSiteManager('sites') 33 | 34 | class Meta: 35 | db_table = 'django_template' 36 | verbose_name = _('template') 37 | verbose_name_plural = _('templates') 38 | ordering = ('name',) 39 | 40 | def __str__(self): 41 | return self.name 42 | 43 | def populate(self, name=None): 44 | """ 45 | Tries to find a template with the same name and populates 46 | the content field if found. 47 | """ 48 | if name is None: 49 | name = self.name 50 | try: 51 | source = get_template_source(name) 52 | if source: 53 | self.content = source 54 | except TemplateDoesNotExist: 55 | pass 56 | 57 | def save(self, *args, **kwargs): 58 | # If content is empty look for a template with the given name and 59 | # populate the template instance with its content. 60 | if settings.DBTEMPLATES_AUTO_POPULATE_CONTENT and not self.content: 61 | self.populate() 62 | super().save(*args, **kwargs) 63 | 64 | 65 | def add_default_site(instance, **kwargs): 66 | """ 67 | Called via Django's signals to cache the templates, if the template 68 | in the database was added or changed, only if 69 | DBTEMPLATES_ADD_DEFAULT_SITE setting is set. 70 | """ 71 | if not settings.DBTEMPLATES_ADD_DEFAULT_SITE: 72 | return 73 | current_site = Site.objects.get_current() 74 | if current_site not in instance.sites.all(): 75 | instance.sites.add(current_site) 76 | 77 | 78 | signals.post_save.connect(add_default_site, sender=Template) 79 | signals.post_save.connect(add_template_to_cache, sender=Template) 80 | signals.pre_delete.connect(remove_cached_template, sender=Template) 81 | -------------------------------------------------------------------------------- /dbtemplates/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: django-dbtemplates\n" 8 | "Report-Msgid-Bugs-To: https://github.com/jezdez/django-dbtemplates/issues\n" 9 | "POT-Creation-Date: 2011-08-15 13:13+0200\n" 10 | "PO-Revision-Date: 2011-08-15 11:14+0000\n" 11 | "Last-Translator: Jannis \n" 12 | "Language-Team: LANGUAGE \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Language: fr\n" 17 | "Plural-Forms: nplurals=2; plural=(n > 1)\n" 18 | 19 | #: admin.py:56 20 | msgid "" 21 | "Leaving this empty causes Django to look for a template with the given name " 22 | "and populate this field with its content." 23 | msgstr "" 24 | "Si vous laissez ceci vide , Django recherchera un modèle avec le nom donné " 25 | "et remplira ce champ avec son contenu." 26 | 27 | #: admin.py:82 28 | msgid "Advanced" 29 | msgstr "" 30 | 31 | #: admin.py:85 32 | msgid "Date/time" 33 | msgstr "Date/heure" 34 | 35 | #: admin.py:102 36 | #, fuzzy, python-format 37 | msgid "Cache of one template successfully invalidated." 38 | msgid_plural "Cache of %(count)d templates successfully invalidated." 39 | msgstr[0] "Le cache d'un modèle a été invalidé avec succès." 40 | msgstr[1] "" 41 | 42 | #: admin.py:106 43 | msgid "Invalidate cache of selected templates" 44 | msgstr "Invalidation du cache des modèles sélectionnés" 45 | 46 | #: admin.py:114 47 | #, fuzzy, python-format 48 | msgid "Cache successfully repopulated with one template." 49 | msgid_plural "Cache successfully repopulated with %(count)d templates." 50 | msgstr[0] "Le cache d'un modèle a été rechargé avec succès." 51 | msgstr[1] "" 52 | 53 | #: admin.py:118 54 | msgid "Repopulate cache with selected templates" 55 | msgstr "Rechargement du cache des modèles sélectionnés" 56 | 57 | #: admin.py:130 58 | #, python-format 59 | msgid "Template syntax check FAILED for %(names)s." 60 | msgid_plural "" 61 | "Template syntax check FAILED for %(count)d templates: %(names)s." 62 | msgstr[0] "" 63 | msgstr[1] "" 64 | 65 | #: admin.py:138 66 | #, python-format 67 | msgid "Template syntax OK." 68 | msgid_plural "Template syntax OK for %(count)d templates." 69 | msgstr[0] "" 70 | msgstr[1] "" 71 | 72 | #: admin.py:141 73 | msgid "Check template syntax" 74 | msgstr "" 75 | 76 | #: admin.py:145 models.py:25 77 | msgid "sites" 78 | msgstr "sites" 79 | 80 | #: models.py:22 81 | msgid "name" 82 | msgstr "nom" 83 | 84 | #: models.py:23 85 | msgid "Example: 'flatpages/default.html'" 86 | msgstr "Exemple : 'flatpages/default.html'" 87 | 88 | #: models.py:24 89 | msgid "content" 90 | msgstr "contenu" 91 | 92 | #: models.py:27 93 | msgid "creation date" 94 | msgstr "date de création" 95 | 96 | #: models.py:29 97 | msgid "last changed" 98 | msgstr "dernier changement" 99 | 100 | #: models.py:37 101 | msgid "template" 102 | msgstr "modèle" 103 | 104 | #: models.py:38 105 | msgid "templates" 106 | msgstr "modèles" 107 | 108 | 109 | -------------------------------------------------------------------------------- /dbtemplates/locale/fi/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Ville Säävuori , 2011. 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-dbtemplates\n" 9 | "Report-Msgid-Bugs-To: https://github.com/jezdez/django-dbtemplates/issues\n" 10 | "POT-Creation-Date: 2011-08-15 13:13+0200\n" 11 | "PO-Revision-Date: 2011-08-15 11:14+0000\n" 12 | "Last-Translator: Jannis \n" 13 | "Language-Team: LANGUAGE \n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: fi\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 19 | 20 | #: admin.py:56 21 | msgid "" 22 | "Leaving this empty causes Django to look for a template with the given name " 23 | "and populate this field with its content." 24 | msgstr "" 25 | "Jos tämä jätetään tyhjäksi, Django etsiin annetulla nimellä olevan " 26 | "mallipohjan ja täyttää tähän kenttään sen sisällön." 27 | 28 | #: admin.py:82 29 | msgid "Advanced" 30 | msgstr "Lisäasetukset" 31 | 32 | #: admin.py:85 33 | msgid "Date/time" 34 | msgstr "Päiväys/aika" 35 | 36 | #: admin.py:102 37 | #, python-format 38 | msgid "Cache of one template successfully invalidated." 39 | msgid_plural "Cache of %(count)d templates successfully invalidated." 40 | msgstr[0] "Yhden mallipohjan välimuisti on onnistuneesti tyhjennetty." 41 | msgstr[1] "%(count)d mallipohjan välimusti on onnistuneesti tyhjennetty." 42 | 43 | #: admin.py:106 44 | msgid "Invalidate cache of selected templates" 45 | msgstr "Tyhjennä valittujen mallipohjien välimuisti." 46 | 47 | #: admin.py:114 48 | #, python-format 49 | msgid "Cache successfully repopulated with one template." 50 | msgid_plural "Cache successfully repopulated with %(count)d templates." 51 | msgstr[0] "Yhden mallipohjan välimuisti on täytetty onnistuneesti." 52 | msgstr[1] "%(count)d mallipohjan välimuisti on täytetty onnistuneesti." 53 | 54 | #: admin.py:118 55 | msgid "Repopulate cache with selected templates" 56 | msgstr "Täytä valittujen mallipohjien välimuisti." 57 | 58 | #: admin.py:130 59 | #, python-format 60 | msgid "Template syntax check FAILED for %(names)s." 61 | msgid_plural "" 62 | "Template syntax check FAILED for %(count)d templates: %(names)s." 63 | msgstr[0] "" 64 | msgstr[1] "" 65 | 66 | #: admin.py:138 67 | #, python-format 68 | msgid "Template syntax OK." 69 | msgid_plural "Template syntax OK for %(count)d templates." 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | #: admin.py:141 74 | msgid "Check template syntax" 75 | msgstr "" 76 | 77 | #: admin.py:145 models.py:25 78 | msgid "sites" 79 | msgstr "sivustot" 80 | 81 | #: models.py:22 82 | msgid "name" 83 | msgstr "nimi" 84 | 85 | #: models.py:23 86 | msgid "Example: 'flatpages/default.html'" 87 | msgstr "Esimerkiksi: 'flatpages/default.html'" 88 | 89 | #: models.py:24 90 | msgid "content" 91 | msgstr "sisätö" 92 | 93 | #: models.py:27 94 | msgid "creation date" 95 | msgstr "luontipäivä" 96 | 97 | #: models.py:29 98 | msgid "last changed" 99 | msgstr "viimeksi muutettu" 100 | 101 | #: models.py:37 102 | msgid "template" 103 | msgstr "mallipohja" 104 | 105 | #: models.py:38 106 | msgid "templates" 107 | msgstr "mallipohjat" 108 | 109 | 110 | -------------------------------------------------------------------------------- /dbtemplates/locale/ru/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2013-07-30 14:03+0600\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 20 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 21 | 22 | #: admin.py:57 23 | msgid "" 24 | "Leaving this empty causes Django to look for a template with the given name " 25 | "and populate this field with its content." 26 | msgstr "Если вы оставите это поле незаполненным, Django будет искать шаблон с введённым именем и заполнит поле его содержимым." 27 | 28 | #: admin.py:92 29 | msgid "Advanced" 30 | msgstr "Дополнительно" 31 | 32 | #: admin.py:95 33 | msgid "Date/time" 34 | msgstr "Дата/время" 35 | 36 | #: admin.py:112 37 | #, python-format 38 | msgid "Cache of one template successfully invalidated." 39 | msgid_plural "Cache of %(count)d templates successfully invalidated." 40 | msgstr[0] "Кэш для шаблона успешно очищен." 41 | msgstr[1] "Кэш для шаблонов (%(count)d шт.) успешно очищен." 42 | 43 | #: admin.py:116 44 | msgid "Invalidate cache of selected templates" 45 | msgstr "Очистить кэш для выделенных шаблонов" 46 | 47 | #: admin.py:124 48 | #, python-format 49 | msgid "Cache successfully repopulated with one template." 50 | msgid_plural "Cache successfully repopulated with %(count)d templates." 51 | msgstr[0] "Кэш для шаблона успешно заполнен." 52 | msgstr[1] "Кэш для шаблонов (%(count)d шт.) успешно заполнен." 53 | 54 | #: admin.py:128 55 | msgid "Repopulate cache with selected templates" 56 | msgstr "Заполнить кэш для выделенных шаблонов" 57 | 58 | #: admin.py:140 59 | #, python-format 60 | msgid "Template syntax check FAILED for %(names)s." 61 | msgid_plural "Template syntax check FAILED for %(count)d templates: %(names)s." 62 | msgstr[0] "Неверный синтаксис у шаблона %(names)s." 63 | msgstr[1] "Неверный синтаксис у следующих шаблонов: %(names)s." 64 | 65 | #: admin.py:148 66 | #, python-format 67 | msgid "Template syntax OK." 68 | msgid_plural "Template syntax OK for %(count)d templates." 69 | msgstr[0] "Синтаксис шаблона корректен." 70 | msgstr[1] "Синтаксис шаблонов корректен." 71 | 72 | #: admin.py:151 73 | msgid "Check template syntax" 74 | msgstr "Проверить синтаксис шаблона" 75 | 76 | #: admin.py:155 models.py:29 77 | msgid "sites" 78 | msgstr "сайты" 79 | 80 | #: models.py:26 81 | msgid "name" 82 | msgstr "название" 83 | 84 | #: models.py:27 85 | msgid "Example: 'flatpages/default.html'" 86 | msgstr "Например: 'flatpages/default.html'" 87 | 88 | #: models.py:28 89 | msgid "content" 90 | msgstr "содержимое" 91 | 92 | #: models.py:31 93 | msgid "creation date" 94 | msgstr "дата создания" 95 | 96 | #: models.py:33 97 | msgid "last changed" 98 | msgstr "последнее изменение" 99 | 100 | #: models.py:41 101 | msgid "template" 102 | msgstr "шаблон" 103 | 104 | #: models.py:42 105 | msgid "templates" 106 | msgstr "шаблоны" 107 | -------------------------------------------------------------------------------- /dbtemplates/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Jannis Leidel , 2011. 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-dbtemplates\n" 9 | "Report-Msgid-Bugs-To: https://github.com/jezdez/django-dbtemplates/issues\n" 10 | "POT-Creation-Date: 2011-08-15 13:13+0200\n" 11 | "PO-Revision-Date: 2011-08-15 11:14+0000\n" 12 | "Last-Translator: Jannis \n" 13 | "Language-Team: LANGUAGE \n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: de\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 19 | 20 | #: admin.py:56 21 | msgid "" 22 | "Leaving this empty causes Django to look for a template with the given name " 23 | "and populate this field with its content." 24 | msgstr "" 25 | "Wenn Sie dieses Feld leer lassen, wird Django versuchen, das Template mit " 26 | "dem angegebenen Namen zu finden und mit dessen Inhalt das Feld zu füllen." 27 | 28 | #: admin.py:82 29 | msgid "Advanced" 30 | msgstr "Erweiterte Einstellungen" 31 | 32 | #: admin.py:85 33 | msgid "Date/time" 34 | msgstr "Datum/Uhrzeit" 35 | 36 | #: admin.py:102 37 | #, python-format 38 | msgid "Cache of one template successfully invalidated." 39 | msgid_plural "Cache of %(count)d templates successfully invalidated." 40 | msgstr[0] "Der Cache eines Templates wurde erfolgreich geleert." 41 | msgstr[1] "Der Cache von %(count)d Templates wurde erfolgreich geleert." 42 | 43 | #: admin.py:106 44 | msgid "Invalidate cache of selected templates" 45 | msgstr "Cache der ausgewählten Templates leeren" 46 | 47 | #: admin.py:114 48 | #, python-format 49 | msgid "Cache successfully repopulated with one template." 50 | msgid_plural "Cache successfully repopulated with %(count)d templates." 51 | msgstr[0] "Der Cache eines Templates wurde erfolgreich geleert und neu gefüllt." 52 | msgstr[1] "" 53 | "Der Cache von %(count)d Templates wurde erfolgreich geleert und neu gefüllt." 54 | 55 | #: admin.py:118 56 | msgid "Repopulate cache with selected templates" 57 | msgstr "Cache der ausgewählten Templates neu füllen" 58 | 59 | #: admin.py:130 60 | #, python-format 61 | msgid "Template syntax check FAILED for %(names)s." 62 | msgid_plural "" 63 | "Template syntax check FAILED for %(count)d templates: %(names)s." 64 | msgstr[0] "Template-Syntax von %(names)s ist FEHLERHAFT." 65 | msgstr[1] "Template-Syntax von %(count)d Templates (%(names)s) ist FEHLERHAFT." 66 | 67 | #: admin.py:138 68 | #, python-format 69 | msgid "Template syntax OK." 70 | msgid_plural "Template syntax OK for %(count)d templates." 71 | msgstr[0] "Template-Syntax ist OK." 72 | msgstr[1] "Template-Syntax von %(count)d Templates ist OK." 73 | 74 | #: admin.py:141 75 | msgid "Check template syntax" 76 | msgstr "Template-Syntax überprüfen" 77 | 78 | #: admin.py:145 models.py:25 79 | msgid "sites" 80 | msgstr "Seiten" 81 | 82 | #: models.py:22 83 | msgid "name" 84 | msgstr "Name" 85 | 86 | #: models.py:23 87 | msgid "Example: 'flatpages/default.html'" 88 | msgstr "Zum Beispiel: 'flatpages/default.html'" 89 | 90 | #: models.py:24 91 | msgid "content" 92 | msgstr "Inhalt" 93 | 94 | #: models.py:27 95 | msgid "creation date" 96 | msgstr "Erstellt" 97 | 98 | #: models.py:29 99 | msgid "last changed" 100 | msgstr "Geändert" 101 | 102 | #: models.py:37 103 | msgid "template" 104 | msgstr "Template" 105 | 106 | #: models.py:38 107 | msgid "templates" 108 | msgstr "Templates" 109 | 110 | 111 | -------------------------------------------------------------------------------- /docs/overview.txt: -------------------------------------------------------------------------------- 1 | Setup 2 | ===== 3 | 4 | 1. Get the source from the `Git repository`_ or install it from the 5 | Python Package Index by running ``pip install django-dbtemplates``. 6 | 2. Edit the settings.py of your Django site: 7 | 8 | * Add ``dbtemplates`` to the ``INSTALLED_APPS`` setting 9 | 10 | Check if ``django.contrib.sites`` and ``django.contrib.admin`` are in 11 | ``INSTALLED_APPS`` and add if necessary. 12 | 13 | It should look something like this:: 14 | 15 | INSTALLED_APPS = ( 16 | 'django.contrib.auth', 17 | 'django.contrib.contenttypes', 18 | 'django.contrib.sessions', 19 | 'django.contrib.sites', 20 | 'django.contrib.admin', 21 | 'django.contrib.flatpages', 22 | # .. 23 | 'dbtemplates', 24 | ) 25 | 26 | * Add ``dbtemplates.loader.Loader`` to the ``TEMPLATES.OPTIONS.loaders`` list 27 | in the settings.py of your Django project. 28 | 29 | It should look something like this:: 30 | 31 | TEMPLATES = [ 32 | { 33 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 34 | 'DIRS': [ # your template dirs here 35 | ], 36 | 'APP_DIRS': False, 37 | 'OPTIONS': { 38 | 'context_processors': [ 39 | 'django.contrib.auth.context_processors.auth', 40 | 'django.template.context_processors.debug', 41 | 'django.template.context_processors.i18n', 42 | 'django.template.context_processors.media', 43 | 'django.template.context_processors.static', 44 | 'django.template.context_processors.tz', 45 | 'django.contrib.messages.context_processors.messages', 46 | 'django.template.context_processors.request', 47 | ], 48 | 'loaders': [ 49 | 'django.template.loaders.filesystem.Loader', 50 | 'django.template.loaders.app_directories.Loader', 51 | 'dbtemplates.loader.Loader', 52 | ], 53 | }, 54 | }, 55 | ] 56 | 57 | The order of ``TEMPLATES.OPTIONS.loaders`` is important. In the former 58 | example, templates from the database will be used as a fallback (ie. when 59 | the template does not exists in other locations). If you want the template 60 | from the database to be used to override templates in other locations, 61 | put ``dbtemplates.loader.Loader`` at the beginning of ``loaders``. 62 | 63 | 3. Sync your database ``python manage.py migrate`` 64 | 4. Restart your Django server 65 | 66 | .. _Git repository: https://github.com/jazzband/django-dbtemplates/ 67 | 68 | Usage 69 | ===== 70 | 71 | Creating database templates is pretty simple: Just open the admin interface 72 | of your Django-based site in your browser and click on "Templates" in the 73 | "Database templates" section. 74 | 75 | There you only need to fill in the ``name`` field with the identifier, Django 76 | is supposed to use while searching for templates, e.g. 77 | ``blog/entry_list.html``. The ``content`` field should be filled with the 78 | content of your template. 79 | 80 | Optionally, by leaving the ``content`` field empty you are able to tell 81 | ``dbtemplates`` to look for a template with the ``name`` by using Django's 82 | other template loaders. For example, if you have a template called 83 | ``blog/entry_list.html`` on your file system and want to save the templates 84 | contents in the database, you just need to leave the content field empty to 85 | automatically populate it. That's especially useful if you don't want to 86 | copy and paste its content manually to the textarea. 87 | -------------------------------------------------------------------------------- /dbtemplates/loader.py: -------------------------------------------------------------------------------- 1 | from django.contrib.sites.models import Site 2 | from django.db import router 3 | from django.template import Origin, TemplateDoesNotExist 4 | from django.template.loaders.base import Loader as BaseLoader 5 | 6 | from dbtemplates.models import Template 7 | from dbtemplates.utils.cache import (cache, get_cache_key, 8 | set_and_return, get_cache_notfound_key) 9 | 10 | 11 | class Loader(BaseLoader): 12 | """ 13 | A custom template loader to load templates from the database. 14 | 15 | Tries to load the template from the dbtemplates cache backend specified 16 | by the DBTEMPLATES_CACHE_BACKEND setting. If it does not find a template 17 | it falls back to query the database field ``name`` with the template path 18 | and ``sites`` with the current site. 19 | """ 20 | is_usable = True 21 | 22 | def get_template_sources(self, template_name, template_dirs=None): 23 | yield Origin( 24 | name=template_name, 25 | template_name=template_name, 26 | loader=self, 27 | ) 28 | 29 | def get_contents(self, origin): 30 | content, _ = self._load_template_source(origin.template_name) 31 | return content 32 | 33 | def _load_and_store_template(self, template_name, cache_key, site, 34 | **params): 35 | template = Template.objects.get(name__exact=template_name, **params) 36 | db = router.db_for_read(Template, instance=template) 37 | display_name = f'dbtemplates:{db}:{template_name}:{site.domain}' 38 | return set_and_return(cache_key, template.content, display_name) 39 | 40 | def _load_template_source(self, template_name, template_dirs=None): 41 | # The logic should work like this: 42 | # * Try to find the template in the cache. If found, return it. 43 | # * Now check the cache if a lookup for the given template 44 | # has failed lately and hand over control to the next template 45 | # loader waiting in line. 46 | # * If this still did not fail we first try to find a site-specific 47 | # template in the database. 48 | # * On a failure from our last attempt we try to load the global 49 | # template from the database. 50 | # * If all of the above steps have failed we generate a new key 51 | # in the cache indicating that queries failed, with the current 52 | # timestamp. 53 | site = Site.objects.get_current() 54 | cache_key = get_cache_key(template_name) 55 | if cache: 56 | try: 57 | backend_template = cache.get(cache_key) 58 | if backend_template: 59 | return backend_template, template_name 60 | except Exception: 61 | pass 62 | 63 | # Not found in cache, move on. 64 | cache_notfound_key = get_cache_notfound_key(template_name) 65 | if cache: 66 | try: 67 | notfound = cache.get(cache_notfound_key) 68 | if notfound: 69 | raise TemplateDoesNotExist(template_name) 70 | except Exception: 71 | raise TemplateDoesNotExist(template_name) 72 | 73 | # Not marked as not-found, move on... 74 | 75 | try: 76 | return self._load_and_store_template(template_name, cache_key, 77 | site, sites__in=[site.id]) 78 | except (Template.MultipleObjectsReturned, Template.DoesNotExist): 79 | try: 80 | return self._load_and_store_template(template_name, cache_key, 81 | site, sites__isnull=True) 82 | except (Template.MultipleObjectsReturned, Template.DoesNotExist): 83 | pass 84 | 85 | # Mark as not-found in cache. 86 | cache.set(cache_notfound_key, '1') 87 | raise TemplateDoesNotExist(template_name) 88 | -------------------------------------------------------------------------------- /dbtemplates/static/dbtemplates/js/util.js: -------------------------------------------------------------------------------- 1 | /* A few useful utility functions. */ 2 | 3 | // Capture a method on an object. 4 | function method(obj, name) { 5 | return function() {obj[name].apply(obj, arguments);}; 6 | } 7 | 8 | // The value used to signal the end of a sequence in iterators. 9 | var StopIteration = {toString: function() {return "StopIteration"}}; 10 | 11 | // Apply a function to each element in a sequence. 12 | function forEach(iter, f) { 13 | if (iter.next) { 14 | try {while (true) f(iter.next());} 15 | catch (e) {if (e != StopIteration) throw e;} 16 | } 17 | else { 18 | for (var i = 0; i < iter.length; i++) 19 | f(iter[i]); 20 | } 21 | } 22 | 23 | // Map a function over a sequence, producing an array of results. 24 | function map(iter, f) { 25 | var accum = []; 26 | forEach(iter, function(val) {accum.push(f(val));}); 27 | return accum; 28 | } 29 | 30 | // Create a predicate function that tests a string againsts a given 31 | // regular expression. No longer used but might be used by 3rd party 32 | // parsers. 33 | function matcher(regexp){ 34 | return function(value){return regexp.test(value);}; 35 | } 36 | 37 | // Test whether a DOM node has a certain CSS class. Much faster than 38 | // the MochiKit equivalent, for some reason. 39 | function hasClass(element, className){ 40 | var classes = element.className; 41 | return classes && new RegExp("(^| )" + className + "($| )").test(classes); 42 | } 43 | 44 | // Insert a DOM node after another node. 45 | function insertAfter(newNode, oldNode) { 46 | var parent = oldNode.parentNode; 47 | parent.insertBefore(newNode, oldNode.nextSibling); 48 | return newNode; 49 | } 50 | 51 | function removeElement(node) { 52 | if (node.parentNode) 53 | node.parentNode.removeChild(node); 54 | } 55 | 56 | function clearElement(node) { 57 | while (node.firstChild) 58 | node.removeChild(node.firstChild); 59 | } 60 | 61 | // Check whether a node is contained in another one. 62 | function isAncestor(node, child) { 63 | while (child = child.parentNode) { 64 | if (node == child) 65 | return true; 66 | } 67 | return false; 68 | } 69 | 70 | // The non-breaking space character. 71 | var nbsp = "\u00a0"; 72 | var matching = {"{": "}", "[": "]", "(": ")", 73 | "}": "{", "]": "[", ")": "("}; 74 | 75 | // Standardize a few unportable event properties. 76 | function normalizeEvent(event) { 77 | if (!event.stopPropagation) { 78 | event.stopPropagation = function() {this.cancelBubble = true;}; 79 | event.preventDefault = function() {this.returnValue = false;}; 80 | } 81 | if (!event.stop) { 82 | event.stop = function() { 83 | this.stopPropagation(); 84 | this.preventDefault(); 85 | }; 86 | } 87 | 88 | if (event.type == "keypress") { 89 | event.code = (event.charCode == null) ? event.keyCode : event.charCode; 90 | event.character = String.fromCharCode(event.code); 91 | } 92 | return event; 93 | } 94 | 95 | // Portably register event handlers. 96 | function addEventHandler(node, type, handler, removeFunc) { 97 | function wrapHandler(event) { 98 | handler(normalizeEvent(event || window.event)); 99 | } 100 | if (typeof node.addEventListener == "function") { 101 | node.addEventListener(type, wrapHandler, false); 102 | if (removeFunc) return function() {node.removeEventListener(type, wrapHandler, false);}; 103 | } 104 | else { 105 | node.attachEvent("on" + type, wrapHandler); 106 | if (removeFunc) return function() {node.detachEvent("on" + type, wrapHandler);}; 107 | } 108 | } 109 | 110 | function nodeText(node) { 111 | return node.textContent || node.innerText || node.nodeValue || ""; 112 | } 113 | 114 | function nodeTop(node) { 115 | var top = 0; 116 | while (node.offsetParent) { 117 | top += node.offsetTop; 118 | node = node.offsetParent; 119 | } 120 | return top; 121 | } 122 | 123 | function isBR(node) { 124 | var nn = node.nodeName; 125 | return nn == "BR" || nn == "br"; 126 | } 127 | function isSpan(node) { 128 | var nn = node.nodeName; 129 | return nn == "SPAN" || nn == "span"; 130 | } 131 | -------------------------------------------------------------------------------- /docs/advanced.txt: -------------------------------------------------------------------------------- 1 | ================= 2 | Advanced features 3 | ================= 4 | 5 | .. _caching: 6 | 7 | Caching 8 | ======= 9 | 10 | ``dbtemplates`` uses Django's default caching infrastructure for caching, and 11 | operates automatically when creating, updating or deleting templates in 12 | the database. 13 | 14 | To enable one of them you need to specify a setting called 15 | ``DBTEMPLATES_CACHE_BACKEND`` to one of the valid values Django's 16 | ``CACHE_BACKEND`` can be set to. E.g.:: 17 | 18 | DBTEMPLATES_CACHE_BACKEND = 'memcached://127.0.0.1:11211/' 19 | 20 | .. note:: 21 | Starting in version 1.0 ``dbtemplates`` allows you also to set the new 22 | dict-based ``CACHES`` setting, which was introduced in Django 1.3. 23 | 24 | All you have to do is to provide a new entry in the ``CACHES`` dict 25 | named ``'dbtemplates'``, e.g.:: 26 | 27 | CACHES = { 28 | 'dbtemplates': { 29 | 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 30 | 'LOCATION': '127.0.0.1:11211', 31 | } 32 | } 33 | 34 | Please see the `cache documentation`_ if you want to know more about it. 35 | 36 | .. _cache documentation: http://docs.djangoproject.com/en/dev/topics/cache/#setting-up-the-cache 37 | 38 | .. _versioned: 39 | 40 | Versioned storage 41 | ================= 42 | 43 | ``dbtemplates`` comes prepared to use the third party Django app 44 | `django-reversion`_, that once installed besides ``dbtemplates`` allows you 45 | to jump back to old versions of your templates. It automatically saves every 46 | state when you save the template in your database and provides an easy to use 47 | interface. 48 | 49 | Please refer to `django-reversion's documentation`_ for more information 50 | about how it works. 51 | 52 | .. hint:: 53 | Just visit the "History" section of each template instance and browse its history. 54 | 55 | Short installation howto 56 | ------------------------ 57 | 58 | 1. Get the source from the `django-reversion`_ project site and put it 59 | somewhere on your `PYTHONPATH`. 60 | 2. Add ``reversion`` to the ``INSTALLED_APPS`` setting of your Django project 61 | 3. Sync your database with ``python manage.py syncdb`` 62 | 4. Set ``DBTEMPLATES_USE_REVERSION`` setting to ``True`` 63 | 64 | History compare view 65 | -------------------- 66 | 67 | You can also use ``dbtemplates`` together with `django-reversion-compare`_ which 68 | provides a history compare view to compare two versions of a model which is under 69 | reversion. 70 | 71 | .. _django-reversion: https://github.com/etianen/django-reversion 72 | .. _django-reversion's documentation: https://django-reversion.readthedocs.io/en/latest/ 73 | .. _django-reversion-compare: https://github.com/jedie/django-reversion-compare 74 | 75 | 76 | .. _commands: 77 | 78 | Management commands 79 | =================== 80 | 81 | ``dbtemplates`` comes with two `Django management commands`_ to be used with 82 | ``django-admin.py`` or ``manage.py``: 83 | 84 | * ``sync_templates`` 85 | 86 | Enables you to sync your already existing file systems templates with the 87 | database. It will guide you through the whole process. 88 | 89 | * ``create_error_templates`` 90 | 91 | Tries to add the two templates ``404.html`` and ``500.html`` that are used 92 | by Django when a error occurs. 93 | 94 | * ``check_template_syntax`` 95 | 96 | .. versionadded:: 1.2 97 | 98 | Checks the saved templates whether they are valid Django templates. 99 | 100 | .. _Django management commands: http://docs.djangoproject.com/en/dev/ref/django-admin/ 101 | 102 | .. _admin_actions: 103 | 104 | Admin actions 105 | ============= 106 | 107 | ``dbtemplates`` provides two `admin actions`_ to be used with Django>=1.1. 108 | 109 | * ``invalidate_cache`` 110 | 111 | Invalidates the cache of the selected templates by calling the appropriate 112 | cache backend methods. 113 | 114 | * ``repopulate_cache`` 115 | 116 | Repopulates the cache with selected templates by invalidating it first and 117 | filling then after that. 118 | 119 | * ``check_syntax`` 120 | 121 | .. versionadded:: 1.2 122 | 123 | Checks the selected tempaltes for syntax errors. 124 | 125 | .. _admin actions: http://docs.djangoproject.com/en/dev/ref/contrib/admin/actions/ 126 | -------------------------------------------------------------------------------- /dbtemplates/static/dbtemplates/js/stringstream.js: -------------------------------------------------------------------------------- 1 | /* String streams are the things fed to parsers (which can feed them 2 | * to a tokenizer if they want). They provide peek and next methods 3 | * for looking at the current character (next 'consumes' this 4 | * character, peek does not), and a get method for retrieving all the 5 | * text that was consumed since the last time get was called. 6 | * 7 | * An easy mistake to make is to let a StopIteration exception finish 8 | * the token stream while there are still characters pending in the 9 | * string stream (hitting the end of the buffer while parsing a 10 | * token). To make it easier to detect such errors, the stringstreams 11 | * throw an exception when this happens. 12 | */ 13 | 14 | // Make a stringstream stream out of an iterator that returns strings. 15 | // This is applied to the result of traverseDOM (see codemirror.js), 16 | // and the resulting stream is fed to the parser. 17 | var stringStream = function(source){ 18 | // String that's currently being iterated over. 19 | var current = ""; 20 | // Position in that string. 21 | var pos = 0; 22 | // Accumulator for strings that have been iterated over but not 23 | // get()-ed yet. 24 | var accum = ""; 25 | // Make sure there are more characters ready, or throw 26 | // StopIteration. 27 | function ensureChars() { 28 | while (pos == current.length) { 29 | accum += current; 30 | current = ""; // In case source.next() throws 31 | pos = 0; 32 | try {current = source.next();} 33 | catch (e) { 34 | if (e != StopIteration) throw e; 35 | else return false; 36 | } 37 | } 38 | return true; 39 | } 40 | 41 | return { 42 | // Return the next character in the stream. 43 | peek: function() { 44 | if (!ensureChars()) return null; 45 | return current.charAt(pos); 46 | }, 47 | // Get the next character, throw StopIteration if at end, check 48 | // for unused content. 49 | next: function() { 50 | if (!ensureChars()) { 51 | if (accum.length > 0) 52 | throw "End of stringstream reached without emptying buffer ('" + accum + "')."; 53 | else 54 | throw StopIteration; 55 | } 56 | return current.charAt(pos++); 57 | }, 58 | // Return the characters iterated over since the last call to 59 | // .get(). 60 | get: function() { 61 | var temp = accum; 62 | accum = ""; 63 | if (pos > 0){ 64 | temp += current.slice(0, pos); 65 | current = current.slice(pos); 66 | pos = 0; 67 | } 68 | return temp; 69 | }, 70 | // Push a string back into the stream. 71 | push: function(str) { 72 | current = current.slice(0, pos) + str + current.slice(pos); 73 | }, 74 | lookAhead: function(str, consume, skipSpaces, caseInsensitive) { 75 | function cased(str) {return caseInsensitive ? str.toLowerCase() : str;} 76 | str = cased(str); 77 | var found = false; 78 | 79 | var _accum = accum, _pos = pos; 80 | if (skipSpaces) this.nextWhileMatches(/[\s\u00a0]/); 81 | 82 | while (true) { 83 | var end = pos + str.length, left = current.length - pos; 84 | if (end <= current.length) { 85 | found = str == cased(current.slice(pos, end)); 86 | pos = end; 87 | break; 88 | } 89 | else if (str.slice(0, left) == cased(current.slice(pos))) { 90 | accum += current; current = ""; 91 | try {current = source.next();} 92 | catch (e) {break;} 93 | pos = 0; 94 | str = str.slice(left); 95 | } 96 | else { 97 | break; 98 | } 99 | } 100 | 101 | if (!(found && consume)) { 102 | current = accum.slice(_accum.length) + current; 103 | pos = _pos; 104 | accum = _accum; 105 | } 106 | 107 | return found; 108 | }, 109 | 110 | // Utils built on top of the above 111 | more: function() { 112 | return this.peek() !== null; 113 | }, 114 | applies: function(test) { 115 | var next = this.peek(); 116 | return (next !== null && test(next)); 117 | }, 118 | nextWhile: function(test) { 119 | var next; 120 | while ((next = this.peek()) !== null && test(next)) 121 | this.next(); 122 | }, 123 | matches: function(re) { 124 | var next = this.peek(); 125 | return (next !== null && re.test(next)); 126 | }, 127 | nextWhileMatches: function(re) { 128 | var next; 129 | while ((next = this.peek()) !== null && re.test(next)) 130 | this.next(); 131 | }, 132 | equals: function(ch) { 133 | return ch === this.peek(); 134 | }, 135 | endOfLine: function() { 136 | var next = this.peek(); 137 | return next == null || next == "\n"; 138 | } 139 | }; 140 | }; 141 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/asd.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/asd.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/asd" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/asd" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\asd.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\asd.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /dbtemplates/management/commands/sync_templates.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dbtemplates.models import Template 4 | from django.contrib.sites.models import Site 5 | from django.core.management.base import BaseCommand, CommandError 6 | from django.template.loader import _engine_list 7 | from django.template.utils import get_app_template_dirs 8 | 9 | ALWAYS_ASK, FILES_TO_DATABASE, DATABASE_TO_FILES = ("0", "1", "2") 10 | 11 | DIRS = [] 12 | for engine in _engine_list(): 13 | DIRS.extend(engine.dirs) 14 | DIRS = tuple(DIRS) 15 | app_template_dirs = get_app_template_dirs("templates") 16 | 17 | 18 | class Command(BaseCommand): 19 | help = "Syncs file system templates with the database bidirectionally." 20 | 21 | def add_arguments(self, parser): 22 | parser.add_argument( 23 | "-e", 24 | "--ext", 25 | dest="ext", 26 | action="store", 27 | default="html", 28 | help="extension of the files you want to " 29 | "sync with the database [default: %(default)s]", 30 | ) 31 | parser.add_argument( 32 | "-f", 33 | "--force", 34 | action="store_true", 35 | dest="force", 36 | default=False, 37 | help="overwrite existing database templates", 38 | ) 39 | parser.add_argument( 40 | "-o", 41 | "--overwrite", 42 | action="store", 43 | dest="overwrite", 44 | default="0", 45 | help="'0' - ask always, '1' - overwrite database " 46 | "templates from template files, '2' - overwrite " 47 | "template files from database templates", 48 | ) 49 | parser.add_argument( 50 | "-a", 51 | "--app-first", 52 | action="store_true", 53 | dest="app_first", 54 | default=False, 55 | help="look for templates in applications " 56 | "directories before project templates", 57 | ) 58 | parser.add_argument( 59 | "-d", 60 | "--delete", 61 | action="store_true", 62 | dest="delete", 63 | default=False, 64 | help="Delete templates after syncing", 65 | ) 66 | 67 | def handle(self, **options): 68 | extension = options.get("ext") 69 | force = options.get("force") 70 | overwrite = options.get("overwrite") 71 | app_first = options.get("app_first") 72 | delete = options.get("delete") 73 | 74 | if not extension.startswith("."): 75 | extension = f".{extension}" 76 | 77 | try: 78 | site = Site.objects.get_current() 79 | except Exception: 80 | raise CommandError( 81 | "Please make sure to have the sites contrib " 82 | "app installed and setup with a site object" 83 | ) 84 | 85 | if app_first: 86 | tpl_dirs = app_template_dirs + DIRS 87 | else: 88 | tpl_dirs = DIRS + app_template_dirs 89 | templatedirs = [str(d) for d in tpl_dirs if os.path.isdir(d)] 90 | 91 | for templatedir in templatedirs: 92 | for dirpath, subdirs, filenames in os.walk(templatedir): 93 | for f in [ 94 | f 95 | for f in filenames 96 | if f.endswith(extension) and not f.startswith(".") 97 | ]: 98 | path = os.path.join(dirpath, f) 99 | name = path.split(str(templatedir))[1] 100 | if name.startswith("/"): 101 | name = name[1:] 102 | try: 103 | t = Template.on_site.get(name__exact=name) 104 | except Template.DoesNotExist: 105 | if not force: 106 | confirm = input( 107 | "\nA '%s' template doesn't exist in the " 108 | "database.\nCreate it with '%s'?" 109 | " (y/[n]): " 110 | "" % (name, path) 111 | ) 112 | if force or confirm.lower().startswith("y"): 113 | with open(path, encoding="utf-8") as f: 114 | t = Template(name=name, content=f.read()) 115 | t.save() 116 | t.sites.add(site) 117 | else: 118 | while True: 119 | if overwrite == ALWAYS_ASK: 120 | _i = ( 121 | "\n%(template)s exists in the database.\n" 122 | "(1) Overwrite %(template)s with '%(path)s'\n" # noqa 123 | "(2) Overwrite '%(path)s' with %(template)s\n" # noqa 124 | "Type 1 or 2 or press to skip: " 125 | % {"template": t.__repr__(), "path": path} 126 | ) 127 | 128 | confirm = input(_i) 129 | else: 130 | confirm = overwrite 131 | if confirm in ( 132 | "", 133 | FILES_TO_DATABASE, 134 | DATABASE_TO_FILES, 135 | ): 136 | if confirm == FILES_TO_DATABASE: 137 | with open(path, encoding="utf-8") as f: 138 | t.content = f.read() 139 | t.save() 140 | t.sites.add(site) 141 | if delete: 142 | try: 143 | os.remove(path) 144 | except OSError: 145 | raise CommandError( 146 | f"Couldn't delete {path}" 147 | ) 148 | elif confirm == DATABASE_TO_FILES: 149 | with open(path, "w", encoding="utf-8") as f: # noqa 150 | f.write(t.content) 151 | if delete: 152 | t.delete() 153 | break 154 | -------------------------------------------------------------------------------- /dbtemplates/admin.py: -------------------------------------------------------------------------------- 1 | import posixpath 2 | from django import forms 3 | from django.contrib import admin 4 | from django.core.exceptions import ImproperlyConfigured 5 | from django.utils.translation import gettext_lazy as _ 6 | from django.utils.translation import ngettext 7 | from django.utils.safestring import mark_safe 8 | 9 | from dbtemplates.conf import settings 10 | from dbtemplates.models import Template, add_template_to_cache, remove_cached_template 11 | from dbtemplates.utils.template import check_template_syntax 12 | 13 | # Check if either django-reversion-compare or django-reversion is installed and 14 | # use reversion_compare's CompareVersionAdmin or reversion's VersionAdmin as 15 | # the base admin class if yes 16 | if settings.DBTEMPLATES_USE_REVERSION_COMPARE: 17 | from reversion_compare.admin import CompareVersionAdmin \ 18 | as TemplateModelAdmin 19 | elif settings.DBTEMPLATES_USE_REVERSION: 20 | from reversion.admin import VersionAdmin as TemplateModelAdmin 21 | else: 22 | from django.contrib.admin import ModelAdmin as TemplateModelAdmin # noqa 23 | 24 | 25 | class CodeMirrorTextArea(forms.Textarea): 26 | 27 | """ 28 | A custom widget for the CodeMirror browser editor to be used with the 29 | content field of the Template model. 30 | """ 31 | class Media: 32 | css = dict(screen=[posixpath.join( 33 | settings.DBTEMPLATES_MEDIA_PREFIX, 'css/editor.css')]) 34 | js = [posixpath.join(settings.DBTEMPLATES_MEDIA_PREFIX, 35 | 'js/codemirror.js')] 36 | 37 | def render(self, name, value, attrs=None, renderer=None): 38 | result = [] 39 | result.append( 40 | super().render(name, value, attrs)) 41 | result.append(""" 42 | 54 | """ % dict(media_prefix=settings.DBTEMPLATES_MEDIA_PREFIX, name=name)) 55 | return mark_safe("".join(result)) 56 | 57 | 58 | if settings.DBTEMPLATES_USE_CODEMIRROR: 59 | TemplateContentTextArea = CodeMirrorTextArea 60 | else: 61 | TemplateContentTextArea = forms.Textarea 62 | 63 | if settings.DBTEMPLATES_AUTO_POPULATE_CONTENT: 64 | content_help_text = _("Leaving this empty causes Django to look for a " 65 | "template with the given name and populate this " 66 | "field with its content.") 67 | else: 68 | content_help_text = "" 69 | 70 | if settings.DBTEMPLATES_USE_CODEMIRROR and settings.DBTEMPLATES_USE_TINYMCE: 71 | raise ImproperlyConfigured("You may use either CodeMirror or TinyMCE " 72 | "with dbtemplates, not both. Please disable " 73 | "one of them.") 74 | 75 | if settings.DBTEMPLATES_USE_TINYMCE: 76 | from tinymce.widgets import AdminTinyMCE 77 | TemplateContentTextArea = AdminTinyMCE 78 | elif settings.DBTEMPLATES_USE_REDACTOR: 79 | from redactor.widgets import RedactorEditor 80 | TemplateContentTextArea = RedactorEditor 81 | 82 | 83 | class TemplateAdminForm(forms.ModelForm): 84 | 85 | """ 86 | Custom AdminForm to make the content textarea wider. 87 | """ 88 | content = forms.CharField( 89 | widget=TemplateContentTextArea(attrs={'rows': '24'}), 90 | help_text=content_help_text, required=False) 91 | 92 | class Meta: 93 | model = Template 94 | fields = ('name', 'content', 'sites', 'creation_date', 'last_changed') 95 | fields = "__all__" 96 | 97 | 98 | class TemplateAdmin(TemplateModelAdmin): 99 | form = TemplateAdminForm 100 | readonly_fields = ['creation_date', 'last_changed'] 101 | fieldsets = ( 102 | (None, { 103 | 'fields': ('name', 'content'), 104 | 'classes': ('monospace',), 105 | }), 106 | (_('Advanced'), { 107 | 'fields': (('sites'),), 108 | }), 109 | (_('Date/time'), { 110 | 'fields': (('creation_date', 'last_changed'),), 111 | 'classes': ('collapse',), 112 | }), 113 | ) 114 | filter_horizontal = ('sites',) 115 | list_display = ('name', 'creation_date', 'last_changed', 'site_list') 116 | list_filter = ('sites',) 117 | save_as = True 118 | search_fields = ('name', 'content') 119 | actions = ['invalidate_cache', 'repopulate_cache', 'check_syntax'] 120 | 121 | def invalidate_cache(self, request, queryset): 122 | for template in queryset: 123 | remove_cached_template(template) 124 | count = queryset.count() 125 | message = ngettext( 126 | "Cache of one template successfully invalidated.", 127 | "Cache of %(count)d templates successfully invalidated.", 128 | count) 129 | self.message_user(request, message % {'count': count}) 130 | invalidate_cache.short_description = _("Invalidate cache of " 131 | "selected templates") 132 | 133 | def repopulate_cache(self, request, queryset): 134 | for template in queryset: 135 | add_template_to_cache(template) 136 | count = queryset.count() 137 | message = ngettext( 138 | "Cache successfully repopulated with one template.", 139 | "Cache successfully repopulated with %(count)d templates.", 140 | count) 141 | self.message_user(request, message % {'count': count}) 142 | repopulate_cache.short_description = _("Repopulate cache with " 143 | "selected templates") 144 | 145 | def check_syntax(self, request, queryset): 146 | errors = [] 147 | for template in queryset: 148 | valid, error = check_template_syntax(template) 149 | if not valid: 150 | errors.append(f'{template.name}: {error}') 151 | if errors: 152 | count = len(errors) 153 | message = ngettext( 154 | "Template syntax check FAILED for %(names)s.", 155 | "Template syntax check FAILED for " 156 | "%(count)d templates: %(names)s.", 157 | count) 158 | self.message_user(request, message % 159 | {'count': count, 'names': ', '.join(errors)}) 160 | else: 161 | count = queryset.count() 162 | message = ngettext( 163 | "Template syntax OK.", 164 | "Template syntax OK for %(count)d templates.", count) 165 | self.message_user(request, message % {'count': count}) 166 | check_syntax.short_description = _("Check template syntax") 167 | 168 | def site_list(self, template): 169 | return ", ".join([site.name for site in template.sites.all()]) 170 | site_list.short_description = _('sites') 171 | 172 | 173 | admin.site.register(Template, TemplateAdmin) 174 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # django-dbtemplates documentation build configuration file, created by 3 | # sphinx-quickstart on Fri Oct 9 14:52:11 2009. 4 | # 5 | # This file is execfile()d with the current directory set to its containing dir. 6 | # 7 | # Note that not all possible configuration values are present in this 8 | # autogenerated file. 9 | # 10 | # All configuration values have a default; values that are commented out 11 | # serve to show the default. 12 | 13 | import sys, os 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | sys.path.append(os.path.abspath('.')) 19 | 20 | # -- General configuration ----------------------------------------------------- 21 | 22 | # Add any Sphinx extension module names here, as strings. They can be extensions 23 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 24 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage'] 25 | 26 | # Add any paths that contain templates here, relative to this directory. 27 | templates_path = ['_templates'] 28 | 29 | # The suffix of source filenames. 30 | source_suffix = '.txt' 31 | 32 | # The encoding of source files. 33 | #source_encoding = 'utf-8' 34 | 35 | # The master toctree document. 36 | master_doc = 'index' 37 | 38 | # General information about the project. 39 | project = 'django-dbtemplates' 40 | copyright = '2007-2019, Jannis Leidel and contributors' 41 | 42 | # The version info for the project you're documenting, acts as replacement for 43 | # |version| and |release|, also used in various other places throughout the 44 | # built documents. 45 | # 46 | # The short X.Y version. 47 | try: 48 | from dbtemplates import __version__ 49 | # The short X.Y version. 50 | version = '.'.join(__version__.split('.')[:2]) 51 | # The full version, including alpha/beta/rc tags. 52 | release = __version__ 53 | except ImportError: 54 | version = release = 'dev' 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of documents that shouldn't be included in the build. 67 | #unused_docs = [] 68 | 69 | # List of directories, relative to source directory, that shouldn't be searched 70 | # for source files. 71 | exclude_trees = ['_build'] 72 | 73 | # The reST default role (used for this markup: `text`) to use for all documents. 74 | #default_role = None 75 | 76 | # If true, '()' will be appended to :func: etc. cross-reference text. 77 | #add_function_parentheses = True 78 | 79 | # If true, the current module name will be prepended to all description 80 | # unit titles (such as .. function::). 81 | #add_module_names = True 82 | 83 | # If true, sectionauthor and moduleauthor directives will be shown in the 84 | # output. They are ignored by default. 85 | #show_authors = False 86 | 87 | # The name of the Pygments (syntax highlighting) style to use. 88 | pygments_style = 'sphinx' 89 | 90 | # A list of ignored prefixes for module index sorting. 91 | #modindex_common_prefix = [] 92 | 93 | 94 | # -- Options for HTML output --------------------------------------------------- 95 | 96 | # The theme to use for HTML and HTML Help pages. Major themes that come with 97 | # Sphinx are currently 'default' and 'sphinxdoc'. 98 | html_theme = 'default' 99 | 100 | # Theme options are theme-specific and customize the look and feel of a theme 101 | # further. For a list of options available for each theme, see the 102 | # documentation. 103 | #html_theme_options = {} 104 | 105 | # Add any paths that contain custom themes here, relative to this directory. 106 | # html_theme_path = ['_theme'] 107 | 108 | # The name for this set of Sphinx documents. If None, it defaults to 109 | # " v documentation". 110 | html_title = "django-dbtemplates documentation" 111 | 112 | # A shorter title for the navigation bar. Default is the same as html_title. 113 | html_short_title = "django-dbtemplates" 114 | 115 | # The name of an image file (relative to this directory) to place at the top 116 | # of the sidebar. 117 | #html_logo = None 118 | 119 | # The name of an image file (within the static path) to use as favicon of the 120 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 121 | # pixels large. 122 | #html_favicon = None 123 | 124 | # Add any paths that contain custom static files (such as style sheets) here, 125 | # relative to this directory. They are copied after the builtin static files, 126 | # so a file named "default.css" will overwrite the builtin "default.css". 127 | # html_static_path = ['_static'] 128 | 129 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 130 | # using the given strftime format. 131 | #html_last_updated_fmt = '%b %d, %Y' 132 | 133 | # If true, SmartyPants will be used to convert quotes and dashes to 134 | # typographically correct entities. 135 | #html_use_smartypants = True 136 | 137 | # Custom sidebar templates, maps document names to template names. 138 | #html_sidebars = {} 139 | 140 | # Additional templates that should be rendered to pages, maps page names to 141 | # template names. 142 | #html_additional_pages = {} 143 | 144 | # If false, no module index is generated. 145 | #html_use_modindex = True 146 | 147 | # If false, no index is generated. 148 | #html_use_index = True 149 | 150 | # If true, the index is split into individual pages for each letter. 151 | #html_split_index = False 152 | 153 | # If true, links to the reST sources are added to the pages. 154 | #html_show_sourcelink = True 155 | 156 | # If true, an OpenSearch description file will be output, and all pages will 157 | # contain a tag referring to it. The value of this option must be the 158 | # base URL from which the finished HTML is served. 159 | #html_use_opensearch = '' 160 | 161 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 162 | #html_file_suffix = '' 163 | 164 | # Output file base name for HTML help builder. 165 | htmlhelp_basename = 'django-dbtemplatesdoc' 166 | 167 | 168 | # -- Options for LaTeX output -------------------------------------------------- 169 | 170 | # The paper size ('letter' or 'a4'). 171 | #latex_paper_size = 'letter' 172 | 173 | # The font size ('10pt', '11pt' or '12pt'). 174 | #latex_font_size = '10pt' 175 | 176 | # Grouping the document tree into LaTeX files. List of tuples 177 | # (source start file, target name, title, author, documentclass [howto/manual]). 178 | latex_documents = [ 179 | ('index', 'django-dbtemplates.tex', 'django-dbtemplates Documentation', 180 | 'Jannis Leidel and contributors', 'manual'), 181 | ] 182 | 183 | # The name of an image file (relative to this directory) to place at the top of 184 | # the title page. 185 | #latex_logo = None 186 | 187 | # For "manual" documents, if this is true, then toplevel headings are parts, 188 | # not chapters. 189 | #latex_use_parts = False 190 | 191 | # Additional stuff for the LaTeX preamble. 192 | #latex_preamble = '' 193 | 194 | # Documents to append as an appendix to all manuals. 195 | #latex_appendices = [] 196 | 197 | # If false, no module index is generated. 198 | #latex_use_modindex = True 199 | -------------------------------------------------------------------------------- /dbtemplates/test_cases.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | from unittest import mock 5 | 6 | from django.conf import settings as django_settings 7 | from django.core.cache.backends.base import BaseCache 8 | from django.core.management import call_command 9 | from django.template import loader, TemplateDoesNotExist 10 | from django.test import TestCase 11 | 12 | from django.contrib.sites.models import Site 13 | 14 | from dbtemplates.conf import settings 15 | from dbtemplates.models import Template 16 | from dbtemplates.utils.cache import get_cache_backend, get_cache_key 17 | from dbtemplates.utils.template import (get_template_source, 18 | check_template_syntax) 19 | from dbtemplates.management.commands.sync_templates import (FILES_TO_DATABASE, 20 | DATABASE_TO_FILES) 21 | 22 | 23 | class DbTemplatesTestCase(TestCase): 24 | def setUp(self): 25 | self.old_TEMPLATES = settings.TEMPLATES 26 | if 'dbtemplates.loader.Loader' not in settings.TEMPLATES: 27 | loader.template_source_loaders = None 28 | settings.TEMPLATES = list(settings.TEMPLATES) + [ 29 | 'dbtemplates.loader.Loader' 30 | ] 31 | 32 | self.site1, created1 = Site.objects.get_or_create( 33 | domain="example.com", name="example.com") 34 | self.site2, created2 = Site.objects.get_or_create( 35 | domain="example.org", name="example.org") 36 | self.t1, _ = Template.objects.get_or_create( 37 | name='base.html', content='base') 38 | self.t2, _ = Template.objects.get_or_create( 39 | name='sub.html', content='sub') 40 | self.t2.sites.add(self.site2) 41 | 42 | def tearDown(self): 43 | loader.template_source_loaders = None 44 | settings.TEMPLATES = self.old_TEMPLATES 45 | 46 | def test_basics(self): 47 | self.assertEqual(list(self.t1.sites.all()), [self.site1]) 48 | self.assertTrue("base" in self.t1.content) 49 | self.assertEqual(list(Template.objects.filter(sites=self.site1)), 50 | [self.t1, self.t2]) 51 | self.assertEqual(list(self.t2.sites.all()), [self.site1, self.site2]) 52 | 53 | def test_empty_sites(self): 54 | old_add_default_site = settings.DBTEMPLATES_ADD_DEFAULT_SITE 55 | try: 56 | settings.DBTEMPLATES_ADD_DEFAULT_SITE = False 57 | self.t3 = Template.objects.create( 58 | name='footer.html', content='footer') 59 | self.assertEqual(list(self.t3.sites.all()), []) 60 | finally: 61 | settings.DBTEMPLATES_ADD_DEFAULT_SITE = old_add_default_site 62 | 63 | def test_load_templates_sites(self): 64 | old_add_default_site = settings.DBTEMPLATES_ADD_DEFAULT_SITE 65 | old_site_id = django_settings.SITE_ID 66 | try: 67 | settings.DBTEMPLATES_ADD_DEFAULT_SITE = False 68 | t_site1 = Template.objects.create( 69 | name='copyright.html', content='(c) example.com') 70 | t_site1.sites.add(self.site1) 71 | t_site2 = Template.objects.create( 72 | name='copyright.html', content='(c) example.org') 73 | t_site2.sites.add(self.site2) 74 | 75 | django_settings.SITE_ID = Site.objects.create( 76 | domain="example.net", name="example.net").id 77 | Site.objects.clear_cache() 78 | 79 | self.assertRaises(TemplateDoesNotExist, 80 | loader.get_template, "copyright.html") 81 | finally: 82 | django_settings.SITE_ID = old_site_id 83 | settings.DBTEMPLATES_ADD_DEFAULT_SITE = old_add_default_site 84 | 85 | def test_load_templates(self): 86 | result = loader.get_template("base.html").render() 87 | self.assertEqual(result, 'base') 88 | result2 = loader.get_template("sub.html").render() 89 | self.assertEqual(result2, 'sub') 90 | 91 | def test_error_templates_creation(self): 92 | call_command('create_error_templates', force=True, verbosity=0) 93 | self.assertEqual(list(Template.objects.filter(sites=self.site1)), 94 | list(Template.objects.filter())) 95 | self.assertTrue(Template.objects.filter(name='404.html').exists()) 96 | 97 | def test_automatic_sync(self): 98 | admin_base_template = get_template_source('admin/base.html') 99 | template = Template.objects.create(name='admin/base.html') 100 | self.assertEqual(admin_base_template, template.content) 101 | 102 | def test_sync_templates(self): 103 | old_template_dirs = settings.TEMPLATES[0].get('DIRS', []) 104 | temp_template_dir = tempfile.mkdtemp('dbtemplates') 105 | temp_template_path = os.path.join(temp_template_dir, 'temp_test.html') 106 | temp_template = open(temp_template_path, 'w', encoding='utf-8') 107 | try: 108 | temp_template.write('temp test') 109 | settings.TEMPLATES[0]['DIRS'] = (temp_template_dir,) 110 | # these works well if is not settings patched at runtime 111 | # for supporting django < 1.7 tests we must patch dirs in runtime 112 | from dbtemplates.management.commands import sync_templates 113 | sync_templates.DIRS = settings.TEMPLATES[0]['DIRS'] 114 | 115 | self.assertFalse( 116 | Template.objects.filter(name='temp_test.html').exists()) 117 | call_command('sync_templates', force=True, 118 | verbosity=0, overwrite=FILES_TO_DATABASE) 119 | self.assertTrue( 120 | Template.objects.filter(name='temp_test.html').exists()) 121 | 122 | t = Template.objects.get(name='temp_test.html') 123 | t.content = 'temp test modified' 124 | t.save() 125 | call_command('sync_templates', force=True, 126 | verbosity=0, overwrite=DATABASE_TO_FILES) 127 | self.assertEqual('temp test modified', 128 | open(temp_template_path, 129 | encoding='utf-8').read()) 130 | 131 | call_command('sync_templates', force=True, verbosity=0, 132 | delete=True, overwrite=DATABASE_TO_FILES) 133 | self.assertTrue(os.path.exists(temp_template_path)) 134 | self.assertFalse( 135 | Template.objects.filter(name='temp_test.html').exists()) 136 | finally: 137 | temp_template.close() 138 | settings.TEMPLATES[0]['DIRS'] = old_template_dirs 139 | shutil.rmtree(temp_template_dir) 140 | 141 | def test_get_cache(self): 142 | self.assertTrue(isinstance(get_cache_backend(), BaseCache)) 143 | 144 | def test_check_template_syntax(self): 145 | bad_template, _ = Template.objects.get_or_create( 146 | name='bad.html', content='{% if foo %}Bar') 147 | good_template, _ = Template.objects.get_or_create( 148 | name='good.html', content='{% if foo %}Bar{% endif %}') 149 | self.assertFalse(check_template_syntax(bad_template)[0]) 150 | self.assertTrue(check_template_syntax(good_template)[0]) 151 | 152 | def test_get_cache_name(self): 153 | self.assertEqual(get_cache_key('name with spaces'), 154 | 'dbtemplates::name-with-spaces::1') 155 | 156 | def test_cache_invalidation(self): 157 | # Add t1 into the cache of site2 158 | self.t1.sites.add(self.site2) 159 | with mock.patch('django.contrib.sites.models.SiteManager.get_current', 160 | return_value=self.site2): 161 | result = loader.get_template("base.html").render() 162 | self.assertEqual(result, 'base') 163 | 164 | # Update content 165 | self.t1.content = 'new content' 166 | self.t1.save() 167 | result = loader.get_template("base.html").render() 168 | self.assertEqual(result, 'new content') 169 | 170 | # Cache invalidation should work across sites. 171 | # Site2 should see the new content. 172 | with mock.patch('django.contrib.sites.models.SiteManager.get_current', 173 | return_value=self.site2): 174 | result = loader.get_template("base.html").render() 175 | self.assertEqual(result, 'new content') 176 | -------------------------------------------------------------------------------- /dbtemplates/static/dbtemplates/js/parsedjango.js: -------------------------------------------------------------------------------- 1 | /* This file defines an XML/Django parser, with a few kludges to make it 2 | * useable for HTML. autoSelfClosers defines a set of tag names that 3 | * are expected to not have a closing tag, and doNotIndent specifies 4 | * the tags inside of which no indentation should happen (see Config 5 | * object). These can be disabled by passing the editor an object like 6 | * {useHTMLKludges: false} as parserConfig option. 7 | */ 8 | 9 | var XMLParser = Editor.Parser = (function() { 10 | var Kludges = { 11 | autoSelfClosers: {"br": true, "img": true, "hr": true, "link": true, "input": true, 12 | "meta": true, "col": true, "frame": true, "base": true, "area": true}, 13 | doNotIndent: {"pre": true, "!cdata": true} 14 | }; 15 | var NoKludges = {autoSelfClosers: {}, doNotIndent: {"!cdata": true}}; 16 | var UseKludges = Kludges; 17 | var alignCDATA = false; 18 | 19 | // Simple stateful tokenizer for XML documents. Returns a 20 | // MochiKit-style iterator, with a state property that contains a 21 | // function encapsulating the current state. See tokenize.js. 22 | var tokenizeXML = (function() { 23 | function inText(source, setState) { 24 | var ch = source.next(); 25 | 26 | if (ch == "{" && source.equals("{")){ 27 | setState(inDjango("}}", false)); 28 | return "django"; 29 | } 30 | 31 | if (ch == "{" && source.equals("%")){ 32 | setState(inDjango("%}", true)); 33 | return "django"; 34 | } 35 | 36 | else if (ch == "<") { 37 | if (source.equals("!")) { 38 | source.next(); 39 | if (source.equals("[")) { 40 | if (source.lookAhead("[CDATA[", true)) { 41 | setState(inBlock("xml-cdata", "]]>")); 42 | return null; 43 | } 44 | else { 45 | return "xml-text"; 46 | } 47 | } 48 | else if (source.lookAhead("--", true)) { 49 | setState(inBlock("xml-comment", "-->")); 50 | return null; 51 | } 52 | else { 53 | return "xml-text"; 54 | } 55 | } 56 | else if (source.equals("?")) { 57 | source.next(); 58 | source.nextWhileMatches(/[\w\._\-]/); 59 | setState(inBlock("xml-processing", "?>")); 60 | return "xml-processing"; 61 | } 62 | else { 63 | if (source.equals("/")) source.next(); 64 | setState(inTag); 65 | return "xml-punctuation"; 66 | } 67 | } 68 | else if (ch == "&") { 69 | while (!source.endOfLine()) { 70 | if (source.next() == ";") 71 | break; 72 | } 73 | return "xml-entity"; 74 | } 75 | else { 76 | source.nextWhileMatches(/[^&<\n{]/); 77 | return "xml-text"; 78 | } 79 | } 80 | 81 | function inTag(source, setState) { 82 | var ch = source.next(); 83 | if (ch == ">") { 84 | setState(inText); 85 | return "xml-punctuation"; 86 | } 87 | else if (/[?\/]/.test(ch) && source.equals(">")) { 88 | source.next(); 89 | setState(inText); 90 | return "xml-punctuation"; 91 | } 92 | else if (ch == "=") { 93 | return "xml-punctuation"; 94 | } 95 | else if (/[\'\"]/.test(ch)) { 96 | setState(inAttribute(ch)); 97 | return null; 98 | } 99 | else { 100 | source.nextWhileMatches(/[^\s\u00a0=<>\"\'\/?]/); 101 | return "xml-name"; 102 | } 103 | } 104 | 105 | function inAttribute(quote) { 106 | return function(source, setState) { 107 | while (!source.endOfLine()) { 108 | if (source.next() == quote) { 109 | setState(inTag); 110 | break; 111 | } 112 | } 113 | return "xml-attribute"; 114 | }; 115 | } 116 | 117 | function inBlock(style, terminator) { 118 | return function(source, setState) { 119 | while (!source.endOfLine()) { 120 | if (source.lookAhead(terminator, true)) { 121 | setState(inText); 122 | break; 123 | } 124 | source.next(); 125 | } 126 | return style; 127 | }; 128 | } 129 | 130 | function inDjangoQuote(quote, terminator){ 131 | return function(source, setState) { 132 | source.next(); 133 | while (!source.endOfLine()) { 134 | if (source.next() == quote) { 135 | setState(inDjango(terminator, false)); 136 | break; 137 | } 138 | } 139 | return "django-quote"; 140 | }; 141 | } 142 | 143 | function inDjangoTagName(terminator){ 144 | return function(source, setState){ 145 | while (!source.endOfLine()) { 146 | if (source.equals(' ')) { 147 | setState(inDjango(terminator, false)); 148 | break; 149 | } 150 | source.next(); 151 | } 152 | return "django-tag-name"; 153 | } 154 | } 155 | 156 | function inDjango(terminator, open){ 157 | return function(source, setState){ 158 | if (open){ 159 | source.next(); 160 | } 161 | 162 | while(!source.endOfLine()){ 163 | ch = source.next(); 164 | 165 | if (open && !source.equals(' ')){ 166 | setState(inDjangoTagName(terminator)); 167 | break; 168 | } 169 | else if (ch == "'" || source.equals("'")){ 170 | setState(inDjangoQuote("'", terminator)); 171 | return "django-quote"; 172 | } 173 | else if (ch == '"' || source.equals('"')){ 174 | setState(inDjangoQuote('"', terminator)); 175 | return "django-quote"; 176 | } 177 | else if (ch == terminator[0] && source.equals(terminator[1])){ 178 | source.next(); 179 | setState(inText); 180 | break; 181 | } 182 | } 183 | return "django"; 184 | }; 185 | } 186 | 187 | return function(source, startState) { 188 | return tokenizer(source, startState || inText); 189 | }; 190 | })(); 191 | 192 | // The parser. The structure of this function largely follows that of 193 | // parseJavaScript in parsejavascript.js (there is actually a bit more 194 | // shared code than I'd like), but it is quite a bit simpler. 195 | function parseXML(source) { 196 | var tokens = tokenizeXML(source), token; 197 | var cc = [base]; 198 | var tokenNr = 0, indented = 0; 199 | var currentTag = null, context = null; 200 | var consume; 201 | 202 | function push(fs) { 203 | for (var i = fs.length - 1; i >= 0; i--) 204 | cc.push(fs[i]); 205 | } 206 | function cont() { 207 | push(arguments); 208 | consume = true; 209 | } 210 | function pass() { 211 | push(arguments); 212 | consume = false; 213 | } 214 | 215 | function markErr() { 216 | token.style += " xml-error"; 217 | } 218 | function expect(text) { 219 | return function(style, content) { 220 | if (content == text) cont(); 221 | else {markErr(); cont(arguments.callee);} 222 | }; 223 | } 224 | 225 | function pushContext(tagname, startOfLine) { 226 | var noIndent = UseKludges.doNotIndent.hasOwnProperty(tagname) || (context && context.noIndent); 227 | context = {prev: context, name: tagname, indent: indented, startOfLine: startOfLine, noIndent: noIndent}; 228 | } 229 | function popContext() { 230 | context = context.prev; 231 | } 232 | function computeIndentation(baseContext) { 233 | return function(nextChars, current) { 234 | var context = baseContext; 235 | if (context && context.noIndent) 236 | return current; 237 | if (alignCDATA && /")); 257 | else if (style == "xml-cdata") { 258 | if (!context || context.name != "!cdata") pushContext("!cdata"); 259 | if (/\]\]>$/.test(content)) popContext(); 260 | cont(); 261 | } 262 | else if (harmlessTokens.hasOwnProperty(style)) cont(); 263 | else {markErr(); cont();} 264 | } 265 | function tagname(style, content) { 266 | if (style == "xml-name") { 267 | currentTag = content.toLowerCase(); 268 | token.style = "xml-tagname"; 269 | cont(); 270 | } 271 | else { 272 | currentTag = null; 273 | pass(); 274 | } 275 | } 276 | function closetagname(style, content) { 277 | if (style == "xml-name") { 278 | token.style = "xml-tagname"; 279 | if (context && content.toLowerCase() == context.name) popContext(); 280 | else markErr(); 281 | } 282 | cont(); 283 | } 284 | function endtag(startOfLine) { 285 | return function(style, content) { 286 | if (content == "/>" || (content == ">" && UseKludges.autoSelfClosers.hasOwnProperty(currentTag))) cont(); 287 | else if (content == ">") {pushContext(currentTag, startOfLine); cont();} 288 | else {markErr(); cont(arguments.callee);} 289 | }; 290 | } 291 | function attributes(style) { 292 | if (style == "xml-name") {token.style = "xml-attname"; cont(attribute, attributes);} 293 | else pass(); 294 | } 295 | function attribute(style, content) { 296 | if (content == "=") cont(value); 297 | else if (content == ">" || content == "/>") pass(endtag); 298 | else pass(); 299 | } 300 | function value(style) { 301 | if (style == "xml-attribute") cont(value); 302 | else pass(); 303 | } 304 | 305 | return { 306 | indentation: function() {return indented;}, 307 | 308 | next: function(){ 309 | token = tokens.next(); 310 | if (token.style == "whitespace" && tokenNr == 0) 311 | indented = token.value.length; 312 | else 313 | tokenNr++; 314 | if (token.content == "\n") { 315 | indented = tokenNr = 0; 316 | token.indentation = computeIndentation(context); 317 | } 318 | 319 | if (token.style == "whitespace" || token.type == "xml-comment") 320 | return token; 321 | 322 | while(true){ 323 | consume = false; 324 | cc.pop()(token.style, token.content); 325 | if (consume) return token; 326 | } 327 | }, 328 | 329 | copy: function(){ 330 | var _cc = cc.concat([]), _tokenState = tokens.state, _context = context; 331 | var parser = this; 332 | 333 | return function(input){ 334 | cc = _cc.concat([]); 335 | tokenNr = indented = 0; 336 | context = _context; 337 | tokens = tokenizeXML(input, _tokenState); 338 | return parser; 339 | }; 340 | } 341 | }; 342 | } 343 | 344 | return { 345 | make: parseXML, 346 | electricChars: "/", 347 | configure: function(config) { 348 | if (config.useHTMLKludges != null) 349 | UseKludges = config.useHTMLKludges ? Kludges : NoKludges; 350 | if (config.alignCDATA) 351 | alignCDATA = config.alignCDATA; 352 | } 353 | }; 354 | })(); -------------------------------------------------------------------------------- /docs/changelog.txt: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | v5.0 (unreleased) 5 | ----------------- 6 | 7 | .. warning:: 8 | 9 | This is a backwards-incompatible release! 10 | 11 | * Dropped support for Python 3.7 and Django < 4.2. 12 | 13 | * Added support for Python 3.11, 3.12, 3.13. 14 | 15 | * Django 5.x support 16 | 17 | v4.0 (2022-09-3) 18 | ----------------- 19 | 20 | .. warning:: 21 | 22 | This is a backwards-incompatible release! 23 | 24 | * Dropped support for Python 2.7 and Django < 3.2. 25 | 26 | * Added support for Python 3.8, 3.9, 3.10. 27 | 28 | * Moved test runner to GitHub Actions: 29 | 30 | http://github.com/jazzband/django-dbtemplates/actions 31 | 32 | * Django 4.x support 33 | 34 | v3.0 (2019-01-27) 35 | ----------------- 36 | 37 | .. warning:: 38 | 39 | This is a backwards-incompatible release! 40 | 41 | * Dropped support for Django < 1.11. 42 | 43 | * Added support for Django 2.0 and 2.1. 44 | 45 | * Added support for Python 3.7. 46 | 47 | * Recompiled Russian locale. 48 | 49 | * Fixed byte string in migration file that caused the migration 50 | system to falsely think that there are new changes. 51 | 52 | * Fixed string representation of template model, e.g. to improve 53 | readability in choice fields. 54 | 55 | v2.0 (2016-09-29) 56 | ----------------- 57 | 58 | .. warning:: 59 | 60 | This is a backwards-incompatible release! 61 | 62 | * Moved maintenance to the `Jazzband `_ 63 | 64 | * Dropped support for Python 2.6 65 | 66 | * Added support for Python 3.4 and 3.5 67 | 68 | * Dropped support for Django < 1.8 69 | 70 | * Removed South migrations. Please use Django's native migration system instead 71 | 72 | * Removed the example project since it's out-of-date quickly 73 | 74 | v1.3.2 (2015-06-15) 75 | ------------------- 76 | 77 | * support for Django 1.8 (not full, but usable) 78 | * support for RedactorJS 79 | 80 | thanks for contrib - @eculver, @kmooney, @volksman 81 | 82 | v1.3.1 (2012-05-23) 83 | ------------------- 84 | 85 | * Minor release to move away from nose again and use own 86 | `django-discover-runner`_. 87 | 88 | .. _`django-discover-runner`: http://pypi.python.org/pypi/django-discover-runner 89 | 90 | v1.3 (2012-05-07) 91 | ----------------- 92 | 93 | * Dropped support for Django < 1.3 **backwards incompatible** 94 | 95 | * Dropped using versiontools in favor of home made solution. 96 | 97 | * Added optional support for TinyMCE editor instead of the CodeMirror 98 | editor (just enable ``DBTEMPLATES_USE_TINYMCE``). 99 | 100 | * Fixed compatibility to Django 1.4's handling of the ``DATABASES`` 101 | setting. Should also respect database routers now. 102 | 103 | * Fixed an issue of the cache key generation in combination with 104 | memcache's inability to stomach spaces. 105 | 106 | * Moved test runner to use nose_ and a hosted CI project at Travis_: 107 | http://travis-ci.org/jazzband/django-dbtemplates 108 | 109 | .. _nose: https://nose.readthedocs.io/ 110 | .. _Travis: http://travis-ci.org 111 | 112 | v1.2.1 (2011-09-07) 113 | ------------------- 114 | 115 | * Fixed a wrong use of the non-lazy localization tools. 116 | 117 | * Fixed bugs in the documentation. 118 | 119 | * Make use of django-appconf and versiontools. 120 | 121 | v1.2 (2011-08-15) 122 | ----------------- 123 | 124 | * Refactored the template loader to be even more cache effective. 125 | 126 | * Added ``check_template_syntax`` management command and admin action 127 | to make sure the saved templates are valid Django templates. 128 | 129 | v1.1.1 (2011-07-08) 130 | ------------------- 131 | 132 | * Fixed bug in cache loading (again). 133 | 134 | * Fixed bugs in the documentation. 135 | 136 | .. note:: 137 | 138 | Since ``dbtemplates`` removed support for Django lower than 1.2 you 139 | have to use the template loader class in the ``TEMPLATE_LOADERS`` 140 | (``'dbtemplates.loader.Loader'``) and **not** the previosly included 141 | function that ended with ``load_template_source``. 142 | 143 | v1.1 (2011-07-06) 144 | ----------------- 145 | 146 | * **BACKWARDS-INCOMPATIBLE** Requires Django 1.2 or higher. 147 | For previous Django versions use an older versions of ``dbtemplates``, 148 | e.g.:: 149 | 150 | $ pip install "django-dbtemplates<1.1" 151 | 152 | * Added South migrations. 153 | 154 | .. note:: 155 | 156 | If you are using South in your Django project, you can easily enable 157 | dbtemplates' migrations, *faking* the first migration by using the 158 | ``--fake`` option of South's ``migrate`` management command:: 159 | 160 | $ manage.py migrate --fake 0001 dbtemplates 161 | 162 | Then run the rest of the migrations:: 163 | 164 | $ manage.py migrate dbtemplates 165 | 166 | * Removed uniqueness on the ``name`` field of the ``Template`` model. This is 167 | needed because there isn't a ``unique_together`` for M2M fields in Django 168 | such as the ``sites`` field in the ``Template`` model. 169 | 170 | * Made the ``sites`` field optional to support a way to apply a template to 171 | all sites. 172 | 173 | * Added ``--delete`` option to ``sync_templates`` managment command to delete 174 | the file or database entry after syncing (depending on used ``--overwrite`` 175 | mode). 176 | 177 | * Updated translations. 178 | 179 | * Fixed issue with incorrectly splitting paths in ``sync_templates``. 180 | 181 | * Extended tests. 182 | 183 | * Fixed issue with cache settings handling. 184 | 185 | v1.0.1 (2011-04-14) 186 | ------------------- 187 | 188 | * Minor bugfixes with regard to the new cache handling. 189 | 190 | v1.0 (2011-04-11) 191 | ----------------- 192 | 193 | .. warning:: 194 | This is the first stable release of django-dbtemplates which comes with a 195 | series of backwards incompatible changes. 196 | 197 | * Removed own caching mechanism in favor of Django based caching mechanism. 198 | The ``DBTEMPLATES_CACHE_BACKEND`` is expected to be a valid cache backend 199 | URI, just like Django's own ``CACHE_BACKEND`` setting. In Django >= 1.3 200 | an ``'dbtemplates'`` entry in the ``CACHES`` setting is also considered 201 | valid. 202 | 203 | * Added tox configuration to test ``dbtemplates`` on Python 2.5, 2.6 and 2.7 204 | with Django 1.1.X, 1.2.X and 1.3.X. 205 | 206 | * Added Transifex configuration. 207 | 208 | * Use ``STATIC_URL`` setting instead of ``MEDIA_URL`` for the media prefix. 209 | Also moved files from media/* to static/* to follow convention introduced 210 | in Django 1.3. 211 | 212 | * Use ReadTheDocs for documentation hosting. 213 | 214 | v0.8.0 (2010-11-07) 215 | ------------------- 216 | 217 | * Added Finnish translation (by jholster) 218 | 219 | * Added --overwrite and --app-first options to sync_templates command (by Alex Kamedov). 220 | 221 | v0.7.4 (2010-09-23) 222 | ------------------- 223 | 224 | * Fixed tests. 225 | 226 | v0.7.3 (2010-09-21) 227 | ------------------- 228 | 229 | * Added ``DBTEMPLATES_AUTO_POPULATE_CONTENT`` setting to be able to disable 230 | to auto-populating of template content. 231 | 232 | * Fixed cosmetic issue in admin with collapsable fields. 233 | 234 | v0.7.2 (2010-09-04) 235 | ------------------- 236 | 237 | * Moved to Github again. Sigh. 238 | 239 | v0.7.1 (2010-07-07) 240 | ------------------- 241 | 242 | * Fixed problem with the CodeMirror textarea, which wasn't completely 243 | disabled before. 244 | 245 | * Fixed problem with the ``DBTEMPLATES_MEDIA_PREFIX`` setting, which defaults 246 | now to ``os.path.join(settings.MEDIA_ROOT, 'dbtemplates')`` now. 247 | 248 | In other words, if you don't specify a ``DBTEMPLATES_MEDIA_PREFIX`` setting 249 | and have the CodeMirror textarea enabled, dbtemplates will look in a 250 | subdirectory of your site's ``MEDIA_ROOT`` for the CodeMirror media files. 251 | 252 | v0.7.0 (2010-06-24) 253 | ------------------- 254 | 255 | * Added CodeMirror_-based syntax highlighting textarea, based on the amaxing 256 | work_ by `Nic Pottier`_. Set the ``DBTEMPLATES_USE_CODEMIRROR`` setting 257 | to ``True`` to enable it. 258 | 259 | * Make use of the full width in plain textarea mode. 260 | 261 | * Added Chinese translation 262 | 263 | * Added support for Django 1.2 264 | 265 | * Updated French translation 266 | 267 | * Added ``DBTEMPLATES_USE_REVERSION`` setting to be able to explicitely enable 268 | reversion support. (Default: ``False``) 269 | 270 | .. _CodeMirror: http://marijn.haverbeke.nl/codemirror/ 271 | .. _work: https://gist.github.com/368758/86bcafe53c438e2e2a0e3442c3b30f2c6011fbba 272 | .. _`Nic Pottier`: http://github.com/nicpottier 273 | 274 | v0.6.1 (2009-10-19) 275 | ------------------- 276 | 277 | * Fixed issue with default site of a template, added ability to disable 278 | default site (``DBTEMPLATES_ADD_DEFAULT_SITE``). 279 | 280 | v0.6.0 (2009-10-09) 281 | ------------------- 282 | 283 | * Updated and added locales (Danish, Brazilian Portuguese) 284 | 285 | * Fixes an ambiguity problem with the cache invalidation 286 | 287 | * Added ``invalidate_cache`` and ``repopulate_cache`` admin actions 288 | 289 | * Added Sphinx documentation 290 | 291 | v0.5.7 292 | ------ 293 | 294 | * Updates to the docs 295 | 296 | * switch back to Bitbucket 297 | 298 | * fixed tests 299 | 300 | * Added Italian translation 301 | 302 | * list of sites the template is used on 303 | 304 | * fixed bug in ``create_error_template`` command. 305 | 306 | v0.5.4 307 | ------ 308 | 309 | * Made loader and cache backends site-aware. 310 | 311 | * The filesystem cache backend now saves the files under 312 | ``//``. 313 | 314 | * The Django cache backend the Site id in the cache key 315 | 316 | * Template is now saved explicitly to backend if not existent in cache 317 | (e.g. if deleted manually or invalidated). 318 | 319 | v0.5.3 320 | ------ 321 | 322 | * Removed automatic creation of 404.html and 50v0.html templates and added a 323 | new management command for those cases called ``create_error_templates`` 324 | 325 | * Also reverted move to Bitbucket 326 | 327 | v0.5.2 328 | ------ 329 | 330 | * Fixed a problem with ``django.contrib.sites`` when its table hasn't been 331 | populated yet on initialization of dbtemplates. Thanks for the report, 332 | Kevin Fricovsky 333 | 334 | * Added an example Django project and docs for it 335 | 336 | v0.5.1 337 | ------ 338 | 339 | * Removed unneeded code that registered the model with reversion. 340 | 341 | * Updated docs a bit. 342 | 343 | * Moved codebase to Bitbucket. 344 | 345 | * Removed legacy ``sync_templates.py`` script, use ``django-admin.py 346 | sync_templates`` from now on. 347 | 348 | v0.5.0 349 | ------ 350 | 351 | * Added support for `django-reversion`_ 352 | 353 | * added feature that populates the content field automatically when left 354 | empty by using Django's other template loaders 355 | 356 | * added caching backend system with two default backends: 357 | 358 | * ``FileSystemBackend`` 359 | * ``DjangoCacheBackend`` 360 | 361 | More about it in the `blog post`_ and in the docs. 362 | 363 | .. _django-reversion: http://code.google.com/p/django-reversion/ 364 | .. _blog post: http://jannisleidel.com/2008/11/updates-to-django-dbtemplates-and-half-assed-promise/ 365 | 366 | v0.4.7 367 | ------ 368 | 369 | * Minor bugfix 370 | 371 | v0.4.6 372 | ------ 373 | 374 | * Minor doc change and PyPI support 375 | 376 | v0.4.5 377 | ------ 378 | 379 | * fixed the --force option of the sync_templates command 380 | 381 | v0.4.4 382 | ------ 383 | 384 | * fixed error in custom model save() after changes in Django `r8670`_. 385 | 386 | .. _r8670: http://code.djangoproject.com/changeset/8670 387 | 388 | v0.4.3 389 | ------ 390 | 391 | * removed oldforms code 392 | 393 | v0.4.2 394 | ------ 395 | 396 | * added Hebrew translation (by mkriheli) 397 | 398 | v0.4.1 399 | ------ 400 | 401 | * added French (by Roland Frederic) and German locale 402 | 403 | v0.4.0 404 | ------ 405 | 406 | * adds better support for newforms-admin 407 | 408 | * don't forget to load the dbtemplates.admin, e.g. by using 409 | django.contrib.admin.autodiscover() in you urls.py 410 | 411 | v0.3.1 412 | ------ 413 | 414 | * adds a new management command *sync_templates* for bidirectional syncing 415 | between filesystem and database (backwards-compatible) and 416 | FilesystemCaching (thanks, Arne Brodowski!) 417 | 418 | v0.2.5 419 | ------ 420 | 421 | * adds support for newforms-admin 422 | 423 | Support 424 | ======= 425 | 426 | Please leave your questions and messages on the designated site: 427 | 428 | http://github.com/jazzband/django-dbtemplates/issues/ 429 | -------------------------------------------------------------------------------- /dbtemplates/static/dbtemplates/js/undo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Storage and control for undo information within a CodeMirror 3 | * editor. 'Why on earth is such a complicated mess required for 4 | * that?', I hear you ask. The goal, in implementing this, was to make 5 | * the complexity of storing and reverting undo information depend 6 | * only on the size of the edited or restored content, not on the size 7 | * of the whole document. This makes it necessary to use a kind of 8 | * 'diff' system, which, when applied to a DOM tree, causes some 9 | * complexity and hackery. 10 | * 11 | * In short, the editor 'touches' BR elements as it parses them, and 12 | * the UndoHistory stores these. When nothing is touched in commitDelay 13 | * milliseconds, the changes are committed: It goes over all touched 14 | * nodes, throws out the ones that did not change since last commit or 15 | * are no longer in the document, and assembles the rest into zero or 16 | * more 'chains' -- arrays of adjacent lines. Links back to these 17 | * chains are added to the BR nodes, while the chain that previously 18 | * spanned these nodes is added to the undo history. Undoing a change 19 | * means taking such a chain off the undo history, restoring its 20 | * content (text is saved per line) and linking it back into the 21 | * document. 22 | */ 23 | 24 | // A history object needs to know about the DOM container holding the 25 | // document, the maximum amount of undo levels it should store, the 26 | // delay (of no input) after which it commits a set of changes, and, 27 | // unfortunately, the 'parent' window -- a window that is not in 28 | // designMode, and on which setTimeout works in every browser. 29 | function UndoHistory(container, maxDepth, commitDelay, editor) { 30 | this.container = container; 31 | this.maxDepth = maxDepth; this.commitDelay = commitDelay; 32 | this.editor = editor; this.parent = editor.parent; 33 | // This line object represents the initial, empty editor. 34 | var initial = {text: "", from: null, to: null}; 35 | // As the borders between lines are represented by BR elements, the 36 | // start of the first line and the end of the last one are 37 | // represented by null. Since you can not store any properties 38 | // (links to line objects) in null, these properties are used in 39 | // those cases. 40 | this.first = initial; this.last = initial; 41 | // Similarly, a 'historyTouched' property is added to the BR in 42 | // front of lines that have already been touched, and 'firstTouched' 43 | // is used for the first line. 44 | this.firstTouched = false; 45 | // History is the set of committed changes, touched is the set of 46 | // nodes touched since the last commit. 47 | this.history = []; this.redoHistory = []; this.touched = []; 48 | } 49 | 50 | UndoHistory.prototype = { 51 | // Schedule a commit (if no other touches come in for commitDelay 52 | // milliseconds). 53 | scheduleCommit: function() { 54 | var self = this; 55 | this.parent.clearTimeout(this.commitTimeout); 56 | this.commitTimeout = this.parent.setTimeout(function(){self.tryCommit();}, this.commitDelay); 57 | }, 58 | 59 | // Mark a node as touched. Null is a valid argument. 60 | touch: function(node) { 61 | this.setTouched(node); 62 | this.scheduleCommit(); 63 | }, 64 | 65 | // Undo the last change. 66 | undo: function() { 67 | // Make sure pending changes have been committed. 68 | this.commit(); 69 | 70 | if (this.history.length) { 71 | // Take the top diff from the history, apply it, and store its 72 | // shadow in the redo history. 73 | var item = this.history.pop(); 74 | this.redoHistory.push(this.updateTo(item, "applyChain")); 75 | this.notifyEnvironment(); 76 | return this.chainNode(item); 77 | } 78 | }, 79 | 80 | // Redo the last undone change. 81 | redo: function() { 82 | this.commit(); 83 | if (this.redoHistory.length) { 84 | // The inverse of undo, basically. 85 | var item = this.redoHistory.pop(); 86 | this.addUndoLevel(this.updateTo(item, "applyChain")); 87 | this.notifyEnvironment(); 88 | return this.chainNode(item); 89 | } 90 | }, 91 | 92 | clear: function() { 93 | this.history = []; 94 | this.redoHistory = []; 95 | }, 96 | 97 | // Ask for the size of the un/redo histories. 98 | historySize: function() { 99 | return {undo: this.history.length, redo: this.redoHistory.length}; 100 | }, 101 | 102 | // Push a changeset into the document. 103 | push: function(from, to, lines) { 104 | var chain = []; 105 | for (var i = 0; i < lines.length; i++) { 106 | var end = (i == lines.length - 1) ? to : this.container.ownerDocument.createElement("BR"); 107 | chain.push({from: from, to: end, text: cleanText(lines[i])}); 108 | from = end; 109 | } 110 | this.pushChains([chain], from == null && to == null); 111 | this.notifyEnvironment(); 112 | }, 113 | 114 | pushChains: function(chains, doNotHighlight) { 115 | this.commit(doNotHighlight); 116 | this.addUndoLevel(this.updateTo(chains, "applyChain")); 117 | this.redoHistory = []; 118 | }, 119 | 120 | // Retrieve a DOM node from a chain (for scrolling to it after undo/redo). 121 | chainNode: function(chains) { 122 | for (var i = 0; i < chains.length; i++) { 123 | var start = chains[i][0], node = start && (start.from || start.to); 124 | if (node) return node; 125 | } 126 | }, 127 | 128 | // Clear the undo history, make the current document the start 129 | // position. 130 | reset: function() { 131 | this.history = []; this.redoHistory = []; 132 | }, 133 | 134 | textAfter: function(br) { 135 | return this.after(br).text; 136 | }, 137 | 138 | nodeAfter: function(br) { 139 | return this.after(br).to; 140 | }, 141 | 142 | nodeBefore: function(br) { 143 | return this.before(br).from; 144 | }, 145 | 146 | // Commit unless there are pending dirty nodes. 147 | tryCommit: function() { 148 | if (!window.UndoHistory) return; // Stop when frame has been unloaded 149 | if (this.editor.highlightDirty()) this.commit(true); 150 | else this.scheduleCommit(); 151 | }, 152 | 153 | // Check whether the touched nodes hold any changes, if so, commit 154 | // them. 155 | commit: function(doNotHighlight) { 156 | this.parent.clearTimeout(this.commitTimeout); 157 | // Make sure there are no pending dirty nodes. 158 | if (!doNotHighlight) this.editor.highlightDirty(true); 159 | // Build set of chains. 160 | var chains = this.touchedChains(), self = this; 161 | 162 | if (chains.length) { 163 | this.addUndoLevel(this.updateTo(chains, "linkChain")); 164 | this.redoHistory = []; 165 | this.notifyEnvironment(); 166 | } 167 | }, 168 | 169 | // [ end of public interface ] 170 | 171 | // Update the document with a given set of chains, return its 172 | // shadow. updateFunc should be "applyChain" or "linkChain". In the 173 | // second case, the chains are taken to correspond the the current 174 | // document, and only the state of the line data is updated. In the 175 | // first case, the content of the chains is also pushed iinto the 176 | // document. 177 | updateTo: function(chains, updateFunc) { 178 | var shadows = [], dirty = []; 179 | for (var i = 0; i < chains.length; i++) { 180 | shadows.push(this.shadowChain(chains[i])); 181 | dirty.push(this[updateFunc](chains[i])); 182 | } 183 | if (updateFunc == "applyChain") 184 | this.notifyDirty(dirty); 185 | return shadows; 186 | }, 187 | 188 | // Notify the editor that some nodes have changed. 189 | notifyDirty: function(nodes) { 190 | forEach(nodes, method(this.editor, "addDirtyNode")) 191 | this.editor.scheduleHighlight(); 192 | }, 193 | 194 | notifyEnvironment: function() { 195 | if (this.onChange) this.onChange(); 196 | // Used by the line-wrapping line-numbering code. 197 | if (window.frameElement && window.frameElement.CodeMirror.updateNumbers) 198 | window.frameElement.CodeMirror.updateNumbers(); 199 | }, 200 | 201 | // Link a chain into the DOM nodes (or the first/last links for null 202 | // nodes). 203 | linkChain: function(chain) { 204 | for (var i = 0; i < chain.length; i++) { 205 | var line = chain[i]; 206 | if (line.from) line.from.historyAfter = line; 207 | else this.first = line; 208 | if (line.to) line.to.historyBefore = line; 209 | else this.last = line; 210 | } 211 | }, 212 | 213 | // Get the line object after/before a given node. 214 | after: function(node) { 215 | return node ? node.historyAfter : this.first; 216 | }, 217 | before: function(node) { 218 | return node ? node.historyBefore : this.last; 219 | }, 220 | 221 | // Mark a node as touched if it has not already been marked. 222 | setTouched: function(node) { 223 | if (node) { 224 | if (!node.historyTouched) { 225 | this.touched.push(node); 226 | node.historyTouched = true; 227 | } 228 | } 229 | else { 230 | this.firstTouched = true; 231 | } 232 | }, 233 | 234 | // Store a new set of undo info, throw away info if there is more of 235 | // it than allowed. 236 | addUndoLevel: function(diffs) { 237 | this.history.push(diffs); 238 | if (this.history.length > this.maxDepth) 239 | this.history.shift(); 240 | }, 241 | 242 | // Build chains from a set of touched nodes. 243 | touchedChains: function() { 244 | var self = this; 245 | 246 | // The temp system is a crummy hack to speed up determining 247 | // whether a (currently touched) node has a line object associated 248 | // with it. nullTemp is used to store the object for the first 249 | // line, other nodes get it stored in their historyTemp property. 250 | var nullTemp = null; 251 | function temp(node) {return node ? node.historyTemp : nullTemp;} 252 | function setTemp(node, line) { 253 | if (node) node.historyTemp = line; 254 | else nullTemp = line; 255 | } 256 | 257 | function buildLine(node) { 258 | var text = []; 259 | for (var cur = node ? node.nextSibling : self.container.firstChild; 260 | cur && !isBR(cur); cur = cur.nextSibling) 261 | if (cur.currentText) text.push(cur.currentText); 262 | return {from: node, to: cur, text: cleanText(text.join(""))}; 263 | } 264 | 265 | // Filter out unchanged lines and nodes that are no longer in the 266 | // document. Build up line objects for remaining nodes. 267 | var lines = []; 268 | if (self.firstTouched) self.touched.push(null); 269 | forEach(self.touched, function(node) { 270 | if (node && node.parentNode != self.container) return; 271 | 272 | if (node) node.historyTouched = false; 273 | else self.firstTouched = false; 274 | 275 | var line = buildLine(node), shadow = self.after(node); 276 | if (!shadow || shadow.text != line.text || shadow.to != line.to) { 277 | lines.push(line); 278 | setTemp(node, line); 279 | } 280 | }); 281 | 282 | // Get the BR element after/before the given node. 283 | function nextBR(node, dir) { 284 | var link = dir + "Sibling", search = node[link]; 285 | while (search && !isBR(search)) 286 | search = search[link]; 287 | return search; 288 | } 289 | 290 | // Assemble line objects into chains by scanning the DOM tree 291 | // around them. 292 | var chains = []; self.touched = []; 293 | forEach(lines, function(line) { 294 | // Note that this makes the loop skip line objects that have 295 | // been pulled into chains by lines before them. 296 | if (!temp(line.from)) return; 297 | 298 | var chain = [], curNode = line.from, safe = true; 299 | // Put any line objects (referred to by temp info) before this 300 | // one on the front of the array. 301 | while (true) { 302 | var curLine = temp(curNode); 303 | if (!curLine) { 304 | if (safe) break; 305 | else curLine = buildLine(curNode); 306 | } 307 | chain.unshift(curLine); 308 | setTemp(curNode, null); 309 | if (!curNode) break; 310 | safe = self.after(curNode); 311 | curNode = nextBR(curNode, "previous"); 312 | } 313 | curNode = line.to; safe = self.before(line.from); 314 | // Add lines after this one at end of array. 315 | while (true) { 316 | if (!curNode) break; 317 | var curLine = temp(curNode); 318 | if (!curLine) { 319 | if (safe) break; 320 | else curLine = buildLine(curNode); 321 | } 322 | chain.push(curLine); 323 | setTemp(curNode, null); 324 | safe = self.before(curNode); 325 | curNode = nextBR(curNode, "next"); 326 | } 327 | chains.push(chain); 328 | }); 329 | 330 | return chains; 331 | }, 332 | 333 | // Find the 'shadow' of a given chain by following the links in the 334 | // DOM nodes at its start and end. 335 | shadowChain: function(chain) { 336 | var shadows = [], next = this.after(chain[0].from), end = chain[chain.length - 1].to; 337 | while (true) { 338 | shadows.push(next); 339 | var nextNode = next.to; 340 | if (!nextNode || nextNode == end) 341 | break; 342 | else 343 | next = nextNode.historyAfter || this.before(end); 344 | // (The this.before(end) is a hack -- FF sometimes removes 345 | // properties from BR nodes, in which case the best we can hope 346 | // for is to not break.) 347 | } 348 | return shadows; 349 | }, 350 | 351 | // Update the DOM tree to contain the lines specified in a given 352 | // chain, link this chain into the DOM nodes. 353 | applyChain: function(chain) { 354 | // Some attempt is made to prevent the cursor from jumping 355 | // randomly when an undo or redo happens. It still behaves a bit 356 | // strange sometimes. 357 | var cursor = select.cursorPos(this.container, false), self = this; 358 | 359 | // Remove all nodes in the DOM tree between from and to (null for 360 | // start/end of container). 361 | function removeRange(from, to) { 362 | var pos = from ? from.nextSibling : self.container.firstChild; 363 | while (pos != to) { 364 | var temp = pos.nextSibling; 365 | removeElement(pos); 366 | pos = temp; 367 | } 368 | } 369 | 370 | var start = chain[0].from, end = chain[chain.length - 1].to; 371 | // Clear the space where this change has to be made. 372 | removeRange(start, end); 373 | 374 | // Insert the content specified by the chain into the DOM tree. 375 | for (var i = 0; i < chain.length; i++) { 376 | var line = chain[i]; 377 | // The start and end of the space are already correct, but BR 378 | // tags inside it have to be put back. 379 | if (i > 0) 380 | self.container.insertBefore(line.from, end); 381 | 382 | // Add the text. 383 | var node = makePartSpan(fixSpaces(line.text), this.container.ownerDocument); 384 | self.container.insertBefore(node, end); 385 | // See if the cursor was on this line. Put it back, adjusting 386 | // for changed line length, if it was. 387 | if (cursor && cursor.node == line.from) { 388 | var cursordiff = 0; 389 | var prev = this.after(line.from); 390 | if (prev && i == chain.length - 1) { 391 | // Only adjust if the cursor is after the unchanged part of 392 | // the line. 393 | for (var match = 0; match < cursor.offset && 394 | line.text.charAt(match) == prev.text.charAt(match); match++); 395 | if (cursor.offset > match) 396 | cursordiff = line.text.length - prev.text.length; 397 | } 398 | select.setCursorPos(this.container, {node: line.from, offset: Math.max(0, cursor.offset + cursordiff)}); 399 | } 400 | // Cursor was in removed line, this is last new line. 401 | else if (cursor && (i == chain.length - 1) && cursor.node && cursor.node.parentNode != this.container) { 402 | select.setCursorPos(this.container, {node: line.from, offset: line.text.length}); 403 | } 404 | } 405 | 406 | // Anchor the chain in the DOM tree. 407 | this.linkChain(chain); 408 | return start; 409 | } 410 | }; 411 | -------------------------------------------------------------------------------- /dbtemplates/static/dbtemplates/js/codemirror.js: -------------------------------------------------------------------------------- 1 | /* CodeMirror main module 2 | * 3 | * Implements the CodeMirror constructor and prototype, which take care 4 | * of initializing the editor frame, and providing the outside interface. 5 | */ 6 | 7 | // The CodeMirrorConfig object is used to specify a default 8 | // configuration. If you specify such an object before loading this 9 | // file, the values you put into it will override the defaults given 10 | // below. You can also assign to it after loading. 11 | var CodeMirrorConfig = window.CodeMirrorConfig || {}; 12 | 13 | var CodeMirror = (function(){ 14 | function setDefaults(object, defaults) { 15 | for (var option in defaults) { 16 | if (!object.hasOwnProperty(option)) 17 | object[option] = defaults[option]; 18 | } 19 | } 20 | function forEach(array, action) { 21 | for (var i = 0; i < array.length; i++) 22 | action(array[i]); 23 | } 24 | 25 | // These default options can be overridden by passing a set of 26 | // options to a specific CodeMirror constructor. See manual.html for 27 | // their meaning. 28 | setDefaults(CodeMirrorConfig, { 29 | stylesheet: [], 30 | path: "", 31 | parserfile: [], 32 | basefiles: ["util.js", "stringstream.js", "select.js", "undo.js", "editor.js", "tokenize.js"], 33 | iframeClass: null, 34 | passDelay: 200, 35 | passTime: 50, 36 | lineNumberDelay: 200, 37 | lineNumberTime: 50, 38 | continuousScanning: false, 39 | saveFunction: null, 40 | onChange: null, 41 | undoDepth: 50, 42 | undoDelay: 800, 43 | disableSpellcheck: true, 44 | textWrapping: true, 45 | readOnly: false, 46 | width: "", 47 | height: "300px", 48 | autoMatchParens: false, 49 | parserConfig: null, 50 | tabMode: "indent", // or "spaces", "default", "shift" 51 | reindentOnLoad: false, 52 | activeTokens: null, 53 | cursorActivity: null, 54 | lineNumbers: false, 55 | indentUnit: 2, 56 | domain: null 57 | }); 58 | 59 | function addLineNumberDiv(container) { 60 | var nums = document.createElement("DIV"), 61 | scroller = document.createElement("DIV"); 62 | nums.style.position = "absolute"; 63 | nums.style.height = "100%"; 64 | if (nums.style.setExpression) { 65 | try {nums.style.setExpression("height", "this.previousSibling.offsetHeight + 'px'");} 66 | catch(e) {} // Seems to throw 'Not Implemented' on some IE8 versions 67 | } 68 | nums.style.top = "0px"; 69 | nums.style.overflow = "hidden"; 70 | container.appendChild(nums); 71 | scroller.className = "CodeMirror-line-numbers"; 72 | nums.appendChild(scroller); 73 | scroller.innerHTML = "
1
"; 74 | return nums; 75 | } 76 | 77 | function frameHTML(options) { 78 | if (typeof options.parserfile == "string") 79 | options.parserfile = [options.parserfile]; 80 | if (typeof options.stylesheet == "string") 81 | options.stylesheet = [options.stylesheet]; 82 | 83 | var html = [""]; 84 | // Hack to work around a bunch of IE8-specific problems. 85 | html.push(""); 86 | forEach(options.stylesheet, function(file) { 87 | html.push(""); 88 | }); 89 | forEach(options.basefiles.concat(options.parserfile), function(file) { 90 | if (!/^https?:/.test(file)) file = options.path + file; 91 | html.push("