Metadata-Version: 2.4
Name: django-heluca-themis
Version: 1.16.0
Summary: Django app providing user preferences, theme management, API key management, Casdoor SSO adapters, and standard navigation templates
Author-email: Robert Helewka <r@helu.ca>
License: MIT
Project-URL: Homepage, https://helu.ca
Project-URL: Repository, ssh://git@git.helu.ca:22022/r/themis.git
Keywords: django,user-profile,themes,daisyui,preferences,heluca
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django<6.0,>=5.2
Requires-Dist: djangorestframework<4.0,>=3.14
Requires-Dist: cryptography<51.0,>=41.0
Requires-Dist: django-allauth[socialaccount]<66.0,>=65.0
Requires-Dist: whitenoise<7,>=6.6
Provides-Extra: dev
Requires-Dist: drf-spectacular>=0.26.0; extra == "dev"
Requires-Dist: django-filter>=23.0; extra == "dev"
Requires-Dist: pyyaml>=6.0; extra == "dev"
Provides-Extra: docs
Requires-Dist: sphinx<10,>=9.0; extra == "docs"
Requires-Dist: heluca-sphinx<1.0,>=0.3; extra == "docs"
Requires-Dist: myst-parser<6,>=5.0; extra == "docs"
Provides-Extra: unfold
Requires-Dist: django-unfold<1.0,>=0.98; extra == "unfold"
Dynamic: license-file

# Themis

Reusable Django app providing user preferences, DaisyUI theme management, API key management, and standard navigation templates for all Heluca applications.

*Themis — titan of order, custom, and law.*

## Features

- **User Preferences** — timezone (home + traveling), date/time/number formatting, week start day
- **Heluca Design System** — the V2 system vendored from `@heluca/svelte`: token layer + daisyUI 5 bridge, `light` / `dark` plus the five temperaments (consulting, business, business-dark, terminal, terminal-light), self-hosted Marcellus, Literata, Public Sans, IBM Plex Sans, JetBrains Mono and Atkinson Hyperlegible; separate light/dark selection, auto (system) mode
- **Notifications** — in-app notification bell, JS polling, browser desktop notifications, user preferences
- **API Key Management** — encrypted storage with per-key instructions and documentation links
- **Standard Navigation** — consistent navbar, user menu, notification bell, theme toggle, and bottom nav across all apps
- **Themed Auth Pages** — every django-allauth page (login, signup, logout, password reset/change, email management, …) rendered in the Themis DaisyUI chrome, with no per-app templates
- **Middleware** — automatic timezone activation and theme context
- **Formatting Utilities** — date, time, number formatting respecting user preferences
- **Health Checks** — Kubernetes-ready `/ready/` and `/live/` endpoints
- **REST API** — complete API for profiles, keys, and notifications

## Installation

Releases are wheels on the estate's Gitea PyPI registry (anonymous read):

```bash
pip install django-heluca-themis --extra-index-url https://git.helu.ca/api/packages/r/pypi/simple/
```

Consumers pin a range in `pyproject.toml` (`django-heluca-themis>=1.13,<2.0`)
and set `PIP_EXTRA_INDEX_URL` in the Dockerfile rather than passing the flag.

## Quick Start

1. Add to `INSTALLED_APPS`:
```python
INSTALLED_APPS = [
    ...
    "rest_framework",
    "themis",
    ...
]
```

2. Configure middleware:
```python
MIDDLEWARE = [
    ...
    "themis.middleware.TimezoneMiddleware",
    "themis.middleware.ThemeMiddleware",
    ...
]
```

3. Configure context processors:
```python
TEMPLATES = [{
    "OPTIONS": {
        "context_processors": [
            ...
            "themis.context_processors.themis_settings",
            "themis.context_processors.user_preferences",
            "themis.context_processors.notifications",
            "themis.context_processors.navigation",
        ],
    },
}]
```

4. Include URLs:
```python
urlpatterns = [
    ...
    path("", include("themis.urls")),
    path("api/v1/", include("themis.api.urls")),
    ...
]
```

