Skip to main content

django-workflows

Reusable Django service helpers for common account flows:

  • Registration and verification-code email flow
  • Forgot-password code flow
  • Reset-code validation and lockout handling
  • Transport-neutral email utility with recipient validation

Installation

Install the private distribution from the configured package source:

pip install BmorricalDjangoWorkflows==2.1.8

In requirements.txt, pin it directly:

BmorricalDjangoWorkflows==2.1.8

Note: distribution name is BmorricalDjangoWorkflows, while imports remain under django_workflows.

Consumption policy:

  • Pin an exact reviewed version in every deployed host.
  • Do not deploy from an editable install, sibling path, floating branch, or unpinned VCS reference.
  • Keep private-index credentials in developer, CI, and deployment environments rather than dependency files.
  • Rebuild and test each host after changing the package pin.

The current tag workflow uploads Python artifacts to public PyPI. That conflicts with a private-distribution policy. Do not push a release tag containing private-only work until the workflow and consuming environments are configured for an approved private Python package source.

Ledger Application

django_workflows.ledger is a concrete reusable Django application for tenant-scoped funds, vendors, line items, budgets, transactions, documents, reports, and history APIs.

The package owns the Ledger models, initial migration, serializers, services, permissions, default views, URL patterns, and admin registrations. The host owns tenant resolution, per-tenant module enablement, audit context and database triggers, structured logging, storage configuration, authentication, and deployment infrastructure.

Required host settings:

LEDGER_TENANT_MODEL = "tenant.Tenant"

LEDGER = {
	"ENABLED": True,
	"DATABASE_ALIAS": "default",
	"TENANT_ID_RESOLVER": "myproject.ledger_adapters.user_tenant_id",
	"TENANT_ENABLED_CHECKER": "myproject.ledger_adapters.tenant_has_ledger_enabled",
	"AUDIT_CONTEXT": "myproject.audit.edit_history_context",
	"EVENT_LOGGER": "myproject.ledger_adapters.log_ledger_event",
	"HISTORY_MODEL": "edit_history.EditHistory",
	"ACCESS_GROUP_NAMES": ("SUPER_USER", "LEDGER_ADMIN", "LEDGER_AUDITOR"),
	"EDIT_GROUP_NAMES": ("SUPER_USER", "LEDGER_ADMIN"),
}

Install the app and mount its stable default routes:

INSTALLED_APPS = [
	# Host tenant and history applications must also be installed.
	"django_workflows.ledger.apps.LedgerConfig",
]

urlpatterns = [
	path("api/", include("django_workflows.ledger.urls")),
]

The tenant resolver receives the authenticated user and returns one tenant ID. Domain services receive that tenant ID explicitly. The tenant enablement checker receives a tenant ID and returns a boolean.

The default audit adapter is intentionally a no-op so the package can be imported in isolated tooling. A host requiring complete auditability must configure AUDIT_CONTEXT and install database triggers for every Ledger table; service hooks alone do not capture direct ORM, admin, or script writes.

The package owns Ledger domain migrations. The ledger app label, model names, and existing database table names are compatibility contracts. A host may own later trigger migrations that depend on package migrations because those triggers reference host audit tables. Moving an existing local Ledger app into this package without rebuilding also requires an explicit migration-history takeover plan; sharing the app label alone does not make every in-place upgrade safe.

Ledger stores a recurring fiscal-year start month and day on each tenant's LedgerConfiguration. April 1 remains the compatibility default, while valid dates such as January 1 and May 17 are supported. The configured boundary drives default fiscal years, transaction-to-line-item validation, budgets, commissioners reports, and dashboard periods. Fiscal years are named for the calendar year in which they start; for example, a May 17 FY 2026 runs from May 17, 2026 through May 16, 2027.

Dashboard dateStart and dateEnd filters scope income, expenses, adjustments, and recent transactions. Balance remains cumulative through dateEnd; omitting both dates produces All Time results. Hosts should present the fiscal calendar as tenant-scoped Ledger configuration. Currency, locale, terminology, transaction workflow, and storage retention remain product configuration still to be defined.

Host Project Requirements

