- 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.
14 KiB
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
pip install git+ssh://git@git.helu.ca:22022/r/themis.git
For Local Development
pip install -e /path/to/themis
Configuration
settings.py:
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:
from django.urls import include, path
urlpatterns = [
...
path("", include("themis.urls")),
path("api/v1/", include("themis.api.urls")),
...
]
Run migrations:
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_timezoneis_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 passedlevel_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:
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:
python manage.py cleanup_notifications
python manage.py cleanup_notifications --max-age-days=60
For details on trigger patterns, see Notification Trigger Pattern.
Templates
Base Template
All consuming apps extend themis/base.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.
MIDDLEWARE = [
...
"themis.middleware.TimezoneMiddleware",
...
]
ThemeMiddleware
Attaches DaisyUI theme information to the request. The context processor reads these values for template rendering.
MIDDLEWARE = [
...
"themis.middleware.ThemeMiddleware",
...
]
Context Processors
themis_settings
Provides app configuration from THEMIS_* settings:
themis_app_namethemis_notification_poll_interval
user_preferences
Provides user preferences:
user_timezoneuser_date_formatuser_time_formatuser_is_travelinguser_theme_modeuser_theme_nameuser_dark_theme_nameuser_profile
notifications
Provides notification state:
themis_unread_notification_countthemis_notifications_enabledthemis_browser_notifications_enabled
Utilities
Formatting
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
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
{% 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
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.