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