This package expects the host Django project to provide:

  • A Django user model available through django.contrib.auth.get_user_model()
  • A user meta model referenced by WORKFLOWS["USER_META_MODEL"]
  • A user manager create_user(...) method that accepts username, because this package explicitly sets username=email

The user meta model should include at least:

  • user relation
  • verify_code field
  • verify_time field
  • attempts field

Optional (used if present):

  • force_password_reset

Django Settings

Add a WORKFLOWS dict in your Django settings.

WORKFLOWS = {
	# Required in most projects unless your model path matches the default.
	"USER_META_MODEL": "users.models_user_meta.UserMeta",

	# Optional settings with defaults shown.
	"BCC_RECIPIENTS": os.getenv("BCC_RECIPIENTS", ""),
	"COMPANY_NAME": "My Company",
	"EMAIL_TEMPLATE_RENDERER": "project.utils.emails.render_branded_email",
	"ENABLE_EMAIL_DELIVERY": False,
	"FORGOT_PASSWORD_URL": "https://app.example.com/forgot-password",
	"RESET_CODE_TTL_MINUTES": 10,
	"MAX_VERIFY_ATTEMPTS": 3,
}

Notes:

  • USER_META_MODEL must be a dotted import path such as myapp.models.UserMeta.
  • EMAIL_TEMPLATE_RENDERER can point at a callable that receives body_html and an optional title keyword and returns branded HTML for package-owned auth emails.
  • BCC_RECIPIENTS is read at send time when provided in WORKFLOWS, avoiding import-order dependencies in host settings.
  • FORGOT_PASSWORD_URL is used in the admin-created account email to link users into the host app's forgot-password flow.
  • Host apps will usually source FORGOT_PASSWORD_URL from an environment variable in their own settings module.
  • If ENABLE_EMAIL_DELIVERY is true, configure the email transport environment variables used by the mail service.

Example host-project wiring:

import os

WORKFLOWS = {
	"USER_META_MODEL": "users.models_user_meta.UserMeta",
	"EMAIL_TEMPLATE_RENDERER": "project.utils.emails.render_branded_email",
	"FORGOT_PASSWORD_URL": os.getenv("FORGOT_PASSWORD_URL", ""),
}

Email Delivery Environment Variables

When email delivery is enabled, set:

  • ENABLE_EMAIL_DELIVERY=true
  • EMAIL_DELIVERY_PROVIDER=mailgun|mailpit|smtp|disabled
  • EMAIL_FROM="My Company <no-reply@example.com>"

For the mailgun provider, also set:

  • EMAIL_API_KEY
  • EMAIL_DOMAIN

Optional:

  • BCC_RECIPIENTS as a comma-, semicolon-, or newline-separated list. A host can instead provide it as WORKFLOWS["BCC_RECIPIENTS"] for runtime configuration.
  • EMAIL_REPLY_TO as a single email or comma-separated list for reply handling
  • EMAIL_LOG_RESPONSE_TEXT=true to log response bodies at debug level

Example:

ENABLE_EMAIL_DELIVERY=true
EMAIL_DELIVERY_PROVIDER=mailgun
EMAIL_FROM="My Company <no-reply@example.com>"
EMAIL_DOMAIN="mg.example.com"
EMAIL_API_KEY="key-example"
BCC_RECIPIENTS="audit@example.com,ops@example.com"
EMAIL_REPLY_TO="support@example.com"

If BCC_RECIPIENTS is not set, no static BCC recipients are added.

Email Delivery Logs

Email delivery emits structured fields that JSON log formatters can expose to Datadog:

  • event_name=email.recipients_prepared includes email.provider, recipient counts, masked email.bcc_recipients, and email.mailgun_domain for Mailgun sends.
  • event_name=email.provider_response includes outcome, email.response_code, email.message_id, and email.mailgun_domain.
  • A non-2xx provider response is logged at warning level with outcome=rejected; a 2xx response uses outcome=accepted.

Recipient local parts are masked in logs. The Mailgun message ID can be used to find the corresponding accepted, delivered, suppressed, or failed event in Mailgun without logging message content or full recipient addresses.

Old To New Env Mapping