5. Configure app settings:
```python
THEMIS_APP_NAME = "My Application"

# Register navigation entries (block-based extension can't cross {% include %},
# so the chrome iterates these instead). Each entry: label, named URL, optional
# SVG icon `d` path. Bad url_names are skipped with a warning, not a 500.
THEMIS_NAV_ITEMS = [
    {"label": "Home", "url_name": "core:home"},
    {"label": "Mail", "url_name": "mail:account_list"},
]
THEMIS_USER_MENU_ITEMS = [
    {"label": "Mail Accounts", "url_name": "mail:account_list"},
]
```

6. Run migrations:
```bash
python manage.py migrate
```

7. Extend the base template:
```html
{% extends "themis/base.html" %}

{% block nav_items %}
<li><a href="{% url 'dashboard' %}">Dashboard</a></li>
{% endblock %}

{% block content %}
<h1 class="text-2xl font-bold">My App</h1>
{% endblock %}
```

## Casdoor SSO (optional)

Themis ships generic django-allauth adapters for Casdoor OIDC. They map the
Casdoor `groups` claim to Django groups, set `is_staff` from configurable
staff groups, and block superusers from SSO (they must use local auth).

```python
SOCIALACCOUNT_ADAPTER = "themis.adapters.CasdoorAccountAdapter"
ACCOUNT_ADAPTER = "themis.adapters.LocalAccountAdapter"
```

Optional settings:

- `THEMIS_STAFF_GROUPS` — Casdoor groups granting `is_staff`
  (default `["staff", "sme", "admin"]`).
- `THEMIS_GROUP_MAPPING` — Casdoor group name → Django group name.
- `THEMIS_ORG_ADAPTER` — dotted path to `fn(user, org_identifier)` invoked
  with the Casdoor `organization` claim, for apps with an Organization model.
  Omit it (the default) to skip organization mapping.
- `THEMIS_TENANT_ADAPTER` — store mode (Agora ADR-0004): dotted path to
  `fn(user, tenant_id, groups)` invoked with the `tenant_id` claim and the
  bare own-org group names, instead of the org adapter, whenever the claim is
  present. Helpers for the projected groups live in `themis.tenancy`.
- `THEMIS_REQUIRED_GROUP_PREFIX` — a product slug; an SSO login holding no
  `<prefix>` / `<prefix>-*` group is refused before provisioning and sent to
  `themis:no-subscription`, which links to `THEMIS_STORE_URL` (required with
  the prefix; system check `themis.E001`). See
  `docs/Pattern_SaaS-Tenancy_V1-01.md` § Store mode.
- `THEMIS_ENCRYPTION_KEY` — any non-empty string; Themis hashes it, so no
  format is required, but it is the passphrase for every stored `UserAPIKey`
  and a long random value is what counts. Generate one with
  `python -c "import secrets; print(secrets.token_urlsafe(48))"`. Required,
  from the environment, no default (system check `themis.E006`). Identical
  across every process/container of one deployment, and never changed once
  `UserAPIKey` rows exist: rows are readable only under the value that wrote
  them, consumers skip undecryptable rows silently (no error), and the only
  remedy is reissuing the keys. Independent of `SECRET_KEY` by design;
  upgrading from a release that derived the key from `SECRET_KEY`: set it to
  the `SECRET_KEY` value in force at the time to keep stored keys readable.
  See `docs/Themis_V1-00.md` § UserAPIKey.
- `THEMIS_PROFILE_SOURCE` — `"local"` (default) keeps preferences on the
  app's own settings page; `"claims"` links the app to the estate's Agora
  (ADR-0018): the projected preference claims (`timezone`, `date_format`,
  `theme_mode`, …) are copied into `UserProfile` at every login — a missing
  claim leaves its field alone — and the settings page turns read-only with
  a "Manage in Agora" link to `{AGORA_URL}/profile/`. `AGORA_URL` is
  required with `"claims"` (system check `themis.E002`; environment value,
  no default). See `docs/Pattern_SSO-Allauth-Casdoor_V1-04.md` § Profile
  Claims.
