# 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 %}
  • Dashboard
  • Reports
  • {% endblock %} {% block content %}

    Dashboard

    {% endblock %} ``` ### Available Blocks | Block | Location | Purpose | |---|---|---| | `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.