If a consuming app previously used the Mailgun-specific names, update them as follows:

  • MAILGUN_COMPANY_NAME -> EMAIL_FROM
  • ENABLE_MAILGUN -> ENABLE_EMAIL_DELIVERY
  • MAILGUN_LOG_RESPONSE_TEXT -> EMAIL_LOG_RESPONSE_TEXT
  • MAILGUN_API_KEY -> EMAIL_API_KEY
  • MAILGUN_DOMAIN -> EMAIL_DOMAIN
  • MAILGUN_BCC_RECIPIENTS -> BCC_RECIPIENTS
  • no old equivalent -> EMAIL_REPLY_TO

Built-In Defaults

If a consuming app does not define one of these values in .env, the package now defaults to:

  • ENABLE_EMAIL_DELIVERY=False
  • EMAIL_API_KEY=""
  • EMAIL_DOMAIN=""
  • EMAIL_DELIVERY_PROVIDER=""
  • DEVELOPMENT_MODE=False
  • DEFAULT_FROM_EMAIL=""
  • BCC_RECIPIENTS=""
  • EMAIL_REPLY_TO=""
  • EMAIL_LOG_RESPONSE_TEXT=False

Additional behavior:

  • EMAIL_FROM falls back to DEFAULT_FROM_EMAIL when EMAIL_FROM is unset.
  • If EMAIL_DELIVERY_PROVIDER is blank, the package resolves the transport from the other flags:
  • If ENABLE_EMAIL_DELIVERY=true, it defaults to the mailgun transport.
  • If DEVELOPMENT_MODE=true, it defaults to the smtp transport.
  • Otherwise delivery remains disabled.

Practical implication:

  • Production apps using Mailgun should set ENABLE_EMAIL_DELIVERY, EMAIL_FROM, EMAIL_API_KEY, and EMAIL_DOMAIN.
  • If replies should go to a real inbox, also set EMAIL_REPLY_TO.
  • Local apps using Mailpit should usually set EMAIL_DELIVERY_PROVIDER=mailpit, EMAIL_FROM, and the Django SMTP settings shown below.
  • Apps that do not want this package to send email can omit everything and leave delivery disabled.

Example for a branded sender with a monitored reply inbox:

EMAIL_FROM="Bourbonnais Township Highway Department <no-reply@mail.bthwy.org>"
EMAIL_REPLY_TO="office@bthwy.org"

This keeps the authenticated sender in the From header while directing user replies to the EMAIL_REPLY_TO inbox.

Django Email Backend Settings

If a consuming app uses EMAIL_DELIVERY_PROVIDER=smtp or EMAIL_DELIVERY_PROVIDER=mailpit, it should configure Django's email backend in settings.py.

import os

EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL")
EMAIL_HOST = os.getenv("EMAIL_HOST", "localhost")
EMAIL_PORT = int(os.getenv("EMAIL_PORT", "25"))
EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "")
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "")
EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "False") == "True"
EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", "False") == "True"

Notes:

  • This is only needed for the smtp and mailpit providers.
  • mailpit commonly listens on port 1025, so set EMAIL_PORT accordingly in local development.
  • If you set EMAIL_FROM, that value is preferred by this package. Otherwise it falls back to DEFAULT_FROM_EMAIL.

Example Usage

from django_workflows.users.services.auth_flow import (
	register_user_and_send_verification_email,
	send_forgot_password_code,
	verify_reset_code,
	change_password,
)

result = register_user_and_send_verification_email(
	email="person@example.com",
	first_name="First",
	last_name="Last",
	password="example-password",
)

forgot = send_forgot_password_code("person@example.com")

verify = verify_reset_code(email="person@example.com", code="AB12CD")

changed = change_password(
	email="person@example.com",
	password="new-password",
	password_verify="new-password",
)

Direct Email Service Usage

Attachment tuple shape:

  • (filename, file_bytes_or_file_object, mimetype)

Example attachment entry:

  • ("report.pdf", pdf_bytes, "application/pdf")
from django_workflows.services.email import send_email

