Persoenliche Liste bleibt beim Owner; zentrale Eintraege oeffnen den Admin-Editor. Admin-Katalog trennt beide Listen, persoenliche bleiben sichtbar. Co-authored-by: Cursor <cursoragent@cursor.com>
122 lines
3.7 KiB
Python
122 lines
3.7 KiB
Python
"""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
|