Files
mnemosyne/docs/Themis_V1-00.md
Robert Helewka 99bdb4ac92 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.
2026-03-21 02:00:18 +00:00

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_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:

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_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

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.