response = send_email(
	to=["primary@example.com", "secondary@example.com"],
	subject="Welcome",
	html="<p>Thanks for joining.</p>",
	cc="manager@example.com",
	bcc=["audit@example.com"],
	reply_to="support@example.com",
	attachments=[("report.pdf", pdf_bytes, "application/pdf")],
)

# Optional: explicit runtime override for enablement
send_email(
	to="user@example.com",
	subject="Dry run",
	html="<p>This will not send.</p>",
	enabled=False,
)

Local Development

Run tests:

make test

Run tests with coverage:

make test-coverage

CI runs tests on push and pull requests using .github/workflows/tests.yml.

Troubleshooting

ImproperlyConfigured for USER_META_MODEL

Error example:

WORKFLOWS['USER_META_MODEL'] must be a dotted path like 'myapp.models.UserMeta'.

Fix:

  • Set WORKFLOWS["USER_META_MODEL"] to a valid dotted import path.
  • Verify the target model is importable by Django at runtime.

UserMeta field errors

If you see attribute errors around verification state, confirm your user meta model provides:

  • verify_code
  • verify_time
  • attempts

Email delivery not sending

Check:

  • ENABLE_EMAIL_DELIVERY=true
  • EMAIL_DELIVERY_PROVIDER
  • EMAIL_FROM or DEFAULT_FROM_EMAIL
  • EMAIL_API_KEY and EMAIL_DOMAIN when using the Mailgun provider

No static BCC recipients applied

This is expected unless you set BCC_RECIPIENTS.

Release

Releases are maintainer-owned. Before releasing:

  1. Confirm the intended distribution source and artifact visibility.
  2. Run the complete package test suite from a clean environment.
  3. Update VERSION through the reviewed release process.
  4. Review the release commit and tag before pushing.
  5. Confirm the built wheel and source distribution install from the approved package source.
  6. Pin the exact version in each consuming host and rebuild it.
  7. Run host migration, trigger, API, and integration checks.

The included release script creates and pushes a version tag:

./release.sh <version>

At present, that tag invokes .github/workflows/publish.yml, which uploads to public PyPI. Replace or reconfigure that workflow before using the script for a private release. The private index endpoint and credentials must be supplied by the release environment; they must not be committed to this repository.

Download files

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

Source Distribution

bmorricaldjangoworkflows-2.1.14.tar.gz (126.9 kB view details)

Uploaded Source

Built Distribution

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

bmorricaldjangoworkflows-2.1.14-py3-none-any.whl (148.6 kB view details)

Uploaded Python 3

File details

Details for the file bmorricaldjangoworkflows-2.1.14.tar.gz.

File metadata

File hashes

Hashes for bmorricaldjangoworkflows-2.1.14.tar.gz
Algorithm Hash digest
SHA256 3913456f15d5e3d183d0ec8b5a72310cb3ccf730d920ab5f086f99c863b3aaed
MD5 8e0d5aa13944875310c8781bee70a18f
BLAKE2b-256 c8764d4a0f25a001d0ecb20d5c704763f45bbb32989be0016c4c37879f0b0094

See more details on using hashes here.

File details

Details for the file bmorricaldjangoworkflows-2.1.14-py3-none-any.whl.

File metadata

File hashes

Hashes for bmorricaldjangoworkflows-2.1.14-py3-none-any.whl
Algorithm Hash digest
SHA256 bc85553d3151a41ff59eab3ff1c471e9661a6c0cc5fe1130c33e991bedc75342
MD5 404e665c3670d329cb64355d6b7e2eeb
BLAKE2b-256 9cc796d8326f9e299baa0f1762640c43e0e2bdbc351eb180f49adc8a6314aae9

See more details on using hashes here.

Release history Release notifications | RSS feed

2.1.17

2 files

2.1.16

2 files

2.1.15

2 files

This release

2.1.14 This release

2 files

2.1.13

2 files

2.1.11

2 files

2.1.10

2 files

2.1.9

2 files

2.1.8

2 files

2.1.7

2 files

2.1.6

2 files

2.1.5

2 files

2.1.4

2 files

2.1.3

2 files

2.1.2

2 files

2.1.1

2 files

2.1.0

2 files

2.0.4

2 files

2.0.3

2 files

2.0.2

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