# 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 %}