"""HQ executive role helpers and DRF permission classes. Settled matrix (v1): - buh, director (+ their admins until specified otherwise): view only - gm + gm_admin: view + register city sites (URL) + sync from cities - gm only (not gm_admin): approve cycle close """ from __future__ import annotations from rest_framework import permissions from apps.accounts.models import User # View-only executives (no site registry, sync, or approvals). VIEW_ONLY_STATUSES = frozenset( { User.STATUS_BUH, User.STATUS_BU_ADMIN, User.STATUS_DIRECTOR, User.STATUS_DIRECTOR_ADMIN, } ) # Can register city dashboard URLs into the HQ mirror registry. SITE_REGISTRY_STATUSES = frozenset( { User.STATUS_GM, User.STATUS_GM_ADMIN, } ) # Can trigger city → HQ mirror sync. SYNC_STATUSES = frozenset( { User.STATUS_GM, User.STATUS_GM_ADMIN, } ) # Cycle close approval — GM only (explicitly not gm_admin). CYCLE_CLOSE_APPROVER_STATUSES = frozenset( { User.STATUS_GM, } ) # Product user-management role (IT Central). Django superuser remains break-glass. USER_ADMIN_STATUSES = frozenset( { User.STATUS_ACCOUNT_ADMIN, } ) def _status(user) -> str | None: return getattr(user, "status", None) if user else None def executive_user(user) -> bool: return bool( user and getattr(user, "is_authenticated", False) and _status(user) in User.EXECUTIVE_STATUSES ) def can_sync(user) -> bool: return bool( user and getattr(user, "is_authenticated", False) and _status(user) in SYNC_STATUSES ) def can_register_city_site(user) -> bool: """GM / GM Admin may add city dashboard URLs to the HQ registry.""" return bool( user and getattr(user, "is_authenticated", False) and _status(user) in SITE_REGISTRY_STATUSES ) def can_approve_cycle_close(user) -> bool: """Only GM (not gm_admin) may approve closing a cycle.""" return bool( user and getattr(user, "is_authenticated", False) and _status(user) in CYCLE_CLOSE_APPROVER_STATUSES ) def can_write_operations(user) -> bool: """No manual ops create/edit/delete for executive roles.""" return False def can_manage_users(user) -> bool: """Account Admin (IT Central) or Django superuser break-glass.""" if not user or not getattr(user, "is_authenticated", False): return False if getattr(user, "is_superuser", False): return True return _status(user) in USER_ADMIN_STATUSES def can_manage_city_registry(user) -> bool: """Alias used by city-site write endpoints — same as register permission.""" return can_register_city_site(user) or bool(getattr(user, "is_superuser", False)) class IsExecutive(permissions.BasePermission): def has_permission(self, request, view): return executive_user(request.user) class IsExecutiveAdmin(permissions.BasePermission): def has_permission(self, request, view): return can_manage_users(request.user) class CanRegisterCitySite(permissions.BasePermission): def has_permission(self, request, view): return can_register_city_site(request.user) or bool( getattr(request.user, "is_superuser", False) ) class CanApproveCycleClose(permissions.BasePermission): def has_permission(self, request, view): return can_approve_cycle_close(request.user) class ReadOrSyncOnly(permissions.BasePermission): """Safe methods for all executives; sync actions for GM / GM Admin only.""" def has_permission(self, request, view): if not executive_user(request.user): return False if request.method in permissions.SAFE_METHODS: return True if getattr(view, "executive_sync_action", False): return can_sync(request.user) return can_write_operations(request.user)