From 16fb7ff4dc0facaa5f18385194c5ca20ca3d7592 Mon Sep 17 00:00:00 2001 From: Robert Helewka Date: Sun, 10 May 2026 11:59:44 -0400 Subject: [PATCH] docs: clarify Daedalus-Pallas integration auth model Refine the phase-2 integration spec to reflect implementation details: - Change `resolved_libraries` from `set[str]` to ordered `list[str]` - Document `MCPToken.allowed_libraries` as JSONField (not M2M) since Library lives in Neo4j, not Django's ORM - Clarify that `Library.workspace_id` is a content-routing attribute, not an authorization axis - Describe retirement of the three-branch `_WORKSPACE_SCOPE_CLAUSE` in favor of a single `lib.uid IN $resolved_libraries` check - Specify team JWT resolution via `TeamWorkspaceAssignment` DB join - Note admin UI materializes full Library UID list explicitly --- docs/DAEDALUS_PALLAS_INTEGRATION_v1.md | 104 +- mnemosyne/library/api/serializers.py | 12 +- mnemosyne/library/api/views.py | 14 +- .../library/management/commands/search.py | 5 + mnemosyne/library/migrations/0001_initial.py | 2 +- mnemosyne/library/services/search.py | 98 +- mnemosyne/library/utils.py | 29 + mnemosyne/library/views.py | 52 +- .../llm_manager/migrations/0001_initial.py | 11 +- .../migrations/0002_add_bedrock_api_type.py | 34 - .../0003_add_vision_model_and_usage.py | 52 - ...f4e7_idx_llm_manager_is_syst_d190bb_idx.py | 18 - mnemosyne/mcp_server/admin.py | 288 +++- mnemosyne/mcp_server/api/__init__.py | 0 mnemosyne/mcp_server/api/serializers.py | 86 ++ mnemosyne/mcp_server/api/teams.py | 251 ++++ mnemosyne/mcp_server/api/urls.py | 35 + mnemosyne/mcp_server/auth.py | 204 ++- mnemosyne/mcp_server/context.py | 49 +- mnemosyne/mcp_server/forms.py | 51 +- .../commands/backfill_library_memberships.py | 150 ++ .../mcp_server/migrations/0001_initial.py | 62 +- .../mcp_server/migrations/0002_hash_token.py | 43 - .../migrations/0003_mcpsigningkey.py | 38 - mnemosyne/mcp_server/models.py | 220 ++- mnemosyne/mcp_server/teams.py | 101 ++ mnemosyne/mcp_server/tools/discovery.py | 99 +- mnemosyne/mcp_server/tools/search.py | 78 +- mnemosyne/mcp_server/urls.py | 18 +- mnemosyne/mcp_server/views.py | 12 +- mnemosyne/mnemosyne/urls.py | 7 +- mnemosyne/themis/migrations/0001_initial.py | 290 +--- ..._browser_notifications_enabled_and_more.py | 84 -- ...r_userprofile_current_timezone_and_more.py | 1259 ----------------- .../0004_alter_userapikey_key_type.py | 18 - 35 files changed, 1839 insertions(+), 2035 deletions(-) delete mode 100644 mnemosyne/llm_manager/migrations/0002_add_bedrock_api_type.py delete mode 100644 mnemosyne/llm_manager/migrations/0003_add_vision_model_and_usage.py delete mode 100644 mnemosyne/llm_manager/migrations/0004_rename_llm_manager__is_syst_b2f4e7_idx_llm_manager_is_syst_d190bb_idx.py create mode 100644 mnemosyne/mcp_server/api/__init__.py create mode 100644 mnemosyne/mcp_server/api/serializers.py create mode 100644 mnemosyne/mcp_server/api/teams.py create mode 100644 mnemosyne/mcp_server/api/urls.py create mode 100644 mnemosyne/mcp_server/management/commands/backfill_library_memberships.py delete mode 100644 mnemosyne/mcp_server/migrations/0002_hash_token.py delete mode 100644 mnemosyne/mcp_server/migrations/0003_mcpsigningkey.py create mode 100644 mnemosyne/mcp_server/teams.py delete mode 100644 mnemosyne/themis/migrations/0002_userprofile_browser_notifications_enabled_and_more.py delete mode 100644 mnemosyne/themis/migrations/0003_alter_userprofile_current_timezone_and_more.py delete mode 100644 mnemosyne/themis/migrations/0004_alter_userapikey_key_type.py diff --git a/docs/DAEDALUS_PALLAS_INTEGRATION_v1.md b/docs/DAEDALUS_PALLAS_INTEGRATION_v1.md index 0c77a05..81999bc 100644 --- a/docs/DAEDALUS_PALLAS_INTEGRATION_v1.md +++ b/docs/DAEDALUS_PALLAS_INTEGRATION_v1.md @@ -23,9 +23,12 @@ model connecting three services: The model replaces the per-turn JWT *forwarding* scheme with a unified **bearer → resolved library set** abstraction. Every authenticated -Mnemosyne request resolves to a set of Library UIDs the caller may -read; the principal type (opaque `MCPToken`, Daedalus per-turn JWT, -team JWT) only determines how that set is derived. +Mnemosyne request resolves to a single ordered `resolved_libraries` +list of Library UIDs the caller may read; the principal type (opaque +`MCPToken`, Daedalus per-turn JWT, team JWT) only determines how that +list is derived. `Library.workspace_id` is a Daedalus content-routing +attribute used by the ingest and workspace-lifecycle APIs; it is **not** +consulted by the auth layer. It also records the UX shift in Daedalus: **workspaces attach Teams (Pallas instances), not individual agents**; the agent picker in chat @@ -86,24 +89,48 @@ and the design collapses to two credential types. ### 3.3 Resolved-library abstraction Mnemosyne's auth middleware populates a single -`resolved_libraries: set[str]` per request. Downstream code (search, -get_document, list_libraries, etc.) only reads that set; it does not -care where the set came from. +`resolved_libraries: list[str]` per request. Downstream code (search, +get_chunk, list_libraries, list_collections, list_items, …) only +reads that list; it does not care where it came from. ``` Bearer → classify → dispatch - ├─ Opaque MCPToken → allowed_libraries M2M + ├─ Opaque MCPToken → token.allowed_libraries (JSON list of UIDs) ├─ per-turn JWT → claims["libs"] - └─ team JWT (typ=team) → live DB: team.workspaces → libraries - (filtered by Library.workspace_id) + └─ team JWT (typ=team) → live DB join: + TeamWorkspaceAssignment.workspace_id + → Library.workspace_id → Library.uid ↓ - resolved_libraries: set[str] + resolved_libraries: list[str] ↓ downstream tools ``` -Fail-closed: if the resolution produces an empty set, the request sees -no Libraries. There is no "empty means everything" path. +Fail-closed: if the resolution produces an empty list, the request +sees no Libraries. There is no "empty means everything" fallback. + +#### 3.3.1 Retirement of the old three-branch scope clause + +The pre-phase-2 search pipeline ran every Cypher query against a +`_WORKSPACE_SCOPE_CLAUSE` with three branches keyed on whether +`workspace_id` and/or `allowed_libraries` were set. Phase 2 removes +that clause entirely. Every authorization check collapses to: + +```cypher +WHERE lib.uid IN $resolved_libraries +``` + +`Library.workspace_id` stays on the node as a Daedalus content-routing +attribute (used by the ingest API to find-or-create the per-workspace +Library, and by the workspace-lifecycle API to cascade-delete that +Library's contents). It is **not** an authorization axis and is not +consulted anywhere in the auth middleware, the MCP tool surface, or +the search service. + +Admin-UI-initiated searches (Django staff logged into the Mnemosyne +admin / search page) materialize `resolved_libraries` explicitly as +"every Library UID the database contains" — the same mechanism used +today as a workaround, now the only code path. --- @@ -133,9 +160,14 @@ class LibraryMembership(models.Model): User can scope a Library into `MCPToken.allowed_libraries` iff they have `owner` or `manager` role on it. -#### `MCPToken.allowed_libraries` (new M2M on existing model) +#### `MCPToken.allowed_libraries` (new field on existing model) ```python -allowed_libraries = models.ManyToManyField(Library, blank=True) +# JSON list of Library.uid strings. A real M2M isn't possible because +# Library lives in Neo4j (neomodel StructuredNode), not Django's ORM. +# The admin/dashboard form materializes the picker by querying +# Library.nodes and filtering to libraries where the token's user has +# an ``owner`` or ``manager`` LibraryMembership. +allowed_libraries = models.JSONField(default=list, blank=True) ``` Fail-closed: empty → token grants access to zero libraries. Admin form filters the picker by the current user's owned/managed @@ -254,6 +286,7 @@ def resolve_mcp_jwt(token_string: str) -> dict: typ = claims.get("typ") if typ == "team": # No replay cache — team tokens are reused on every request. + # Validate sub=="team:" shape; stash the uuid on claims. pass else: if _remember_jti(jti, float(exp)): @@ -262,19 +295,31 @@ def resolve_mcp_jwt(token_string: str) -> dict: return claims ``` -Downstream, the middleware branches: +Middleware populates `STATE_KEY_RESOLVED_LIBRARIES` per request: + ```python -if claims.get("typ") == "team": - team = Team.objects.get(id=uuid_from_sub(claims["sub"]), - active=True, - active_jti=claims["jti"]) - resolved_libraries = _libraries_for_team(team) -else: - resolved_libraries = claims["libs"] +# Opaque MCPToken +resolved_libraries = list(token.allowed_libraries or []) + +# Per-turn JWT (legacy; retires phase 4) +resolved_libraries = list(claims.get("libs") or []) + +# Team JWT +team = Team.objects.get(id=uuid_from_sub(claims["sub"]), + active=True, + active_jti=claims["jti"]) +resolved_libraries = _libraries_for_team(team) # see below ``` -`_libraries_for_team(team)` = all `Library` UIDs whose `workspace_id` -is in the team's `TeamWorkspaceAssignment` set. +`_libraries_for_team(team)` runs a single Cypher query against Neo4j: + +```cypher +MATCH (l:Library) +WHERE l.workspace_id IN $workspace_ids +RETURN l.uid +``` + +where `$workspace_ids` is `list(team.workspace_assignments.values_list("workspace_id", flat=True))`. --- @@ -283,13 +328,16 @@ is in the team's `TeamWorkspaceAssignment` set. ### 6.1 Third-party MCP client with opaque `MCPToken` 1. Client sends `Authorization: Bearer `. 2. Middleware hashes → looks up `MCPToken` → validates active/expired. -3. `resolved_libraries = token.allowed_libraries.values_list("uid")`. +3. `resolved_libraries = list(token.allowed_libraries or [])` — the + JSON list of Library UIDs the admin / dashboard granted at mint. 4. Fails closed if empty. ### 6.2 Daedalus chat per-turn JWT (legacy, retires Phase 4) -Unchanged from today. `iss=daedalus`, `typ` absent, `libs` carries the -workspace's user-managed libraries, `ws` carries the workspace id. -Mnemosyne validates against `MCPSigningKey` keyed by `kid`. +`iss=daedalus`, `typ` absent, `libs` carries the full library set +Daedalus pre-computed for that turn (the workspace's auto-Library +plus any user-managed extras), `ws` is present but no longer consulted +server-side. Middleware assigns `resolved_libraries = claims["libs"]`. +Mnemosyne validates the JWT against `MCPSigningKey` keyed by `kid`. ### 6.3 Agent team (Kottos / Mentor / Iolaus / post-migration Daedalus-chat) 1. Pallas sends `Authorization: Bearer <team-jwt>` (static, read from diff --git a/mnemosyne/library/api/serializers.py b/mnemosyne/library/api/serializers.py index 934724e..22eea70 100644 --- a/mnemosyne/library/api/serializers.py +++ b/mnemosyne/library/api/serializers.py @@ -99,6 +99,17 @@ class ImageSerializer(serializers.Serializer): class SearchRequestSerializer(serializers.Serializer): + """Request body for ``/library/api/search/``. + + Authorization scope is resolved server-side from the request's + Django session (this endpoint is gated by + ``permission_classes=[IsAuthenticated]``), not from the request + body — see ``library.utils.all_library_uids`` and the unified + auth model in ``docs/DAEDALUS_PALLAS_INTEGRATION_v1.md`` §3.3. + ``library_uid`` / ``library_type`` / ``collection_uid`` are + filters inside that scope, not scope itself. + """ + query = serializers.CharField(max_length=2000) library_uid = serializers.CharField(required=False, allow_blank=True) library_type = serializers.ChoiceField( @@ -106,7 +117,6 @@ class SearchRequestSerializer(serializers.Serializer): required=False, ) collection_uid = serializers.CharField(required=False, allow_blank=True) - workspace_id = serializers.CharField(required=False, allow_blank=True) search_types = serializers.ListField( child=serializers.ChoiceField(choices=["vector", "fulltext", "graph"]), required=False, diff --git a/mnemosyne/library/api/views.py b/mnemosyne/library/api/views.py index 748d652..1b04032 100644 --- a/mnemosyne/library/api/views.py +++ b/mnemosyne/library/api/views.py @@ -479,17 +479,23 @@ def search(request): from django.conf import settings as django_settings from library.services.search import SearchRequest, SearchService + from library.utils import all_library_uids serializer = SearchRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) data = serializer.validated_data + # This DRF endpoint is gated by ``IsAuthenticated`` against a + # Django session, not an MCP bearer. The session is trusted; + # expose every library to the request. MCP-bearer callers go + # through ``mcp_server`` and get a narrower ``resolved_libraries`` + # materialized by the auth middleware. search_request = SearchRequest( query=data["query"], library_uid=data.get("library_uid") or None, library_type=data.get("library_type") or None, collection_uid=data.get("collection_uid") or None, - workspace_id=data.get("workspace_id") or None, + resolved_libraries=all_library_uids(), search_types=data.get("search_types", ["vector", "fulltext", "graph"]), limit=data.get("limit", getattr(django_settings, "SEARCH_DEFAULT_LIMIT", 20)), vector_top_k=getattr(django_settings, "SEARCH_VECTOR_TOP_K", 50), @@ -511,6 +517,7 @@ def search_vector(request): from django.conf import settings as django_settings from library.services.search import SearchRequest, SearchService + from library.utils import all_library_uids serializer = SearchRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -521,7 +528,7 @@ def search_vector(request): library_uid=data.get("library_uid") or None, library_type=data.get("library_type") or None, collection_uid=data.get("collection_uid") or None, - workspace_id=data.get("workspace_id") or None, + resolved_libraries=all_library_uids(), search_types=["vector"], limit=data.get("limit", 20), vector_top_k=getattr(django_settings, "SEARCH_VECTOR_TOP_K", 50), @@ -542,6 +549,7 @@ def search_fulltext(request): from django.conf import settings as django_settings from library.services.search import SearchRequest, SearchService + from library.utils import all_library_uids serializer = SearchRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -552,7 +560,7 @@ def search_fulltext(request): library_uid=data.get("library_uid") or None, library_type=data.get("library_type") or None, collection_uid=data.get("collection_uid") or None, - workspace_id=data.get("workspace_id") or None, + resolved_libraries=all_library_uids(), search_types=["fulltext"], limit=data.get("limit", 20), fulltext_top_k=getattr(django_settings, "SEARCH_FULLTEXT_TOP_K", 30), diff --git a/mnemosyne/library/management/commands/search.py b/mnemosyne/library/management/commands/search.py index dbb92a5..2524278 100644 --- a/mnemosyne/library/management/commands/search.py +++ b/mnemosyne/library/management/commands/search.py @@ -74,6 +74,11 @@ class Command(BaseCommand): query=query, library_uid=options["library_uid"] or None, library_type=options["library_type"] or None, + # Unrestricted: the CLI is a shell-level operator tool; it + # bypasses the MCP bearer-resolver and sees every library. + # ``resolved_libraries=None`` is the "no auth clause" branch + # (see ``library/services/search.py::_RESOLVED_LIBRARIES_CLAUSE``). + resolved_libraries=None, search_types=search_types, limit=limit, vector_top_k=getattr(settings, "SEARCH_VECTOR_TOP_K", 50), diff --git a/mnemosyne/library/migrations/0001_initial.py b/mnemosyne/library/migrations/0001_initial.py index abec455..510e6b4 100644 --- a/mnemosyne/library/migrations/0001_initial.py +++ b/mnemosyne/library/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.13 on 2026-04-28 12:36 +# Generated by Django 5.2.13 on 2026-05-10 15:31 from django.db import migrations, models diff --git a/mnemosyne/library/services/search.py b/mnemosyne/library/services/search.py index 690300a..1e3c12e 100644 --- a/mnemosyne/library/services/search.py +++ b/mnemosyne/library/services/search.py @@ -26,36 +26,47 @@ from .fusion import ImageSearchResult, SearchCandidate, reciprocal_rank_fusion logger = logging.getLogger(__name__) -# Search-scope clause appended to every search Cypher query. +# Search-scope clause appended to every Cypher query. # -# Three modes, picked structurally by which params are set: +# Authorization is expressed by the caller as a ``resolved_libraries`` +# list — see §3.3 of ``docs/DAEDALUS_PALLAS_INTEGRATION_v1.md``. The +# MCP auth middleware materializes it from the bearer token (opaque +# MCPToken.allowed_libraries, per-turn JWT ``libs`` claim, or live +# ``Team → TeamWorkspaceAssignment → Library.workspace_id`` join) and +# trusted in-process callers (Django admin page, DRF session-auth'd +# search endpoint, ``manage.py search``) either pass the full set from +# ``library.utils.all_library_uids()`` or pass ``None`` to bypass the +# clause entirely. # -# 1. ``workspace_id`` set, ``allowed_libraries`` empty → workspace-scoped. -# Returns ONLY content from libraries whose workspace_id matches. -# 2. ``workspace_id`` set + ``allowed_libraries`` non-empty → workspace -# PLUS the listed user-managed libraries (typical Phase-2 chat turn). -# 3. Both null → global. Returns ONLY libraries with no workspace_id -# (legacy opaque-token callers / dashboard). +# Two Cypher branches, picked by whether ``resolved_libraries`` is the +# parameter value: # -# When ``allowed_libraries`` is non-empty alone (no workspace_id), it -# narrows results to those libraries. -_WORKSPACE_SCOPE_CLAUSE = ( - " AND (" - "($workspace_id IS NOT NULL AND lib.workspace_id = $workspace_id) " - "OR ($allowed_libraries IS NOT NULL AND lib.uid IN $allowed_libraries) " - "OR ($workspace_id IS NULL AND $allowed_libraries IS NULL " - " AND lib.workspace_id IS NULL)" - ")" -) +# * ``None`` — no clause; trusted in-process admin / CLI +# use. Returns every library the query hits. +# * non-empty list — ``WHERE lib.uid IN $resolved_libraries``. +# * empty list — fail-closed: no row passes because ``uid IN []`` +# is false for every row (Cypher semantics). +# +# ``Library.workspace_id`` is NOT consulted here. It remains on the +# node as a Daedalus content-routing attribute (used by the ingest +# API and the workspace-lifecycle cascade) but it is not an auth axis. +_RESOLVED_LIBRARIES_CLAUSE = " AND ($resolved_libraries IS NULL OR lib.uid IN $resolved_libraries)" @dataclass class SearchRequest: """Parameters for a search query. - Scope is single-mode: a request is either workspace-scoped (workspace_id - set) or global (workspace_id is None). There is no parameter combination - that returns both workspace and global content in one call. + Authorization scope is expressed by ``resolved_libraries``: + + * ``None`` — unrestricted (trusted admin / CLI callers). + * ``[]`` — fail-closed; zero results. + * ``["lib_x", …]`` — restrict to these Library UIDs. + + ``library_uid`` / ``library_type`` / ``collection_uid`` are + orthogonal *filters* supplied by the caller (e.g. "search only + within Fiction"); they narrow further inside whatever + ``resolved_libraries`` already permits. """ query: str @@ -64,11 +75,9 @@ class SearchRequest: library_uid: Optional[str] = None library_type: Optional[str] = None collection_uid: Optional[str] = None - workspace_id: Optional[str] = None - # Phase-2 token claim: user-managed libraries the caller may include - # alongside their workspace's auto-library. Cypher uses ``IS NULL`` vs - # non-empty list to gate the second branch of the scope clause. - allowed_libraries: Optional[list[str]] = None + # Authorization-resolved Library UID set. See the module-level + # ``_RESOLVED_LIBRARIES_CLAUSE`` docstring for semantics. + resolved_libraries: Optional[list[str]] = None search_types: list[str] = field( default_factory=lambda: ["vector", "fulltext", "graph"] ) @@ -82,19 +91,17 @@ class SearchRequest: def __post_init__(self): # Normalize empty strings to None so "" doesn't slip through as # truthy at the Cypher boundary. - if self.workspace_id == "": - self.workspace_id = None if self.library_uid == "": self.library_uid = None if self.library_type == "": self.library_type = None if self.collection_uid == "": self.collection_uid = None - # Empty list collapses to None so the Cypher branch reads - # "$allowed_libraries IS NOT NULL" rather than "size > 0" — keeps - # the parameter binding straightforward and the predicate sargable. - if self.allowed_libraries is not None and len(self.allowed_libraries) == 0: - self.allowed_libraries = None + # resolved_libraries: preserve the distinction between None (no + # auth clause — trusted caller) and [] (fail-closed). Only + # normalize list contents to strip falsy entries. + if isinstance(self.resolved_libraries, list): + self.resolved_libraries = [u for u in self.resolved_libraries if u] @dataclass @@ -347,7 +354,7 @@ class SearchService: AND ($library_type IS NULL OR lib.library_type = $library_type) AND ($collection_uid IS NULL OR col.uid = $collection_uid) """ - + _WORKSPACE_SCOPE_CLAUSE + + _RESOLVED_LIBRARIES_CLAUSE + """ RETURN chunk.uid AS chunk_uid, chunk.text_preview AS text_preview, chunk.chunk_s3_key AS chunk_s3_key, chunk.chunk_index AS chunk_index, @@ -364,8 +371,7 @@ class SearchService: "library_uid": request.library_uid, "library_type": request.library_type, "collection_uid": request.collection_uid, - "workspace_id": request.workspace_id, - "allowed_libraries": request.allowed_libraries, + "resolved_libraries": request.resolved_libraries, } try: @@ -459,7 +465,7 @@ class SearchService: AND ($library_type IS NULL OR lib.library_type = $library_type) AND ($collection_uid IS NULL OR col.uid = $collection_uid) """ - + _WORKSPACE_SCOPE_CLAUSE + + _RESOLVED_LIBRARIES_CLAUSE + """ RETURN chunk.uid AS chunk_uid, chunk.text_preview AS text_preview, chunk.chunk_s3_key AS chunk_s3_key, chunk.chunk_index AS chunk_index, @@ -476,8 +482,7 @@ class SearchService: "library_uid": request.library_uid, "library_type": request.library_type, "collection_uid": request.collection_uid, - "workspace_id": request.workspace_id, - "allowed_libraries": request.allowed_libraries, + "resolved_libraries": request.resolved_libraries, } try: @@ -520,7 +525,7 @@ class SearchService: WHERE ($library_uid IS NULL OR lib.uid = $library_uid) AND ($library_type IS NULL OR lib.library_type = $library_type) """ - + _WORKSPACE_SCOPE_CLAUSE + + _RESOLVED_LIBRARIES_CLAUSE + """ RETURN chunk.uid AS chunk_uid, chunk.text_preview AS text_preview, chunk.chunk_s3_key AS chunk_s3_key, chunk.chunk_index AS chunk_index, @@ -537,8 +542,7 @@ class SearchService: "top_k": top_k, "library_uid": request.library_uid, "library_type": request.library_type, - "workspace_id": request.workspace_id, - "allowed_libraries": request.allowed_libraries, + "resolved_libraries": request.resolved_libraries, } try: @@ -593,7 +597,7 @@ class SearchService: WHERE ($library_uid IS NULL OR lib.uid = $library_uid) AND ($library_type IS NULL OR lib.library_type = $library_type) """ - + _WORKSPACE_SCOPE_CLAUSE + + _RESOLVED_LIBRARIES_CLAUSE + """ WITH chunk, item, lib, max(concept_score) AS score, @@ -613,8 +617,7 @@ class SearchService: "limit": request.fulltext_top_k, "library_uid": request.library_uid, "library_type": request.library_type, - "workspace_id": request.workspace_id, - "allowed_libraries": request.allowed_libraries, + "resolved_libraries": request.resolved_libraries, } try: @@ -682,7 +685,7 @@ class SearchService: WHERE ($library_uid IS NULL OR lib.uid = $library_uid) AND ($library_type IS NULL OR lib.library_type = $library_type) """ - + _WORKSPACE_SCOPE_CLAUSE + + _RESOLVED_LIBRARIES_CLAUSE + """ RETURN img.uid AS image_uid, img.image_type AS image_type, img.description AS description, img.s3_key AS s3_key, @@ -698,8 +701,7 @@ class SearchService: "query_vector": query_vector, "library_uid": request.library_uid, "library_type": request.library_type, - "workspace_id": request.workspace_id, - "allowed_libraries": request.allowed_libraries, + "resolved_libraries": request.resolved_libraries, } try: diff --git a/mnemosyne/library/utils.py b/mnemosyne/library/utils.py index e279fa3..34afe86 100644 --- a/mnemosyne/library/utils.py +++ b/mnemosyne/library/utils.py @@ -21,3 +21,32 @@ def neo4j_available(): return True except Exception: return False + + +def all_library_uids() -> list[str]: + """Return the UIDs of every ``Library`` node in Neo4j. + + Used by trusted in-process callers — the Django admin HTML search + page, the ``/library/api/search/`` DRF endpoint (gated by Django + session auth) and the ``search`` management command — as the + ``resolved_libraries`` argument to :class:`SearchRequest`. These + callers have already been authenticated/authorized at a coarser + layer (Django login / DRF session) and the unified auth middleware + (see ``mcp_server/auth.py``) is the one that resolves narrower + library sets for MCP bearer tokens. + + Returns ``[]`` when Neo4j is unreachable. Callers that want the + unrestricted / "admin sees everything" semantics should feed this + result directly into ``SearchRequest.resolved_libraries``; callers + that want to distinguish "unrestricted" from "fail-closed empty" + must pass ``resolved_libraries=None`` for the former instead. + """ + if not neo4j_available(): + return [] + try: + from .models import Library + + return [lib.uid for lib in Library.nodes.all() if lib.uid] + except Exception as exc: # pragma: no cover - Neo4j unreachable paths + logger.warning("Failed to enumerate library UIDs for search: %s", exc) + return [] diff --git a/mnemosyne/library/views.py b/mnemosyne/library/views.py index 1d4be7e..35dce82 100644 --- a/mnemosyne/library/views.py +++ b/mnemosyne/library/views.py @@ -141,40 +141,16 @@ _MAX_QUERY_IMAGE_BYTES = 8 * 1024 * 1024 def _all_library_uids() -> list[str]: - """Return the UIDs of every Library node in Neo4j. + """Legacy alias for :func:`library.utils.all_library_uids`. - The Django-side HTML search views (``search_page`` and - ``library_search``) are admin/debug tools gated by ``@login_required`` - against a local Django account; they are not exposed to external - MCP callers and have no workspace-scoping contract to honour. - - The underlying ``SearchService`` always appends - ``_WORKSPACE_SCOPE_CLAUSE`` to every Cypher query, and that clause's - default branch — "``$workspace_id`` IS NULL AND ``$allowed_libraries`` - IS NULL" — only matches libraries whose own ``workspace_id`` is - ``NULL``. So an authenticated admin searching from the UI would - silently miss every Daedalus-ingested document, because those - libraries always carry a non-null ``workspace_id``. - - Passing the full set of library UIDs as ``allowed_libraries`` flips - the clause into its second branch - (``lib.uid IN $allowed_libraries``) which matches every library - regardless of ``workspace_id``. This reuses the exact mechanism - Phase-2 chat turns use for "user-managed libraries"; we're simply - granting the admin access to all of them. Returning ``[]`` is fine - when Neo4j is unreachable — ``SearchRequest.__post_init__`` - collapses an empty list to ``None``, reverting to the legacy global - behaviour. + Kept here so existing tests that patch + ``library.views._all_library_uids`` continue to work during the + Phase-2 refactor. New code should import ``all_library_uids`` + directly from ``library.utils``. """ - if not neo4j_available(): - return [] - try: - from .models import Library + from .utils import all_library_uids - return [lib.uid for lib in Library.nodes.all() if lib.uid] - except Exception as exc: # pragma: no cover - Neo4j unreachable paths - logger.warning("Failed to enumerate library UIDs for search: %s", exc) - return [] + return all_library_uids() @login_required @@ -240,7 +216,7 @@ def library_search(request, uid): query_image=image_bytes, query_image_ext=image_ext, library_uid=uid, - allowed_libraries=allowed, + resolved_libraries=allowed, limit=getattr(django_settings, "SEARCH_DEFAULT_LIMIT", 20), vector_top_k=getattr(django_settings, "SEARCH_VECTOR_TOP_K", 50), fulltext_top_k=getattr(django_settings, "SEARCH_FULLTEXT_TOP_K", 30), @@ -830,11 +806,13 @@ def search_page(request): query=query, library_uid=library_uid or None, library_type=library_type or None, - # Admin UI sees everything — workspace-scoped libraries - # included. Without this, ``_WORKSPACE_SCOPE_CLAUSE`` - # falls back to its "global-only" branch and silently - # hides all Daedalus-ingested content. - allowed_libraries=_all_library_uids(), + # Admin UI is session-authenticated and sees every + # library, Daedalus-workspace-scoped or global. + # ``library.utils.all_library_uids`` materializes the + # full Library UID set as the request's + # ``resolved_libraries`` — see the unified auth model + # in ``docs/DAEDALUS_PALLAS_INTEGRATION_v1.md`` §3.3. + resolved_libraries=_all_library_uids(), limit=getattr(django_settings, "SEARCH_DEFAULT_LIMIT", 20), vector_top_k=getattr(django_settings, "SEARCH_VECTOR_TOP_K", 50), fulltext_top_k=getattr(django_settings, "SEARCH_FULLTEXT_TOP_K", 30), diff --git a/mnemosyne/llm_manager/migrations/0001_initial.py b/mnemosyne/llm_manager/migrations/0001_initial.py index fba0d4d..9d973e7 100644 --- a/mnemosyne/llm_manager/migrations/0001_initial.py +++ b/mnemosyne/llm_manager/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.12 on 2026-03-10 16:59 +# Generated by Django 5.2.13 on 2026-05-10 15:31 import django.db.models.deletion import llm_manager.encryption @@ -22,7 +22,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('name', models.CharField(max_length=100, unique=True)), - ('api_type', models.CharField(choices=[('openai', 'OpenAI Compatible'), ('azure', 'Azure OpenAI'), ('ollama', 'Ollama'), ('anthropic', 'Anthropic'), ('llama-cpp', 'Llama.cpp'), ('vllm', 'vLLM')], max_length=20)), + ('api_type', models.CharField(choices=[('openai', 'OpenAI Compatible'), ('azure', 'Azure OpenAI'), ('ollama', 'Ollama'), ('anthropic', 'Anthropic'), ('llama-cpp', 'Llama.cpp'), ('vllm', 'vLLM'), ('bedrock', 'Amazon Bedrock')], max_length=20)), ('base_url', models.URLField()), ('api_key', llm_manager.encryption.EncryptedCharField(blank=True, default='', max_length=500)), ('is_active', models.BooleanField(default=True)), @@ -64,6 +64,7 @@ class Migration(migrations.Migration): ('is_system_embedding_model', models.BooleanField(default=False, help_text='Mark this as the system-wide embedding model. Only ONE embedding model should have this set to True.')), ('is_system_chat_model', models.BooleanField(default=False, help_text='Mark this as the system-wide chat model. Only ONE chat model should have this set to True.')), ('is_system_reranker_model', models.BooleanField(default=False, help_text='Mark this as the system-wide reranker model. Only ONE reranker model should have this set to True.')), + ('is_system_vision_model', models.BooleanField(default=False, help_text='Mark this as the system-wide vision model for image analysis. Only ONE vision model should have this set to True.')), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), ('api', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='models', to='llm_manager.llmapi')), @@ -82,7 +83,7 @@ class Migration(migrations.Migration): ('cached_tokens', models.PositiveIntegerField(default=0)), ('total_cost', models.DecimalField(decimal_places=6, default=Decimal('0'), help_text='Total cost in USD', max_digits=12)), ('session_id', models.CharField(blank=True, db_index=True, max_length=100)), - ('purpose', models.CharField(choices=[('responder', 'RAG Responder'), ('reviewer', 'RAG Reviewer'), ('embeddings', 'Document Embeddings'), ('search', 'Vector Search'), ('reranking', 'Re-ranking'), ('multimodal_embed', 'Multimodal Embedding'), ('other', 'Other')], db_index=True, default='other', max_length=50)), + ('purpose', models.CharField(choices=[('responder', 'RAG Responder'), ('reviewer', 'RAG Reviewer'), ('embeddings', 'Document Embeddings'), ('search', 'Vector Search'), ('reranking', 'Re-ranking'), ('multimodal_embed', 'Multimodal Embedding'), ('vision_analysis', 'Vision Analysis'), ('other', 'Other')], db_index=True, default='other', max_length=50)), ('request_metadata', models.JSONField(blank=True, help_text='Additional context (prompt, temperature, etc.)', null=True)), ('model', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='usage_records', to='llm_manager.llmmodel')), ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='llm_usage', to=settings.AUTH_USER_MODEL)), @@ -107,6 +108,10 @@ class Migration(migrations.Migration): model_name='llmmodel', index=models.Index(fields=['is_system_reranker_model', 'model_type'], name='llm_manager_is_syst_cc73c6_idx'), ), + migrations.AddIndex( + model_name='llmmodel', + index=models.Index(fields=['is_system_vision_model', 'model_type'], name='llm_manager_is_syst_d190bb_idx'), + ), migrations.AlterUniqueTogether( name='llmmodel', unique_together={('api', 'name')}, diff --git a/mnemosyne/llm_manager/migrations/0002_add_bedrock_api_type.py b/mnemosyne/llm_manager/migrations/0002_add_bedrock_api_type.py deleted file mode 100644 index b7ab1e6..0000000 --- a/mnemosyne/llm_manager/migrations/0002_add_bedrock_api_type.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -Add 'bedrock' to LLMApi.api_type choices. - -Django migrations track field changes including choices — this migration -updates the api_type field to include the new Amazon Bedrock option. -""" - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("llm_manager", "0001_initial"), - ] - - operations = [ - migrations.AlterField( - model_name="llmapi", - name="api_type", - field=models.CharField( - choices=[ - ("openai", "OpenAI Compatible"), - ("azure", "Azure OpenAI"), - ("ollama", "Ollama"), - ("anthropic", "Anthropic"), - ("llama-cpp", "Llama.cpp"), - ("vllm", "vLLM"), - ("bedrock", "Amazon Bedrock"), - ], - max_length=20, - ), - ), - ] diff --git a/mnemosyne/llm_manager/migrations/0003_add_vision_model_and_usage.py b/mnemosyne/llm_manager/migrations/0003_add_vision_model_and_usage.py deleted file mode 100644 index 5c7cdaf..0000000 --- a/mnemosyne/llm_manager/migrations/0003_add_vision_model_and_usage.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Add is_system_vision_model to LLMModel and vision_analysis purpose to LLMUsage. -""" - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("llm_manager", "0002_add_bedrock_api_type"), - ] - - operations = [ - migrations.AddField( - model_name="llmmodel", - name="is_system_vision_model", - field=models.BooleanField( - default=False, - help_text=( - "Mark this as the system-wide vision model for image analysis. " - "Only ONE vision model should have this set to True." - ), - ), - ), - migrations.AddIndex( - model_name="llmmodel", - index=models.Index( - fields=["is_system_vision_model", "model_type"], - name="llm_manager__is_syst_b2f4e7_idx", - ), - ), - migrations.AlterField( - model_name="llmusage", - name="purpose", - field=models.CharField( - choices=[ - ("responder", "RAG Responder"), - ("reviewer", "RAG Reviewer"), - ("embeddings", "Document Embeddings"), - ("search", "Vector Search"), - ("reranking", "Re-ranking"), - ("multimodal_embed", "Multimodal Embedding"), - ("vision_analysis", "Vision Analysis"), - ("other", "Other"), - ], - db_index=True, - default="other", - max_length=50, - ), - ), - ] diff --git a/mnemosyne/llm_manager/migrations/0004_rename_llm_manager__is_syst_b2f4e7_idx_llm_manager_is_syst_d190bb_idx.py b/mnemosyne/llm_manager/migrations/0004_rename_llm_manager__is_syst_b2f4e7_idx_llm_manager_is_syst_d190bb_idx.py deleted file mode 100644 index 0a4a055..0000000 --- a/mnemosyne/llm_manager/migrations/0004_rename_llm_manager__is_syst_b2f4e7_idx_llm_manager_is_syst_d190bb_idx.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.11 on 2026-03-22 15:15 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("llm_manager", "0003_add_vision_model_and_usage"), - ] - - operations = [ - migrations.RenameIndex( - model_name="llmmodel", - new_name="llm_manager_is_syst_d190bb_idx", - old_name="llm_manager__is_syst_b2f4e7_idx", - ), - ] diff --git a/mnemosyne/mcp_server/admin.py b/mnemosyne/mcp_server/admin.py index 086a2d7..faabf2d 100644 --- a/mnemosyne/mcp_server/admin.py +++ b/mnemosyne/mcp_server/admin.py @@ -1,15 +1,174 @@ -from django.contrib import admin +"""Django admin registrations for the mcp_server app. -from .models import MCPSigningKey, MCPToken +Three surfaces are exposed: + +* :class:`MCPTokenAdmin` — read/edit opaque bearer tokens. Token + creation still goes through the self-service dashboard so the + plaintext can be shown exactly once; admin gets a filtered + ``allowed_libraries`` picker so operators can scope an existing + token without leaving the admin. +* :class:`TeamAdmin` + :class:`TeamWorkspaceAssignmentInline` — read / + soft-delete / rotate / reattach Pallas teams. Creation usually comes + from Daedalus (``POST /mcp_server/api/teams/``) but the admin is the + break-glass path when Daedalus is offline. +* :class:`LibraryMembershipAdmin` — manage who can grant each + Neo4j-resident Library into a ``MCPToken.allowed_libraries``. + +See ``docs/DAEDALUS_PALLAS_INTEGRATION_v1.md`` for the overall model. +""" + +from __future__ import annotations + +from django import forms +from django.contrib import admin, messages + +from .models import ( + LibraryMembership, + MCPSigningKey, + MCPToken, + Team, + TeamWorkspaceAssignment, +) +from .teams import TeamJWTError, mint_team_jwt + + +# --------------------------------------------------------------------------- +# Helpers for the Library picker +# --------------------------------------------------------------------------- + + +def _library_choices_for_user(user) -> list[tuple[str, str]]: + """Return ``[(uid, label), …]`` of Libraries the user may grant. + + A user may grant a Library into a token's ``allowed_libraries`` iff + they hold ``owner`` or ``manager`` membership on it (see §4.1 of the + design doc). Superusers bypass the filter so they can mint tokens on + behalf of any principal. + + The label is ``"<name> [<library_type>]"`` when the Library node + exists in Neo4j at render time, otherwise the bare UID — the + membership row can legitimately outlive the Neo4j node during an + unclean delete, and we shouldn't crash the admin in that case. + """ + if user is None: + return [] + + if getattr(user, "is_superuser", False): + uids = set(_neo4j_library_map().keys()) + else: + uids = set( + LibraryMembership.objects + .filter( + user=user, + role__in=( + LibraryMembership.Role.OWNER, + LibraryMembership.Role.MANAGER, + ), + ) + .values_list("library_uid", flat=True) + ) + + labels = _neo4j_library_map() + return sorted( + [(uid, labels.get(uid, uid)) for uid in uids], + key=lambda pair: pair[1].lower(), + ) + + +def _neo4j_library_map() -> dict[str, str]: + """``{uid: display_label}`` for every Neo4j ``Library`` node. + + Fails softly if Neo4j is unreachable — the admin page still renders + with whatever membership rows exist, using the raw UIDs as labels. + """ + try: + from library.models import Library + + out: dict[str, str] = {} + for lib in Library.nodes.all(): + uid = getattr(lib, "uid", None) + if not uid: + continue + name = getattr(lib, "name", uid) + lib_type = getattr(lib, "library_type", "") + out[uid] = f"{name} [{lib_type}]" if lib_type else name + return out + except Exception: # pragma: no cover - Neo4j offline paths + return {} + + +class _LibraryPickerField(forms.MultipleChoiceField): + """``MultipleChoiceField`` whose choices are rebuilt per-request. + + Django ``ModelForm`` instantiates fields at class-definition time, + but the set of grantable libraries depends on the request user. + We override :meth:`_bound_choices` indirectly by setting + ``self.choices`` in :meth:`MCPTokenAdminForm.__init__`. + """ + + def __init__(self, *args, **kwargs): + kwargs.setdefault("required", False) + kwargs.setdefault( + "help_text", + "Libraries this token may read. Empty = zero libraries (fail-closed).", + ) + kwargs.setdefault("widget", forms.CheckboxSelectMultiple) + super().__init__(*args, choices=[], **kwargs) + + +class MCPTokenAdminForm(forms.ModelForm): + """``MCPToken`` admin form with a membership-filtered library picker. + + The underlying field is a ``JSONField(list)``; the form substitutes + a checkbox multi-select that writes the same JSON list shape. We + bind the choices against the *token's* user rather than the admin + viewer — that matches the scoping rule in the design doc (whether + the GRANTEE may hold these libraries, not whether the admin can). + """ + + allowed_libraries = _LibraryPickerField() + + class Meta: + model = MCPToken + fields = [ + "user", + "name", + "is_active", + "expires_at", + "allowed_tools", + "allowed_libraries", + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + user = None + if self.instance and self.instance.pk: + user = self.instance.user + elif self.initial.get("user"): + user = self.initial.get("user") + self.fields["allowed_libraries"].choices = _library_choices_for_user(user) + + if self.instance and self.instance.pk: + self.fields["allowed_libraries"].initial = list( + self.instance.allowed_libraries or [] + ) + + def clean_allowed_libraries(self): + # Multi-choice returns a list; we want it verbatim as JSON. + value = self.cleaned_data.get("allowed_libraries") or [] + return list(value) @admin.register(MCPToken) class MCPTokenAdmin(admin.ModelAdmin): + form = MCPTokenAdminForm + list_display = [ "name", "user", "is_active", "masked_token", + "library_count", "expires_at", "last_used_at", "created_at", @@ -19,7 +178,17 @@ class MCPTokenAdmin(admin.ModelAdmin): readonly_fields = ["token_hash", "last_used_at", "created_at", "updated_at"] fieldsets = ( (None, {"fields": ("user", "name", "is_active")}), - ("Restrictions", {"fields": ("allowed_tools", "expires_at")}), + ( + "Scope", + { + "fields": ("allowed_tools", "allowed_libraries", "expires_at"), + "description": ( + "``allowed_libraries`` is fail-closed: empty = the token " + "can read no libraries. Picker shows libraries the token's " + "user has owner/manager membership on." + ), + }, + ), ( "Token (hashed at rest — plaintext is shown only once at creation)", {"fields": ("token_hash",)}, @@ -31,6 +200,10 @@ class MCPTokenAdmin(admin.ModelAdmin): def masked_token(self, obj): return obj.get_masked_token() + @admin.display(description="Libraries") + def library_count(self, obj): + return len(obj.allowed_libraries or []) + def has_add_permission(self, request): # Tokens must be created via the dashboard or management command # so the plaintext can be surfaced to the user. Adding via admin @@ -38,6 +211,11 @@ class MCPTokenAdmin(admin.ModelAdmin): return False +# --------------------------------------------------------------------------- +# MCPSigningKey +# --------------------------------------------------------------------------- + + @admin.register(MCPSigningKey) class MCPSigningKeyAdmin(admin.ModelAdmin): list_display = ["kid", "is_active", "created_at", "retired_at", "note"] @@ -45,3 +223,107 @@ class MCPSigningKeyAdmin(admin.ModelAdmin): search_fields = ["kid", "note"] readonly_fields = ["created_at", "retired_at"] fields = ["kid", "secret_hex", "is_active", "note", "created_at", "retired_at"] + + +# --------------------------------------------------------------------------- +# LibraryMembership +# --------------------------------------------------------------------------- + + +@admin.register(LibraryMembership) +class LibraryMembershipAdmin(admin.ModelAdmin): + list_display = ["user", "library_uid", "role", "created_at"] + list_filter = ["role"] + search_fields = ["user__username", "user__email", "library_uid"] + autocomplete_fields = ["user"] + readonly_fields = ["created_at"] + + +# --------------------------------------------------------------------------- +# Team + TeamWorkspaceAssignment +# --------------------------------------------------------------------------- + + +class TeamWorkspaceAssignmentInline(admin.TabularInline): + model = TeamWorkspaceAssignment + extra = 0 + readonly_fields = ["created_at"] + fields = ["workspace_id", "created_at"] + + +@admin.register(Team) +class TeamAdmin(admin.ModelAdmin): + list_display = ["name", "id", "active", "workspace_count", "updated_at"] + list_filter = ["active"] + search_fields = ["name", "id"] + readonly_fields = [ + "id", + "active_jti", + "created_at", + "updated_at", + ] + fields = [ + "id", + "name", + "active", + "active_jti", + "created_at", + "updated_at", + ] + inlines = [TeamWorkspaceAssignmentInline] + actions = ["action_rotate_jwt", "action_deactivate"] + + @admin.display(description="Workspaces") + def workspace_count(self, obj: Team) -> int: + return obj.workspace_assignments.count() + + @admin.action(description="Rotate JWT (mint new, reveal once)") + def action_rotate_jwt(self, request, queryset): + revealed = [] + for team in queryset: + if not team.active: + self.message_user( + request, + f"Skipped inactive team {team.name}", + level=messages.WARNING, + ) + continue + team.rotate_jti() + try: + jwt_string = mint_team_jwt(team) + except TeamJWTError as exc: + self.message_user( + request, + f"Failed to mint JWT for {team.name}: {exc}", + level=messages.ERROR, + ) + continue + revealed.append((team.name, jwt_string)) + + for name, jwt_string in revealed: + # Messages are surfaced in the admin banner. Operator is + # expected to copy the JWT immediately — there is no retrieval + # path afterward. + self.message_user( + request, + f"{name}: {jwt_string}", + level=messages.SUCCESS, + ) + + @admin.action(description="Deactivate (soft-delete)") + def action_deactivate(self, request, queryset): + for team in queryset: + team.deactivate() + self.message_user( + request, + f"Deactivated {queryset.count()} team(s). Their JWTs are now invalid.", + level=messages.SUCCESS, + ) + + +@admin.register(TeamWorkspaceAssignment) +class TeamWorkspaceAssignmentAdmin(admin.ModelAdmin): + list_display = ["team", "workspace_id", "created_at"] + list_filter = ["team"] + search_fields = ["team__name", "workspace_id"] + readonly_fields = ["created_at"] diff --git a/mnemosyne/mcp_server/api/__init__.py b/mnemosyne/mcp_server/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mnemosyne/mcp_server/api/serializers.py b/mnemosyne/mcp_server/api/serializers.py new file mode 100644 index 0000000..a08da7c --- /dev/null +++ b/mnemosyne/mcp_server/api/serializers.py @@ -0,0 +1,86 @@ +"""DRF serializers for the ``/mcp_server/api/teams/`` control plane. + +These endpoints are the Daedalus → Mnemosyne control plane described +in §7 of ``docs/DAEDALUS_PALLAS_INTEGRATION_v1.md``. They are called +by the ``daedalus-service`` account, not by end users or bearer +tokens. +""" + +from __future__ import annotations + +from rest_framework import serializers + +from ..models import Team + + +class TeamCreateRequestSerializer(serializers.Serializer): + """Inbound payload for ``POST /mcp_server/api/teams/``. + + ``id`` is the Daedalus ``PallasInstance.id`` UUID. It is supplied + by the caller (not generated by Mnemosyne) so the two control + planes share a stable key. + """ + + id = serializers.UUIDField() + name = serializers.CharField(max_length=200) + + +class TeamPublicSerializer(serializers.ModelSerializer): + """Outbound payload for team reads / updates. + + Excludes the JWT — it is surfaced exactly once on create / rotate + and never recoverable after. Include ``workspace_ids`` so Daedalus + can diff its local state against what Mnemosyne actually holds. + """ + + workspace_ids = serializers.SerializerMethodField() + + class Meta: + model = Team + fields = [ + "id", + "name", + "active", + "active_jti", + "created_at", + "updated_at", + "workspace_ids", + ] + read_only_fields = fields + + def get_workspace_ids(self, obj: Team) -> list[str]: + return list( + obj.workspace_assignments.order_by("workspace_id") + .values_list("workspace_id", flat=True) + ) + + +class TeamWorkspacesUpdateSerializer(serializers.Serializer): + """Inbound payload for ``PUT /mcp_server/api/teams/{id}/workspaces/``. + + The request is an idempotent *replace* — the team's assignment set + after the call equals ``workspace_ids`` exactly. Daedalus is the + source of truth; Mnemosyne mirrors. + """ + + workspace_ids = serializers.ListField( + child=serializers.CharField(max_length=64, allow_blank=False), + allow_empty=True, + ) + + def validate_workspace_ids(self, value: list[str]) -> list[str]: + # De-duplicate while preserving order; strip whitespace. ``unique_together`` + # would already reject dupes at the DB layer but failing earlier gives + # the caller a cleaner 400. + seen = set() + out = [] + for ws in value: + ws = ws.strip() + if not ws: + raise serializers.ValidationError( + "workspace_ids entries must be non-empty strings." + ) + if ws not in seen: + seen.add(ws) + out.append(ws) + return out diff --git a/mnemosyne/mcp_server/api/teams.py b/mnemosyne/mcp_server/api/teams.py new file mode 100644 index 0000000..fee8882 --- /dev/null +++ b/mnemosyne/mcp_server/api/teams.py @@ -0,0 +1,251 @@ +"""REST endpoints for the Mnemosyne ↔ Daedalus team control plane. + +Mounted under ``/mcp_server/api/teams/`` — see §7 of +``docs/DAEDALUS_PALLAS_INTEGRATION_v1.md``. Every endpoint is +``IsAuthenticated``-gated against the ``daedalus-service`` HTTP Basic +account (same surface as ``/library/api/workspaces/``). Endpoints are +designed to be idempotent where possible: + +* ``POST /`` — create a team by UUID; a second POST with + the same id returns 200 without a new JWT. +* ``DELETE /{id}/`` — soft-delete (``active=False``). 204 even if + the team was already inactive. +* ``PUT /{id}/workspaces/`` — replace the team's workspace assignment + set; idempotent. +* ``POST /{id}/rotate/`` — generate a new ``active_jti`` and sign a + fresh JWT. Old JWT invalid immediately. +* ``GET /{id}/`` — read-only detail (no JWT). + +All mutating endpoints log an audit line at INFO — these are the audit +points ``docs/DAEDALUS_PALLAS_INTEGRATION_v1.md`` §13.4 refers to. +""" + +from __future__ import annotations + +import logging + +from django.db import transaction +from rest_framework import status +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from ..models import Team, TeamWorkspaceAssignment +from ..teams import TeamJWTError, mint_team_jwt +from .serializers import ( + TeamCreateRequestSerializer, + TeamPublicSerializer, + TeamWorkspacesUpdateSerializer, +) + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _get_team(team_id): + """Load a team by UUID or None. Callers 404 on None.""" + return Team.objects.filter(pk=team_id).first() + + +def _mint_with_fresh_jti(team: Team) -> str: + """Rotate the team's jti and return a freshly-signed JWT.""" + team.rotate_jti() + return mint_team_jwt(team) + + +# --------------------------------------------------------------------------- +# POST /mcp_server/api/teams/ +# --------------------------------------------------------------------------- + + +@api_view(["POST"]) +@permission_classes([IsAuthenticated]) +def team_create(request): + """Create a team. Idempotent on ``id``. + + * Fresh id → 201 with ``jwt`` surfaced exactly once. + * Existing id → 200 with the team's current state, **no** ``jwt``. + To get a new JWT for an existing team, call ``/rotate/``. + """ + serializer = TeamCreateRequestSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + data = serializer.validated_data + + team = _get_team(data["id"]) + if team is not None: + # Idempotent: surface current state without a new JWT. If the + # caller wants to reactivate a soft-deleted team they must do + # so explicitly via the admin or a future endpoint; re-POST is + # not "undelete" because that would silently return a new JWT + # on every retry storm. + logger.info( + "team_create idempotent_hit team_id=%s name=%s active=%s", + team.id, team.name, team.active, + ) + return Response( + TeamPublicSerializer(team).data, status=status.HTTP_200_OK + ) + + try: + with transaction.atomic(): + team = Team.objects.create(id=data["id"], name=data["name"]) + jwt_string = _mint_with_fresh_jti(team) + except TeamJWTError as exc: + # Rolling back the create is fine — we have no signing key yet + # and cannot hand the caller a usable credential. + logger.error( + "team_create mint_failed team_id=%s reason=%s", + data["id"], exc, + ) + return Response( + {"detail": str(exc)}, + status=status.HTTP_503_SERVICE_UNAVAILABLE, + ) + + logger.info( + "team_create created team_id=%s name=%s jti=%s", + team.id, team.name, team.active_jti, + ) + payload = TeamPublicSerializer(team).data + payload["jwt"] = jwt_string + return Response(payload, status=status.HTTP_201_CREATED) + + +# --------------------------------------------------------------------------- +# DELETE /mcp_server/api/teams/{id}/ + GET /mcp_server/api/teams/{id}/ +# --------------------------------------------------------------------------- + + +@api_view(["GET", "DELETE"]) +@permission_classes([IsAuthenticated]) +def team_detail(request, team_id): + """Soft-delete the team (``DELETE``) or read its state (``GET``). + + The DELETE path sets ``active=False`` and clears ``active_jti`` so + any token still in use is rejected on the next request. Row stays + in the database for audit; call POST ``/`` with the same id to + re-materialize a fresh team if needed (operator decision). + """ + team = _get_team(team_id) + if team is None: + return Response( + {"detail": "Team not found."}, status=status.HTTP_404_NOT_FOUND + ) + + if request.method == "GET": + return Response(TeamPublicSerializer(team).data) + + # DELETE + was_active = team.active + team.deactivate() + logger.info( + "team_delete team_id=%s was_active=%s", + team.id, was_active, + ) + return Response(status=status.HTTP_204_NO_CONTENT) + + +# --------------------------------------------------------------------------- +# PUT /mcp_server/api/teams/{id}/workspaces/ +# --------------------------------------------------------------------------- + + +@api_view(["PUT"]) +@permission_classes([IsAuthenticated]) +def team_workspaces(request, team_id): + """Replace the team's workspace assignment set. + + Idempotent: the stored set equals ``workspace_ids`` after the call. + Diff is computed in-DB so we don't thrash rows that already match — + only the net-add and net-remove rows are touched. + """ + team = _get_team(team_id) + if team is None: + return Response( + {"detail": "Team not found."}, status=status.HTTP_404_NOT_FOUND + ) + + serializer = TeamWorkspacesUpdateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + target = set(serializer.validated_data["workspace_ids"]) + + with transaction.atomic(): + current = set( + team.workspace_assignments.values_list("workspace_id", flat=True) + ) + to_add = target - current + to_remove = current - target + + if to_remove: + team.workspace_assignments.filter( + workspace_id__in=to_remove + ).delete() + if to_add: + TeamWorkspaceAssignment.objects.bulk_create( + [ + TeamWorkspaceAssignment(team=team, workspace_id=ws) + for ws in to_add + ], + ignore_conflicts=True, + ) + + logger.info( + "team_workspaces_updated team_id=%s added=%d removed=%d total=%d", + team.id, len(to_add), len(to_remove), len(target), + ) + return Response( + {"workspace_ids": sorted(target)}, + status=status.HTTP_200_OK, + ) + + +# --------------------------------------------------------------------------- +# POST /mcp_server/api/teams/{id}/rotate/ +# --------------------------------------------------------------------------- + + +@api_view(["POST"]) +@permission_classes([IsAuthenticated]) +def team_rotate(request, team_id): + """Generate a fresh ``active_jti`` and JWT. + + The previously-issued JWT stops validating immediately — the auth + middleware compares the incoming ``jti`` against ``Team.active_jti`` + on every request. + + Does NOT reactivate a soft-deleted team: if ``active=False``, + returns 409 so the operator is forced to go through the explicit + create/readd flow rather than quietly resurrecting a team. + """ + team = _get_team(team_id) + if team is None: + return Response( + {"detail": "Team not found."}, status=status.HTTP_404_NOT_FOUND + ) + if not team.active: + return Response( + {"detail": "Team is inactive; cannot rotate. Recreate it instead."}, + status=status.HTTP_409_CONFLICT, + ) + + try: + jwt_string = _mint_with_fresh_jti(team) + except TeamJWTError as exc: + logger.error( + "team_rotate mint_failed team_id=%s reason=%s", + team.id, exc, + ) + return Response( + {"detail": str(exc)}, + status=status.HTTP_503_SERVICE_UNAVAILABLE, + ) + + logger.info( + "team_rotate team_id=%s new_jti=%s", + team.id, team.active_jti, + ) + return Response({"jwt": jwt_string}, status=status.HTTP_200_OK) diff --git a/mnemosyne/mcp_server/api/urls.py b/mnemosyne/mcp_server/api/urls.py new file mode 100644 index 0000000..b0e3ae8 --- /dev/null +++ b/mnemosyne/mcp_server/api/urls.py @@ -0,0 +1,35 @@ +"""URL patterns for the ``/mcp_server/api/`` DRF control-plane API. + +These endpoints are called by the Daedalus backend (HTTP Basic auth +as ``daedalus-service``). End-user MCP traffic does NOT go through +this surface — that's ``mnemosyne.asgi`` / ``mcp_server/server.py``. +""" + +from __future__ import annotations + +from django.urls import path + +from . import teams + +app_name = "mcp-server-api" + +urlpatterns = [ + # Teams (Daedalus-Pallas integration; see + # docs/DAEDALUS_PALLAS_INTEGRATION_v1.md §7). + path("teams/", teams.team_create, name="team-create"), + path( + "teams/<uuid:team_id>/", + teams.team_detail, + name="team-detail", + ), + path( + "teams/<uuid:team_id>/workspaces/", + teams.team_workspaces, + name="team-workspaces", + ), + path( + "teams/<uuid:team_id>/rotate/", + teams.team_rotate, + name="team-rotate", + ), +] diff --git a/mnemosyne/mcp_server/auth.py b/mnemosyne/mcp_server/auth.py index c405261..4d0641e 100644 --- a/mnemosyne/mcp_server/auth.py +++ b/mnemosyne/mcp_server/auth.py @@ -1,27 +1,35 @@ """MCP token resolution and FastMCP middleware for bearer-token auth. -Two token shapes are supported: +Three credential types are accepted — see +``docs/DAEDALUS_PALLAS_INTEGRATION_v1.md`` §3.2 for the full model: -* **Opaque** — the original `MCPToken` row. Long-lived, hashed at rest, - used by the dashboard / Claude Desktop / admin tooling. Plaintext - hashes to a row in `mcp_token`. -* **Signed JWT** — per-turn token minted by Daedalus. Carries - `{ws, libs}` claims. Validated entirely off the signature + claims; - no database lookup of the token itself, only of the signing key - (`MCPSigningKey`) referenced by the JWT header's `kid`. +1. **Opaque ``MCPToken``** (long-lived, hashed at rest). Authorization + scope is its ``allowed_libraries`` JSON list. +2. **Per-turn signed JWT** (``iss=daedalus``, ≤10 min, legacy — retires + in Phase 4 when Daedalus chat itself becomes a Pallas Team). Scope + is the ``libs`` claim. +3. **Team JWT** (``iss=mnemosyne``, ``typ=team``, 10-year lifetime). + Scope is resolved live by joining ``TeamWorkspaceAssignment`` rows + to Neo4j ``Library.workspace_id``. + +Every branch populates a single :data:`STATE_KEY_RESOLVED_LIBRARIES` +value on the FastMCP context — a ``list[str]`` of Library UIDs the +downstream tools are permitted to read. Tools never consult claim +shapes; they read this list via +``mcp_server.context.get_mcp_resolved_libraries``. Detection: a bearer with three base64url segments separated by dots and -a parseable `{"alg":"HS256","kid":...}` header is treated as JWT; anything +a parseable ``{"alg":"HS256","kid":...}`` header is treated as JWT; anything else falls through to the opaque path. """ from __future__ import annotations import base64 -import hashlib import json import logging import time +import uuid from collections import OrderedDict import jwt as pyjwt @@ -32,20 +40,26 @@ from fastmcp.server.dependencies import get_http_request from fastmcp.server.middleware import Middleware, MiddlewareContext from .metrics import mcp_auth_failures_total -from .models import MCPSigningKey, MCPToken, hash_token +from .models import MCPSigningKey, MCPToken, Team, hash_token logger = logging.getLogger(__name__) STATE_KEY_USER = "mcp_user" STATE_KEY_TOKEN = "mcp_token" STATE_KEY_CLAIMS = "mcp_claims" +STATE_KEY_RESOLVED_LIBRARIES = "mcp_resolved_libraries" # Permitted clock skew when validating JWT exp/iat. PyJWT applies this # symmetrically as ``leeway``. _JWT_LEEWAY_SECONDS = 30 -# Mnemosyne is the audience; Daedalus is the only accepted issuer. -_JWT_ISS = "daedalus" +# Accepted JWT issuers. +# +# ``daedalus`` — per-turn tokens minted by Daedalus chat (legacy path, +# retires with Phase 4). +# ``mnemosyne`` — team tokens minted by this service. ``typ=team`` +# distinguishes them from any future self-issued credential. +_JWT_ISS_VALUES = {"daedalus", "mnemosyne"} # Bounded LRU of recently-seen jti values to discourage replay within # a single Mnemosyne process. Real defense is short ``exp`` + HMAC; this @@ -59,6 +73,10 @@ _JWT_ISS = "daedalus" # ``exp`` has passed — that's the scenario PyJWT's own ``exp`` check # would have already rejected, this is belt-and-braces for clock skew # or a resurrected captured token. +# +# Team tokens (``typ=team``) bypass this cache entirely — they are +# reused on every request by design. Revocation for those tokens runs +# against the live ``Team`` row (``active`` + ``active_jti``). _JTI_CACHE_MAX = 4096 _JTI_CACHE: "OrderedDict[str, float]" = OrderedDict() @@ -159,8 +177,22 @@ def _remember_jti(jti: str, exp: float) -> bool: def resolve_mcp_jwt(token_string: str) -> dict: """Validate a signed JWT and return its claims dict. - Raises ``MCPAuthError`` on any failure. Does not touch ``MCPToken`` — - JWTs are stateless and stored only as their signing key (``MCPSigningKey``). + Accepts both the legacy per-turn issuer (``iss=daedalus``) and the + new team issuer (``iss=mnemosyne``, ``typ=team``). The returned + claims dict is normalized so the middleware doesn't have to guess: + + * ``claims["iss"]`` — as presented (``daedalus`` or ``mnemosyne``). + * ``claims["typ"]`` — ``"team"`` for team tokens, otherwise absent. + * ``claims["libs"]`` — per-turn only; normalized to ``list[str]``. + * ``claims["ws"]`` — per-turn only; may be ``None``. Not consulted + for authorization (kept for diagnostics). + * ``claims["team_id"]`` — team only; ``UUID`` parsed from + ``sub == "team:<uuid>"``. + * ``claims["kid"]`` — copy of the JWT header's ``kid``. + + Raises :class:`MCPAuthError` on any failure. The per-turn path runs + the ``_remember_jti`` replay check; the team path skips it (team + JWTs are intentionally reused across the token's lifetime). """ try: unverified_header = pyjwt.get_unverified_header(token_string) @@ -191,7 +223,9 @@ def resolve_mcp_jwt(token_string: str) -> dict: algorithms=["HS256"], leeway=_JWT_LEEWAY_SECONDS, options={"require": ["exp", "iat", "iss", "sub", "jti"]}, - issuer=_JWT_ISS, + # ``issuer=`` accepts ``str | Iterable[str]`` and raises + # ``InvalidIssuerError`` if the claim is outside the set. + issuer=list(_JWT_ISS_VALUES), ) except pyjwt.ExpiredSignatureError: raise MCPAuthError("Token has expired.") @@ -212,19 +246,79 @@ def resolve_mcp_jwt(token_string: str) -> dict: # ``require=["exp", ...]`` above guarantees presence + numeric; this # is defence in depth against future PyJWT changes. raise MCPAuthError("JWT exp must be numeric.") - if _remember_jti(jti, float(exp)): - raise MCPAuthError("Token replay detected.") - # Normalize claim shapes: ws may be null/absent, libs default to []. - claims["ws"] = claims.get("ws") or None - libs = claims.get("libs") or [] - if not isinstance(libs, list) or not all(isinstance(x, str) for x in libs): - raise MCPAuthError("JWT libs must be a list of strings.") - claims["libs"] = libs + typ = claims.get("typ") + + if typ == "team": + # Team tokens: no replay cache, no ``libs`` or ``ws`` claims. + # Verify the ``sub`` shape and parse the embedded team UUID so + # the middleware doesn't have to re-parse it later. + sub = claims.get("sub") + if not isinstance(sub, str) or not sub.startswith("team:"): + raise MCPAuthError("Invalid MCP token.") + try: + claims["team_id"] = uuid.UUID(sub[len("team:"):]) + except ValueError: + raise MCPAuthError("Invalid MCP token.") + else: + # Per-turn (legacy) path: replay-cache gate + normalize claims. + if _remember_jti(jti, float(exp)): + raise MCPAuthError("Token replay detected.") + + claims["ws"] = claims.get("ws") or None + libs = claims.get("libs") or [] + if not isinstance(libs, list) or not all(isinstance(x, str) for x in libs): + raise MCPAuthError("JWT libs must be a list of strings.") + claims["libs"] = libs + claims["kid"] = kid return claims +# --- Team-JWT library resolution ------------------------------------------ + + +def _libraries_for_team(team_id: uuid.UUID, jti: str) -> list[str]: + """Resolve a team token to the Library UIDs it may read. + + Runs two cheap queries in sequence: + + 1. Fetch the ``Team`` row by UUID. Reject if it doesn't exist, is + inactive, or its ``active_jti`` doesn't match the incoming + ``jti`` — this is how rotation / soft-delete revocation becomes + effective on the *next* request. + 2. If active: read every ``TeamWorkspaceAssignment.workspace_id`` for + the team and translate them into Library UIDs via a single + Cypher query against Neo4j. + + Returns an empty list when the team has no workspace assignments + (fail-closed — a team pointing at no workspaces sees no libraries). + """ + try: + team = Team.objects.get(pk=team_id) + except Team.DoesNotExist: + raise MCPAuthError("Invalid MCP token.") + + if not team.active: + raise MCPAuthError("Token has been deactivated.") + if team.active_jti is None or str(team.active_jti) != jti: + raise MCPAuthError("Invalid MCP token.") + + workspace_ids = list( + team.workspace_assignments.values_list("workspace_id", flat=True) + ) + if not workspace_ids: + return [] + + from neomodel import db + + rows, _ = db.cypher_query( + "MATCH (l:Library) WHERE l.workspace_id IN $ws RETURN l.uid", + {"ws": workspace_ids}, + ) + return [row[0] for row in rows if row and row[0]] + + # --- Middleware ------------------------------------------------------------ @@ -234,7 +328,18 @@ class MCPAuthMiddleware(Middleware): Listing tools/resources is permitted unauthenticated so clients can discover the surface; calling a tool requires a valid token unless - MCP_REQUIRE_AUTH=False. + ``MCP_REQUIRE_AUTH=False``. + + On every authenticated call the middleware attaches four values to + the FastMCP ``Context`` state for downstream tools to consume via + :mod:`mcp_server.context`: + + * ``STATE_KEY_USER`` — Django user. + * ``STATE_KEY_TOKEN`` — MCPToken row (opaque callers only). + * ``STATE_KEY_CLAIMS`` — JWT claims dict (JWT callers only). + * ``STATE_KEY_RESOLVED_LIBRARIES`` — authorization-resolved Library + UID list. Tools read this; they never read ``STATE_KEY_CLAIMS`` + for authorization. """ # Tools that don't touch user data and must be callable without a token @@ -261,6 +366,7 @@ class MCPAuthMiddleware(Middleware): user = None token = None claims: dict | None = None + resolved_libraries: list[str] | None = None if token_string: try: @@ -271,10 +377,16 @@ class MCPAuthMiddleware(Middleware): user = await sync_to_async( _resolve_jwt_actor, thread_sensitive=True )(claims) + resolved_libraries = await sync_to_async( + _resolved_libraries_for_jwt, thread_sensitive=True + )(claims) else: user, token = await sync_to_async( resolve_mcp_user, thread_sensitive=True )(token_string) + # Opaque tokens store the Library UID list directly. + # Empty list = fail-closed; not "everything". + resolved_libraries = list(token.allowed_libraries or []) except MCPAuthError as exc: mcp_auth_failures_total.labels(reason=str(exc)).inc() if require_auth: @@ -283,7 +395,6 @@ class MCPAuthMiddleware(Middleware): mcp_auth_failures_total.labels(reason="missing_token").inc() raise PermissionError("Authentication required. Provide a Bearer token.") - tool_name = self._extract_tool_name(context) if token and tool_name and not token.can_use_tool(tool_name): mcp_auth_failures_total.labels(reason="tool_not_allowed").inc() raise PermissionError( @@ -298,6 +409,18 @@ class MCPAuthMiddleware(Middleware): await fastmcp_ctx.set_state(STATE_KEY_TOKEN, token) if claims is not None: await fastmcp_ctx.set_state(STATE_KEY_CLAIMS, claims) + # Always publish resolved_libraries — None means "no auth + # information" and the tools treat that as fail-closed. + await fastmcp_ctx.set_state( + STATE_KEY_RESOLVED_LIBRARIES, resolved_libraries + ) + + logger.info( + "mcp_auth.resolved tool=%s principal=%s lib_count=%s", + tool_name, + self._describe_principal(user, token, claims), + "none" if resolved_libraries is None else len(resolved_libraries), + ) return await self._call_next_with_trace(tool_name, call_next, context) @@ -334,6 +457,20 @@ class MCPAuthMiddleware(Middleware): ) return result + @staticmethod + def _describe_principal(user, token, claims) -> str: + """Compact, log-friendly principal summary. No PII beyond usernames.""" + if claims is not None: + typ = claims.get("typ") + if typ == "team": + return f"team:{claims.get('team_id')}" + return f"jwt:{claims.get('sub')}" + if token is not None: + return f"mcptoken:{token.get_masked_token()}" + if user is not None: + return f"user:{user.username}" + return "anonymous" + @staticmethod def _extract_token() -> str | None: """Pull the Bearer token off the current HTTP request, if any. @@ -412,6 +549,10 @@ def _resolve_jwt_actor(claims: dict): Returns the system service user (``MCP_JWT_SERVICE_USERNAME``, default ``daedalus-service``). The user must exist and be active. JWT tokens are not tied to per-user accounts — claims encode all authorization. + + Used for both per-turn and team JWTs. The service user is a hook for + usage accounting (LLMUsage / search metrics) and for the audit trail; + authorization does not depend on it. """ from django.contrib.auth import get_user_model @@ -426,3 +567,14 @@ def _resolve_jwt_actor(claims: dict): if not user.is_active: raise MCPAuthError(f"JWT service user {username!r} is disabled.") return user + + +def _resolved_libraries_for_jwt(claims: dict) -> list[str]: + """Pick the right resolver branch for a validated JWT claims dict. + + * ``typ == "team"`` → live lookup via :func:`_libraries_for_team`. + * otherwise → legacy ``claims["libs"]`` (per-turn JWT). + """ + if claims.get("typ") == "team": + return _libraries_for_team(claims["team_id"], claims["jti"]) + return list(claims.get("libs") or []) diff --git a/mnemosyne/mcp_server/context.py b/mnemosyne/mcp_server/context.py index dbf1c52..1a0c5f7 100644 --- a/mnemosyne/mcp_server/context.py +++ b/mnemosyne/mcp_server/context.py @@ -1,10 +1,35 @@ -"""Helpers for accessing the request-scoped MCP user/token from inside tools.""" +"""Helpers for accessing the request-scoped MCP auth state from inside tools. + +The authoritative values are set by :class:`mcp_server.auth.MCPAuthMiddleware` +on the FastMCP ``Context``: + +* ``STATE_KEY_USER`` — the Django user the bearer resolved to (synthetic + service user for JWT callers, concrete ``mcp_tokens.user`` for opaque + MCPToken callers, ``None`` for team JWTs which are not tied to any + per-user account). +* ``STATE_KEY_TOKEN`` — the ``MCPToken`` row for opaque-token callers; + ``None`` for JWT callers. +* ``STATE_KEY_CLAIMS`` — the JWT claims dict for JWT callers; ``None`` + for opaque-token callers. Intentionally exposed for debugging / + metrics; tools should NOT branch on claim shape for authorization, + they should read :func:`get_mcp_resolved_libraries` instead. +* ``STATE_KEY_RESOLVED_LIBRARIES`` — the authorization-resolved Library + UID set for this request. ``None`` means the caller is unauthenticated + or the auth middleware was bypassed (shouldn't happen in practice); + ``[]`` means the caller is authenticated but scoped to zero libraries + (fail-closed); a non-empty list enumerates the UIDs the caller may read. +""" from __future__ import annotations from fastmcp.server.context import Context -from .auth import STATE_KEY_CLAIMS, STATE_KEY_TOKEN, STATE_KEY_USER +from .auth import ( + STATE_KEY_CLAIMS, + STATE_KEY_RESOLVED_LIBRARIES, + STATE_KEY_TOKEN, + STATE_KEY_USER, +) async def get_mcp_user(ctx: Context | None): @@ -24,3 +49,23 @@ async def get_mcp_claims(ctx: Context | None) -> dict | None: if ctx is None: return None return await ctx.get_state(STATE_KEY_CLAIMS) + + +async def get_mcp_resolved_libraries(ctx: Context | None) -> list[str] | None: + """Return the authorization-resolved Library UID list for this request. + + Semantics (matching ``SearchRequest.resolved_libraries``): + + * ``None`` — no auth information available (e.g. the middleware did + not run, or the tool was invoked outside a request context). Tools + should treat this as fail-closed and refuse to return content. + * ``[]`` — the caller was authenticated but is scoped to zero + libraries. Tools MAY proceed and return empty results. + * ``["lib_x", …]`` — the caller may read exactly these libraries. + + See ``docs/DAEDALUS_PALLAS_INTEGRATION_v1.md`` §3.3 for the unified + auth model that populates this value. + """ + if ctx is None: + return None + return await ctx.get_state(STATE_KEY_RESOLVED_LIBRARIES) diff --git a/mnemosyne/mcp_server/forms.py b/mnemosyne/mcp_server/forms.py index b691892..a7804a0 100644 --- a/mnemosyne/mcp_server/forms.py +++ b/mnemosyne/mcp_server/forms.py @@ -1,4 +1,19 @@ -"""Forms for the MCP token self-service dashboard.""" +"""Forms for the MCP token self-service dashboard. + +The dashboard is where humans mint their own opaque :class:`MCPToken` +rows for external MCP clients (Claude Desktop, Cline, ...). The +plaintext is surfaced exactly once on the "created" page and never +retrievable again — see ``mcp_server/views.py``. + +Two pickers are rendered: + +* ``allowed_tools`` — multi-select over the FastMCP tool registry. + Empty = all tools permitted. Backs ``MCPToken.allowed_tools``. +* ``allowed_libraries`` — multi-select over Neo4j Libraries the + current request user has ``owner`` or ``manager`` membership on. + Empty = **zero** libraries (fail-closed), matching the semantics + in §4.1 of ``docs/DAEDALUS_PALLAS_INTEGRATION_v1.md``. +""" from __future__ import annotations @@ -7,6 +22,7 @@ import functools from django import forms +from .admin import _library_choices_for_user from .models import MCPToken @@ -51,10 +67,19 @@ class MCPTokenCreateForm(forms.Form): widget=forms.CheckboxSelectMultiple(attrs={"class": "checkbox checkbox-sm"}), help_text="Leave all unchecked to permit every tool.", ) + allowed_libraries = forms.MultipleChoiceField( + required=False, + widget=forms.CheckboxSelectMultiple(attrs={"class": "checkbox checkbox-sm"}), + help_text=( + "Libraries this token may read. Empty = zero libraries (fail-closed). " + "You only see libraries where you hold owner/manager membership." + ), + ) - def __init__(self, *args, **kwargs): + def __init__(self, *args, user=None, **kwargs): super().__init__(*args, **kwargs) self.fields["allowed_tools"].choices = _tool_choices() + self.fields["allowed_libraries"].choices = _library_choices_for_user(user) class MCPTokenEditForm(forms.ModelForm): @@ -65,10 +90,18 @@ class MCPTokenEditForm(forms.ModelForm): widget=forms.CheckboxSelectMultiple(attrs={"class": "checkbox checkbox-sm"}), help_text="Leave all unchecked to permit every tool.", ) + allowed_libraries = forms.MultipleChoiceField( + required=False, + widget=forms.CheckboxSelectMultiple(attrs={"class": "checkbox checkbox-sm"}), + help_text=( + "Libraries this token may read. Empty = zero libraries (fail-closed). " + "You only see libraries where you hold owner/manager membership." + ), + ) class Meta: model = MCPToken - fields = ["name", "is_active", "expires_at", "allowed_tools"] + fields = ["name", "is_active", "expires_at", "allowed_tools", "allowed_libraries"] widgets = { "name": forms.TextInput(attrs={"class": "input input-bordered w-full"}), "is_active": forms.CheckboxInput(attrs={"class": "toggle toggle-primary"}), @@ -78,8 +111,18 @@ class MCPTokenEditForm(forms.ModelForm): }), } - def __init__(self, *args, **kwargs): + def __init__(self, *args, user=None, **kwargs): super().__init__(*args, **kwargs) self.fields["allowed_tools"].choices = _tool_choices() + self.fields["allowed_libraries"].choices = _library_choices_for_user( + user or (self.instance.user if self.instance and self.instance.pk else None) + ) if self.instance and self.instance.pk: self.fields["allowed_tools"].initial = self.instance.allowed_tools or [] + self.fields["allowed_libraries"].initial = ( + list(self.instance.allowed_libraries or []) + ) + + def clean_allowed_libraries(self): + value = self.cleaned_data.get("allowed_libraries") or [] + return list(value) diff --git a/mnemosyne/mcp_server/management/commands/backfill_library_memberships.py b/mnemosyne/mcp_server/management/commands/backfill_library_memberships.py new file mode 100644 index 0000000..d3ab231 --- /dev/null +++ b/mnemosyne/mcp_server/management/commands/backfill_library_memberships.py @@ -0,0 +1,150 @@ +"""One-off backfill for ``LibraryMembership`` rows. + +Phase 2 introduces ``LibraryMembership`` as the Postgres-side gate on +who may scope a Neo4j-resident Library into a token's +``allowed_libraries``. Any Library that existed *before* this migration +has no membership rows, so nobody can grant it. + +Running this command assigns **owner** membership on every global +Library (``workspace_id IS NULL``) to the first active superuser. It +skips: + +* Workspace-scoped Libraries (``workspace_id`` is not null). Those + belong to a Daedalus workspace and will be reachable via team JWTs + once Phase 4 wires up ``TeamWorkspaceAssignment``. Granting them to + a superuser MCPToken would silently widen the blast radius of a + leaked token. +* Libraries that already have any :class:`LibraryMembership` row. We + do not stack roles for idempotency. + +Idempotent. Safe to run on every deploy. +""" + +from __future__ import annotations + +import logging + +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand, CommandError + +from mcp_server.models import LibraryMembership + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = ( + "Backfill LibraryMembership(role=owner) for every global Neo4j Library, " + "assigning it to the first active superuser. Skips workspace-scoped " + "Libraries and Libraries that already have any membership row." + ) + + def add_arguments(self, parser): + parser.add_argument( + "--user", + dest="username", + default=None, + help=( + "Username to grant ownership to. Defaults to the first active " + "superuser, ordered by id ascending." + ), + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Report what would be inserted, don't persist.", + ) + + def handle(self, *args, **options): + User = get_user_model() + + username = options.get("username") + if username: + try: + target = User.objects.get(username=username, is_active=True) + except User.DoesNotExist as exc: + raise CommandError( + f"No active user {username!r} found." + ) from exc + else: + target = ( + User.objects + .filter(is_superuser=True, is_active=True) + .order_by("id") + .first() + ) + if target is None: + raise CommandError( + "No active superuser to own libraries. " + "Pass --user=<username> or create a superuser first." + ) + + try: + from library.models import Library + except Exception as exc: # pragma: no cover + raise CommandError( + f"Could not import library.models.Library (Neo4j unreachable?): {exc}" + ) from exc + + libraries = list( + Library.nodes.filter(workspace_id__isnull=True) + ) + + already_scoped = set( + LibraryMembership.objects + .values_list("library_uid", flat=True) + ) + + to_create = [] + skipped_workspace = 0 + skipped_existing = 0 + for lib in libraries: + if not getattr(lib, "uid", None): + continue + if getattr(lib, "workspace_id", None): + # Shouldn't happen — the .filter above should eliminate + # them — but belt-and-braces against a misconfigured query. + skipped_workspace += 1 + continue + if lib.uid in already_scoped: + skipped_existing += 1 + continue + to_create.append( + LibraryMembership( + user=target, + library_uid=lib.uid, + role=LibraryMembership.Role.OWNER, + ) + ) + + self.stdout.write( + self.style.HTTP_INFO( + f"Target user: {target.username} (id={target.id})" + ) + ) + self.stdout.write( + f"Global libraries found: {len(libraries)}" + ) + self.stdout.write( + f" already have a membership: {skipped_existing}" + ) + self.stdout.write( + f" workspace-scoped (skipped): {skipped_workspace}" + ) + self.stdout.write( + self.style.SUCCESS(f"Will insert: {len(to_create)}") + ) + + if options["dry_run"]: + self.stdout.write(self.style.WARNING("--dry-run: no rows inserted.")) + for m in to_create: + self.stdout.write(f" would insert: {m.library_uid} → {target.username}") + return + + if to_create: + LibraryMembership.objects.bulk_create(to_create, ignore_conflicts=True) + logger.info( + "backfill_library_memberships inserted=%d user=%s", + len(to_create), target.username, + ) + self.stdout.write(self.style.SUCCESS("Done.")) diff --git a/mnemosyne/mcp_server/migrations/0001_initial.py b/mnemosyne/mcp_server/migrations/0001_initial.py index 674ec87..905c7cb 100644 --- a/mnemosyne/mcp_server/migrations/0001_initial.py +++ b/mnemosyne/mcp_server/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.13 on 2026-04-26 18:59 +# Generated by Django 5.2.13 on 2026-05-10 15:31 import django.db.models.deletion from django.conf import settings @@ -14,16 +14,46 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='MCPSigningKey', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('kid', models.CharField(db_index=True, max_length=64, unique=True)), + ('secret_hex', models.CharField(max_length=128)), + ('is_active', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('retired_at', models.DateTimeField(blank=True, null=True)), + ('note', models.TextField(blank=True)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='Team', + fields=[ + ('id', models.UUIDField(editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=200)), + ('active', models.BooleanField(default=True)), + ('active_jti', models.UUIDField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'ordering': ['name'], + }, + ), migrations.CreateModel( name='MCPToken', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('token', models.CharField(db_index=True, max_length=64, unique=True)), + ('token_hash', models.CharField(db_index=True, max_length=64, unique=True)), ('name', models.CharField(max_length=100)), ('is_active', models.BooleanField(default=True)), ('expires_at', models.DateTimeField(blank=True, null=True)), ('last_used_at', models.DateTimeField(blank=True, null=True)), ('allowed_tools', models.JSONField(blank=True, default=list)), + ('allowed_libraries', models.JSONField(blank=True, default=list)), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mcp_tokens', to=settings.AUTH_USER_MODEL)), @@ -32,4 +62,32 @@ class Migration(migrations.Migration): 'ordering': ['-created_at'], }, ), + migrations.CreateModel( + name='LibraryMembership', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('library_uid', models.CharField(db_index=True, max_length=64)), + ('role', models.CharField(choices=[('owner', 'Owner'), ('manager', 'Manager'), ('reader', 'Reader')], max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='library_memberships', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['library_uid', 'role'], + 'indexes': [models.Index(fields=['library_uid', 'role'], name='mcp_server__library_f19411_idx')], + 'constraints': [models.UniqueConstraint(fields=('user', 'library_uid'), name='unique_library_membership_per_user')], + }, + ), + migrations.CreateModel( + name='TeamWorkspaceAssignment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('workspace_id', models.CharField(db_index=True, max_length=64)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_assignments', to='mcp_server.team')), + ], + options={ + 'ordering': ['team', 'workspace_id'], + 'constraints': [models.UniqueConstraint(fields=('team', 'workspace_id'), name='unique_team_workspace')], + }, + ), ] diff --git a/mnemosyne/mcp_server/migrations/0002_hash_token.py b/mnemosyne/mcp_server/migrations/0002_hash_token.py deleted file mode 100644 index 55a8aab..0000000 --- a/mnemosyne/mcp_server/migrations/0002_hash_token.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Hash MCPToken values at rest. - -Renames ``token`` → ``token_hash`` and rewrites any pre-existing plaintext -values into SHA-256 hex digests in-place. Forward-only: hashing is one-way, -so no reverse migration is provided. - -Existing tokens issued before this migration keep working only because -``resolve_mcp_user`` hashes the incoming bearer before lookup; the original -plaintext the client holds still hashes to what we just wrote. -""" - -import hashlib - -from django.db import migrations, models - - -def hash_existing_tokens(apps, schema_editor): - MCPToken = apps.get_model("mcp_server", "MCPToken") - for token in MCPToken.objects.all(): - plaintext = token.token_hash # post-rename, still holds original plaintext - token.token_hash = hashlib.sha256(plaintext.encode("utf-8")).hexdigest() - token.save(update_fields=["token_hash"]) - - -def noop_reverse(apps, schema_editor): - # Cannot reverse a hash. Leaving as no-op so the schema can be rolled - # back, but operators must understand any hashed rows are unrecoverable. - pass - - -class Migration(migrations.Migration): - dependencies = [ - ("mcp_server", "0001_initial"), - ] - - operations = [ - migrations.RenameField( - model_name="mcptoken", - old_name="token", - new_name="token_hash", - ), - migrations.RunPython(hash_existing_tokens, noop_reverse), - ] diff --git a/mnemosyne/mcp_server/migrations/0003_mcpsigningkey.py b/mnemosyne/mcp_server/migrations/0003_mcpsigningkey.py deleted file mode 100644 index 8eeb3c5..0000000 --- a/mnemosyne/mcp_server/migrations/0003_mcpsigningkey.py +++ /dev/null @@ -1,38 +0,0 @@ -"""HMAC signing keys for per-turn JWTs minted by Daedalus. - -Adds the MCPSigningKey table. Per-turn tokens (workspace + library claims, -exp <= 600s) are not stored — only the signing key, indexed by ``kid``, -so the signature can be validated and rotated cleanly. -""" - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("mcp_server", "0002_hash_token"), - ] - - operations = [ - migrations.CreateModel( - name="MCPSigningKey", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("kid", models.CharField(max_length=64, unique=True, db_index=True)), - ("secret_hex", models.CharField(max_length=128)), - ("is_active", models.BooleanField(default=True)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("retired_at", models.DateTimeField(blank=True, null=True)), - ("note", models.TextField(blank=True)), - ], - options={"ordering": ["-created_at"]}, - ), - ] diff --git a/mnemosyne/mcp_server/models.py b/mnemosyne/mcp_server/models.py index d5426dd..c6eddd1 100644 --- a/mnemosyne/mcp_server/models.py +++ b/mnemosyne/mcp_server/models.py @@ -1,5 +1,31 @@ +"""Django ORM models for the MCP server app. + +This module defines every Postgres-backed row the Mnemosyne MCP surface +relies on: + +* :class:`MCPToken` — opaque bearer tokens (SHA-256 hashed at rest). +* :class:`MCPSigningKey` — HMAC signing keys (``HS256``) for JWTs, + keyed by ``kid``. Used by the legacy per-turn path *and* by team + JWTs minted in §7 of ``DAEDALUS_PALLAS_INTEGRATION_v1.md``. +* :class:`LibraryMembership` — Postgres-side role membership for + Neo4j-resident Libraries. Referenced by Library ``uid`` string + because Library is a neomodel ``StructuredNode``, not a Django + ORM model. +* :class:`Team` — Pallas deployment identity inside Mnemosyne. + Stable UUID = ``PallasInstance.id`` on the Daedalus side. +* :class:`TeamWorkspaceAssignment` — which Daedalus workspaces a + given team is allowed to see. Queried live on every request so + revocation via ``DELETE`` / ``PUT /workspaces/`` is instantaneous. + +See ``mnemosyne/docs/DAEDALUS_PALLAS_INTEGRATION_v1.md`` for the +complete credential / authorization model. +""" + +from __future__ import annotations + import hashlib import secrets +import uuid from django.conf import settings from django.db import models @@ -11,8 +37,75 @@ def hash_token(plaintext: str) -> str: return hashlib.sha256(plaintext.encode("utf-8")).hexdigest() +# --------------------------------------------------------------------------- +# Library memberships +# --------------------------------------------------------------------------- + + +class LibraryMembership(models.Model): + """Role of a user on a Neo4j-resident Library. + + Library lives in Neo4j (``library.models.Library``, a neomodel + ``StructuredNode``), so this table joins by the Library's + ``uid`` string rather than a foreign key. Consumers that want + the Library's live state (name, description, workspace_id, …) + must look it up separately via ``Library.nodes.get(uid=…)``. + + Roles are ordered (owner > manager > reader) but not hierarchical + in storage: a user with owner rights is represented by a single + row with ``role="owner"``, not multiple rows. Callers deciding + whether a user may *grant* a Library into an ``MCPToken`` should + check for ``role__in=("owner", "manager")``. + """ + + class Role(models.TextChoices): + OWNER = "owner", "Owner" + MANAGER = "manager", "Manager" + READER = "reader", "Reader" + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="library_memberships", + ) + library_uid = models.CharField(max_length=64, db_index=True) + role = models.CharField(max_length=10, choices=Role.choices) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + # One (user, library_uid, role) triple per row. A user may not + # hold both ``manager`` and ``reader`` on the same library — + # callers must consolidate to the higher role. + constraints = [ + models.UniqueConstraint( + fields=("user", "library_uid"), + name="unique_library_membership_per_user", + ) + ] + indexes = [ + models.Index(fields=("library_uid", "role")), + ] + ordering = ["library_uid", "role"] + + def __str__(self): # pragma: no cover - trivial + return f"{self.user} → {self.library_uid} ({self.role})" + + +# --------------------------------------------------------------------------- +# Opaque bearer tokens +# --------------------------------------------------------------------------- + + class MCPTokenManager(models.Manager): - def create_token(self, *, user, name, allowed_tools=None, expires_at=None): + def create_token( + self, + *, + user, + name, + allowed_tools=None, + allowed_libraries=None, + expires_at=None, + ): """Generate a new bearer token, store its hash, and return (instance, plaintext). The plaintext is returned exactly once and is never persisted. Callers @@ -25,6 +118,7 @@ class MCPTokenManager(models.Manager): name=name, token_hash=hash_token(plaintext), allowed_tools=list(allowed_tools or []), + allowed_libraries=list(allowed_libraries or []), expires_at=expires_at, ) return instance, plaintext @@ -36,6 +130,12 @@ class MCPToken(models.Model): Tokens are hashed at rest (SHA-256, 64-char hex). Plaintext exists only in memory at creation time, on the wire to the client, and in the user's own storage. A leaked database backup discloses no usable credentials. + + ``allowed_libraries`` is a JSON list of Library ``uid`` strings. It is + the sole authorization axis for opaque-token callers: the auth + middleware materializes ``resolved_libraries = list(allowed_libraries)`` + on every request. An empty list is fail-closed (the token sees nothing), + not an implicit "all". """ user = models.ForeignKey( @@ -49,6 +149,12 @@ class MCPToken(models.Model): expires_at = models.DateTimeField(null=True, blank=True) last_used_at = models.DateTimeField(null=True, blank=True) allowed_tools = models.JSONField(default=list, blank=True) + + # JSON list of Library.uid strings. Fail-closed: empty → zero libraries. + # We cannot use a ``ManyToManyField(Library)`` because Library is a + # neomodel ``StructuredNode`` in Neo4j, not a Django ORM model. + allowed_libraries = models.JSONField(default=list, blank=True) + created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -86,6 +192,11 @@ class MCPToken(models.Model): return f"mcp_…{self.token_hash[:8]}" +# --------------------------------------------------------------------------- +# Signing keys +# --------------------------------------------------------------------------- + + class MCPSigningKeyManager(models.Manager): def active(self): """Active keys, newest first. Multiple may overlap during rotation.""" @@ -94,17 +205,26 @@ class MCPSigningKeyManager(models.Manager): def by_kid(self, kid: str): return self.filter(kid=kid).first() + def current(self): + """Most recently seeded active key — used when minting new tokens.""" + return self.filter(is_active=True).order_by("-created_at").first() + class MCPSigningKey(models.Model): - """HMAC signing key for per-turn JWTs minted by Daedalus. + """HMAC signing key used for every Mnemosyne JWT (``HS256``). - Per-turn tokens carry workspace + library claims and expire in minutes. - They are validated entirely off the signature + claims; no row is stored - per token. Only the *signing key* is persisted here, indexed by ``kid``. + Two populations of tokens share this keyring: - Rotation: seed a new active key, distribute the secret to Daedalus, - flip the old one ``is_active=False``. In-flight tokens with the retired - ``kid`` fail at ``exp`` (bounded by the per-turn TTL). + * **Per-turn JWTs** (legacy, category 2) minted by Daedalus with + ``exp`` ≤ 10 minutes. Retired in Phase 4. + * **Team JWTs** (category 3) minted by Mnemosyne itself with + ``exp`` = 10 years. Signed with whichever ``MCPSigningKey`` was + ``objects.current()`` at mint time. + + Rotation: seed a new active key, distribute the secret to + Daedalus (for the per-turn path) and re-issue every team token + via ``POST /mcp_server/api/teams/{id}/rotate/`` (for the team + path), then flip the old one ``is_active=False``. """ kid = models.CharField(max_length=64, unique=True, db_index=True) @@ -127,3 +247,87 @@ class MCPSigningKey(models.Model): self.is_active = False self.retired_at = timezone.now() self.save(update_fields=["is_active", "retired_at"]) + + +# --------------------------------------------------------------------------- +# Pallas teams +# --------------------------------------------------------------------------- + + +class Team(models.Model): + """A Pallas deployment as seen by Mnemosyne. + + ``id`` is the Daedalus ``PallasInstance.id`` UUID and is stable + across re-deployments / host moves of the same Pallas instance. + + ``active_jti`` identifies the single currently-valid team JWT for + this team. On ``POST /rotate/`` we generate a new UUID here and + re-mint; the previous JWT is invalidated immediately because the + auth middleware compares the incoming ``jti`` against this value. + + ``active=False`` soft-deletes the team — every validation will + reject tokens whose ``sub`` resolves to this row, so revocation + survives restart without needing a cache purge. + """ + + id = models.UUIDField(primary_key=True, editable=False) + name = models.CharField(max_length=200) + active = models.BooleanField(default=True) + active_jti = models.UUIDField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["name"] + + def __str__(self): + suffix = "" if self.active else " [inactive]" + return f"{self.name}{suffix}" + + def rotate_jti(self) -> uuid.UUID: + """Install a fresh ``active_jti``. Returns the new UUID.""" + self.active_jti = uuid.uuid4() + self.save(update_fields=["active_jti", "updated_at"]) + return self.active_jti + + def deactivate(self): + """Soft-delete the team. All its JWTs stop validating on next request.""" + self.active = False + self.active_jti = None + self.save(update_fields=["active", "active_jti", "updated_at"]) + + +class TeamWorkspaceAssignment(models.Model): + """Grant a team read access to a Daedalus workspace's libraries. + + Queried live on every request via:: + + MATCH (l:Library) + WHERE l.workspace_id IN $workspace_ids + RETURN l.uid + + so attach/detach is visible to subsequent requests without any + cache invalidation. ``workspace_id`` is a plain string (Daedalus + owns the namespace) rather than a foreign key, mirroring how + ``library.models.IngestJob.library_uid`` references Neo4j state. + """ + + team = models.ForeignKey( + Team, + on_delete=models.CASCADE, + related_name="workspace_assignments", + ) + workspace_id = models.CharField(max_length=64, db_index=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=("team", "workspace_id"), + name="unique_team_workspace", + ) + ] + ordering = ["team", "workspace_id"] + + def __str__(self): # pragma: no cover - trivial + return f"{self.team} ↔ {self.workspace_id}" diff --git a/mnemosyne/mcp_server/teams.py b/mnemosyne/mcp_server/teams.py new file mode 100644 index 0000000..c30d83d --- /dev/null +++ b/mnemosyne/mcp_server/teams.py @@ -0,0 +1,101 @@ +"""Team JWT minting. + +Team tokens are the ``typ=team`` variant described in §5.2 of +``docs/DAEDALUS_PALLAS_INTEGRATION_v1.md``: long-lived (10 years), +no per-request scope claims, revocable via ``Team.active`` and +``Team.active_jti``. They are signed with whichever +:class:`~mcp_server.models.MCPSigningKey` is currently +``MCPSigningKey.objects.current()`` — the same keyring the legacy +per-turn path uses. + +Typical flow from the REST API: + + team = Team.objects.get(pk=...) + team.rotate_jti() # installs a fresh active_jti + jwt_string = mint_team_jwt(team) # signs with current kid + new jti +""" + +from __future__ import annotations + +import logging +import time + +import jwt as pyjwt + +from .models import MCPSigningKey, Team + +logger = logging.getLogger(__name__) + + +# Team JWT lifetime. 10 years in seconds — matches the "operator cannot +# tolerate a silent expiry-induced outage" rationale in §13.1. +_TEAM_TOKEN_LIFETIME_SECONDS = 10 * 365 * 24 * 60 * 60 + + +class TeamJWTError(Exception): + """Raised when a team JWT cannot be minted (e.g. no active signing key).""" + + +def mint_team_jwt(team: Team) -> str: + """Mint a team JWT for ``team`` using the current active signing key. + + Requires: + + * An active :class:`MCPSigningKey` to exist (seed one via + ``python manage.py seed_signing_key``). + * ``team.active_jti`` to be set — callers should invoke + :meth:`Team.rotate_jti` immediately before minting so each + issuance ties to a fresh UUID, invalidating whatever token + preceded it. + + The claim shape mirrors §5.2: + + * ``iss`` = ``"mnemosyne"`` — distinguishes from Daedalus per-turn. + * ``aud`` = ``"mnemosyne"`` — informational; not enforced at verify. + * ``typ`` = ``"team"`` — the single discriminator the middleware + uses to pick the team branch. + * ``sub`` = ``"team:<uuid>"`` — carries the team's primary key. + * ``jti`` = ``str(active_jti)`` — validated against the DB on every + request, so rotate → old token dies. + """ + key = MCPSigningKey.objects.current() + if key is None: + raise TeamJWTError( + "No active MCPSigningKey to sign the team JWT. " + "Seed one via `python manage.py seed_signing_key`." + ) + if team.active_jti is None: + raise TeamJWTError( + f"Team {team.id} has no active_jti; call rotate_jti() before mint." + ) + + try: + secret = bytes.fromhex(key.secret_hex) + except ValueError as exc: + raise TeamJWTError( + f"Stored secret for kid={key.kid!r} is not valid hex: {exc}" + ) + + now = int(time.time()) + payload = { + "iss": "mnemosyne", + "aud": "mnemosyne", + "sub": f"team:{team.id}", + "typ": "team", + "iat": now, + "exp": now + _TEAM_TOKEN_LIFETIME_SECONDS, + "jti": str(team.active_jti), + } + token = pyjwt.encode( + payload, + secret, + algorithm="HS256", + headers={"kid": key.kid}, + ) + logger.info( + "team_jwt_minted team_id=%s kid=%s jti=%s", + team.id, + key.kid, + team.active_jti, + ) + return token diff --git a/mnemosyne/mcp_server/tools/discovery.py b/mnemosyne/mcp_server/tools/discovery.py index 0a08d6d..bd7b711 100644 --- a/mnemosyne/mcp_server/tools/discovery.py +++ b/mnemosyne/mcp_server/tools/discovery.py @@ -1,4 +1,14 @@ -"""Discovery MCP tools: list libraries, collections, and items.""" +"""Discovery MCP tools: list libraries, collections, and items. + +Authorization for every call is expressed as ``resolved_libraries`` — +the Library UID list the auth middleware attached to the FastMCP +``Context``. Tools never consult token claim shapes; they read +:func:`mcp_server.context.get_mcp_resolved_libraries` and forward the +result as a Cypher parameter. + +See ``docs/DAEDALUS_PALLAS_INTEGRATION_v1.md`` §3.3 for the unified +auth model. +""" from __future__ import annotations @@ -7,27 +17,13 @@ from typing import Any from asgiref.sync import sync_to_async from fastmcp.server.context import Context -from ..context import get_mcp_claims +from ..context import get_mcp_resolved_libraries from ..metrics import record_tool_call DEFAULT_LIMIT = 50 MAX_LIMIT = 200 -def _scope_from_claims(claims: dict | None, - arg_workspace_id: str | None) -> tuple[str | None, list[str] | None]: - """Return (workspace_id, allowed_libraries) for a tool call. - - Token claims, when present, trump tool args — that's the security - contract. Opaque-token callers (no claims) keep the legacy behavior - where the caller may pass workspace_id explicitly (typically null, - yielding global scope). - """ - if claims is not None: - return claims.get("ws"), claims.get("libs") or None - return arg_workspace_id, None - - def _clamp(limit: int) -> int: if limit < 1: return 1 @@ -39,8 +35,6 @@ def register_discovery_tools(mcp): async def list_libraries( limit: int = DEFAULT_LIMIT, offset: int = 0, - # System-injected; deliberately absent from the docstring. - workspace_id: str | None = None, ctx: Context | None = None, ) -> dict[str, Any]: """List Mnemosyne libraries. Each library has a content-aware library_type @@ -49,11 +43,10 @@ def register_discovery_tools(mcp): name, library_type, description for each library — use the uid or library_type to scope a subsequent search. """ - claims = await get_mcp_claims(ctx) - ws, libs = _scope_from_claims(claims, workspace_id) + resolved_libraries = await get_mcp_resolved_libraries(ctx) with record_tool_call("list_libraries"): return await sync_to_async(_query_libraries, thread_sensitive=True)( - _clamp(limit), max(offset, 0), ws, libs + _clamp(limit), max(offset, 0), resolved_libraries ) @mcp.tool @@ -61,8 +54,6 @@ def register_discovery_tools(mcp): library_uid: str | None = None, limit: int = DEFAULT_LIMIT, offset: int = 0, - # System-injected; deliberately absent from the docstring. - workspace_id: str | None = None, ctx: Context | None = None, ) -> dict[str, Any]: """List collections, optionally filtered by parent library_uid. @@ -70,11 +61,10 @@ def register_discovery_tools(mcp): a multi-volume manual). Returns uid, name, description, library_uid, library_name. Use the uid to scope a subsequent search to one collection. """ - claims = await get_mcp_claims(ctx) - ws, libs = _scope_from_claims(claims, workspace_id) + resolved_libraries = await get_mcp_resolved_libraries(ctx) with record_tool_call("list_collections"): return await sync_to_async(_query_collections, thread_sensitive=True)( - library_uid, _clamp(limit), max(offset, 0), ws, libs + library_uid, _clamp(limit), max(offset, 0), resolved_libraries ) @mcp.tool @@ -83,8 +73,6 @@ def register_discovery_tools(mcp): library_uid: str | None = None, limit: int = DEFAULT_LIMIT, offset: int = 0, - # System-injected; deliberately absent from the docstring. - workspace_id: str | None = None, ctx: Context | None = None, ) -> dict[str, Any]: """List items (the indexed documents/files), optionally filtered by @@ -93,39 +81,42 @@ def register_discovery_tools(mcp): document size; use embedding_status to skip items that are not yet searchable (only 'completed' items appear in search results). """ - claims = await get_mcp_claims(ctx) - ws, libs = _scope_from_claims(claims, workspace_id) + resolved_libraries = await get_mcp_resolved_libraries(ctx) with record_tool_call("list_items"): return await sync_to_async(_query_items, thread_sensitive=True)( - collection_uid, library_uid, _clamp(limit), max(offset, 0), ws, libs + collection_uid, + library_uid, + _clamp(limit), + max(offset, 0), + resolved_libraries, ) -_WORKSPACE_SCOPE = ( - "(($workspace_id IS NOT NULL AND l.workspace_id = $workspace_id) " - "OR ($allowed_libraries IS NOT NULL AND l.uid IN $allowed_libraries) " - "OR ($workspace_id IS NULL AND $allowed_libraries IS NULL " - " AND l.workspace_id IS NULL))" +# Single authorization clause shared across every discovery query. +# Matches ``library.services.search._RESOLVED_LIBRARIES_CLAUSE`` — the +# ``None`` branch lets trusted callers bypass, a non-empty list scopes +# to those UIDs, and an empty list is fail-closed (yields nothing). +_RESOLVED_LIBRARIES_CLAUSE = ( + "($resolved_libraries IS NULL OR l.uid IN $resolved_libraries)" ) def _query_libraries( limit: int, offset: int, - workspace_id: str | None = None, - allowed_libraries: list[str] | None = None, + resolved_libraries: list[str] | None, ) -> dict[str, Any]: from neomodel import db rows, _ = db.cypher_query( "MATCH (l:Library) " - f"WHERE {_WORKSPACE_SCOPE} " + f"WHERE {_RESOLVED_LIBRARIES_CLAUSE} " "RETURN l.uid, l.name, l.library_type, l.description " "ORDER BY l.name SKIP $offset LIMIT $limit", { - "offset": offset, "limit": limit, - "workspace_id": workspace_id, - "allowed_libraries": allowed_libraries, + "offset": offset, + "limit": limit, + "resolved_libraries": resolved_libraries, }, ) return { @@ -147,20 +138,19 @@ def _query_collections( library_uid: str | None, limit: int, offset: int, - workspace_id: str | None = None, - allowed_libraries: list[str] | None = None, + resolved_libraries: list[str] | None, ) -> dict[str, Any]: from neomodel import db base_params = { - "offset": offset, "limit": limit, - "workspace_id": workspace_id, - "allowed_libraries": allowed_libraries, + "offset": offset, + "limit": limit, + "resolved_libraries": resolved_libraries, } if library_uid: cypher = ( "MATCH (l:Library {uid: $library_uid})-[:CONTAINS]->(c:Collection) " - f"WHERE {_WORKSPACE_SCOPE} " + f"WHERE {_RESOLVED_LIBRARIES_CLAUSE} " "RETURN c.uid, c.name, c.description, l.uid, l.name " "ORDER BY c.name SKIP $offset LIMIT $limit" ) @@ -168,7 +158,7 @@ def _query_collections( else: cypher = ( "MATCH (l:Library)-[:CONTAINS]->(c:Collection) " - f"WHERE {_WORKSPACE_SCOPE} " + f"WHERE {_RESOLVED_LIBRARIES_CLAUSE} " "RETURN c.uid, c.name, c.description, l.uid, l.name " "ORDER BY l.name, c.name SKIP $offset LIMIT $limit" ) @@ -196,16 +186,15 @@ def _query_items( library_uid: str | None, limit: int, offset: int, - workspace_id: str | None = None, - allowed_libraries: list[str] | None = None, + resolved_libraries: list[str] | None, ) -> dict[str, Any]: from neomodel import db - where = [_WORKSPACE_SCOPE] + where = [_RESOLVED_LIBRARIES_CLAUSE] params: dict[str, Any] = { - "offset": offset, "limit": limit, - "workspace_id": workspace_id, - "allowed_libraries": allowed_libraries, + "offset": offset, + "limit": limit, + "resolved_libraries": resolved_libraries, } if collection_uid: where.append("c.uid = $collection_uid") diff --git a/mnemosyne/mcp_server/tools/search.py b/mnemosyne/mcp_server/tools/search.py index 3647b5b..f5f7427 100644 --- a/mnemosyne/mcp_server/tools/search.py +++ b/mnemosyne/mcp_server/tools/search.py @@ -1,4 +1,14 @@ -"""Search-related MCP tools: hybrid `search` and `get_chunk` for full text.""" +"""Search-related MCP tools: hybrid ``search`` and ``get_chunk`` for full text. + +Authorization for every call is expressed as ``resolved_libraries`` — +the Library UID list the auth middleware attached to the FastMCP +``Context``. Tools never consult token claim shapes; they read +:func:`mcp_server.context.get_mcp_resolved_libraries` and pass the +result straight through to the search layer. + +See ``docs/DAEDALUS_PALLAS_INTEGRATION_v1.md`` §3.3 for the unified +auth model. +""" from __future__ import annotations @@ -10,18 +20,9 @@ from django.conf import settings from django.core.files.storage import default_storage from fastmcp.server.context import Context -from ..context import get_mcp_claims, get_mcp_user +from ..context import get_mcp_resolved_libraries, get_mcp_user from ..metrics import record_tool_call - -def _scope_from_claims(claims: dict | None, - arg_workspace_id: str | None) -> tuple[str | None, list[str] | None]: - """Return (workspace_id, allowed_libraries) for a tool call. Token claims - trump tool args when present.""" - if claims is not None: - return claims.get("ws"), claims.get("libs") or None - return arg_workspace_id, None - DEFAULT_SEARCH_TYPES = ["vector", "fulltext", "graph"] @@ -36,11 +37,6 @@ def register_search_tools(mcp): rerank: bool = True, include_images: bool = True, search_types: list[str] | None = None, - # workspace_id is system-injected by Daedalus's chat path. It is - # intentionally absent from the docstring so the calling LLM is - # never told it exists. Whatever value the LLM produces here is - # overwritten by Daedalus before the call reaches Mnemosyne. - workspace_id: str | None = None, ctx: Context | None = None, ) -> dict[str, Any]: """Hybrid retrieval over Mnemosyne: vector + full-text + concept-graph @@ -56,8 +52,7 @@ def register_search_tools(mcp): score, and source. Also returns matching images when include_images=True. """ types = search_types or DEFAULT_SEARCH_TYPES - claims = await get_mcp_claims(ctx) - ws, libs = _scope_from_claims(claims, workspace_id) + resolved_libraries = await get_mcp_resolved_libraries(ctx) with record_tool_call("search"): user = await get_mcp_user(ctx) return await sync_to_async(_run_search, thread_sensitive=True)( @@ -66,8 +61,7 @@ def register_search_tools(mcp): library_uid=library_uid, library_type=library_type, collection_uid=collection_uid, - workspace_id=ws, - allowed_libraries=libs, + resolved_libraries=resolved_libraries, limit=limit, rerank=rerank, include_images=include_images, @@ -77,8 +71,6 @@ def register_search_tools(mcp): @mcp.tool async def get_chunk( chunk_uid: str, - # System-injected; deliberately absent from the docstring. - workspace_id: str | None = None, ctx: Context | None = None, ) -> dict[str, Any]: """Fetch the full text of a chunk by its uid (typically obtained from `search`). @@ -87,17 +79,26 @@ def register_search_tools(mcp): item_uid, item_title, library_type, text. Use this when the 500-character text_preview from `search` isn't enough. """ - claims = await get_mcp_claims(ctx) - ws, libs = _scope_from_claims(claims, workspace_id) + resolved_libraries = await get_mcp_resolved_libraries(ctx) with record_tool_call("get_chunk"): return await sync_to_async(_load_chunk, thread_sensitive=True)( - chunk_uid, ws, libs + chunk_uid, resolved_libraries ) -def _run_search(*, user, query, library_uid, library_type, collection_uid, - workspace_id, allowed_libraries, limit, rerank, include_images, - search_types) -> dict[str, Any]: +def _run_search( + *, + user, + query, + library_uid, + library_type, + collection_uid, + resolved_libraries, + limit, + rerank, + include_images, + search_types, +) -> dict[str, Any]: from library.services.search import SearchRequest, SearchService req = SearchRequest( @@ -105,8 +106,7 @@ def _run_search(*, user, query, library_uid, library_type, collection_uid, library_uid=library_uid, library_type=library_type, collection_uid=collection_uid, - workspace_id=workspace_id, - allowed_libraries=allowed_libraries, + resolved_libraries=resolved_libraries, search_types=search_types, limit=limit, vector_top_k=getattr(settings, "SEARCH_VECTOR_TOP_K", 50), @@ -130,24 +130,26 @@ def _run_search(*, user, query, library_uid, library_type, collection_uid, def _load_chunk( chunk_uid: str, - workspace_id: str | None = None, - allowed_libraries: list[str] | None = None, + resolved_libraries: list[str] | None, ) -> dict[str, Any]: + """Load a single chunk's full text, subject to the caller's library scope. + + ``resolved_libraries`` is enforced at the Cypher layer: ``None`` allows + any library (only used for trusted in-process callers — MCP middleware + never passes ``None`` for authenticated clients), ``[]`` matches zero + rows (fail-closed), a non-empty list restricts to those UIDs. + """ from neomodel import db rows, _ = db.cypher_query( "MATCH (l:Library)-[:CONTAINS]->(:Collection)-[:CONTAINS]->" "(i:Item)-[:HAS_CHUNK]->(c:Chunk {uid: $uid}) " - "WHERE (($workspace_id IS NOT NULL AND l.workspace_id = $workspace_id) " - " OR ($allowed_libraries IS NOT NULL AND l.uid IN $allowed_libraries) " - " OR ($workspace_id IS NULL AND $allowed_libraries IS NULL " - " AND l.workspace_id IS NULL)) " + "WHERE ($resolved_libraries IS NULL OR l.uid IN $resolved_libraries) " "RETURN c.uid, c.chunk_index, c.chunk_s3_key, " "i.uid, i.title, l.library_type LIMIT 1", { "uid": chunk_uid, - "workspace_id": workspace_id, - "allowed_libraries": allowed_libraries, + "resolved_libraries": resolved_libraries, }, ) if not rows: diff --git a/mnemosyne/mcp_server/urls.py b/mnemosyne/mcp_server/urls.py index ddd8d52..2beb4c0 100644 --- a/mnemosyne/mcp_server/urls.py +++ b/mnemosyne/mcp_server/urls.py @@ -1,4 +1,19 @@ -"""URL routes for the MCP token self-service dashboard.""" +"""URL routes for the per-user MCP token self-service dashboard. + +Mounted at ``/profile/mcp-tokens/…``. Humans use this surface to mint +opaque :class:`mcp_server.models.MCPToken` rows for third-party MCP +clients (Claude Desktop, Cline, etc.). + +Other MCP-server surfaces live elsewhere: + +* ``/mcp_server/api/…`` (DRF control plane consumed by Daedalus) is + mounted at project root — see ``mnemosyne.urls`` and + ``mcp_server.api.urls`` — and keeps its own ``mcp-server-api`` + namespace. +* The MCP bearer-auth surface itself (tool calls via + ``Authorization: Bearer …``) is mounted by ``mnemosyne.asgi`` at + ``/mcp/`` and is not routed here. +""" from django.urls import path @@ -7,6 +22,7 @@ from . import views app_name = "mcp_server" urlpatterns = [ + # Self-service token dashboard (human-facing). path("profile/mcp-tokens/", views.mcp_token_list, name="mcp-token-list"), path("profile/mcp-tokens/add/", views.mcp_token_create, name="mcp-token-create"), path("profile/mcp-tokens/<int:pk>/", views.mcp_token_detail, name="mcp-token-detail"), diff --git a/mnemosyne/mcp_server/views.py b/mnemosyne/mcp_server/views.py index 30c7cf6..2a90775 100644 --- a/mnemosyne/mcp_server/views.py +++ b/mnemosyne/mcp_server/views.py @@ -28,12 +28,13 @@ def mcp_token_list(request: HttpRequest) -> HttpResponse: @require_http_methods(["GET", "POST"]) def mcp_token_create(request: HttpRequest) -> HttpResponse: if request.method == "POST": - form = MCPTokenCreateForm(request.POST) + form = MCPTokenCreateForm(request.POST, user=request.user) if form.is_valid(): token, plaintext = MCPToken.objects.create_token( user=request.user, name=form.cleaned_data["name"], allowed_tools=form.cleaned_data.get("allowed_tools") or [], + allowed_libraries=form.cleaned_data.get("allowed_libraries") or [], expires_at=form.cleaned_data.get("expires_at") or None, ) return render( @@ -42,7 +43,7 @@ def mcp_token_create(request: HttpRequest) -> HttpResponse: {"token": token, "plaintext": plaintext}, ) else: - form = MCPTokenCreateForm() + form = MCPTokenCreateForm(user=request.user) return render(request, "mcp_server/tokens/create.html", {"form": form}) @@ -60,15 +61,18 @@ def mcp_token_edit(request: HttpRequest, pk: int) -> HttpResponse: token = get_object_or_404(MCPToken, pk=pk, user=request.user) if request.method == "POST": - form = MCPTokenEditForm(request.POST, instance=token) + form = MCPTokenEditForm(request.POST, instance=token, user=request.user) if form.is_valid(): instance = form.save(commit=False) instance.allowed_tools = form.cleaned_data.get("allowed_tools") or [] + instance.allowed_libraries = ( + form.cleaned_data.get("allowed_libraries") or [] + ) instance.save() messages.success(request, "MCP token updated.") return redirect("mcp_server:mcp-token-detail", pk=token.pk) else: - form = MCPTokenEditForm(instance=token) + form = MCPTokenEditForm(instance=token, user=request.user) return render( request, "mcp_server/tokens/edit.html", {"form": form, "token": token} diff --git a/mnemosyne/mnemosyne/urls.py b/mnemosyne/mnemosyne/urls.py index d595b63..6b5e360 100644 --- a/mnemosyne/mnemosyne/urls.py +++ b/mnemosyne/mnemosyne/urls.py @@ -28,6 +28,11 @@ urlpatterns = [ path("library/", include("library.urls")), # LLM Manager path("llm/", include("llm_manager.urls")), - # MCP server (token dashboard at /profile/mcp-tokens/) + # MCP server — two surfaces: + # /profile/mcp-tokens/… — per-user self-service token dashboard (HTML, session auth) + # /mcp_server/api/… — Daedalus-facing team control plane (DRF, Basic auth) + # The MCP bearer-auth surface itself (tool calls) is mounted by + # mnemosyne.asgi at /mcp/ and is not routed here. path("", include("mcp_server.urls")), + path("mcp_server/api/", include("mcp_server.api.urls")), ] diff --git a/mnemosyne/themis/migrations/0001_initial.py b/mnemosyne/themis/migrations/0001_initial.py index df29bb4..6dda7f0 100644 --- a/mnemosyne/themis/migrations/0001_initial.py +++ b/mnemosyne/themis/migrations/0001_initial.py @@ -1,11 +1,9 @@ -""" -Initial migration for Themis — creates UserProfile and UserAPIKey tables. -""" -import uuid +# Generated by Django 5.2.13 on 2026-05-10 15:31 +import django.db.models.deletion +import uuid from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): @@ -18,234 +16,78 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name="UserProfile", + name='UserAPIKey', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "home_timezone", - models.CharField( - default="UTC", - help_text="User's home/permanent timezone", - max_length=50, - ), - ), - ( - "current_timezone", - models.CharField( - blank=True, - default="", - help_text="User's current timezone when traveling (leave blank if at home)", - max_length=50, - ), - ), - ( - "date_format", - models.CharField( - choices=[ - ("YYYY-MM-DD", "2024-12-25"), - ("DD/MM/YYYY", "25/12/2024"), - ("MM/DD/YYYY", "12/25/2024"), - ("DD.MM.YYYY", "25.12.2024"), - ("DD-MM-YYYY", "25-12-2024"), - ], - default="YYYY-MM-DD", - help_text="Preferred date display format", - max_length=20, - ), - ), - ( - "time_format", - models.CharField( - choices=[ - ("12-hour", "12-hour (3:30 PM)"), - ("24-hour", "24-hour (15:30)"), - ], - default="24-hour", - help_text="12-hour or 24-hour time format", - max_length=10, - ), - ), - ( - "thousand_separator", - models.CharField( - choices=[ - ("comma", "Comma (1,000)"), - ("period", "Period (1.000)"), - ("space", "Space (1 000)"), - ("none", "None (1000)"), - ], - default="comma", - help_text="Number formatting preference", - max_length=10, - ), - ), - ( - "week_start", - models.CharField( - choices=[ - ("monday", "Monday"), - ("sunday", "Sunday"), - ("saturday", "Saturday"), - ], - default="monday", - help_text="First day of the week", - max_length=10, - ), - ), - ( - "theme_mode", - models.CharField( - choices=[ - ("light", "Light"), - ("dark", "Dark"), - ("auto", "Auto (System)"), - ], - default="auto", - help_text="Theme mode: light, dark, or auto (follows system)", - max_length=10, - ), - ), - ( - "theme_name", - models.CharField( - default="corporate", - help_text="DaisyUI theme for light mode", - max_length=30, - ), - ), - ( - "dark_theme_name", - models.CharField( - default="business", - help_text="DaisyUI theme for dark mode", - max_length=30, - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "user", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="profile", - to=settings.AUTH_USER_MODEL, - ), - ), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('service_name', models.CharField(help_text='Service this key belongs to (e.g. OpenAI, Anthropic, CalDAV)', max_length=100)), + ('key_type', models.CharField(choices=[('api', 'API Key'), ('dav', 'DAV Credentials'), ('token', 'Access Token'), ('secret', 'Secret Key'), ('other', 'Other')], default='api', help_text='Type of credential', max_length=30)), + ('label', models.CharField(blank=True, default='', help_text="Your nickname for this key (e.g. 'Work account')", max_length=100)), + ('encrypted_value', models.TextField(help_text='Fernet-encrypted credential value')), + ('instructions', models.TextField(blank=True, default='', help_text='How to obtain and use this key')), + ('help_url', models.URLField(blank=True, default='', help_text='Link to service documentation')), + ('is_active', models.BooleanField(default=True, help_text='Whether this key is currently in use')), + ('last_used_at', models.DateTimeField(blank=True, help_text='Last time this key was used', null=True)), + ('expires_at', models.DateTimeField(blank=True, help_text='When this key expires (if applicable)', null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='api_keys', to=settings.AUTH_USER_MODEL)), ], options={ - "verbose_name": "User Profile", - "verbose_name_plural": "User Profiles", + 'verbose_name': 'API Key', + 'verbose_name_plural': 'API Keys', }, ), migrations.CreateModel( - name="UserAPIKey", + name='UserProfile', fields=[ - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - ), - ), - ( - "service_name", - models.CharField( - help_text="Service this key belongs to (e.g. OpenAI, Anthropic, CalDAV)", - max_length=100, - ), - ), - ( - "key_type", - models.CharField( - choices=[ - ("api", "API Key"), - ("mcp", "MCP Server"), - ("dav", "DAV Credentials"), - ("token", "Access Token"), - ("secret", "Secret Key"), - ("other", "Other"), - ], - default="api", - help_text="Type of credential", - max_length=30, - ), - ), - ( - "label", - models.CharField( - blank=True, - default="", - help_text="Your nickname for this key (e.g. 'Work account')", - max_length=100, - ), - ), - ( - "encrypted_value", - models.TextField(help_text="Fernet-encrypted credential value"), - ), - ( - "instructions", - models.TextField( - blank=True, - default="", - help_text="How to obtain and use this key", - ), - ), - ( - "help_url", - models.URLField( - blank=True, - default="", - help_text="Link to service documentation", - ), - ), - ( - "is_active", - models.BooleanField( - default=True, - help_text="Whether this key is currently in use", - ), - ), - ( - "last_used_at", - models.DateTimeField( - blank=True, - help_text="Last time this key was used", - null=True, - ), - ), - ( - "expires_at", - models.DateTimeField( - blank=True, - help_text="When this key expires (if applicable)", - null=True, - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="api_keys", - to=settings.AUTH_USER_MODEL, - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('home_timezone', models.CharField(choices=[('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmara', 'Africa/Asmara'), ('Africa/Asmera', 'Africa/Asmera'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Timbuktu', 'Africa/Timbuktu'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/Buenos_Aires', 'America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'America/Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'America/Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'America/Argentina/Cordoba'), ('America/Argentina/Jujuy', 'America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Atikokan', 'America/Atikokan'), ('America/Atka', 'America/Atka'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Buenos_Aires', 'America/Buenos_Aires'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Catamarca', 'America/Catamarca'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Ciudad_Juarez', 'America/Ciudad_Juarez'), ('America/Coral_Harbour', 'America/Coral_Harbour'), ('America/Cordoba', 'America/Cordoba'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Coyhaique', 'America/Coyhaique'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Ensenada', 'America/Ensenada'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fort_Wayne', 'America/Fort_Wayne'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Godthab', 'America/Godthab'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Indianapolis', 'America/Indiana/Indianapolis'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Indianapolis', 'America/Indianapolis'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Jujuy', 'America/Jujuy'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Louisville', 'America/Kentucky/Louisville'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Knox_IN', 'America/Knox_IN'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Louisville', 'America/Louisville'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Mendoza', 'America/Mendoza'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montreal', 'America/Montreal'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nipigon', 'America/Nipigon'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Nuuk', 'America/Nuuk'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Pangnirtung', 'America/Pangnirtung'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Acre', 'America/Porto_Acre'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rainy_River', 'America/Rainy_River'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Rosario', 'America/Rosario'), ('America/Santa_Isabel', 'America/Santa_Isabel'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Shiprock', 'America/Shiprock'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Thunder_Bay', 'America/Thunder_Bay'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Virgin', 'America/Virgin'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('America/Yellowknife', 'America/Yellowknife'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/South_Pole', 'Antarctica/South_Pole'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Ashkhabad', 'Asia/Ashkhabad'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Calcutta', 'Asia/Calcutta'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Choibalsan', 'Asia/Choibalsan'), ('Asia/Chongqing', 'Asia/Chongqing'), ('Asia/Chungking', 'Asia/Chungking'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Dacca', 'Asia/Dacca'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Harbin', 'Asia/Harbin'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Ho_Chi_Minh', 'Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Istanbul', 'Asia/Istanbul'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Kashgar', 'Asia/Kashgar'), ('Asia/Kathmandu', 'Asia/Kathmandu'), ('Asia/Katmandu', 'Asia/Katmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Kolkata', 'Asia/Kolkata'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macao', 'Asia/Macao'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Rangoon', 'Asia/Rangoon'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Saigon', 'Asia/Saigon'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Tel_Aviv', 'Asia/Tel_Aviv'), ('Asia/Thimbu', 'Asia/Thimbu'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ujung_Pandang', 'Asia/Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Ulan_Bator', 'Asia/Ulan_Bator'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yangon', 'Asia/Yangon'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faeroe', 'Atlantic/Faeroe'), ('Atlantic/Faroe', 'Atlantic/Faroe'), ('Atlantic/Jan_Mayen', 'Atlantic/Jan_Mayen'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/ACT', 'Australia/ACT'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Canberra', 'Australia/Canberra'), ('Australia/Currie', 'Australia/Currie'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/LHI', 'Australia/LHI'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/NSW', 'Australia/NSW'), ('Australia/North', 'Australia/North'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Queensland', 'Australia/Queensland'), ('Australia/South', 'Australia/South'), ('Australia/Sydney', 'Australia/Sydney'), ('Australia/Tasmania', 'Australia/Tasmania'), ('Australia/Victoria', 'Australia/Victoria'), ('Australia/West', 'Australia/West'), ('Australia/Yancowinna', 'Australia/Yancowinna'), ('Brazil/Acre', 'Brazil/Acre'), ('Brazil/DeNoronha', 'Brazil/DeNoronha'), ('Brazil/East', 'Brazil/East'), ('Brazil/West', 'Brazil/West'), ('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Canada/Atlantic', 'Canada/Atlantic'), ('Canada/Central', 'Canada/Central'), ('Canada/Eastern', 'Canada/Eastern'), ('Canada/Mountain', 'Canada/Mountain'), ('Canada/Newfoundland', 'Canada/Newfoundland'), ('Canada/Pacific', 'Canada/Pacific'), ('Canada/Saskatchewan', 'Canada/Saskatchewan'), ('Canada/Yukon', 'Canada/Yukon'), ('Chile/Continental', 'Chile/Continental'), ('Chile/EasterIsland', 'Chile/EasterIsland'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('Etc/GMT', 'Etc/GMT'), ('Etc/GMT+0', 'Etc/GMT+0'), ('Etc/GMT+1', 'Etc/GMT+1'), ('Etc/GMT+10', 'Etc/GMT+10'), ('Etc/GMT+11', 'Etc/GMT+11'), ('Etc/GMT+12', 'Etc/GMT+12'), ('Etc/GMT+2', 'Etc/GMT+2'), ('Etc/GMT+3', 'Etc/GMT+3'), ('Etc/GMT+4', 'Etc/GMT+4'), ('Etc/GMT+5', 'Etc/GMT+5'), ('Etc/GMT+6', 'Etc/GMT+6'), ('Etc/GMT+7', 'Etc/GMT+7'), ('Etc/GMT+8', 'Etc/GMT+8'), ('Etc/GMT+9', 'Etc/GMT+9'), ('Etc/GMT-0', 'Etc/GMT-0'), ('Etc/GMT-1', 'Etc/GMT-1'), ('Etc/GMT-10', 'Etc/GMT-10'), ('Etc/GMT-11', 'Etc/GMT-11'), ('Etc/GMT-12', 'Etc/GMT-12'), ('Etc/GMT-13', 'Etc/GMT-13'), ('Etc/GMT-14', 'Etc/GMT-14'), ('Etc/GMT-2', 'Etc/GMT-2'), ('Etc/GMT-3', 'Etc/GMT-3'), ('Etc/GMT-4', 'Etc/GMT-4'), ('Etc/GMT-5', 'Etc/GMT-5'), ('Etc/GMT-6', 'Etc/GMT-6'), ('Etc/GMT-7', 'Etc/GMT-7'), ('Etc/GMT-8', 'Etc/GMT-8'), ('Etc/GMT-9', 'Etc/GMT-9'), ('Etc/GMT0', 'Etc/GMT0'), ('Etc/Greenwich', 'Etc/Greenwich'), ('Etc/UCT', 'Etc/UCT'), ('Etc/UTC', 'Etc/UTC'), ('Etc/Universal', 'Etc/Universal'), ('Etc/Zulu', 'Etc/Zulu'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belfast', 'Europe/Belfast'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kiev', 'Europe/Kiev'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Kyiv', 'Europe/Kyiv'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Nicosia', 'Europe/Nicosia'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Tiraspol', 'Europe/Tiraspol'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Uzhgorod', 'Europe/Uzhgorod'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zaporozhye', 'Europe/Zaporozhye'), ('Europe/Zurich', 'Europe/Zurich'), ('Factory', 'Factory'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('GMT', 'GMT'), ('GMT+0', 'GMT+0'), ('GMT-0', 'GMT-0'), ('GMT0', 'GMT0'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('Mexico/BajaNorte', 'Mexico/BajaNorte'), ('Mexico/BajaSur', 'Mexico/BajaSur'), ('Mexico/General', 'Mexico/General'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Chuuk', 'Pacific/Chuuk'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Enderbury', 'Pacific/Enderbury'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Johnston', 'Pacific/Johnston'), ('Pacific/Kanton', 'Pacific/Kanton'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Pohnpei', 'Pacific/Pohnpei'), ('Pacific/Ponape', 'Pacific/Ponape'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Samoa', 'Pacific/Samoa'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Truk', 'Pacific/Truk'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis'), ('Pacific/Yap', 'Pacific/Yap'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('US/Alaska', 'US/Alaska'), ('US/Aleutian', 'US/Aleutian'), ('US/Arizona', 'US/Arizona'), ('US/Central', 'US/Central'), ('US/East-Indiana', 'US/East-Indiana'), ('US/Eastern', 'US/Eastern'), ('US/Hawaii', 'US/Hawaii'), ('US/Indiana-Starke', 'US/Indiana-Starke'), ('US/Michigan', 'US/Michigan'), ('US/Mountain', 'US/Mountain'), ('US/Pacific', 'US/Pacific'), ('US/Samoa', 'US/Samoa'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu'), ('localtime', 'localtime')], default='UTC', help_text="User's home/permanent timezone", max_length=50)), + ('current_timezone', models.CharField(blank=True, choices=[('', '---------'), ('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmara', 'Africa/Asmara'), ('Africa/Asmera', 'Africa/Asmera'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Timbuktu', 'Africa/Timbuktu'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/Buenos_Aires', 'America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'America/Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'America/Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'America/Argentina/Cordoba'), ('America/Argentina/Jujuy', 'America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Atikokan', 'America/Atikokan'), ('America/Atka', 'America/Atka'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Buenos_Aires', 'America/Buenos_Aires'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Catamarca', 'America/Catamarca'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Ciudad_Juarez', 'America/Ciudad_Juarez'), ('America/Coral_Harbour', 'America/Coral_Harbour'), ('America/Cordoba', 'America/Cordoba'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Coyhaique', 'America/Coyhaique'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Ensenada', 'America/Ensenada'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fort_Wayne', 'America/Fort_Wayne'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Godthab', 'America/Godthab'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Indianapolis', 'America/Indiana/Indianapolis'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Indianapolis', 'America/Indianapolis'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Jujuy', 'America/Jujuy'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Louisville', 'America/Kentucky/Louisville'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Knox_IN', 'America/Knox_IN'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Louisville', 'America/Louisville'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Mendoza', 'America/Mendoza'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montreal', 'America/Montreal'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nipigon', 'America/Nipigon'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Nuuk', 'America/Nuuk'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Pangnirtung', 'America/Pangnirtung'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Acre', 'America/Porto_Acre'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rainy_River', 'America/Rainy_River'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Rosario', 'America/Rosario'), ('America/Santa_Isabel', 'America/Santa_Isabel'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Shiprock', 'America/Shiprock'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Thunder_Bay', 'America/Thunder_Bay'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Virgin', 'America/Virgin'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('America/Yellowknife', 'America/Yellowknife'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/South_Pole', 'Antarctica/South_Pole'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Ashkhabad', 'Asia/Ashkhabad'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Calcutta', 'Asia/Calcutta'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Choibalsan', 'Asia/Choibalsan'), ('Asia/Chongqing', 'Asia/Chongqing'), ('Asia/Chungking', 'Asia/Chungking'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Dacca', 'Asia/Dacca'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Harbin', 'Asia/Harbin'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Ho_Chi_Minh', 'Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Istanbul', 'Asia/Istanbul'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Kashgar', 'Asia/Kashgar'), ('Asia/Kathmandu', 'Asia/Kathmandu'), ('Asia/Katmandu', 'Asia/Katmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Kolkata', 'Asia/Kolkata'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macao', 'Asia/Macao'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Rangoon', 'Asia/Rangoon'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Saigon', 'Asia/Saigon'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Tel_Aviv', 'Asia/Tel_Aviv'), ('Asia/Thimbu', 'Asia/Thimbu'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ujung_Pandang', 'Asia/Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Ulan_Bator', 'Asia/Ulan_Bator'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yangon', 'Asia/Yangon'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faeroe', 'Atlantic/Faeroe'), ('Atlantic/Faroe', 'Atlantic/Faroe'), ('Atlantic/Jan_Mayen', 'Atlantic/Jan_Mayen'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/ACT', 'Australia/ACT'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Canberra', 'Australia/Canberra'), ('Australia/Currie', 'Australia/Currie'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/LHI', 'Australia/LHI'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/NSW', 'Australia/NSW'), ('Australia/North', 'Australia/North'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Queensland', 'Australia/Queensland'), ('Australia/South', 'Australia/South'), ('Australia/Sydney', 'Australia/Sydney'), ('Australia/Tasmania', 'Australia/Tasmania'), ('Australia/Victoria', 'Australia/Victoria'), ('Australia/West', 'Australia/West'), ('Australia/Yancowinna', 'Australia/Yancowinna'), ('Brazil/Acre', 'Brazil/Acre'), ('Brazil/DeNoronha', 'Brazil/DeNoronha'), ('Brazil/East', 'Brazil/East'), ('Brazil/West', 'Brazil/West'), ('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Canada/Atlantic', 'Canada/Atlantic'), ('Canada/Central', 'Canada/Central'), ('Canada/Eastern', 'Canada/Eastern'), ('Canada/Mountain', 'Canada/Mountain'), ('Canada/Newfoundland', 'Canada/Newfoundland'), ('Canada/Pacific', 'Canada/Pacific'), ('Canada/Saskatchewan', 'Canada/Saskatchewan'), ('Canada/Yukon', 'Canada/Yukon'), ('Chile/Continental', 'Chile/Continental'), ('Chile/EasterIsland', 'Chile/EasterIsland'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('Etc/GMT', 'Etc/GMT'), ('Etc/GMT+0', 'Etc/GMT+0'), ('Etc/GMT+1', 'Etc/GMT+1'), ('Etc/GMT+10', 'Etc/GMT+10'), ('Etc/GMT+11', 'Etc/GMT+11'), ('Etc/GMT+12', 'Etc/GMT+12'), ('Etc/GMT+2', 'Etc/GMT+2'), ('Etc/GMT+3', 'Etc/GMT+3'), ('Etc/GMT+4', 'Etc/GMT+4'), ('Etc/GMT+5', 'Etc/GMT+5'), ('Etc/GMT+6', 'Etc/GMT+6'), ('Etc/GMT+7', 'Etc/GMT+7'), ('Etc/GMT+8', 'Etc/GMT+8'), ('Etc/GMT+9', 'Etc/GMT+9'), ('Etc/GMT-0', 'Etc/GMT-0'), ('Etc/GMT-1', 'Etc/GMT-1'), ('Etc/GMT-10', 'Etc/GMT-10'), ('Etc/GMT-11', 'Etc/GMT-11'), ('Etc/GMT-12', 'Etc/GMT-12'), ('Etc/GMT-13', 'Etc/GMT-13'), ('Etc/GMT-14', 'Etc/GMT-14'), ('Etc/GMT-2', 'Etc/GMT-2'), ('Etc/GMT-3', 'Etc/GMT-3'), ('Etc/GMT-4', 'Etc/GMT-4'), ('Etc/GMT-5', 'Etc/GMT-5'), ('Etc/GMT-6', 'Etc/GMT-6'), ('Etc/GMT-7', 'Etc/GMT-7'), ('Etc/GMT-8', 'Etc/GMT-8'), ('Etc/GMT-9', 'Etc/GMT-9'), ('Etc/GMT0', 'Etc/GMT0'), ('Etc/Greenwich', 'Etc/Greenwich'), ('Etc/UCT', 'Etc/UCT'), ('Etc/UTC', 'Etc/UTC'), ('Etc/Universal', 'Etc/Universal'), ('Etc/Zulu', 'Etc/Zulu'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belfast', 'Europe/Belfast'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kiev', 'Europe/Kiev'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Kyiv', 'Europe/Kyiv'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Nicosia', 'Europe/Nicosia'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Tiraspol', 'Europe/Tiraspol'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Uzhgorod', 'Europe/Uzhgorod'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zaporozhye', 'Europe/Zaporozhye'), ('Europe/Zurich', 'Europe/Zurich'), ('Factory', 'Factory'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('GMT', 'GMT'), ('GMT+0', 'GMT+0'), ('GMT-0', 'GMT-0'), ('GMT0', 'GMT0'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('Mexico/BajaNorte', 'Mexico/BajaNorte'), ('Mexico/BajaSur', 'Mexico/BajaSur'), ('Mexico/General', 'Mexico/General'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Chuuk', 'Pacific/Chuuk'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Enderbury', 'Pacific/Enderbury'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Johnston', 'Pacific/Johnston'), ('Pacific/Kanton', 'Pacific/Kanton'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Pohnpei', 'Pacific/Pohnpei'), ('Pacific/Ponape', 'Pacific/Ponape'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Samoa', 'Pacific/Samoa'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Truk', 'Pacific/Truk'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis'), ('Pacific/Yap', 'Pacific/Yap'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('US/Alaska', 'US/Alaska'), ('US/Aleutian', 'US/Aleutian'), ('US/Arizona', 'US/Arizona'), ('US/Central', 'US/Central'), ('US/East-Indiana', 'US/East-Indiana'), ('US/Eastern', 'US/Eastern'), ('US/Hawaii', 'US/Hawaii'), ('US/Indiana-Starke', 'US/Indiana-Starke'), ('US/Michigan', 'US/Michigan'), ('US/Mountain', 'US/Mountain'), ('US/Pacific', 'US/Pacific'), ('US/Samoa', 'US/Samoa'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu'), ('localtime', 'localtime')], default='', help_text="User's current timezone when traveling (leave blank if at home)", max_length=50)), + ('date_format', models.CharField(choices=[('YYYY-MM-DD', '2024-12-25'), ('DD/MM/YYYY', '25/12/2024'), ('MM/DD/YYYY', '12/25/2024'), ('DD.MM.YYYY', '25.12.2024'), ('DD-MM-YYYY', '25-12-2024')], default='YYYY-MM-DD', help_text='Preferred date display format', max_length=20)), + ('time_format', models.CharField(choices=[('12-hour', '12-hour (3:30 PM)'), ('24-hour', '24-hour (15:30)')], default='24-hour', help_text='12-hour or 24-hour time format', max_length=10)), + ('thousand_separator', models.CharField(choices=[('comma', 'Comma (1,000)'), ('period', 'Period (1.000)'), ('space', 'Space (1 000)'), ('none', 'None (1000)')], default='comma', help_text='Number formatting preference', max_length=10)), + ('week_start', models.CharField(choices=[('monday', 'Monday'), ('sunday', 'Sunday'), ('saturday', 'Saturday')], default='monday', help_text='First day of the week', max_length=10)), + ('theme_mode', models.CharField(choices=[('light', 'Light'), ('dark', 'Dark'), ('auto', 'Auto (System)')], default='auto', help_text='Theme mode: light, dark, or auto (follows system)', max_length=10)), + ('theme_name', models.CharField(choices=[('light', 'Light'), ('dark', 'Dark'), ('cupcake', 'Cupcake'), ('bumblebee', 'Bumblebee'), ('emerald', 'Emerald'), ('corporate', 'Corporate'), ('synthwave', 'Synthwave'), ('retro', 'Retro'), ('cyberpunk', 'Cyberpunk'), ('valentine', 'Valentine'), ('halloween', 'Halloween'), ('garden', 'Garden'), ('forest', 'Forest'), ('aqua', 'Aqua'), ('lofi', 'Lo-Fi'), ('pastel', 'Pastel'), ('fantasy', 'Fantasy'), ('wireframe', 'Wireframe'), ('black', 'Black'), ('luxury', 'Luxury'), ('dracula', 'Dracula'), ('cmyk', 'CMYK'), ('autumn', 'Autumn'), ('business', 'Business'), ('acid', 'Acid'), ('lemonade', 'Lemonade'), ('night', 'Night'), ('coffee', 'Coffee'), ('winter', 'Winter'), ('dim', 'Dim'), ('nord', 'Nord'), ('sunset', 'Sunset')], default='corporate', help_text='DaisyUI theme for light mode', max_length=30)), + ('dark_theme_name', models.CharField(choices=[('light', 'Light'), ('dark', 'Dark'), ('cupcake', 'Cupcake'), ('bumblebee', 'Bumblebee'), ('emerald', 'Emerald'), ('corporate', 'Corporate'), ('synthwave', 'Synthwave'), ('retro', 'Retro'), ('cyberpunk', 'Cyberpunk'), ('valentine', 'Valentine'), ('halloween', 'Halloween'), ('garden', 'Garden'), ('forest', 'Forest'), ('aqua', 'Aqua'), ('lofi', 'Lo-Fi'), ('pastel', 'Pastel'), ('fantasy', 'Fantasy'), ('wireframe', 'Wireframe'), ('black', 'Black'), ('luxury', 'Luxury'), ('dracula', 'Dracula'), ('cmyk', 'CMYK'), ('autumn', 'Autumn'), ('business', 'Business'), ('acid', 'Acid'), ('lemonade', 'Lemonade'), ('night', 'Night'), ('coffee', 'Coffee'), ('winter', 'Winter'), ('dim', 'Dim'), ('nord', 'Nord'), ('sunset', 'Sunset')], default='business', help_text='DaisyUI theme for dark mode', max_length=30)), + ('notifications_enabled', models.BooleanField(default=True, help_text='Enable in-app notifications')), + ('notifications_min_level', models.CharField(choices=[('info', 'All notifications'), ('warning', 'Warnings and errors only'), ('danger', 'Errors only')], default='info', help_text='Minimum notification level to display', max_length=10)), + ('browser_notifications_enabled', models.BooleanField(default=False, help_text='Enable browser desktop notifications (requires permission)')), + ('notification_retention_days', models.PositiveIntegerField(default=30, help_text='Days to keep read notifications before auto-cleanup')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), ], options={ - "verbose_name": "API Key", - "verbose_name_plural": "API Keys", + 'verbose_name': 'User Profile', + 'verbose_name_plural': 'User Profiles', + }, + ), + migrations.CreateModel( + name='UserNotification', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('title', models.CharField(help_text='Short notification headline', max_length=200)), + ('message', models.TextField(blank=True, default='', help_text='Notification body text')), + ('level', models.CharField(choices=[('info', 'Info'), ('success', 'Success'), ('warning', 'Warning'), ('danger', 'Danger')], default='info', help_text='Notification severity level', max_length=10)), + ('url', models.CharField(blank=True, default='', help_text='URL to navigate to when notification is clicked', max_length=500)), + ('source_app', models.CharField(blank=True, db_index=True, default='', help_text='App label that created this notification', max_length=100)), + ('source_model', models.CharField(blank=True, default='', help_text='Model name that triggered this notification', max_length=100)), + ('source_id', models.CharField(blank=True, default='', help_text='Primary key of the source object', max_length=100)), + ('is_read', models.BooleanField(db_index=True, default=False, help_text='Whether the user has read this notification')), + ('read_at', models.DateTimeField(blank=True, help_text='When the notification was read', null=True)), + ('is_dismissed', models.BooleanField(default=False, help_text='Whether the user has dismissed this notification')), + ('dismissed_at', models.DateTimeField(blank=True, help_text='When the notification was dismissed', null=True)), + ('expires_at', models.DateTimeField(blank=True, help_text='Auto-dismiss after this time (optional)', null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Notification', + 'verbose_name_plural': 'Notifications', + 'ordering': ['-created_at'], + 'indexes': [models.Index(fields=['user', 'is_read', 'is_dismissed'], name='themis_notif_user_state'), models.Index(fields=['user', 'created_at'], name='themis_notif_user_created'), models.Index(fields=['source_app', 'source_model', 'source_id'], name='themis_notif_source')], }, ), ] diff --git a/mnemosyne/themis/migrations/0002_userprofile_browser_notifications_enabled_and_more.py b/mnemosyne/themis/migrations/0002_userprofile_browser_notifications_enabled_and_more.py deleted file mode 100644 index eacd415..0000000 --- a/mnemosyne/themis/migrations/0002_userprofile_browser_notifications_enabled_and_more.py +++ /dev/null @@ -1,84 +0,0 @@ -# Generated by Django 6.0.3 on 2026-03-09 17:57 - -import django.db.models.deletion -import uuid -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('themis', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='userprofile', - name='browser_notifications_enabled', - field=models.BooleanField(default=False, help_text='Enable browser desktop notifications (requires permission)'), - ), - migrations.AddField( - model_name='userprofile', - name='notification_retention_days', - field=models.PositiveIntegerField(default=30, help_text='Days to keep read notifications before auto-cleanup'), - ), - migrations.AddField( - model_name='userprofile', - name='notifications_enabled', - field=models.BooleanField(default=True, help_text='Enable in-app notifications'), - ), - migrations.AddField( - model_name='userprofile', - name='notifications_min_level', - field=models.CharField(choices=[('info', 'All notifications'), ('warning', 'Warnings and errors only'), ('danger', 'Errors only')], default='info', help_text='Minimum notification level to display', max_length=10), - ), - migrations.AlterField( - model_name='userprofile', - name='current_timezone', - field=models.CharField(blank=True, choices=[('', '---------'), ('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmara', 'Africa/Asmara'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Timbuktu', 'Africa/Timbuktu'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/Buenos_Aires', 'America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'America/Argentina/Catamarca'), ('America/Argentina/Cordoba', 'America/Argentina/Cordoba'), ('America/Argentina/Jujuy', 'America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Atikokan', 'America/Atikokan'), ('America/Atka', 'America/Atka'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Ciudad_Juarez', 'America/Ciudad_Juarez'), ('America/Coral_Harbour', 'America/Coral_Harbour'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Coyhaique', 'America/Coyhaique'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Ensenada', 'America/Ensenada'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Indianapolis', 'America/Indiana/Indianapolis'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Louisville', 'America/Kentucky/Louisville'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montreal', 'America/Montreal'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nipigon', 'America/Nipigon'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Nuuk', 'America/Nuuk'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Pangnirtung', 'America/Pangnirtung'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Acre', 'America/Porto_Acre'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rainy_River', 'America/Rainy_River'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Santa_Isabel', 'America/Santa_Isabel'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Shiprock', 'America/Shiprock'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Thunder_Bay', 'America/Thunder_Bay'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Virgin', 'America/Virgin'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('America/Yellowknife', 'America/Yellowknife'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Chongqing', 'Asia/Chongqing'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Harbin', 'Asia/Harbin'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Ho_Chi_Minh', 'Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Istanbul', 'Asia/Istanbul'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Kashgar', 'Asia/Kashgar'), ('Asia/Kathmandu', 'Asia/Kathmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Kolkata', 'Asia/Kolkata'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Tel_Aviv', 'Asia/Tel_Aviv'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yangon', 'Asia/Yangon'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faroe', 'Atlantic/Faroe'), ('Atlantic/Jan_Mayen', 'Atlantic/Jan_Mayen'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Canberra', 'Australia/Canberra'), ('Australia/Currie', 'Australia/Currie'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Sydney', 'Australia/Sydney'), ('Australia/Yancowinna', 'Australia/Yancowinna'), ('Etc/GMT', 'Etc/GMT'), ('Etc/GMT+0', 'Etc/GMT+0'), ('Etc/GMT+1', 'Etc/GMT+1'), ('Etc/GMT+10', 'Etc/GMT+10'), ('Etc/GMT+11', 'Etc/GMT+11'), ('Etc/GMT+12', 'Etc/GMT+12'), ('Etc/GMT+2', 'Etc/GMT+2'), ('Etc/GMT+3', 'Etc/GMT+3'), ('Etc/GMT+4', 'Etc/GMT+4'), ('Etc/GMT+5', 'Etc/GMT+5'), ('Etc/GMT+6', 'Etc/GMT+6'), ('Etc/GMT+7', 'Etc/GMT+7'), ('Etc/GMT+8', 'Etc/GMT+8'), ('Etc/GMT+9', 'Etc/GMT+9'), ('Etc/GMT-0', 'Etc/GMT-0'), ('Etc/GMT-1', 'Etc/GMT-1'), ('Etc/GMT-10', 'Etc/GMT-10'), ('Etc/GMT-11', 'Etc/GMT-11'), ('Etc/GMT-12', 'Etc/GMT-12'), ('Etc/GMT-13', 'Etc/GMT-13'), ('Etc/GMT-14', 'Etc/GMT-14'), ('Etc/GMT-2', 'Etc/GMT-2'), ('Etc/GMT-3', 'Etc/GMT-3'), ('Etc/GMT-4', 'Etc/GMT-4'), ('Etc/GMT-5', 'Etc/GMT-5'), ('Etc/GMT-6', 'Etc/GMT-6'), ('Etc/GMT-7', 'Etc/GMT-7'), ('Etc/GMT-8', 'Etc/GMT-8'), ('Etc/GMT-9', 'Etc/GMT-9'), ('Etc/GMT0', 'Etc/GMT0'), ('Etc/Greenwich', 'Etc/Greenwich'), ('Etc/UCT', 'Etc/UCT'), ('Etc/UTC', 'Etc/UTC'), ('Etc/Universal', 'Etc/Universal'), ('Etc/Zulu', 'Etc/Zulu'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belfast', 'Europe/Belfast'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Kyiv', 'Europe/Kyiv'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Nicosia', 'Europe/Nicosia'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Tiraspol', 'Europe/Tiraspol'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zurich', 'Europe/Zurich'), ('Factory', 'Factory'), ('GMT', 'GMT'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Chuuk', 'Pacific/Chuuk'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Johnston', 'Pacific/Johnston'), ('Pacific/Kanton', 'Pacific/Kanton'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Pohnpei', 'Pacific/Pohnpei'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Samoa', 'Pacific/Samoa'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis'), ('Pacific/Yap', 'Pacific/Yap'), ('UTC', 'UTC'), ('localtime', 'localtime')], default='', help_text="User's current timezone when traveling (leave blank if at home)", max_length=50), - ), - migrations.AlterField( - model_name='userprofile', - name='dark_theme_name', - field=models.CharField(choices=[('light', 'Light'), ('dark', 'Dark'), ('cupcake', 'Cupcake'), ('bumblebee', 'Bumblebee'), ('emerald', 'Emerald'), ('corporate', 'Corporate'), ('synthwave', 'Synthwave'), ('retro', 'Retro'), ('cyberpunk', 'Cyberpunk'), ('valentine', 'Valentine'), ('halloween', 'Halloween'), ('garden', 'Garden'), ('forest', 'Forest'), ('aqua', 'Aqua'), ('lofi', 'Lo-Fi'), ('pastel', 'Pastel'), ('fantasy', 'Fantasy'), ('wireframe', 'Wireframe'), ('black', 'Black'), ('luxury', 'Luxury'), ('dracula', 'Dracula'), ('cmyk', 'CMYK'), ('autumn', 'Autumn'), ('business', 'Business'), ('acid', 'Acid'), ('lemonade', 'Lemonade'), ('night', 'Night'), ('coffee', 'Coffee'), ('winter', 'Winter'), ('dim', 'Dim'), ('nord', 'Nord'), ('sunset', 'Sunset')], default='business', help_text='DaisyUI theme for dark mode', max_length=30), - ), - migrations.AlterField( - model_name='userprofile', - name='home_timezone', - field=models.CharField(choices=[('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmara', 'Africa/Asmara'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Timbuktu', 'Africa/Timbuktu'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/Buenos_Aires', 'America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'America/Argentina/Catamarca'), ('America/Argentina/Cordoba', 'America/Argentina/Cordoba'), ('America/Argentina/Jujuy', 'America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Atikokan', 'America/Atikokan'), ('America/Atka', 'America/Atka'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Ciudad_Juarez', 'America/Ciudad_Juarez'), ('America/Coral_Harbour', 'America/Coral_Harbour'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Coyhaique', 'America/Coyhaique'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Ensenada', 'America/Ensenada'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Indianapolis', 'America/Indiana/Indianapolis'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Louisville', 'America/Kentucky/Louisville'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montreal', 'America/Montreal'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nipigon', 'America/Nipigon'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Nuuk', 'America/Nuuk'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Pangnirtung', 'America/Pangnirtung'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Acre', 'America/Porto_Acre'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rainy_River', 'America/Rainy_River'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Santa_Isabel', 'America/Santa_Isabel'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Shiprock', 'America/Shiprock'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Thunder_Bay', 'America/Thunder_Bay'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Virgin', 'America/Virgin'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('America/Yellowknife', 'America/Yellowknife'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Chongqing', 'Asia/Chongqing'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Harbin', 'Asia/Harbin'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Ho_Chi_Minh', 'Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Istanbul', 'Asia/Istanbul'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Kashgar', 'Asia/Kashgar'), ('Asia/Kathmandu', 'Asia/Kathmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Kolkata', 'Asia/Kolkata'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Tel_Aviv', 'Asia/Tel_Aviv'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yangon', 'Asia/Yangon'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faroe', 'Atlantic/Faroe'), ('Atlantic/Jan_Mayen', 'Atlantic/Jan_Mayen'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Canberra', 'Australia/Canberra'), ('Australia/Currie', 'Australia/Currie'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Sydney', 'Australia/Sydney'), ('Australia/Yancowinna', 'Australia/Yancowinna'), ('Etc/GMT', 'Etc/GMT'), ('Etc/GMT+0', 'Etc/GMT+0'), ('Etc/GMT+1', 'Etc/GMT+1'), ('Etc/GMT+10', 'Etc/GMT+10'), ('Etc/GMT+11', 'Etc/GMT+11'), ('Etc/GMT+12', 'Etc/GMT+12'), ('Etc/GMT+2', 'Etc/GMT+2'), ('Etc/GMT+3', 'Etc/GMT+3'), ('Etc/GMT+4', 'Etc/GMT+4'), ('Etc/GMT+5', 'Etc/GMT+5'), ('Etc/GMT+6', 'Etc/GMT+6'), ('Etc/GMT+7', 'Etc/GMT+7'), ('Etc/GMT+8', 'Etc/GMT+8'), ('Etc/GMT+9', 'Etc/GMT+9'), ('Etc/GMT-0', 'Etc/GMT-0'), ('Etc/GMT-1', 'Etc/GMT-1'), ('Etc/GMT-10', 'Etc/GMT-10'), ('Etc/GMT-11', 'Etc/GMT-11'), ('Etc/GMT-12', 'Etc/GMT-12'), ('Etc/GMT-13', 'Etc/GMT-13'), ('Etc/GMT-14', 'Etc/GMT-14'), ('Etc/GMT-2', 'Etc/GMT-2'), ('Etc/GMT-3', 'Etc/GMT-3'), ('Etc/GMT-4', 'Etc/GMT-4'), ('Etc/GMT-5', 'Etc/GMT-5'), ('Etc/GMT-6', 'Etc/GMT-6'), ('Etc/GMT-7', 'Etc/GMT-7'), ('Etc/GMT-8', 'Etc/GMT-8'), ('Etc/GMT-9', 'Etc/GMT-9'), ('Etc/GMT0', 'Etc/GMT0'), ('Etc/Greenwich', 'Etc/Greenwich'), ('Etc/UCT', 'Etc/UCT'), ('Etc/UTC', 'Etc/UTC'), ('Etc/Universal', 'Etc/Universal'), ('Etc/Zulu', 'Etc/Zulu'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belfast', 'Europe/Belfast'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Kyiv', 'Europe/Kyiv'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Nicosia', 'Europe/Nicosia'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Tiraspol', 'Europe/Tiraspol'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zurich', 'Europe/Zurich'), ('Factory', 'Factory'), ('GMT', 'GMT'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Chuuk', 'Pacific/Chuuk'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Johnston', 'Pacific/Johnston'), ('Pacific/Kanton', 'Pacific/Kanton'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Pohnpei', 'Pacific/Pohnpei'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Samoa', 'Pacific/Samoa'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis'), ('Pacific/Yap', 'Pacific/Yap'), ('UTC', 'UTC'), ('localtime', 'localtime')], default='UTC', help_text="User's home/permanent timezone", max_length=50), - ), - migrations.AlterField( - model_name='userprofile', - name='theme_name', - field=models.CharField(choices=[('light', 'Light'), ('dark', 'Dark'), ('cupcake', 'Cupcake'), ('bumblebee', 'Bumblebee'), ('emerald', 'Emerald'), ('corporate', 'Corporate'), ('synthwave', 'Synthwave'), ('retro', 'Retro'), ('cyberpunk', 'Cyberpunk'), ('valentine', 'Valentine'), ('halloween', 'Halloween'), ('garden', 'Garden'), ('forest', 'Forest'), ('aqua', 'Aqua'), ('lofi', 'Lo-Fi'), ('pastel', 'Pastel'), ('fantasy', 'Fantasy'), ('wireframe', 'Wireframe'), ('black', 'Black'), ('luxury', 'Luxury'), ('dracula', 'Dracula'), ('cmyk', 'CMYK'), ('autumn', 'Autumn'), ('business', 'Business'), ('acid', 'Acid'), ('lemonade', 'Lemonade'), ('night', 'Night'), ('coffee', 'Coffee'), ('winter', 'Winter'), ('dim', 'Dim'), ('nord', 'Nord'), ('sunset', 'Sunset')], default='corporate', help_text='DaisyUI theme for light mode', max_length=30), - ), - migrations.CreateModel( - name='UserNotification', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('title', models.CharField(help_text='Short notification headline', max_length=200)), - ('message', models.TextField(blank=True, default='', help_text='Notification body text')), - ('level', models.CharField(choices=[('info', 'Info'), ('success', 'Success'), ('warning', 'Warning'), ('danger', 'Danger')], default='info', help_text='Notification severity level', max_length=10)), - ('url', models.CharField(blank=True, default='', help_text='URL to navigate to when notification is clicked', max_length=500)), - ('source_app', models.CharField(blank=True, db_index=True, default='', help_text='App label that created this notification', max_length=100)), - ('source_model', models.CharField(blank=True, default='', help_text='Model name that triggered this notification', max_length=100)), - ('source_id', models.CharField(blank=True, default='', help_text='Primary key of the source object', max_length=100)), - ('is_read', models.BooleanField(db_index=True, default=False, help_text='Whether the user has read this notification')), - ('read_at', models.DateTimeField(blank=True, help_text='When the notification was read', null=True)), - ('is_dismissed', models.BooleanField(default=False, help_text='Whether the user has dismissed this notification')), - ('dismissed_at', models.DateTimeField(blank=True, help_text='When the notification was dismissed', null=True)), - ('expires_at', models.DateTimeField(blank=True, help_text='Auto-dismiss after this time (optional)', null=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Notification', - 'verbose_name_plural': 'Notifications', - 'ordering': ['-created_at'], - 'indexes': [models.Index(fields=['user', 'is_read', 'is_dismissed'], name='themis_notif_user_state'), models.Index(fields=['user', 'created_at'], name='themis_notif_user_created'), models.Index(fields=['source_app', 'source_model', 'source_id'], name='themis_notif_source')], - }, - ), - ] diff --git a/mnemosyne/themis/migrations/0003_alter_userprofile_current_timezone_and_more.py b/mnemosyne/themis/migrations/0003_alter_userprofile_current_timezone_and_more.py deleted file mode 100644 index d185466..0000000 --- a/mnemosyne/themis/migrations/0003_alter_userprofile_current_timezone_and_more.py +++ /dev/null @@ -1,1259 +0,0 @@ -# Generated by Django 5.2.11 on 2026-03-11 11:53 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("themis", "0002_userprofile_browser_notifications_enabled_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="userprofile", - name="current_timezone", - field=models.CharField( - blank=True, - choices=[ - ("", "---------"), - ("Africa/Abidjan", "Africa/Abidjan"), - ("Africa/Accra", "Africa/Accra"), - ("Africa/Addis_Ababa", "Africa/Addis_Ababa"), - ("Africa/Algiers", "Africa/Algiers"), - ("Africa/Asmara", "Africa/Asmara"), - ("Africa/Asmera", "Africa/Asmera"), - ("Africa/Bamako", "Africa/Bamako"), - ("Africa/Bangui", "Africa/Bangui"), - ("Africa/Banjul", "Africa/Banjul"), - ("Africa/Bissau", "Africa/Bissau"), - ("Africa/Blantyre", "Africa/Blantyre"), - ("Africa/Brazzaville", "Africa/Brazzaville"), - ("Africa/Bujumbura", "Africa/Bujumbura"), - ("Africa/Cairo", "Africa/Cairo"), - ("Africa/Casablanca", "Africa/Casablanca"), - ("Africa/Ceuta", "Africa/Ceuta"), - ("Africa/Conakry", "Africa/Conakry"), - ("Africa/Dakar", "Africa/Dakar"), - ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"), - ("Africa/Djibouti", "Africa/Djibouti"), - ("Africa/Douala", "Africa/Douala"), - ("Africa/El_Aaiun", "Africa/El_Aaiun"), - ("Africa/Freetown", "Africa/Freetown"), - ("Africa/Gaborone", "Africa/Gaborone"), - ("Africa/Harare", "Africa/Harare"), - ("Africa/Johannesburg", "Africa/Johannesburg"), - ("Africa/Juba", "Africa/Juba"), - ("Africa/Kampala", "Africa/Kampala"), - ("Africa/Khartoum", "Africa/Khartoum"), - ("Africa/Kigali", "Africa/Kigali"), - ("Africa/Kinshasa", "Africa/Kinshasa"), - ("Africa/Lagos", "Africa/Lagos"), - ("Africa/Libreville", "Africa/Libreville"), - ("Africa/Lome", "Africa/Lome"), - ("Africa/Luanda", "Africa/Luanda"), - ("Africa/Lubumbashi", "Africa/Lubumbashi"), - ("Africa/Lusaka", "Africa/Lusaka"), - ("Africa/Malabo", "Africa/Malabo"), - ("Africa/Maputo", "Africa/Maputo"), - ("Africa/Maseru", "Africa/Maseru"), - ("Africa/Mbabane", "Africa/Mbabane"), - ("Africa/Mogadishu", "Africa/Mogadishu"), - ("Africa/Monrovia", "Africa/Monrovia"), - ("Africa/Nairobi", "Africa/Nairobi"), - ("Africa/Ndjamena", "Africa/Ndjamena"), - ("Africa/Niamey", "Africa/Niamey"), - ("Africa/Nouakchott", "Africa/Nouakchott"), - ("Africa/Ouagadougou", "Africa/Ouagadougou"), - ("Africa/Porto-Novo", "Africa/Porto-Novo"), - ("Africa/Sao_Tome", "Africa/Sao_Tome"), - ("Africa/Timbuktu", "Africa/Timbuktu"), - ("Africa/Tripoli", "Africa/Tripoli"), - ("Africa/Tunis", "Africa/Tunis"), - ("Africa/Windhoek", "Africa/Windhoek"), - ("America/Adak", "America/Adak"), - ("America/Anchorage", "America/Anchorage"), - ("America/Anguilla", "America/Anguilla"), - ("America/Antigua", "America/Antigua"), - ("America/Araguaina", "America/Araguaina"), - ( - "America/Argentina/Buenos_Aires", - "America/Argentina/Buenos_Aires", - ), - ("America/Argentina/Catamarca", "America/Argentina/Catamarca"), - ( - "America/Argentina/ComodRivadavia", - "America/Argentina/ComodRivadavia", - ), - ("America/Argentina/Cordoba", "America/Argentina/Cordoba"), - ("America/Argentina/Jujuy", "America/Argentina/Jujuy"), - ("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"), - ("America/Argentina/Mendoza", "America/Argentina/Mendoza"), - ( - "America/Argentina/Rio_Gallegos", - "America/Argentina/Rio_Gallegos", - ), - ("America/Argentina/Salta", "America/Argentina/Salta"), - ("America/Argentina/San_Juan", "America/Argentina/San_Juan"), - ("America/Argentina/San_Luis", "America/Argentina/San_Luis"), - ("America/Argentina/Tucuman", "America/Argentina/Tucuman"), - ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"), - ("America/Aruba", "America/Aruba"), - ("America/Asuncion", "America/Asuncion"), - ("America/Atikokan", "America/Atikokan"), - ("America/Atka", "America/Atka"), - ("America/Bahia", "America/Bahia"), - ("America/Bahia_Banderas", "America/Bahia_Banderas"), - ("America/Barbados", "America/Barbados"), - ("America/Belem", "America/Belem"), - ("America/Belize", "America/Belize"), - ("America/Blanc-Sablon", "America/Blanc-Sablon"), - ("America/Boa_Vista", "America/Boa_Vista"), - ("America/Bogota", "America/Bogota"), - ("America/Boise", "America/Boise"), - ("America/Buenos_Aires", "America/Buenos_Aires"), - ("America/Cambridge_Bay", "America/Cambridge_Bay"), - ("America/Campo_Grande", "America/Campo_Grande"), - ("America/Cancun", "America/Cancun"), - ("America/Caracas", "America/Caracas"), - ("America/Catamarca", "America/Catamarca"), - ("America/Cayenne", "America/Cayenne"), - ("America/Cayman", "America/Cayman"), - ("America/Chicago", "America/Chicago"), - ("America/Chihuahua", "America/Chihuahua"), - ("America/Ciudad_Juarez", "America/Ciudad_Juarez"), - ("America/Coral_Harbour", "America/Coral_Harbour"), - ("America/Cordoba", "America/Cordoba"), - ("America/Costa_Rica", "America/Costa_Rica"), - ("America/Coyhaique", "America/Coyhaique"), - ("America/Creston", "America/Creston"), - ("America/Cuiaba", "America/Cuiaba"), - ("America/Curacao", "America/Curacao"), - ("America/Danmarkshavn", "America/Danmarkshavn"), - ("America/Dawson", "America/Dawson"), - ("America/Dawson_Creek", "America/Dawson_Creek"), - ("America/Denver", "America/Denver"), - ("America/Detroit", "America/Detroit"), - ("America/Dominica", "America/Dominica"), - ("America/Edmonton", "America/Edmonton"), - ("America/Eirunepe", "America/Eirunepe"), - ("America/El_Salvador", "America/El_Salvador"), - ("America/Ensenada", "America/Ensenada"), - ("America/Fort_Nelson", "America/Fort_Nelson"), - ("America/Fort_Wayne", "America/Fort_Wayne"), - ("America/Fortaleza", "America/Fortaleza"), - ("America/Glace_Bay", "America/Glace_Bay"), - ("America/Godthab", "America/Godthab"), - ("America/Goose_Bay", "America/Goose_Bay"), - ("America/Grand_Turk", "America/Grand_Turk"), - ("America/Grenada", "America/Grenada"), - ("America/Guadeloupe", "America/Guadeloupe"), - ("America/Guatemala", "America/Guatemala"), - ("America/Guayaquil", "America/Guayaquil"), - ("America/Guyana", "America/Guyana"), - ("America/Halifax", "America/Halifax"), - ("America/Havana", "America/Havana"), - ("America/Hermosillo", "America/Hermosillo"), - ("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"), - ("America/Indiana/Knox", "America/Indiana/Knox"), - ("America/Indiana/Marengo", "America/Indiana/Marengo"), - ("America/Indiana/Petersburg", "America/Indiana/Petersburg"), - ("America/Indiana/Tell_City", "America/Indiana/Tell_City"), - ("America/Indiana/Vevay", "America/Indiana/Vevay"), - ("America/Indiana/Vincennes", "America/Indiana/Vincennes"), - ("America/Indiana/Winamac", "America/Indiana/Winamac"), - ("America/Indianapolis", "America/Indianapolis"), - ("America/Inuvik", "America/Inuvik"), - ("America/Iqaluit", "America/Iqaluit"), - ("America/Jamaica", "America/Jamaica"), - ("America/Jujuy", "America/Jujuy"), - ("America/Juneau", "America/Juneau"), - ("America/Kentucky/Louisville", "America/Kentucky/Louisville"), - ("America/Kentucky/Monticello", "America/Kentucky/Monticello"), - ("America/Knox_IN", "America/Knox_IN"), - ("America/Kralendijk", "America/Kralendijk"), - ("America/La_Paz", "America/La_Paz"), - ("America/Lima", "America/Lima"), - ("America/Los_Angeles", "America/Los_Angeles"), - ("America/Louisville", "America/Louisville"), - ("America/Lower_Princes", "America/Lower_Princes"), - ("America/Maceio", "America/Maceio"), - ("America/Managua", "America/Managua"), - ("America/Manaus", "America/Manaus"), - ("America/Marigot", "America/Marigot"), - ("America/Martinique", "America/Martinique"), - ("America/Matamoros", "America/Matamoros"), - ("America/Mazatlan", "America/Mazatlan"), - ("America/Mendoza", "America/Mendoza"), - ("America/Menominee", "America/Menominee"), - ("America/Merida", "America/Merida"), - ("America/Metlakatla", "America/Metlakatla"), - ("America/Mexico_City", "America/Mexico_City"), - ("America/Miquelon", "America/Miquelon"), - ("America/Moncton", "America/Moncton"), - ("America/Monterrey", "America/Monterrey"), - ("America/Montevideo", "America/Montevideo"), - ("America/Montreal", "America/Montreal"), - ("America/Montserrat", "America/Montserrat"), - ("America/Nassau", "America/Nassau"), - ("America/New_York", "America/New_York"), - ("America/Nipigon", "America/Nipigon"), - ("America/Nome", "America/Nome"), - ("America/Noronha", "America/Noronha"), - ("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"), - ("America/North_Dakota/Center", "America/North_Dakota/Center"), - ( - "America/North_Dakota/New_Salem", - "America/North_Dakota/New_Salem", - ), - ("America/Nuuk", "America/Nuuk"), - ("America/Ojinaga", "America/Ojinaga"), - ("America/Panama", "America/Panama"), - ("America/Pangnirtung", "America/Pangnirtung"), - ("America/Paramaribo", "America/Paramaribo"), - ("America/Phoenix", "America/Phoenix"), - ("America/Port-au-Prince", "America/Port-au-Prince"), - ("America/Port_of_Spain", "America/Port_of_Spain"), - ("America/Porto_Acre", "America/Porto_Acre"), - ("America/Porto_Velho", "America/Porto_Velho"), - ("America/Puerto_Rico", "America/Puerto_Rico"), - ("America/Punta_Arenas", "America/Punta_Arenas"), - ("America/Rainy_River", "America/Rainy_River"), - ("America/Rankin_Inlet", "America/Rankin_Inlet"), - ("America/Recife", "America/Recife"), - ("America/Regina", "America/Regina"), - ("America/Resolute", "America/Resolute"), - ("America/Rio_Branco", "America/Rio_Branco"), - ("America/Rosario", "America/Rosario"), - ("America/Santa_Isabel", "America/Santa_Isabel"), - ("America/Santarem", "America/Santarem"), - ("America/Santiago", "America/Santiago"), - ("America/Santo_Domingo", "America/Santo_Domingo"), - ("America/Sao_Paulo", "America/Sao_Paulo"), - ("America/Scoresbysund", "America/Scoresbysund"), - ("America/Shiprock", "America/Shiprock"), - ("America/Sitka", "America/Sitka"), - ("America/St_Barthelemy", "America/St_Barthelemy"), - ("America/St_Johns", "America/St_Johns"), - ("America/St_Kitts", "America/St_Kitts"), - ("America/St_Lucia", "America/St_Lucia"), - ("America/St_Thomas", "America/St_Thomas"), - ("America/St_Vincent", "America/St_Vincent"), - ("America/Swift_Current", "America/Swift_Current"), - ("America/Tegucigalpa", "America/Tegucigalpa"), - ("America/Thule", "America/Thule"), - ("America/Thunder_Bay", "America/Thunder_Bay"), - ("America/Tijuana", "America/Tijuana"), - ("America/Toronto", "America/Toronto"), - ("America/Tortola", "America/Tortola"), - ("America/Vancouver", "America/Vancouver"), - ("America/Virgin", "America/Virgin"), - ("America/Whitehorse", "America/Whitehorse"), - ("America/Winnipeg", "America/Winnipeg"), - ("America/Yakutat", "America/Yakutat"), - ("America/Yellowknife", "America/Yellowknife"), - ("Antarctica/Casey", "Antarctica/Casey"), - ("Antarctica/Davis", "Antarctica/Davis"), - ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"), - ("Antarctica/Macquarie", "Antarctica/Macquarie"), - ("Antarctica/Mawson", "Antarctica/Mawson"), - ("Antarctica/McMurdo", "Antarctica/McMurdo"), - ("Antarctica/Palmer", "Antarctica/Palmer"), - ("Antarctica/Rothera", "Antarctica/Rothera"), - ("Antarctica/South_Pole", "Antarctica/South_Pole"), - ("Antarctica/Syowa", "Antarctica/Syowa"), - ("Antarctica/Troll", "Antarctica/Troll"), - ("Antarctica/Vostok", "Antarctica/Vostok"), - ("Arctic/Longyearbyen", "Arctic/Longyearbyen"), - ("Asia/Aden", "Asia/Aden"), - ("Asia/Almaty", "Asia/Almaty"), - ("Asia/Amman", "Asia/Amman"), - ("Asia/Anadyr", "Asia/Anadyr"), - ("Asia/Aqtau", "Asia/Aqtau"), - ("Asia/Aqtobe", "Asia/Aqtobe"), - ("Asia/Ashgabat", "Asia/Ashgabat"), - ("Asia/Ashkhabad", "Asia/Ashkhabad"), - ("Asia/Atyrau", "Asia/Atyrau"), - ("Asia/Baghdad", "Asia/Baghdad"), - ("Asia/Bahrain", "Asia/Bahrain"), - ("Asia/Baku", "Asia/Baku"), - ("Asia/Bangkok", "Asia/Bangkok"), - ("Asia/Barnaul", "Asia/Barnaul"), - ("Asia/Beirut", "Asia/Beirut"), - ("Asia/Bishkek", "Asia/Bishkek"), - ("Asia/Brunei", "Asia/Brunei"), - ("Asia/Calcutta", "Asia/Calcutta"), - ("Asia/Chita", "Asia/Chita"), - ("Asia/Choibalsan", "Asia/Choibalsan"), - ("Asia/Chongqing", "Asia/Chongqing"), - ("Asia/Chungking", "Asia/Chungking"), - ("Asia/Colombo", "Asia/Colombo"), - ("Asia/Dacca", "Asia/Dacca"), - ("Asia/Damascus", "Asia/Damascus"), - ("Asia/Dhaka", "Asia/Dhaka"), - ("Asia/Dili", "Asia/Dili"), - ("Asia/Dubai", "Asia/Dubai"), - ("Asia/Dushanbe", "Asia/Dushanbe"), - ("Asia/Famagusta", "Asia/Famagusta"), - ("Asia/Gaza", "Asia/Gaza"), - ("Asia/Harbin", "Asia/Harbin"), - ("Asia/Hebron", "Asia/Hebron"), - ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"), - ("Asia/Hong_Kong", "Asia/Hong_Kong"), - ("Asia/Hovd", "Asia/Hovd"), - ("Asia/Irkutsk", "Asia/Irkutsk"), - ("Asia/Istanbul", "Asia/Istanbul"), - ("Asia/Jakarta", "Asia/Jakarta"), - ("Asia/Jayapura", "Asia/Jayapura"), - ("Asia/Jerusalem", "Asia/Jerusalem"), - ("Asia/Kabul", "Asia/Kabul"), - ("Asia/Kamchatka", "Asia/Kamchatka"), - ("Asia/Karachi", "Asia/Karachi"), - ("Asia/Kashgar", "Asia/Kashgar"), - ("Asia/Kathmandu", "Asia/Kathmandu"), - ("Asia/Katmandu", "Asia/Katmandu"), - ("Asia/Khandyga", "Asia/Khandyga"), - ("Asia/Kolkata", "Asia/Kolkata"), - ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"), - ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"), - ("Asia/Kuching", "Asia/Kuching"), - ("Asia/Kuwait", "Asia/Kuwait"), - ("Asia/Macao", "Asia/Macao"), - ("Asia/Macau", "Asia/Macau"), - ("Asia/Magadan", "Asia/Magadan"), - ("Asia/Makassar", "Asia/Makassar"), - ("Asia/Manila", "Asia/Manila"), - ("Asia/Muscat", "Asia/Muscat"), - ("Asia/Nicosia", "Asia/Nicosia"), - ("Asia/Novokuznetsk", "Asia/Novokuznetsk"), - ("Asia/Novosibirsk", "Asia/Novosibirsk"), - ("Asia/Omsk", "Asia/Omsk"), - ("Asia/Oral", "Asia/Oral"), - ("Asia/Phnom_Penh", "Asia/Phnom_Penh"), - ("Asia/Pontianak", "Asia/Pontianak"), - ("Asia/Pyongyang", "Asia/Pyongyang"), - ("Asia/Qatar", "Asia/Qatar"), - ("Asia/Qostanay", "Asia/Qostanay"), - ("Asia/Qyzylorda", "Asia/Qyzylorda"), - ("Asia/Rangoon", "Asia/Rangoon"), - ("Asia/Riyadh", "Asia/Riyadh"), - ("Asia/Saigon", "Asia/Saigon"), - ("Asia/Sakhalin", "Asia/Sakhalin"), - ("Asia/Samarkand", "Asia/Samarkand"), - ("Asia/Seoul", "Asia/Seoul"), - ("Asia/Shanghai", "Asia/Shanghai"), - ("Asia/Singapore", "Asia/Singapore"), - ("Asia/Srednekolymsk", "Asia/Srednekolymsk"), - ("Asia/Taipei", "Asia/Taipei"), - ("Asia/Tashkent", "Asia/Tashkent"), - ("Asia/Tbilisi", "Asia/Tbilisi"), - ("Asia/Tehran", "Asia/Tehran"), - ("Asia/Tel_Aviv", "Asia/Tel_Aviv"), - ("Asia/Thimbu", "Asia/Thimbu"), - ("Asia/Thimphu", "Asia/Thimphu"), - ("Asia/Tokyo", "Asia/Tokyo"), - ("Asia/Tomsk", "Asia/Tomsk"), - ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"), - ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"), - ("Asia/Ulan_Bator", "Asia/Ulan_Bator"), - ("Asia/Urumqi", "Asia/Urumqi"), - ("Asia/Ust-Nera", "Asia/Ust-Nera"), - ("Asia/Vientiane", "Asia/Vientiane"), - ("Asia/Vladivostok", "Asia/Vladivostok"), - ("Asia/Yakutsk", "Asia/Yakutsk"), - ("Asia/Yangon", "Asia/Yangon"), - ("Asia/Yekaterinburg", "Asia/Yekaterinburg"), - ("Asia/Yerevan", "Asia/Yerevan"), - ("Atlantic/Azores", "Atlantic/Azores"), - ("Atlantic/Bermuda", "Atlantic/Bermuda"), - ("Atlantic/Canary", "Atlantic/Canary"), - ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"), - ("Atlantic/Faeroe", "Atlantic/Faeroe"), - ("Atlantic/Faroe", "Atlantic/Faroe"), - ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"), - ("Atlantic/Madeira", "Atlantic/Madeira"), - ("Atlantic/Reykjavik", "Atlantic/Reykjavik"), - ("Atlantic/South_Georgia", "Atlantic/South_Georgia"), - ("Atlantic/St_Helena", "Atlantic/St_Helena"), - ("Atlantic/Stanley", "Atlantic/Stanley"), - ("Australia/ACT", "Australia/ACT"), - ("Australia/Adelaide", "Australia/Adelaide"), - ("Australia/Brisbane", "Australia/Brisbane"), - ("Australia/Broken_Hill", "Australia/Broken_Hill"), - ("Australia/Canberra", "Australia/Canberra"), - ("Australia/Currie", "Australia/Currie"), - ("Australia/Darwin", "Australia/Darwin"), - ("Australia/Eucla", "Australia/Eucla"), - ("Australia/Hobart", "Australia/Hobart"), - ("Australia/LHI", "Australia/LHI"), - ("Australia/Lindeman", "Australia/Lindeman"), - ("Australia/Lord_Howe", "Australia/Lord_Howe"), - ("Australia/Melbourne", "Australia/Melbourne"), - ("Australia/NSW", "Australia/NSW"), - ("Australia/North", "Australia/North"), - ("Australia/Perth", "Australia/Perth"), - ("Australia/Queensland", "Australia/Queensland"), - ("Australia/South", "Australia/South"), - ("Australia/Sydney", "Australia/Sydney"), - ("Australia/Tasmania", "Australia/Tasmania"), - ("Australia/Victoria", "Australia/Victoria"), - ("Australia/West", "Australia/West"), - ("Australia/Yancowinna", "Australia/Yancowinna"), - ("Brazil/Acre", "Brazil/Acre"), - ("Brazil/DeNoronha", "Brazil/DeNoronha"), - ("Brazil/East", "Brazil/East"), - ("Brazil/West", "Brazil/West"), - ("CET", "CET"), - ("CST6CDT", "CST6CDT"), - ("Canada/Atlantic", "Canada/Atlantic"), - ("Canada/Central", "Canada/Central"), - ("Canada/Eastern", "Canada/Eastern"), - ("Canada/Mountain", "Canada/Mountain"), - ("Canada/Newfoundland", "Canada/Newfoundland"), - ("Canada/Pacific", "Canada/Pacific"), - ("Canada/Saskatchewan", "Canada/Saskatchewan"), - ("Canada/Yukon", "Canada/Yukon"), - ("Chile/Continental", "Chile/Continental"), - ("Chile/EasterIsland", "Chile/EasterIsland"), - ("Cuba", "Cuba"), - ("EET", "EET"), - ("EST", "EST"), - ("EST5EDT", "EST5EDT"), - ("Egypt", "Egypt"), - ("Eire", "Eire"), - ("Etc/GMT", "Etc/GMT"), - ("Etc/GMT+0", "Etc/GMT+0"), - ("Etc/GMT+1", "Etc/GMT+1"), - ("Etc/GMT+10", "Etc/GMT+10"), - ("Etc/GMT+11", "Etc/GMT+11"), - ("Etc/GMT+12", "Etc/GMT+12"), - ("Etc/GMT+2", "Etc/GMT+2"), - ("Etc/GMT+3", "Etc/GMT+3"), - ("Etc/GMT+4", "Etc/GMT+4"), - ("Etc/GMT+5", "Etc/GMT+5"), - ("Etc/GMT+6", "Etc/GMT+6"), - ("Etc/GMT+7", "Etc/GMT+7"), - ("Etc/GMT+8", "Etc/GMT+8"), - ("Etc/GMT+9", "Etc/GMT+9"), - ("Etc/GMT-0", "Etc/GMT-0"), - ("Etc/GMT-1", "Etc/GMT-1"), - ("Etc/GMT-10", "Etc/GMT-10"), - ("Etc/GMT-11", "Etc/GMT-11"), - ("Etc/GMT-12", "Etc/GMT-12"), - ("Etc/GMT-13", "Etc/GMT-13"), - ("Etc/GMT-14", "Etc/GMT-14"), - ("Etc/GMT-2", "Etc/GMT-2"), - ("Etc/GMT-3", "Etc/GMT-3"), - ("Etc/GMT-4", "Etc/GMT-4"), - ("Etc/GMT-5", "Etc/GMT-5"), - ("Etc/GMT-6", "Etc/GMT-6"), - ("Etc/GMT-7", "Etc/GMT-7"), - ("Etc/GMT-8", "Etc/GMT-8"), - ("Etc/GMT-9", "Etc/GMT-9"), - ("Etc/GMT0", "Etc/GMT0"), - ("Etc/Greenwich", "Etc/Greenwich"), - ("Etc/UCT", "Etc/UCT"), - ("Etc/UTC", "Etc/UTC"), - ("Etc/Universal", "Etc/Universal"), - ("Etc/Zulu", "Etc/Zulu"), - ("Europe/Amsterdam", "Europe/Amsterdam"), - ("Europe/Andorra", "Europe/Andorra"), - ("Europe/Astrakhan", "Europe/Astrakhan"), - ("Europe/Athens", "Europe/Athens"), - ("Europe/Belfast", "Europe/Belfast"), - ("Europe/Belgrade", "Europe/Belgrade"), - ("Europe/Berlin", "Europe/Berlin"), - ("Europe/Bratislava", "Europe/Bratislava"), - ("Europe/Brussels", "Europe/Brussels"), - ("Europe/Bucharest", "Europe/Bucharest"), - ("Europe/Budapest", "Europe/Budapest"), - ("Europe/Busingen", "Europe/Busingen"), - ("Europe/Chisinau", "Europe/Chisinau"), - ("Europe/Copenhagen", "Europe/Copenhagen"), - ("Europe/Dublin", "Europe/Dublin"), - ("Europe/Gibraltar", "Europe/Gibraltar"), - ("Europe/Guernsey", "Europe/Guernsey"), - ("Europe/Helsinki", "Europe/Helsinki"), - ("Europe/Isle_of_Man", "Europe/Isle_of_Man"), - ("Europe/Istanbul", "Europe/Istanbul"), - ("Europe/Jersey", "Europe/Jersey"), - ("Europe/Kaliningrad", "Europe/Kaliningrad"), - ("Europe/Kiev", "Europe/Kiev"), - ("Europe/Kirov", "Europe/Kirov"), - ("Europe/Kyiv", "Europe/Kyiv"), - ("Europe/Lisbon", "Europe/Lisbon"), - ("Europe/Ljubljana", "Europe/Ljubljana"), - ("Europe/London", "Europe/London"), - ("Europe/Luxembourg", "Europe/Luxembourg"), - ("Europe/Madrid", "Europe/Madrid"), - ("Europe/Malta", "Europe/Malta"), - ("Europe/Mariehamn", "Europe/Mariehamn"), - ("Europe/Minsk", "Europe/Minsk"), - ("Europe/Monaco", "Europe/Monaco"), - ("Europe/Moscow", "Europe/Moscow"), - ("Europe/Nicosia", "Europe/Nicosia"), - ("Europe/Oslo", "Europe/Oslo"), - ("Europe/Paris", "Europe/Paris"), - ("Europe/Podgorica", "Europe/Podgorica"), - ("Europe/Prague", "Europe/Prague"), - ("Europe/Riga", "Europe/Riga"), - ("Europe/Rome", "Europe/Rome"), - ("Europe/Samara", "Europe/Samara"), - ("Europe/San_Marino", "Europe/San_Marino"), - ("Europe/Sarajevo", "Europe/Sarajevo"), - ("Europe/Saratov", "Europe/Saratov"), - ("Europe/Simferopol", "Europe/Simferopol"), - ("Europe/Skopje", "Europe/Skopje"), - ("Europe/Sofia", "Europe/Sofia"), - ("Europe/Stockholm", "Europe/Stockholm"), - ("Europe/Tallinn", "Europe/Tallinn"), - ("Europe/Tirane", "Europe/Tirane"), - ("Europe/Tiraspol", "Europe/Tiraspol"), - ("Europe/Ulyanovsk", "Europe/Ulyanovsk"), - ("Europe/Uzhgorod", "Europe/Uzhgorod"), - ("Europe/Vaduz", "Europe/Vaduz"), - ("Europe/Vatican", "Europe/Vatican"), - ("Europe/Vienna", "Europe/Vienna"), - ("Europe/Vilnius", "Europe/Vilnius"), - ("Europe/Volgograd", "Europe/Volgograd"), - ("Europe/Warsaw", "Europe/Warsaw"), - ("Europe/Zagreb", "Europe/Zagreb"), - ("Europe/Zaporozhye", "Europe/Zaporozhye"), - ("Europe/Zurich", "Europe/Zurich"), - ("Factory", "Factory"), - ("GB", "GB"), - ("GB-Eire", "GB-Eire"), - ("GMT", "GMT"), - ("GMT+0", "GMT+0"), - ("GMT-0", "GMT-0"), - ("GMT0", "GMT0"), - ("Greenwich", "Greenwich"), - ("HST", "HST"), - ("Hongkong", "Hongkong"), - ("Iceland", "Iceland"), - ("Indian/Antananarivo", "Indian/Antananarivo"), - ("Indian/Chagos", "Indian/Chagos"), - ("Indian/Christmas", "Indian/Christmas"), - ("Indian/Cocos", "Indian/Cocos"), - ("Indian/Comoro", "Indian/Comoro"), - ("Indian/Kerguelen", "Indian/Kerguelen"), - ("Indian/Mahe", "Indian/Mahe"), - ("Indian/Maldives", "Indian/Maldives"), - ("Indian/Mauritius", "Indian/Mauritius"), - ("Indian/Mayotte", "Indian/Mayotte"), - ("Indian/Reunion", "Indian/Reunion"), - ("Iran", "Iran"), - ("Israel", "Israel"), - ("Jamaica", "Jamaica"), - ("Japan", "Japan"), - ("Kwajalein", "Kwajalein"), - ("Libya", "Libya"), - ("MET", "MET"), - ("MST", "MST"), - ("MST7MDT", "MST7MDT"), - ("Mexico/BajaNorte", "Mexico/BajaNorte"), - ("Mexico/BajaSur", "Mexico/BajaSur"), - ("Mexico/General", "Mexico/General"), - ("NZ", "NZ"), - ("NZ-CHAT", "NZ-CHAT"), - ("Navajo", "Navajo"), - ("PRC", "PRC"), - ("PST8PDT", "PST8PDT"), - ("Pacific/Apia", "Pacific/Apia"), - ("Pacific/Auckland", "Pacific/Auckland"), - ("Pacific/Bougainville", "Pacific/Bougainville"), - ("Pacific/Chatham", "Pacific/Chatham"), - ("Pacific/Chuuk", "Pacific/Chuuk"), - ("Pacific/Easter", "Pacific/Easter"), - ("Pacific/Efate", "Pacific/Efate"), - ("Pacific/Enderbury", "Pacific/Enderbury"), - ("Pacific/Fakaofo", "Pacific/Fakaofo"), - ("Pacific/Fiji", "Pacific/Fiji"), - ("Pacific/Funafuti", "Pacific/Funafuti"), - ("Pacific/Galapagos", "Pacific/Galapagos"), - ("Pacific/Gambier", "Pacific/Gambier"), - ("Pacific/Guadalcanal", "Pacific/Guadalcanal"), - ("Pacific/Guam", "Pacific/Guam"), - ("Pacific/Honolulu", "Pacific/Honolulu"), - ("Pacific/Johnston", "Pacific/Johnston"), - ("Pacific/Kanton", "Pacific/Kanton"), - ("Pacific/Kiritimati", "Pacific/Kiritimati"), - ("Pacific/Kosrae", "Pacific/Kosrae"), - ("Pacific/Kwajalein", "Pacific/Kwajalein"), - ("Pacific/Majuro", "Pacific/Majuro"), - ("Pacific/Marquesas", "Pacific/Marquesas"), - ("Pacific/Midway", "Pacific/Midway"), - ("Pacific/Nauru", "Pacific/Nauru"), - ("Pacific/Niue", "Pacific/Niue"), - ("Pacific/Norfolk", "Pacific/Norfolk"), - ("Pacific/Noumea", "Pacific/Noumea"), - ("Pacific/Pago_Pago", "Pacific/Pago_Pago"), - ("Pacific/Palau", "Pacific/Palau"), - ("Pacific/Pitcairn", "Pacific/Pitcairn"), - ("Pacific/Pohnpei", "Pacific/Pohnpei"), - ("Pacific/Ponape", "Pacific/Ponape"), - ("Pacific/Port_Moresby", "Pacific/Port_Moresby"), - ("Pacific/Rarotonga", "Pacific/Rarotonga"), - ("Pacific/Saipan", "Pacific/Saipan"), - ("Pacific/Samoa", "Pacific/Samoa"), - ("Pacific/Tahiti", "Pacific/Tahiti"), - ("Pacific/Tarawa", "Pacific/Tarawa"), - ("Pacific/Tongatapu", "Pacific/Tongatapu"), - ("Pacific/Truk", "Pacific/Truk"), - ("Pacific/Wake", "Pacific/Wake"), - ("Pacific/Wallis", "Pacific/Wallis"), - ("Pacific/Yap", "Pacific/Yap"), - ("Poland", "Poland"), - ("Portugal", "Portugal"), - ("ROC", "ROC"), - ("ROK", "ROK"), - ("Singapore", "Singapore"), - ("Turkey", "Turkey"), - ("UCT", "UCT"), - ("US/Alaska", "US/Alaska"), - ("US/Aleutian", "US/Aleutian"), - ("US/Arizona", "US/Arizona"), - ("US/Central", "US/Central"), - ("US/East-Indiana", "US/East-Indiana"), - ("US/Eastern", "US/Eastern"), - ("US/Hawaii", "US/Hawaii"), - ("US/Indiana-Starke", "US/Indiana-Starke"), - ("US/Michigan", "US/Michigan"), - ("US/Mountain", "US/Mountain"), - ("US/Pacific", "US/Pacific"), - ("US/Samoa", "US/Samoa"), - ("UTC", "UTC"), - ("Universal", "Universal"), - ("W-SU", "W-SU"), - ("WET", "WET"), - ("Zulu", "Zulu"), - ("localtime", "localtime"), - ], - default="", - help_text="User's current timezone when traveling (leave blank if at home)", - max_length=50, - ), - ), - migrations.AlterField( - model_name="userprofile", - name="home_timezone", - field=models.CharField( - choices=[ - ("Africa/Abidjan", "Africa/Abidjan"), - ("Africa/Accra", "Africa/Accra"), - ("Africa/Addis_Ababa", "Africa/Addis_Ababa"), - ("Africa/Algiers", "Africa/Algiers"), - ("Africa/Asmara", "Africa/Asmara"), - ("Africa/Asmera", "Africa/Asmera"), - ("Africa/Bamako", "Africa/Bamako"), - ("Africa/Bangui", "Africa/Bangui"), - ("Africa/Banjul", "Africa/Banjul"), - ("Africa/Bissau", "Africa/Bissau"), - ("Africa/Blantyre", "Africa/Blantyre"), - ("Africa/Brazzaville", "Africa/Brazzaville"), - ("Africa/Bujumbura", "Africa/Bujumbura"), - ("Africa/Cairo", "Africa/Cairo"), - ("Africa/Casablanca", "Africa/Casablanca"), - ("Africa/Ceuta", "Africa/Ceuta"), - ("Africa/Conakry", "Africa/Conakry"), - ("Africa/Dakar", "Africa/Dakar"), - ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"), - ("Africa/Djibouti", "Africa/Djibouti"), - ("Africa/Douala", "Africa/Douala"), - ("Africa/El_Aaiun", "Africa/El_Aaiun"), - ("Africa/Freetown", "Africa/Freetown"), - ("Africa/Gaborone", "Africa/Gaborone"), - ("Africa/Harare", "Africa/Harare"), - ("Africa/Johannesburg", "Africa/Johannesburg"), - ("Africa/Juba", "Africa/Juba"), - ("Africa/Kampala", "Africa/Kampala"), - ("Africa/Khartoum", "Africa/Khartoum"), - ("Africa/Kigali", "Africa/Kigali"), - ("Africa/Kinshasa", "Africa/Kinshasa"), - ("Africa/Lagos", "Africa/Lagos"), - ("Africa/Libreville", "Africa/Libreville"), - ("Africa/Lome", "Africa/Lome"), - ("Africa/Luanda", "Africa/Luanda"), - ("Africa/Lubumbashi", "Africa/Lubumbashi"), - ("Africa/Lusaka", "Africa/Lusaka"), - ("Africa/Malabo", "Africa/Malabo"), - ("Africa/Maputo", "Africa/Maputo"), - ("Africa/Maseru", "Africa/Maseru"), - ("Africa/Mbabane", "Africa/Mbabane"), - ("Africa/Mogadishu", "Africa/Mogadishu"), - ("Africa/Monrovia", "Africa/Monrovia"), - ("Africa/Nairobi", "Africa/Nairobi"), - ("Africa/Ndjamena", "Africa/Ndjamena"), - ("Africa/Niamey", "Africa/Niamey"), - ("Africa/Nouakchott", "Africa/Nouakchott"), - ("Africa/Ouagadougou", "Africa/Ouagadougou"), - ("Africa/Porto-Novo", "Africa/Porto-Novo"), - ("Africa/Sao_Tome", "Africa/Sao_Tome"), - ("Africa/Timbuktu", "Africa/Timbuktu"), - ("Africa/Tripoli", "Africa/Tripoli"), - ("Africa/Tunis", "Africa/Tunis"), - ("Africa/Windhoek", "Africa/Windhoek"), - ("America/Adak", "America/Adak"), - ("America/Anchorage", "America/Anchorage"), - ("America/Anguilla", "America/Anguilla"), - ("America/Antigua", "America/Antigua"), - ("America/Araguaina", "America/Araguaina"), - ( - "America/Argentina/Buenos_Aires", - "America/Argentina/Buenos_Aires", - ), - ("America/Argentina/Catamarca", "America/Argentina/Catamarca"), - ( - "America/Argentina/ComodRivadavia", - "America/Argentina/ComodRivadavia", - ), - ("America/Argentina/Cordoba", "America/Argentina/Cordoba"), - ("America/Argentina/Jujuy", "America/Argentina/Jujuy"), - ("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"), - ("America/Argentina/Mendoza", "America/Argentina/Mendoza"), - ( - "America/Argentina/Rio_Gallegos", - "America/Argentina/Rio_Gallegos", - ), - ("America/Argentina/Salta", "America/Argentina/Salta"), - ("America/Argentina/San_Juan", "America/Argentina/San_Juan"), - ("America/Argentina/San_Luis", "America/Argentina/San_Luis"), - ("America/Argentina/Tucuman", "America/Argentina/Tucuman"), - ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"), - ("America/Aruba", "America/Aruba"), - ("America/Asuncion", "America/Asuncion"), - ("America/Atikokan", "America/Atikokan"), - ("America/Atka", "America/Atka"), - ("America/Bahia", "America/Bahia"), - ("America/Bahia_Banderas", "America/Bahia_Banderas"), - ("America/Barbados", "America/Barbados"), - ("America/Belem", "America/Belem"), - ("America/Belize", "America/Belize"), - ("America/Blanc-Sablon", "America/Blanc-Sablon"), - ("America/Boa_Vista", "America/Boa_Vista"), - ("America/Bogota", "America/Bogota"), - ("America/Boise", "America/Boise"), - ("America/Buenos_Aires", "America/Buenos_Aires"), - ("America/Cambridge_Bay", "America/Cambridge_Bay"), - ("America/Campo_Grande", "America/Campo_Grande"), - ("America/Cancun", "America/Cancun"), - ("America/Caracas", "America/Caracas"), - ("America/Catamarca", "America/Catamarca"), - ("America/Cayenne", "America/Cayenne"), - ("America/Cayman", "America/Cayman"), - ("America/Chicago", "America/Chicago"), - ("America/Chihuahua", "America/Chihuahua"), - ("America/Ciudad_Juarez", "America/Ciudad_Juarez"), - ("America/Coral_Harbour", "America/Coral_Harbour"), - ("America/Cordoba", "America/Cordoba"), - ("America/Costa_Rica", "America/Costa_Rica"), - ("America/Coyhaique", "America/Coyhaique"), - ("America/Creston", "America/Creston"), - ("America/Cuiaba", "America/Cuiaba"), - ("America/Curacao", "America/Curacao"), - ("America/Danmarkshavn", "America/Danmarkshavn"), - ("America/Dawson", "America/Dawson"), - ("America/Dawson_Creek", "America/Dawson_Creek"), - ("America/Denver", "America/Denver"), - ("America/Detroit", "America/Detroit"), - ("America/Dominica", "America/Dominica"), - ("America/Edmonton", "America/Edmonton"), - ("America/Eirunepe", "America/Eirunepe"), - ("America/El_Salvador", "America/El_Salvador"), - ("America/Ensenada", "America/Ensenada"), - ("America/Fort_Nelson", "America/Fort_Nelson"), - ("America/Fort_Wayne", "America/Fort_Wayne"), - ("America/Fortaleza", "America/Fortaleza"), - ("America/Glace_Bay", "America/Glace_Bay"), - ("America/Godthab", "America/Godthab"), - ("America/Goose_Bay", "America/Goose_Bay"), - ("America/Grand_Turk", "America/Grand_Turk"), - ("America/Grenada", "America/Grenada"), - ("America/Guadeloupe", "America/Guadeloupe"), - ("America/Guatemala", "America/Guatemala"), - ("America/Guayaquil", "America/Guayaquil"), - ("America/Guyana", "America/Guyana"), - ("America/Halifax", "America/Halifax"), - ("America/Havana", "America/Havana"), - ("America/Hermosillo", "America/Hermosillo"), - ("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"), - ("America/Indiana/Knox", "America/Indiana/Knox"), - ("America/Indiana/Marengo", "America/Indiana/Marengo"), - ("America/Indiana/Petersburg", "America/Indiana/Petersburg"), - ("America/Indiana/Tell_City", "America/Indiana/Tell_City"), - ("America/Indiana/Vevay", "America/Indiana/Vevay"), - ("America/Indiana/Vincennes", "America/Indiana/Vincennes"), - ("America/Indiana/Winamac", "America/Indiana/Winamac"), - ("America/Indianapolis", "America/Indianapolis"), - ("America/Inuvik", "America/Inuvik"), - ("America/Iqaluit", "America/Iqaluit"), - ("America/Jamaica", "America/Jamaica"), - ("America/Jujuy", "America/Jujuy"), - ("America/Juneau", "America/Juneau"), - ("America/Kentucky/Louisville", "America/Kentucky/Louisville"), - ("America/Kentucky/Monticello", "America/Kentucky/Monticello"), - ("America/Knox_IN", "America/Knox_IN"), - ("America/Kralendijk", "America/Kralendijk"), - ("America/La_Paz", "America/La_Paz"), - ("America/Lima", "America/Lima"), - ("America/Los_Angeles", "America/Los_Angeles"), - ("America/Louisville", "America/Louisville"), - ("America/Lower_Princes", "America/Lower_Princes"), - ("America/Maceio", "America/Maceio"), - ("America/Managua", "America/Managua"), - ("America/Manaus", "America/Manaus"), - ("America/Marigot", "America/Marigot"), - ("America/Martinique", "America/Martinique"), - ("America/Matamoros", "America/Matamoros"), - ("America/Mazatlan", "America/Mazatlan"), - ("America/Mendoza", "America/Mendoza"), - ("America/Menominee", "America/Menominee"), - ("America/Merida", "America/Merida"), - ("America/Metlakatla", "America/Metlakatla"), - ("America/Mexico_City", "America/Mexico_City"), - ("America/Miquelon", "America/Miquelon"), - ("America/Moncton", "America/Moncton"), - ("America/Monterrey", "America/Monterrey"), - ("America/Montevideo", "America/Montevideo"), - ("America/Montreal", "America/Montreal"), - ("America/Montserrat", "America/Montserrat"), - ("America/Nassau", "America/Nassau"), - ("America/New_York", "America/New_York"), - ("America/Nipigon", "America/Nipigon"), - ("America/Nome", "America/Nome"), - ("America/Noronha", "America/Noronha"), - ("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"), - ("America/North_Dakota/Center", "America/North_Dakota/Center"), - ( - "America/North_Dakota/New_Salem", - "America/North_Dakota/New_Salem", - ), - ("America/Nuuk", "America/Nuuk"), - ("America/Ojinaga", "America/Ojinaga"), - ("America/Panama", "America/Panama"), - ("America/Pangnirtung", "America/Pangnirtung"), - ("America/Paramaribo", "America/Paramaribo"), - ("America/Phoenix", "America/Phoenix"), - ("America/Port-au-Prince", "America/Port-au-Prince"), - ("America/Port_of_Spain", "America/Port_of_Spain"), - ("America/Porto_Acre", "America/Porto_Acre"), - ("America/Porto_Velho", "America/Porto_Velho"), - ("America/Puerto_Rico", "America/Puerto_Rico"), - ("America/Punta_Arenas", "America/Punta_Arenas"), - ("America/Rainy_River", "America/Rainy_River"), - ("America/Rankin_Inlet", "America/Rankin_Inlet"), - ("America/Recife", "America/Recife"), - ("America/Regina", "America/Regina"), - ("America/Resolute", "America/Resolute"), - ("America/Rio_Branco", "America/Rio_Branco"), - ("America/Rosario", "America/Rosario"), - ("America/Santa_Isabel", "America/Santa_Isabel"), - ("America/Santarem", "America/Santarem"), - ("America/Santiago", "America/Santiago"), - ("America/Santo_Domingo", "America/Santo_Domingo"), - ("America/Sao_Paulo", "America/Sao_Paulo"), - ("America/Scoresbysund", "America/Scoresbysund"), - ("America/Shiprock", "America/Shiprock"), - ("America/Sitka", "America/Sitka"), - ("America/St_Barthelemy", "America/St_Barthelemy"), - ("America/St_Johns", "America/St_Johns"), - ("America/St_Kitts", "America/St_Kitts"), - ("America/St_Lucia", "America/St_Lucia"), - ("America/St_Thomas", "America/St_Thomas"), - ("America/St_Vincent", "America/St_Vincent"), - ("America/Swift_Current", "America/Swift_Current"), - ("America/Tegucigalpa", "America/Tegucigalpa"), - ("America/Thule", "America/Thule"), - ("America/Thunder_Bay", "America/Thunder_Bay"), - ("America/Tijuana", "America/Tijuana"), - ("America/Toronto", "America/Toronto"), - ("America/Tortola", "America/Tortola"), - ("America/Vancouver", "America/Vancouver"), - ("America/Virgin", "America/Virgin"), - ("America/Whitehorse", "America/Whitehorse"), - ("America/Winnipeg", "America/Winnipeg"), - ("America/Yakutat", "America/Yakutat"), - ("America/Yellowknife", "America/Yellowknife"), - ("Antarctica/Casey", "Antarctica/Casey"), - ("Antarctica/Davis", "Antarctica/Davis"), - ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"), - ("Antarctica/Macquarie", "Antarctica/Macquarie"), - ("Antarctica/Mawson", "Antarctica/Mawson"), - ("Antarctica/McMurdo", "Antarctica/McMurdo"), - ("Antarctica/Palmer", "Antarctica/Palmer"), - ("Antarctica/Rothera", "Antarctica/Rothera"), - ("Antarctica/South_Pole", "Antarctica/South_Pole"), - ("Antarctica/Syowa", "Antarctica/Syowa"), - ("Antarctica/Troll", "Antarctica/Troll"), - ("Antarctica/Vostok", "Antarctica/Vostok"), - ("Arctic/Longyearbyen", "Arctic/Longyearbyen"), - ("Asia/Aden", "Asia/Aden"), - ("Asia/Almaty", "Asia/Almaty"), - ("Asia/Amman", "Asia/Amman"), - ("Asia/Anadyr", "Asia/Anadyr"), - ("Asia/Aqtau", "Asia/Aqtau"), - ("Asia/Aqtobe", "Asia/Aqtobe"), - ("Asia/Ashgabat", "Asia/Ashgabat"), - ("Asia/Ashkhabad", "Asia/Ashkhabad"), - ("Asia/Atyrau", "Asia/Atyrau"), - ("Asia/Baghdad", "Asia/Baghdad"), - ("Asia/Bahrain", "Asia/Bahrain"), - ("Asia/Baku", "Asia/Baku"), - ("Asia/Bangkok", "Asia/Bangkok"), - ("Asia/Barnaul", "Asia/Barnaul"), - ("Asia/Beirut", "Asia/Beirut"), - ("Asia/Bishkek", "Asia/Bishkek"), - ("Asia/Brunei", "Asia/Brunei"), - ("Asia/Calcutta", "Asia/Calcutta"), - ("Asia/Chita", "Asia/Chita"), - ("Asia/Choibalsan", "Asia/Choibalsan"), - ("Asia/Chongqing", "Asia/Chongqing"), - ("Asia/Chungking", "Asia/Chungking"), - ("Asia/Colombo", "Asia/Colombo"), - ("Asia/Dacca", "Asia/Dacca"), - ("Asia/Damascus", "Asia/Damascus"), - ("Asia/Dhaka", "Asia/Dhaka"), - ("Asia/Dili", "Asia/Dili"), - ("Asia/Dubai", "Asia/Dubai"), - ("Asia/Dushanbe", "Asia/Dushanbe"), - ("Asia/Famagusta", "Asia/Famagusta"), - ("Asia/Gaza", "Asia/Gaza"), - ("Asia/Harbin", "Asia/Harbin"), - ("Asia/Hebron", "Asia/Hebron"), - ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"), - ("Asia/Hong_Kong", "Asia/Hong_Kong"), - ("Asia/Hovd", "Asia/Hovd"), - ("Asia/Irkutsk", "Asia/Irkutsk"), - ("Asia/Istanbul", "Asia/Istanbul"), - ("Asia/Jakarta", "Asia/Jakarta"), - ("Asia/Jayapura", "Asia/Jayapura"), - ("Asia/Jerusalem", "Asia/Jerusalem"), - ("Asia/Kabul", "Asia/Kabul"), - ("Asia/Kamchatka", "Asia/Kamchatka"), - ("Asia/Karachi", "Asia/Karachi"), - ("Asia/Kashgar", "Asia/Kashgar"), - ("Asia/Kathmandu", "Asia/Kathmandu"), - ("Asia/Katmandu", "Asia/Katmandu"), - ("Asia/Khandyga", "Asia/Khandyga"), - ("Asia/Kolkata", "Asia/Kolkata"), - ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"), - ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"), - ("Asia/Kuching", "Asia/Kuching"), - ("Asia/Kuwait", "Asia/Kuwait"), - ("Asia/Macao", "Asia/Macao"), - ("Asia/Macau", "Asia/Macau"), - ("Asia/Magadan", "Asia/Magadan"), - ("Asia/Makassar", "Asia/Makassar"), - ("Asia/Manila", "Asia/Manila"), - ("Asia/Muscat", "Asia/Muscat"), - ("Asia/Nicosia", "Asia/Nicosia"), - ("Asia/Novokuznetsk", "Asia/Novokuznetsk"), - ("Asia/Novosibirsk", "Asia/Novosibirsk"), - ("Asia/Omsk", "Asia/Omsk"), - ("Asia/Oral", "Asia/Oral"), - ("Asia/Phnom_Penh", "Asia/Phnom_Penh"), - ("Asia/Pontianak", "Asia/Pontianak"), - ("Asia/Pyongyang", "Asia/Pyongyang"), - ("Asia/Qatar", "Asia/Qatar"), - ("Asia/Qostanay", "Asia/Qostanay"), - ("Asia/Qyzylorda", "Asia/Qyzylorda"), - ("Asia/Rangoon", "Asia/Rangoon"), - ("Asia/Riyadh", "Asia/Riyadh"), - ("Asia/Saigon", "Asia/Saigon"), - ("Asia/Sakhalin", "Asia/Sakhalin"), - ("Asia/Samarkand", "Asia/Samarkand"), - ("Asia/Seoul", "Asia/Seoul"), - ("Asia/Shanghai", "Asia/Shanghai"), - ("Asia/Singapore", "Asia/Singapore"), - ("Asia/Srednekolymsk", "Asia/Srednekolymsk"), - ("Asia/Taipei", "Asia/Taipei"), - ("Asia/Tashkent", "Asia/Tashkent"), - ("Asia/Tbilisi", "Asia/Tbilisi"), - ("Asia/Tehran", "Asia/Tehran"), - ("Asia/Tel_Aviv", "Asia/Tel_Aviv"), - ("Asia/Thimbu", "Asia/Thimbu"), - ("Asia/Thimphu", "Asia/Thimphu"), - ("Asia/Tokyo", "Asia/Tokyo"), - ("Asia/Tomsk", "Asia/Tomsk"), - ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"), - ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"), - ("Asia/Ulan_Bator", "Asia/Ulan_Bator"), - ("Asia/Urumqi", "Asia/Urumqi"), - ("Asia/Ust-Nera", "Asia/Ust-Nera"), - ("Asia/Vientiane", "Asia/Vientiane"), - ("Asia/Vladivostok", "Asia/Vladivostok"), - ("Asia/Yakutsk", "Asia/Yakutsk"), - ("Asia/Yangon", "Asia/Yangon"), - ("Asia/Yekaterinburg", "Asia/Yekaterinburg"), - ("Asia/Yerevan", "Asia/Yerevan"), - ("Atlantic/Azores", "Atlantic/Azores"), - ("Atlantic/Bermuda", "Atlantic/Bermuda"), - ("Atlantic/Canary", "Atlantic/Canary"), - ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"), - ("Atlantic/Faeroe", "Atlantic/Faeroe"), - ("Atlantic/Faroe", "Atlantic/Faroe"), - ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"), - ("Atlantic/Madeira", "Atlantic/Madeira"), - ("Atlantic/Reykjavik", "Atlantic/Reykjavik"), - ("Atlantic/South_Georgia", "Atlantic/South_Georgia"), - ("Atlantic/St_Helena", "Atlantic/St_Helena"), - ("Atlantic/Stanley", "Atlantic/Stanley"), - ("Australia/ACT", "Australia/ACT"), - ("Australia/Adelaide", "Australia/Adelaide"), - ("Australia/Brisbane", "Australia/Brisbane"), - ("Australia/Broken_Hill", "Australia/Broken_Hill"), - ("Australia/Canberra", "Australia/Canberra"), - ("Australia/Currie", "Australia/Currie"), - ("Australia/Darwin", "Australia/Darwin"), - ("Australia/Eucla", "Australia/Eucla"), - ("Australia/Hobart", "Australia/Hobart"), - ("Australia/LHI", "Australia/LHI"), - ("Australia/Lindeman", "Australia/Lindeman"), - ("Australia/Lord_Howe", "Australia/Lord_Howe"), - ("Australia/Melbourne", "Australia/Melbourne"), - ("Australia/NSW", "Australia/NSW"), - ("Australia/North", "Australia/North"), - ("Australia/Perth", "Australia/Perth"), - ("Australia/Queensland", "Australia/Queensland"), - ("Australia/South", "Australia/South"), - ("Australia/Sydney", "Australia/Sydney"), - ("Australia/Tasmania", "Australia/Tasmania"), - ("Australia/Victoria", "Australia/Victoria"), - ("Australia/West", "Australia/West"), - ("Australia/Yancowinna", "Australia/Yancowinna"), - ("Brazil/Acre", "Brazil/Acre"), - ("Brazil/DeNoronha", "Brazil/DeNoronha"), - ("Brazil/East", "Brazil/East"), - ("Brazil/West", "Brazil/West"), - ("CET", "CET"), - ("CST6CDT", "CST6CDT"), - ("Canada/Atlantic", "Canada/Atlantic"), - ("Canada/Central", "Canada/Central"), - ("Canada/Eastern", "Canada/Eastern"), - ("Canada/Mountain", "Canada/Mountain"), - ("Canada/Newfoundland", "Canada/Newfoundland"), - ("Canada/Pacific", "Canada/Pacific"), - ("Canada/Saskatchewan", "Canada/Saskatchewan"), - ("Canada/Yukon", "Canada/Yukon"), - ("Chile/Continental", "Chile/Continental"), - ("Chile/EasterIsland", "Chile/EasterIsland"), - ("Cuba", "Cuba"), - ("EET", "EET"), - ("EST", "EST"), - ("EST5EDT", "EST5EDT"), - ("Egypt", "Egypt"), - ("Eire", "Eire"), - ("Etc/GMT", "Etc/GMT"), - ("Etc/GMT+0", "Etc/GMT+0"), - ("Etc/GMT+1", "Etc/GMT+1"), - ("Etc/GMT+10", "Etc/GMT+10"), - ("Etc/GMT+11", "Etc/GMT+11"), - ("Etc/GMT+12", "Etc/GMT+12"), - ("Etc/GMT+2", "Etc/GMT+2"), - ("Etc/GMT+3", "Etc/GMT+3"), - ("Etc/GMT+4", "Etc/GMT+4"), - ("Etc/GMT+5", "Etc/GMT+5"), - ("Etc/GMT+6", "Etc/GMT+6"), - ("Etc/GMT+7", "Etc/GMT+7"), - ("Etc/GMT+8", "Etc/GMT+8"), - ("Etc/GMT+9", "Etc/GMT+9"), - ("Etc/GMT-0", "Etc/GMT-0"), - ("Etc/GMT-1", "Etc/GMT-1"), - ("Etc/GMT-10", "Etc/GMT-10"), - ("Etc/GMT-11", "Etc/GMT-11"), - ("Etc/GMT-12", "Etc/GMT-12"), - ("Etc/GMT-13", "Etc/GMT-13"), - ("Etc/GMT-14", "Etc/GMT-14"), - ("Etc/GMT-2", "Etc/GMT-2"), - ("Etc/GMT-3", "Etc/GMT-3"), - ("Etc/GMT-4", "Etc/GMT-4"), - ("Etc/GMT-5", "Etc/GMT-5"), - ("Etc/GMT-6", "Etc/GMT-6"), - ("Etc/GMT-7", "Etc/GMT-7"), - ("Etc/GMT-8", "Etc/GMT-8"), - ("Etc/GMT-9", "Etc/GMT-9"), - ("Etc/GMT0", "Etc/GMT0"), - ("Etc/Greenwich", "Etc/Greenwich"), - ("Etc/UCT", "Etc/UCT"), - ("Etc/UTC", "Etc/UTC"), - ("Etc/Universal", "Etc/Universal"), - ("Etc/Zulu", "Etc/Zulu"), - ("Europe/Amsterdam", "Europe/Amsterdam"), - ("Europe/Andorra", "Europe/Andorra"), - ("Europe/Astrakhan", "Europe/Astrakhan"), - ("Europe/Athens", "Europe/Athens"), - ("Europe/Belfast", "Europe/Belfast"), - ("Europe/Belgrade", "Europe/Belgrade"), - ("Europe/Berlin", "Europe/Berlin"), - ("Europe/Bratislava", "Europe/Bratislava"), - ("Europe/Brussels", "Europe/Brussels"), - ("Europe/Bucharest", "Europe/Bucharest"), - ("Europe/Budapest", "Europe/Budapest"), - ("Europe/Busingen", "Europe/Busingen"), - ("Europe/Chisinau", "Europe/Chisinau"), - ("Europe/Copenhagen", "Europe/Copenhagen"), - ("Europe/Dublin", "Europe/Dublin"), - ("Europe/Gibraltar", "Europe/Gibraltar"), - ("Europe/Guernsey", "Europe/Guernsey"), - ("Europe/Helsinki", "Europe/Helsinki"), - ("Europe/Isle_of_Man", "Europe/Isle_of_Man"), - ("Europe/Istanbul", "Europe/Istanbul"), - ("Europe/Jersey", "Europe/Jersey"), - ("Europe/Kaliningrad", "Europe/Kaliningrad"), - ("Europe/Kiev", "Europe/Kiev"), - ("Europe/Kirov", "Europe/Kirov"), - ("Europe/Kyiv", "Europe/Kyiv"), - ("Europe/Lisbon", "Europe/Lisbon"), - ("Europe/Ljubljana", "Europe/Ljubljana"), - ("Europe/London", "Europe/London"), - ("Europe/Luxembourg", "Europe/Luxembourg"), - ("Europe/Madrid", "Europe/Madrid"), - ("Europe/Malta", "Europe/Malta"), - ("Europe/Mariehamn", "Europe/Mariehamn"), - ("Europe/Minsk", "Europe/Minsk"), - ("Europe/Monaco", "Europe/Monaco"), - ("Europe/Moscow", "Europe/Moscow"), - ("Europe/Nicosia", "Europe/Nicosia"), - ("Europe/Oslo", "Europe/Oslo"), - ("Europe/Paris", "Europe/Paris"), - ("Europe/Podgorica", "Europe/Podgorica"), - ("Europe/Prague", "Europe/Prague"), - ("Europe/Riga", "Europe/Riga"), - ("Europe/Rome", "Europe/Rome"), - ("Europe/Samara", "Europe/Samara"), - ("Europe/San_Marino", "Europe/San_Marino"), - ("Europe/Sarajevo", "Europe/Sarajevo"), - ("Europe/Saratov", "Europe/Saratov"), - ("Europe/Simferopol", "Europe/Simferopol"), - ("Europe/Skopje", "Europe/Skopje"), - ("Europe/Sofia", "Europe/Sofia"), - ("Europe/Stockholm", "Europe/Stockholm"), - ("Europe/Tallinn", "Europe/Tallinn"), - ("Europe/Tirane", "Europe/Tirane"), - ("Europe/Tiraspol", "Europe/Tiraspol"), - ("Europe/Ulyanovsk", "Europe/Ulyanovsk"), - ("Europe/Uzhgorod", "Europe/Uzhgorod"), - ("Europe/Vaduz", "Europe/Vaduz"), - ("Europe/Vatican", "Europe/Vatican"), - ("Europe/Vienna", "Europe/Vienna"), - ("Europe/Vilnius", "Europe/Vilnius"), - ("Europe/Volgograd", "Europe/Volgograd"), - ("Europe/Warsaw", "Europe/Warsaw"), - ("Europe/Zagreb", "Europe/Zagreb"), - ("Europe/Zaporozhye", "Europe/Zaporozhye"), - ("Europe/Zurich", "Europe/Zurich"), - ("Factory", "Factory"), - ("GB", "GB"), - ("GB-Eire", "GB-Eire"), - ("GMT", "GMT"), - ("GMT+0", "GMT+0"), - ("GMT-0", "GMT-0"), - ("GMT0", "GMT0"), - ("Greenwich", "Greenwich"), - ("HST", "HST"), - ("Hongkong", "Hongkong"), - ("Iceland", "Iceland"), - ("Indian/Antananarivo", "Indian/Antananarivo"), - ("Indian/Chagos", "Indian/Chagos"), - ("Indian/Christmas", "Indian/Christmas"), - ("Indian/Cocos", "Indian/Cocos"), - ("Indian/Comoro", "Indian/Comoro"), - ("Indian/Kerguelen", "Indian/Kerguelen"), - ("Indian/Mahe", "Indian/Mahe"), - ("Indian/Maldives", "Indian/Maldives"), - ("Indian/Mauritius", "Indian/Mauritius"), - ("Indian/Mayotte", "Indian/Mayotte"), - ("Indian/Reunion", "Indian/Reunion"), - ("Iran", "Iran"), - ("Israel", "Israel"), - ("Jamaica", "Jamaica"), - ("Japan", "Japan"), - ("Kwajalein", "Kwajalein"), - ("Libya", "Libya"), - ("MET", "MET"), - ("MST", "MST"), - ("MST7MDT", "MST7MDT"), - ("Mexico/BajaNorte", "Mexico/BajaNorte"), - ("Mexico/BajaSur", "Mexico/BajaSur"), - ("Mexico/General", "Mexico/General"), - ("NZ", "NZ"), - ("NZ-CHAT", "NZ-CHAT"), - ("Navajo", "Navajo"), - ("PRC", "PRC"), - ("PST8PDT", "PST8PDT"), - ("Pacific/Apia", "Pacific/Apia"), - ("Pacific/Auckland", "Pacific/Auckland"), - ("Pacific/Bougainville", "Pacific/Bougainville"), - ("Pacific/Chatham", "Pacific/Chatham"), - ("Pacific/Chuuk", "Pacific/Chuuk"), - ("Pacific/Easter", "Pacific/Easter"), - ("Pacific/Efate", "Pacific/Efate"), - ("Pacific/Enderbury", "Pacific/Enderbury"), - ("Pacific/Fakaofo", "Pacific/Fakaofo"), - ("Pacific/Fiji", "Pacific/Fiji"), - ("Pacific/Funafuti", "Pacific/Funafuti"), - ("Pacific/Galapagos", "Pacific/Galapagos"), - ("Pacific/Gambier", "Pacific/Gambier"), - ("Pacific/Guadalcanal", "Pacific/Guadalcanal"), - ("Pacific/Guam", "Pacific/Guam"), - ("Pacific/Honolulu", "Pacific/Honolulu"), - ("Pacific/Johnston", "Pacific/Johnston"), - ("Pacific/Kanton", "Pacific/Kanton"), - ("Pacific/Kiritimati", "Pacific/Kiritimati"), - ("Pacific/Kosrae", "Pacific/Kosrae"), - ("Pacific/Kwajalein", "Pacific/Kwajalein"), - ("Pacific/Majuro", "Pacific/Majuro"), - ("Pacific/Marquesas", "Pacific/Marquesas"), - ("Pacific/Midway", "Pacific/Midway"), - ("Pacific/Nauru", "Pacific/Nauru"), - ("Pacific/Niue", "Pacific/Niue"), - ("Pacific/Norfolk", "Pacific/Norfolk"), - ("Pacific/Noumea", "Pacific/Noumea"), - ("Pacific/Pago_Pago", "Pacific/Pago_Pago"), - ("Pacific/Palau", "Pacific/Palau"), - ("Pacific/Pitcairn", "Pacific/Pitcairn"), - ("Pacific/Pohnpei", "Pacific/Pohnpei"), - ("Pacific/Ponape", "Pacific/Ponape"), - ("Pacific/Port_Moresby", "Pacific/Port_Moresby"), - ("Pacific/Rarotonga", "Pacific/Rarotonga"), - ("Pacific/Saipan", "Pacific/Saipan"), - ("Pacific/Samoa", "Pacific/Samoa"), - ("Pacific/Tahiti", "Pacific/Tahiti"), - ("Pacific/Tarawa", "Pacific/Tarawa"), - ("Pacific/Tongatapu", "Pacific/Tongatapu"), - ("Pacific/Truk", "Pacific/Truk"), - ("Pacific/Wake", "Pacific/Wake"), - ("Pacific/Wallis", "Pacific/Wallis"), - ("Pacific/Yap", "Pacific/Yap"), - ("Poland", "Poland"), - ("Portugal", "Portugal"), - ("ROC", "ROC"), - ("ROK", "ROK"), - ("Singapore", "Singapore"), - ("Turkey", "Turkey"), - ("UCT", "UCT"), - ("US/Alaska", "US/Alaska"), - ("US/Aleutian", "US/Aleutian"), - ("US/Arizona", "US/Arizona"), - ("US/Central", "US/Central"), - ("US/East-Indiana", "US/East-Indiana"), - ("US/Eastern", "US/Eastern"), - ("US/Hawaii", "US/Hawaii"), - ("US/Indiana-Starke", "US/Indiana-Starke"), - ("US/Michigan", "US/Michigan"), - ("US/Mountain", "US/Mountain"), - ("US/Pacific", "US/Pacific"), - ("US/Samoa", "US/Samoa"), - ("UTC", "UTC"), - ("Universal", "Universal"), - ("W-SU", "W-SU"), - ("WET", "WET"), - ("Zulu", "Zulu"), - ("localtime", "localtime"), - ], - default="UTC", - help_text="User's home/permanent timezone", - max_length=50, - ), - ), - ] diff --git a/mnemosyne/themis/migrations/0004_alter_userapikey_key_type.py b/mnemosyne/themis/migrations/0004_alter_userapikey_key_type.py deleted file mode 100644 index 28c5367..0000000 --- a/mnemosyne/themis/migrations/0004_alter_userapikey_key_type.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.13 on 2026-04-27 11:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('themis', '0003_alter_userprofile_current_timezone_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='userapikey', - name='key_type', - field=models.CharField(choices=[('api', 'API Key'), ('dav', 'DAV Credentials'), ('token', 'Access Token'), ('secret', 'Secret Key'), ('other', 'Other')], default='api', help_text='Type of credential', max_length=30), - ), - ]