- `AGORA_URL`, `AGORA_PRODUCT_API_KEY` — the entitlements client
  (`themis.entitlements`: `get_limits`, `check_quota`, `quota_blocked`,
  `report_usage`, `flush_usage`). Both from the environment, no defaults, set
  together (system check `themis.E004`); the usage PUT is named by
  `THEMIS_REQUIRED_GROUP_PREFIX` (`themis.E005`). Optional:
  `THEMIS_TENANT_RESOLVER` (dotted path `fn(request) -> tenant id`; default
  reads `request.tenant.uuid`), `THEMIS_QUOTA_WARN_RATIO` (0.8). Add
  `themis.context_processors.entitlements` for the warn banner and schedule
  `manage.py flush_usage` every minute. See `docs/Pattern_Entitlements_V1-00.md`.

### Reconnecting an account after an IdP reset

OIDC matches a returning user on the subject id allauth stored as
`SocialAccount.uid` — for Casdoor, the user's `id`. Re-provisioning Casdoor
re-issues those ids, so every stored connection is orphaned and SSO login
falls through to auto-signup. Re-point it:

```bash
# Preview the change (old -> new uid), write nothing
python manage.py reconnect_socialaccount user@example.com --uid <new-casdoor-id> --dry-run

python manage.py reconnect_socialaccount user@example.com --uid <new-casdoor-id>
```

Resolves the user by username or email, creates the connection when none
exists, no-ops when the uid already matches, and refuses when that uid
belongs to another user. `--provider` defaults to `casdoor` — the
`provider_id` allauth stores, not `openid_connect`. Full procedure, including
how to do it without this command: [SSO with Allauth & Casdoor](docs/Pattern_SSO-Allauth-Casdoor_V1-04.md#recovering-from-an-idp-reset).

For sandbox environments with self-signed Casdoor certs, import the SSL bypass
at the top of `settings.py` **before** any requests are made (activates only
when `CASDOOR_SSL_VERIFY=false`):

```python
import themis.ssl_patch  # noqa: F401
```

## Documentation

- **[Themis App](docs/Themis_V1-00.md)** — Full documentation
- **[SSO with Allauth & Casdoor](docs/Pattern_SSO-Allauth-Casdoor_V1-04.md)** — Adapters, claim mapping, and IdP-reset recovery
- **[Notification Trigger Pattern](docs/Pattern_Notification_V1-00.md)** — How to trigger notifications from your app
- **[Organization Pattern](docs/Pattern_Organization_V1-00.md)** — Standard Organization model pattern
- **[Ansible Deployment Pattern](docs/Pattern_Ansible-Deploy_V1-00.md)** — Compose + nginx on the private estates
- **[Kubernetes Deployment Pattern](docs/Pattern_Kubernetes_V1-00.md)** — `charts/themis-app` for the public tier; WhiteNoise static standard (`themis.settings.static`)
- **[Red Panda Standards](docs/Red%20Panda%20Standards_Django_V1-00.md)** — Django development standards
- **[Documentation Style Guide](docs/DocumentationStyleGuide_Markdown_V1-00.md)** — Documentation standards

## 🐾 Red Panda Approval™

This project follows Red Panda Approval standards — our gold standard for Django application quality.

### The 5 Sacred Django Criteria
1. **Fresh Migration Test** — Clean migrations from empty database
2. **Elegant Simplicity** — No unnecessary complexity
3. **Observable & Debuggable** — Proper logging and error handling
4. **Consistent Patterns** — Follow Django conventions
5. **Actually Works** — Passes all checks and serves real user needs

## License

MIT License — see [LICENSE](LICENSE) file for details.

## Author

Robert Helewka <r@helu.ca>

## Package Information

- **Package Name:** django-heluca-themis
- **Django:** >=5.2, <6.0
- **Python:** >=3.10
