"""Creation and assignment policy shared by equipment API endpoints.""" from dataclasses import dataclass from enum import StrEnum from typing import Any from api.v1.permissions import has_dept_perm from employees.constants import DeptPerm from employees.models import Department, EmployeeDepartment, RoleAssignment class EquipmentCreationLevel(StrEnum): FULL = "full" DEPARTMENT_HEAD = "dept_head" SCOPED = "scoped" class EquipmentPolicyValidationError(Exception): """Structured validation failure produced by the equipment policy.""" def __init__(self, errors: dict[str, str]): self.errors = errors super().__init__(next(iter(errors.values()))) def _scoped_candidate_department_ids(user) -> set[int]: membership_ids = EmployeeDepartment.objects.filter( employee_id=user.id, is_active=True, ).values_list("department_id", flat=True) assignment_ids = RoleAssignment.objects.filter( employee_id=user.id, is_active=True, ).values_list("role__department_id", flat=True) return set(membership_ids).union(assignment_ids) def _scoped_departments(user) -> tuple[Department, ...]: candidate_ids = _scoped_candidate_department_ids(user) if not candidate_ids: return () departments = Department.objects.filter(id__in=candidate_ids).select_related("head") return tuple( department for department in departments if has_dept_perm( user, department.id, DeptPerm.MANAGE_EQUIPMENT, ) ) @dataclass(frozen=True) class EquipmentCreationPolicy: user: Any level: EquipmentCreationLevel | None departments: tuple[Department, ...] @classmethod def resolve(cls, user) -> "EquipmentCreationPolicy": if not user or not user.is_authenticated: return cls(user=user, level=None, departments=()) if ( user.is_staff or user.is_superuser or user.has_perm("procurement.add_equipment") ): return cls( user=user, level=EquipmentCreationLevel.FULL, departments=(), ) headed_departments = tuple( Department.objects.filter(head_id=user.id).select_related("head") ) if headed_departments: return cls( user=user, level=EquipmentCreationLevel.DEPARTMENT_HEAD, departments=headed_departments, ) scoped_departments = _scoped_departments(user) if scoped_departments: return cls( user=user, level=EquipmentCreationLevel.SCOPED, departments=scoped_departments, ) return cls(user=user, level=None, departments=()) def validate_create(self, attributes: dict[str, Any]) -> dict[str, Any]: values = dict(attributes) department = values.get("department") if self.level is EquipmentCreationLevel.FULL: pass elif self.level is EquipmentCreationLevel.DEPARTMENT_HEAD: department = self._use_allowed_department(department) values["department"] = department values.setdefault("responsible_person", self.user) elif self.level is EquipmentCreationLevel.SCOPED: department = self._use_allowed_department(department) values["department"] = department values["responsible_person"] = department.head else: raise EquipmentPolicyValidationError( {"non_field_errors": "У вас нет прав на создание оборудования."} ) validate_responsible_department( department=department, responsible=values.get("responsible_person"), ) return values def _use_allowed_department(self, department) -> Department: allowed_by_id = {item.id: item for item in self.departments} if department is not None and department.id not in allowed_by_id: raise EquipmentPolicyValidationError( { "department": ( "Вы можете создавать оборудование только в своём отделе." ) } ) if department is not None: return allowed_by_id[department.id] return self.departments[0] def as_options(self) -> dict[str, Any]: if self.level is EquipmentCreationLevel.FULL: departments = Department.objects.all() else: departments = self.departments default_responsible = None if self.level is EquipmentCreationLevel.DEPARTMENT_HEAD: default_responsible = self.user elif self.level is EquipmentCreationLevel.SCOPED: default_responsible = next( (department.head for department in departments if department.head), None, ) return { "allowed_departments": [ {"id": department.id, "name": department.name} for department in departments ], "can_choose_department": self.level is EquipmentCreationLevel.FULL, "can_choose_responsible": self.level in { EquipmentCreationLevel.FULL, EquipmentCreationLevel.DEPARTMENT_HEAD, }, "default_responsible": ( { "id": default_responsible.id, "name": default_responsible.get_full_name(), } if default_responsible else None ), "permission_level": self.level.value if self.level else None, } def validate_responsible_department(*, department, responsible) -> None: if not department or not responsible: return if department.head_id == responsible.id: return if EmployeeDepartment.objects.filter( employee_id=responsible.id, department_id=department.id, is_active=True, ).exists(): return raise EquipmentPolicyValidationError( { "responsible_person": ( "Ответственный должен состоять в том же отделе, что и оборудование." ) } )