Skip to main content

vintasend-django-templates-manager

The Django storage backend for vintasend-managed-templates: a DjangoTemplateManager implementing the storage seam, the models behind it, and an admin for the people who edit the copy.

vintasend-managed-templates defines what a managed template is — versions, a draft/active/inactive/archived lifecycle with an audit trail, tags, filtering, and composition. It deliberately does not say where any of it lives. This package puts it in your Django database.

Install

poetry add vintasend-django-templates-manager
# or
pip install vintasend-django-templates-manager

Python 3.12–3.14, Django 4.2–6.0.

Add the app and migrate:

# settings.py
INSTALLED_APPS = [
    ...,
    "vintasend_django_templates_manager",
]
python manage.py migrate

Wiring it up

DjangoTemplateManager is the backend; hand it to the renderer and the service from vintasend-managed-templates:

from vintasend_managed_templates.managed_template_renderer import ManagedTemplateEmailRenderer
from vintasend_managed_templates.managed_template_service import ManagedTemplateService

from vintasend_django_templates_manager.django_templates_manager import DjangoTemplateManager

manager_backend = DjangoTemplateManager()
renderer = ManagedTemplateEmailRenderer(manager_backend, inner_renderer)
service = ManagedTemplateService(manager_backend, renderer)

inner_renderer is any vintasend email renderer. It receives template source rather than a template name, so a renderer that resolves names through a loader — DjangoTemplatedEmailRenderer included — needs a loader that accepts source. See What the inner renderer has to do.

Everything else — creating templates, publishing versions, tagging, filtering — is ManagedTemplateService's API, unchanged.

What is stored

Model What it holds
ManagedTemplate One version of a template. (key, version) is unique, and a key has as many rows as it has versions. Nothing about a row changes once it exists except its status, its tags, and the derived is_abstract flag.
ManagedTemplateStatusRecord The audit trail: who moved a version to which status, and when.
ManagedTemplateTag A label shared across templates, identified by the slug vintasend_managed_templates.tags.slugify_tag derives from its text.

Versions are the reason a published template can never change under a notification that already referenced it: update_template inserts the next version and leaves its predecessor exactly as it was, content, status and history alike.

Filtering and ordering

Every filter the library's vocabulary defines is translated into a Django Q and answered by the database, so this backend declines nothing. What get_filter_capabilities reports is the other direction — what it can do that the library does not assume:

service.get_backend_supported_filter_capabilities()
# {..., 'orderBy.key': True, 'orderBy.name': True, 'orderBy.version': True,
#       'orderBy.status': True, 'orderBy.createdAt': True, 'orderBy.updatedAt': True}

The six orderBy.* keys are declared explicitly because they default to False in the library: ordering is newer vocabulary than the filters, so a backend that can sort has to say so rather than be assumed to. All six are real indexed columns on ManagedTemplate, so each is answered by the database:

service.get_paginated_templates(
    page=1, page_size=20, order_by={"field": "version", "direction": "desc"}
)

Two details worth knowing:

  • The order is composed into the SQL, not applied to the page. A page ordered after it was chosen sorts rows within the page while the rows selected for it came back in the store's own order — right on page 1, wrong on every page after it.
  • version is a PositiveIntegerField, so v10 sorts after v2. A store keeping versions as strings gets that wrong silently, which is why every orderable field is pinned by a test that runs the sort rather than reads the column definition.

An unordered read still orders by -created, -id: a key has a row per version, so an unordered offset page is free to return one row twice and skip another.

The admin

All three models are registered.

  • Templateskey and version lock once the row exists, since they are its identity. Every status change made here is written to the audit trail, which is inlined read-only on the page.
  • Tags — the slug is derived from the text on save rather than typed in, and collides safely (black-friday-2). Archive and restore are bulk actions; archiving retires a tag from the pickers without severing it from the templates carrying it.
  • Status history — browsable, never editable.

Composition

A template stored here can build on another one: extend a base, fill its hole, override its blocks, splice in a shared fragment. All of it is resolved before the template engine runs, so what Django's engine receives is one flat string with its own syntax untouched.

