Metadata-Version: 2.4
Name: BmorricalDjangoWorkflows
Version: 2.1.4
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 from PyPI:

```bash
pip install BmorricalDjangoWorkflows==1.2.3
```

In `requirements.txt`, pin it directly:

```txt
BmorricalDjangoWorkflows==1.2.3
```

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

## 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.
	"COMPANY_NAME": "My Company",
	"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`.
- `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",
	"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-separated list
- `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

Update code and ./VERSION

Commit changes

Create a release tag:

```bash
./release.sh 0.1.0
```

After release, update the package version in your consuming application's dependency files.

Tag pushes like `v1.2.3` trigger [`.github/workflows/publish.yml`](.github/workflows/publish.yml), which publishes to PyPI.

## Publishing To PyPI (Maintainer Steps)

1. Check whether the target package name is available.

```bash
curl -I https://pypi.org/pypi/BmorricalDjangoWorkflows/json
```

If this returns 404, the name is usually available.

2. Create a PyPI account: https://pypi.org/account/register/

3. Create a PyPI API token: PyPI Account -> Account settings -> API tokens.

4. Add repository secret in GitHub:

- Name: `PYPI_API_TOKEN`
- Value: your PyPI token (starts with `pypi-`)

5. Bump and tag a release:

```bash
./release.sh 1.2.3
```

6. Confirm the workflow run succeeded and the package appears on PyPI.

7. Update consuming applications:

```txt
BmorricalDjangoWorkflows==1.2.3
```

If upload fails because the name is already taken, change `name=` in [setup.py](setup.py), bump version, and release again.
