Add Themis application with custom widgets, views, and utilities
- Implemented custom form widgets for date, time, and datetime fields with DaisyUI styling. - Created utility functions for formatting dates, times, and numbers according to user preferences. - Developed views for profile settings, API key management, and notifications, including health check endpoints. - Added URL configurations for Themis tests and main application routes. - Established test cases for custom widgets to ensure proper functionality and integration. - Defined project metadata and dependencies in pyproject.toml for package management.
This commit is contained in:
475
docs/Themis_V1-00.md
Normal file
475
docs/Themis_V1-00.md
Normal file
@@ -0,0 +1,475 @@
|
||||
# Themis v1.0.0
|
||||
|
||||
Reusable Django app providing user preferences, DaisyUI theme management, API key management, and standard navigation templates.
|
||||
|
||||
**Package:** django-heluca-themis
|
||||
**Django:** >=5.2, <6.0
|
||||
**Python:** >=3.10
|
||||
**License:** MIT
|
||||
|
||||
## 🐾 Red Panda Approval™
|
||||
|
||||
This project follows Red Panda Approval standards.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Themis provides the foundational elements every Django application needs:
|
||||
|
||||
- **UserProfile** — timezone, date/time/number formatting, DaisyUI theme selection
|
||||
- **Notifications** — in-app notification bell, polling, browser desktop notifications, user preferences
|
||||
- **API Key Management** — encrypted storage with per-key instructions
|
||||
- **Standard Navigation** — navbar, user menu, notification bell, theme toggle, bottom nav
|
||||
- **Middleware** — automatic timezone activation and theme context
|
||||
- **Formatting Utilities** — date, time, number formatting respecting user preferences
|
||||
- **Health Checks** — Kubernetes-ready `/ready/` and `/live/` endpoints
|
||||
|
||||
Themis does not provide domain models (Organization, etc.) or notification triggers. Those are documented as patterns for consuming apps to implement.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### From Git Repository
|
||||
|
||||
```bash
|
||||
pip install git+ssh://git@git.helu.ca:22022/r/themis.git
|
||||
```
|
||||
|
||||
### For Local Development
|
||||
|
||||
```bash
|
||||
pip install -e /path/to/themis
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
**settings.py:**
|
||||
|
||||
```python
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
"rest_framework",
|
||||
"themis",
|
||||
...
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
...
|
||||
"themis.middleware.TimezoneMiddleware",
|
||||
"themis.middleware.ThemeMiddleware",
|
||||
...
|
||||
]
|
||||
|
||||
TEMPLATES = [{
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
...
|
||||
"themis.context_processors.themis_settings",
|
||||
"themis.context_processors.user_preferences",
|
||||
"themis.context_processors.notifications",
|
||||
],
|
||||
},
|
||||
}]
|
||||
|
||||
# Themis app settings
|
||||
THEMIS_APP_NAME = "My Application"
|
||||
THEMIS_NOTIFICATION_POLL_INTERVAL = 60 # seconds (0 to disable polling)
|
||||
THEMIS_NOTIFICATION_MAX_AGE_DAYS = 90 # cleanup ceiling for read notifications
|
||||
```
|
||||
|
||||
**urls.py:**
|
||||
|
||||
```python
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
...
|
||||
path("", include("themis.urls")),
|
||||
path("api/v1/", include("themis.api.urls")),
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
**Run migrations:**
|
||||
|
||||
```bash
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Models
|
||||
|
||||
### UserProfile
|
||||
|
||||
Extends Django's User model with display preferences. Automatically created via `post_save` signal when a User is created.
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| user | OneToOneField | required | Link to Django User |
|
||||
| home_timezone | CharField(50) | UTC | Permanent timezone |
|
||||
| current_timezone | CharField(50) | (blank) | Current timezone when traveling |
|
||||
| date_format | CharField(20) | YYYY-MM-DD | Date display format |
|
||||
| time_format | CharField(10) | 24-hour | 12-hour or 24-hour |
|
||||
| thousand_separator | CharField(10) | comma | Number formatting |
|
||||
| week_start | CharField(10) | monday | First day of week |
|
||||
| theme_mode | CharField(10) | auto | light / dark / auto |
|
||||
| theme_name | CharField(30) | corporate | DaisyUI light theme |
|
||||
| dark_theme_name | CharField(30) | business | DaisyUI dark theme |
|
||||
| created_at | DateTimeField | auto | Record creation |
|
||||
| updated_at | DateTimeField | auto | Last update |
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `effective_timezone` — returns current_timezone if set, otherwise home_timezone
|
||||
- `is_traveling` — True if current_timezone differs from home_timezone
|
||||
|
||||
**Why two timezone fields?**
|
||||
|
||||
Users who travel frequently need to see times in their current location while still knowing what time it is "at home." Setting `current_timezone` enables this without losing the home timezone setting.
|
||||
|
||||
### UserAPIKey
|
||||
|
||||
Stores encrypted API keys, MCP credentials, DAV passwords, and other service credentials.
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| id | UUIDField | auto | Primary key |
|
||||
| user | ForeignKey | required | Owner |
|
||||
| service_name | CharField(100) | required | Service name (e.g. "OpenAI") |
|
||||
| key_type | CharField(30) | api | api / mcp / dav / token / secret / other |
|
||||
| label | CharField(100) | (blank) | User nickname for this key |
|
||||
| encrypted_value | TextField | required | Fernet-encrypted credential |
|
||||
| instructions | TextField | (blank) | How to obtain and use this key |
|
||||
| help_url | URLField | (blank) | Link to service documentation |
|
||||
| is_active | BooleanField | True | Whether key is in use |
|
||||
| last_used_at | DateTimeField | null | Last usage timestamp |
|
||||
| expires_at | DateTimeField | null | Expiration date |
|
||||
| created_at | DateTimeField | auto | Record creation |
|
||||
| updated_at | DateTimeField | auto | Last update |
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `masked_value` — shows only last 4 characters (e.g. `****7xQ2`)
|
||||
- `display_name` — returns label if set, otherwise service_name
|
||||
|
||||
**Encryption:**
|
||||
|
||||
Keys are encrypted at rest using Fernet symmetric encryption derived from Django's `SECRET_KEY`. The plaintext value is never stored and is only shown at creation time.
|
||||
|
||||
### UserNotification
|
||||
|
||||
In-app notification for a user. Created by consuming apps via `notify_user()`.
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| id | UUIDField | auto | Primary key |
|
||||
| user | ForeignKey | required | Recipient |
|
||||
| title | CharField(200) | required | Short headline |
|
||||
| message | TextField | (blank) | Body text |
|
||||
| level | CharField(10) | info | info / success / warning / danger |
|
||||
| url | CharField(500) | (blank) | Link to navigate on click |
|
||||
| source_app | CharField(100) | (blank) | App label of sender |
|
||||
| source_model | CharField(100) | (blank) | Model that triggered this |
|
||||
| source_id | CharField(100) | (blank) | PK of source object |
|
||||
| is_read | BooleanField | False | Whether user has read this |
|
||||
| read_at | DateTimeField | null | When it was read |
|
||||
| is_dismissed | BooleanField | False | Whether user dismissed this |
|
||||
| dismissed_at | DateTimeField | null | When it was dismissed |
|
||||
| expires_at | DateTimeField | null | Auto-expire datetime |
|
||||
| created_at | DateTimeField | auto | Record creation |
|
||||
| updated_at | DateTimeField | auto | Last update |
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `level_weight` — numeric weight for level comparison (info=0, success=0, warning=1, danger=2)
|
||||
- `is_expired` — True if expires_at has passed
|
||||
- `level_css_class` — DaisyUI alert class (e.g. `alert-warning`)
|
||||
- `level_badge_class` — DaisyUI badge class (e.g. `badge-warning`)
|
||||
|
||||
### UserProfile Notification Preferences
|
||||
|
||||
The UserProfile model includes four notification preference fields:
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| notifications_enabled | BooleanField | True | Master on/off switch |
|
||||
| notifications_min_level | CharField(10) | info | Minimum level to display |
|
||||
| browser_notifications_enabled | BooleanField | False | Browser desktop notifications |
|
||||
| notification_retention_days | PositiveIntegerField | 30 | Days to keep read notifications |
|
||||
|
||||
---
|
||||
|
||||
## Notifications
|
||||
|
||||
### Creating Notifications
|
||||
|
||||
All notification creation goes through the `notify_user()` utility:
|
||||
|
||||
```python
|
||||
from themis.notifications import notify_user
|
||||
|
||||
notify_user(
|
||||
user=user,
|
||||
title="Task overdue",
|
||||
message="Task 'Deploy v2' was due yesterday.",
|
||||
level="warning",
|
||||
url="/tasks/42/",
|
||||
source_app="tasks",
|
||||
source_model="Task",
|
||||
source_id="42",
|
||||
deduplicate=True,
|
||||
)
|
||||
```
|
||||
|
||||
This function respects user preferences (enabled flag, minimum level) and supports deduplication via source tracking fields.
|
||||
|
||||
### Notification Bell
|
||||
|
||||
The notification bell appears in the navbar for authenticated users with notifications enabled. It shows an unread count badge and a dropdown with a link to the full notification list.
|
||||
|
||||
### Polling
|
||||
|
||||
The `notifications.js` script polls the `/notifications/count/` endpoint at a configurable interval (default: 60 seconds) and updates the badge. Set `THEMIS_NOTIFICATION_POLL_INTERVAL = 0` to disable polling.
|
||||
|
||||
### Browser Desktop Notifications
|
||||
|
||||
When a user enables browser notifications in their preferences, Themis will request permission from the browser and show OS-level desktop notifications when new notifications arrive via polling.
|
||||
|
||||
### Cleanup
|
||||
|
||||
Old read/dismissed/expired notifications can be cleaned up with:
|
||||
|
||||
```bash
|
||||
python manage.py cleanup_notifications
|
||||
python manage.py cleanup_notifications --max-age-days=60
|
||||
```
|
||||
|
||||
For details on trigger patterns, see **[Notification Trigger Pattern](Pattern_Notification_V1-00.md)**.
|
||||
|
||||
---
|
||||
|
||||
## Templates
|
||||
|
||||
### Base Template
|
||||
|
||||
All consuming apps extend `themis/base.html`:
|
||||
|
||||
```html
|
||||
{% extends "themis/base.html" %}
|
||||
|
||||
{% block title %}Dashboard — My App{% endblock %}
|
||||
|
||||
{% block nav_items %}
|
||||
<li><a href="{% url 'dashboard' %}">Dashboard</a></li>
|
||||
<li><a href="{% url 'reports' %}">Reports</a></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-2xl font-bold">Dashboard</h1>
|
||||
<!-- app content -->
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### Available Blocks
|
||||
|
||||
| Block | Location | Purpose |
|
||||
|---|---|---|
|
||||
| `title` | `<title>` | Page title |
|
||||
| `extra_head` | `<head>` | Additional CSS/meta |
|
||||
| `navbar` | Top of `<body>` | Entire navbar (override to customize) |
|
||||
| `nav_items` | Navbar (mobile) | Navigation links |
|
||||
| `nav_items_desktop` | Navbar (desktop) | Desktop-only nav links |
|
||||
| `nav_items_mobile` | Navbar (mobile) | Mobile-only nav links |
|
||||
| `body_attrs` | `<body>` | Extra body attributes |
|
||||
| `content` | `<main>` | Page content |
|
||||
| `footer` | Bottom of `<body>` | Entire footer (override to customize) |
|
||||
| `extra_scripts` | Before `</body>` | Additional JavaScript |
|
||||
|
||||
### Navigation Structure
|
||||
|
||||
**Navbar (fixed):**
|
||||
|
||||
```
|
||||
[App Logo/Name] [Nav Items] [Theme ☀/🌙] [🔔 3] [User ▾]
|
||||
├─ Settings
|
||||
├─ API Keys
|
||||
└─ Logout
|
||||
```
|
||||
|
||||
Collapses to hamburger menu on mobile.
|
||||
|
||||
**Bottom Nav (fixed):**
|
||||
|
||||
```
|
||||
© 2026 App Name
|
||||
```
|
||||
|
||||
### What Apps Cannot Change
|
||||
|
||||
- Navbar is always a horizontal bar at the top
|
||||
- User menu is always on the right
|
||||
- Theme toggle is always in the navbar
|
||||
- Bottom nav is always present
|
||||
- Messages display below the navbar
|
||||
- Content is in a centered container
|
||||
|
||||
---
|
||||
|
||||
## Middleware
|
||||
|
||||
### TimezoneMiddleware
|
||||
|
||||
Activates the user's effective timezone for each request using `zoneinfo`. All datetime operations within the request use the user's timezone. Falls back to UTC for anonymous users.
|
||||
|
||||
```python
|
||||
MIDDLEWARE = [
|
||||
...
|
||||
"themis.middleware.TimezoneMiddleware",
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
### ThemeMiddleware
|
||||
|
||||
Attaches DaisyUI theme information to the request. The context processor reads these values for template rendering.
|
||||
|
||||
```python
|
||||
MIDDLEWARE = [
|
||||
...
|
||||
"themis.middleware.ThemeMiddleware",
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Context Processors
|
||||
|
||||
### themis_settings
|
||||
|
||||
Provides app configuration from `THEMIS_*` settings:
|
||||
|
||||
- `themis_app_name`
|
||||
- `themis_notification_poll_interval`
|
||||
|
||||
### user_preferences
|
||||
|
||||
Provides user preferences:
|
||||
|
||||
- `user_timezone`
|
||||
- `user_date_format`
|
||||
- `user_time_format`
|
||||
- `user_is_traveling`
|
||||
- `user_theme_mode`
|
||||
- `user_theme_name`
|
||||
- `user_dark_theme_name`
|
||||
- `user_profile`
|
||||
|
||||
### notifications
|
||||
|
||||
Provides notification state:
|
||||
|
||||
- `themis_unread_notification_count`
|
||||
- `themis_notifications_enabled`
|
||||
- `themis_browser_notifications_enabled`
|
||||
|
||||
---
|
||||
|
||||
## Utilities
|
||||
|
||||
### Formatting
|
||||
|
||||
```python
|
||||
from themis.utils import format_date_for_user, format_time_for_user, format_number_for_user
|
||||
|
||||
formatted_date = format_date_for_user(date_obj, request.user)
|
||||
formatted_time = format_time_for_user(time_obj, request.user)
|
||||
formatted_num = format_number_for_user(1000000, request.user)
|
||||
```
|
||||
|
||||
### Timezone
|
||||
|
||||
```python
|
||||
from themis.utils import convert_to_user_timezone, get_timezone_display
|
||||
|
||||
user_time = convert_to_user_timezone(utc_datetime, request.user)
|
||||
tz_name = get_timezone_display(request.user)
|
||||
```
|
||||
|
||||
### Template Tags
|
||||
|
||||
```html
|
||||
{% load themis_tags %}
|
||||
|
||||
{{ event.date|user_date:request.user }}
|
||||
{{ event.time|user_time:request.user }}
|
||||
{{ revenue|user_number:request.user }}
|
||||
{% user_timezone_name request.user %}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## URL Patterns
|
||||
|
||||
| URL | View | Purpose |
|
||||
|---|---|---|
|
||||
| `/ready/` | `ready` | Kubernetes readiness probe |
|
||||
| `/live/` | `live` | Kubernetes liveness probe |
|
||||
| `/profile/settings/` | `profile_settings` | User preferences page |
|
||||
| `/profile/keys/` | `key_list` | API key list |
|
||||
| `/profile/keys/add/` | `key_create` | Add new key |
|
||||
| `/profile/keys/<uuid>/` | `key_detail` | Key detail + instructions |
|
||||
| `/profile/keys/<uuid>/edit/` | `key_edit` | Edit key metadata |
|
||||
| `/profile/keys/<uuid>/delete/` | `key_delete` | Delete key (POST only) |
|
||||
| `/notifications/` | `notification_list` | Notification list page |
|
||||
| `/notifications/<uuid>/read/` | `notification_mark_read` | Mark as read (POST) |
|
||||
| `/notifications/read-all/` | `notification_mark_all_read` | Mark all read (POST) |
|
||||
| `/notifications/<uuid>/dismiss/` | `notification_dismiss` | Dismiss (POST) |
|
||||
| `/notifications/count/` | `notification_count` | Unread count JSON |
|
||||
|
||||
---
|
||||
|
||||
## REST API
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|---|---|---|
|
||||
| `/api/v1/profiles/` | GET | List profiles (own only, admin sees all) |
|
||||
| `/api/v1/profiles/{id}/` | GET/PATCH | View/update profile |
|
||||
| `/api/v1/keys/` | GET | List own API keys |
|
||||
| `/api/v1/keys/` | POST | Create new key |
|
||||
| `/api/v1/keys/{uuid}/` | GET/PATCH/DELETE | View/update/delete key |
|
||||
| `/api/v1/notifications/` | GET | List own notifications (filterable) |
|
||||
| `/api/v1/notifications/{uuid}/` | GET/PATCH/DELETE | View/update/delete notification |
|
||||
| `/api/v1/notifications/{uuid}/mark_read/` | PATCH | Mark as read |
|
||||
| `/api/v1/notifications/mark-all-read/` | PATCH | Mark all as read |
|
||||
| `/api/v1/notifications/{uuid}/dismiss/` | PATCH | Dismiss notification |
|
||||
| `/api/v1/notifications/count/` | GET | Unread count |
|
||||
|
||||
---
|
||||
|
||||
## DaisyUI Themes
|
||||
|
||||
Themis supports all 32 built-in DaisyUI themes. Users select separate themes for light and dark modes. The theme toggle cycles: light → dark → auto (system).
|
||||
|
||||
No database table needed — themes are a simple CharField storing the DaisyUI theme name.
|
||||
|
||||
### Available Themes
|
||||
|
||||
light, dark, cupcake, bumblebee, emerald, corporate, synthwave, retro, cyberpunk, valentine, halloween, garden, forest, aqua, lofi, pastel, fantasy, wireframe, black, luxury, dracula, cmyk, autumn, business, acid, lemonade, night, coffee, winter, dim, nord, sunset
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
```toml
|
||||
dependencies = [
|
||||
"Django>=5.2,<6.0",
|
||||
"djangorestframework>=3.14,<4.0",
|
||||
"cryptography>=41.0,<45.0",
|
||||
]
|
||||
```
|
||||
|
||||
No `pytz` (uses stdlib `zoneinfo`). No `Pillow`. No database-stored themes.
|
||||
Reference in New Issue
Block a user