Corp/backend/api/v1/procurement/equipment_policy.py

191 lines
6.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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": (
"Ответственный должен состоять в том же отделе, что и оборудование."
)
}
)