"""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", )