base-email    <html><body>
                {% managed_block header %}<h1>Acme</h1>{% managed_endblock %}
                {% managed_children %}
                {% managed_include "footer" %}
              </body></html>

welcome       {% managed_extends "base-email" %}
              {% managed_block header %}<h1>Welcome!</h1>{% managed_endblock %}
              <p>Hi {{ name }}, welcome aboard.</p>

The tag language, the abstract-template rule and the version semantics are vintasend-managed-templates': see Composition. Composition itself is read off the template source — there is no separate model for it, no join, and nothing to keep in step except the one flag below.

What this package adds is the editing side:

  • The admin refuses what will not compose. Saving a template runs every one of its three sources through the composer against the database. A base that does not exist, a block left open, a chain of bases that loops back to the row being edited — each becomes an error on the field that carries it, rather than a notification that fails to send days later. Set validate_composition = False on a ManagedTemplateAdminForm subclass to save a template whose base has not been written yet.
  • The change form previews the result. A read-only composed body under the content fields shows what the engine will actually receive, resolved against whatever the referenced templates are now.
  • The changelist marks the bases, and filters on them, through the stored flag below.

is_abstract

Whether a template is a base to build on rather than one to send is a fact about its source. It is also the thing a "pick a template" screen most needs to filter on, so it is denormalized onto a column:

ManagedTemplate.objects.sendable()  # what a picker should offer
ManagedTemplate.objects.abstract()  # the bases
service.get_filtered_templates({"is_abstract": False})  # the same, through the library

ManagedTemplate.save() derives it, so every write path keeps it honest — the admin, DjangoTemplateManager, a data migration, a shell session. It is editable=False: nobody types it in, and a template whose composition tags are malformed saves as concrete rather than blowing up a write (the admin form has already refused that template anyway).

To recompute rather than trust the column — for a row edited in memory, or one written before the column existed:

from vintasend_django_templates_manager.composition import template_is_abstract

template_is_abstract(template)  # raises on a malformed tag
template_is_abstract(template, strict=False)  # reads an unparseable template as concrete

Composition resolves references through this backend, which means an unpinned {% managed_extends "base-email" %} picks up the latest version of that key, draft included — the same rule get_template(key) follows everywhere else. Pin it with version=2 when a template has to keep composing against an exact base.

Pinning the notification rather than the base is vintasend's job, through requested_template_version — see Template Version Pinning. This app stores those templates; it does not store the notifications.

Development

poetry install
poetry run pytest
poetry run mypy
poetry run tox                     # the Python × Django matrix
poetry run pre-commit run --all-files   # ruff lint + format

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

vintasend_django_templates_manager-3.1.1.tar.gz (29.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

File details

Details for the file vintasend_django_templates_manager-3.1.1.tar.gz.

File metadata

File hashes

Hashes for vintasend_django_templates_manager-3.1.1.tar.gz
Algorithm Hash digest
SHA256 b72159f5d7318f05a72a376627b852d9bf06d50071772477109db40bb29cc298
MD5 247bff6d60e0e950d6fc6c1fb932c6a3
BLAKE2b-256 12a3256e7977859b56d4740984615662a9e3ac56387e872e4be57f0a74053576

See more details on using hashes here.

Provenance

The following attestation bundles were made for vintasend_django_templates_manager-3.1.1.tar.gz:

Publisher: publish.yml on vintasoftware/vintasend-django-templates-manager

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file vintasend_django_templates_manager-3.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for vintasend_django_templates_manager-3.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 f787aafc25b28f6e11992a2e164a7f8e4c7882b206a0fc4ed7630f0917eebfcd
MD5 ac0b2e611517f4657bee97f7047eb0eb
BLAKE2b-256 c5a2987229a2590f84405be39cfe7ef2aa0952ba2c7b474dc6a4e35190830f51

See more details on using hashes here.

Provenance

The following attestation bundles were made for vintasend_django_templates_manager-3.1.1-py3-none-any.whl:

Publisher: publish.yml on vintasoftware/vintasend-django-templates-manager

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

3.1.1 This release

2 files

3.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page