first commit

This commit is contained in:
Alberto-Audrix committed 2026-09-08 15:21:23 +07:00
commit b54624be96
226 files changed
+108840

No files matched your search

+145
View File
@@ -0,0 +1,145 @@
"""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)