"""Who may see and map a catalog food. Scopes: - personal — ``manual_user`` with ``owner_profile_id`` (only that profile) - global — ``manual_admin`` (all users; no owner) - bls — official BLS (all users; no owner) Name-mappings: a *user* mapping may target any food visible to that user. A *global* mapping (``profile_id IS NULL``) may only target global/BLS foods, never another user's private entry. """ from __future__ import annotations from typing import Any VIS_PERSONAL = "personal" VIS_GLOBAL = "global" VIS_BLS = "bls" SQL_VISIBLE_TO_PROFILE = "(owner_profile_id IS NULL OR owner_profile_id = %s)" SQL_GLOBAL_ONLY = "owner_profile_id IS NULL" _KIND_VIS = { "official_bls": VIS_BLS, "manual_admin": VIS_GLOBAL, "manual_user": VIS_PERSONAL, } _VIS_RANK = {VIS_PERSONAL: 0, VIS_GLOBAL: 1, VIS_BLS: 2} def same_id(a, b) -> bool: return str(a or "").replace("-", "").lower() == str(b or "").replace("-", "").lower() def visibility_of(food: dict[str, Any] | None) -> str: if not food: return VIS_BLS kind = food.get("catalog_kind") if kind in _KIND_VIS: return _KIND_VIS[kind] if food.get("owner_profile_id"): return VIS_PERSONAL return VIS_GLOBAL def visibility_rank(food: dict[str, Any] | None) -> int: return _VIS_RANK.get(visibility_of(food), 3) def is_active(food: dict[str, Any] | None) -> bool: if not food: return False if "is_active" not in food: return True return bool(food.get("is_active")) def is_visible_to(food: dict[str, Any] | None, profile_id: str | None) -> bool: if not is_active(food): return False owner = food.get("owner_profile_id") if owner is None: return True return profile_id is not None and same_id(owner, profile_id) def is_global_mapping_target(food: dict[str, Any] | None) -> bool: return is_active(food) and food.get("owner_profile_id") is None def load_catalog_food(cur, food_id: str) -> dict[str, Any] | None: cur.execute( """ SELECT id, name_de, catalog_kind, owner_profile_id, is_active, bls_code FROM food_catalog WHERE id = %s """, (food_id,), ) row = cur.fetchone() return dict(row) if row else None def require_visible_food(cur, food_id: str, profile_id: str) -> dict[str, Any]: food = load_catalog_food(cur, food_id) if not is_visible_to(food, profile_id): raise LookupError("Lebensmittel nicht gefunden") return food def require_global_mapping_target(cur, food_id: str) -> dict[str, Any]: food = load_catalog_food(cur, food_id) if not food or not is_active(food): raise LookupError("Lebensmittel nicht gefunden") if not is_global_mapping_target(food): raise PermissionError("Globales Mapping darf nur auf zentrale oder BLS-Lebensmittel zeigen") return food def require_mapping_target(cur, food_id: str, mapping_profile_id: str | None) -> dict[str, Any]: if mapping_profile_id: return require_visible_food(cur, food_id, mapping_profile_id) return require_global_mapping_target(cur, food_id) def can_edit_food(food: dict[str, Any] | None, profile_id: str | None, *, is_admin: bool = False) -> bool: """Owner ändert persönlich; Admin ändert zentral (und im Admin-Katalog auch fremde Manuals).""" if not is_active(food): return False kind = food.get("catalog_kind") if kind == "manual_user": return same_id(food.get("owner_profile_id"), profile_id) or bool(is_admin) if kind == "manual_admin": return bool(is_admin) return False def attach_visibility(row: dict[str, Any]) -> dict[str, Any]: row["visibility"] = visibility_of(row) return row