Metadata-Version: 2.4
Name: BmorricalDjangoWorkflows
Version: 2.1.8
Summary: Reusable Django Workflows
Home-page: https://github.com/Bmorrical/django-workflows
Author: Bradley Morrical
Classifier: Programming Language :: Python :: 3
Classifier: Framework :: Django
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django>=4.0
Requires-Dist: djangorestframework>=3.14
Requires-Dist: requests>=2.31.0
Dynamic: author
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license-file
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# 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:

```bash
pip install BmorricalDjangoWorkflows==2.1.8
```

In `requirements.txt`, pin it directly:

```txt
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:

```python
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:

```python
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 currently uses an April 1 fiscal-year boundary. Hosts should treat fiscal calendar, currency, locale, terminology, transaction workflow, and storage retention as 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.

```python
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:

```python
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:

```bash
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.

### 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:

```bash
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`.

```python
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

```python
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")`

```python
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:

```bash
make test
```

Run tests with coverage:

```bash
make test-coverage
```

CI runs tests on push and pull requests using [`.github/workflows/tests.yml`](.github/workflows/tests.yml).

## Troubleshooting

### ImproperlyConfigured for USER_META_MODEL

Error example:

```text
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:

```bash
./release.sh <version>
```

At present, that tag invokes [`.github/workflows/publish.yml`](.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.
