Base layout with top bar, role-based menu, profile menu and phone layout; theme, language and German form of address kept in cookies; German formal, German informal and English text; Fluent avatar bundled in the image by a Node build stage; placeholder pages for the Publisher and Admin groups. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3t4WL7bd9bdHHNjEsxzqC
83 lines
2.5 KiB
Python
83 lines
2.5 KiB
Python
"""Per-device preferences: theme, language and German form of address (AD-15).
|
|
|
|
They live in cookies and are never stored on the server. Values are checked against fixed
|
|
lists, so a forged cookie can only fall back to the default.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from django.http import HttpRequest, HttpResponse
|
|
from django.utils import translation
|
|
|
|
THEME_COOKIE = "pm_theme"
|
|
LANGUAGE_COOKIE = "pm_lang"
|
|
FORM_COOKIE = "pm_form"
|
|
COOKIE_MAX_AGE = 365 * 24 * 60 * 60
|
|
|
|
THEMES = ("light", "dark", "system")
|
|
LANGUAGES = ("de", "en")
|
|
FORMS = ("sie", "du")
|
|
|
|
DEFAULT_THEME = "system"
|
|
DEFAULT_FORM = "sie"
|
|
INFORMAL_GERMAN = "de-du" # catalogue with only the informal differences; falls back to "de"
|
|
|
|
_CHOICES = {
|
|
THEME_COOKIE: ("theme", THEMES),
|
|
LANGUAGE_COOKIE: ("language", LANGUAGES),
|
|
FORM_COOKIE: ("form", FORMS),
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Preferences:
|
|
theme: str = DEFAULT_THEME
|
|
language: str = "en"
|
|
form: str = DEFAULT_FORM
|
|
|
|
@property
|
|
def language_code(self) -> str:
|
|
"""The code the translations are activated with."""
|
|
if self.language == "de" and self.form == "du":
|
|
return INFORMAL_GERMAN
|
|
return self.language
|
|
|
|
|
|
def from_request(request: HttpRequest) -> Preferences:
|
|
theme = request.COOKIES.get(THEME_COOKIE)
|
|
language = request.COOKIES.get(LANGUAGE_COOKIE)
|
|
form = request.COOKIES.get(FORM_COOKIE)
|
|
if language not in LANGUAGES:
|
|
# Default from Accept-Language: German or English, English otherwise.
|
|
language = translation.get_language_from_request(request, check_path=False)
|
|
language = language if language in LANGUAGES else "en"
|
|
return Preferences(
|
|
theme=theme if theme in THEMES else DEFAULT_THEME,
|
|
language=language,
|
|
form=form if form in FORMS else DEFAULT_FORM,
|
|
)
|
|
|
|
|
|
def chosen(values) -> dict[str, str]:
|
|
"""The cookie name and value for every valid preference in `values` (a POST or a dict)."""
|
|
result = {}
|
|
for cookie, (field, allowed) in _CHOICES.items():
|
|
value = values.get(field)
|
|
if value in allowed:
|
|
result[cookie] = value
|
|
return result
|
|
|
|
|
|
def set_cookies(response: HttpResponse, cookies: dict[str, str], *, secure: bool) -> None:
|
|
for name, value in cookies.items():
|
|
response.set_cookie(
|
|
name,
|
|
value,
|
|
max_age=COOKIE_MAX_AGE,
|
|
secure=secure,
|
|
httponly=False, # shell.js applies a change without a reload
|
|
samesite="Lax",
|
|
)
|