refactor(departments): упростить DepartmentViewSet, убрать явную LDAP логику

- Удалены импорты DirectoryService, DirectoryDepartmentDTO, LDAP errors
- Удален импорт _is_ldap_enabled
- Убраны все блоки if ldap_enabled: ... else: ...
- ViewSet теперь только работает с БД (сигналы синхронизируют LDAP)

Методы упрощены до чистой БД логики:
- partial_update: просто обновляет поля
- set_head: просто устанавливает head + head_appointed_at
- set_member_role: обновляет link.role и RoleAssignment
- add_member: создает/активирует EmployeeDepartment
- remove_member: деактивирует EmployeeDepartment
- create: просто создает Department
- destroy: просто удаляет Department

DepartmentViewSet теперь чистый CRUD: -189 строк (699 → 510)
This commit is contained in:
lizerk 2026-03-18 17:12:56 +03:00
parent adc72aa814
commit 9eb1487cee

View File

@ -12,10 +12,6 @@ from django.db.models import (Case, Count, Exists, F, IntegerField, OuterRef,
from django.db.models.functions import Coalesce
from django.shortcuts import get_object_or_404
from django.utils import timezone
from employees.ldap.directory_service import (DirectoryDepartmentDTO,
DirectoryService)
from employees.ldap.errors import (DirectoryDbError, DirectoryLdapError,
DirectoryServiceError)
from employees.models import (Department, DepartmentRole, DeptPerm,
EmployeeDepartment, RoleAssignment)
from employees.utils import (_build_links_for_dept, _head_choices_for_dept,
@ -31,7 +27,6 @@ from ..serializers import (AddMemberInput, DepartmentBriefSerializer,
DepartmentRoleSerializer, DepartmentSerializer,
EmployeeBriefSerializer, RemoveMemberInput,
SetHeadInput, SetMemberRoleInput)
from ._helpers import _is_ldap_enabled
logger = logging.getLogger(__name__)
@ -190,43 +185,20 @@ class DepartmentViewSet(viewsets.ModelViewSet):
if not ok:
return Response(errs, status=status.HTTP_400_BAD_REQUEST)
# LDAP → DB
ldap_enabled = _is_ldap_enabled()
if ldap_enabled:
svc = DirectoryService()
try:
instance = svc.set_head(instance, new_head)
except (
DirectoryLdapError,
DirectoryDbError,
DirectoryServiceError,
) as e:
code = (
status.HTTP_502_BAD_GATEWAY
if isinstance(e, DirectoryLdapError)
else (
status.HTTP_400_BAD_REQUEST
if isinstance(e, DirectoryServiceError)
else status.HTTP_500_INTERNAL_SERVER_ERROR
)
)
return Response({"detail": str(e)}, status=code)
# Обновляем руководителя (сигналы синхронизируют с LDAP)
instance.head = new_head
if new_head:
instance.head_appointed_at = timezone.now()
else:
instance.head = new_head
if new_head:
instance.head_appointed_at = timezone.now()
else:
instance.head_appointed_at = None
instance.save(update_fields=["head", "head_appointed_at"])
instance.head_appointed_at = None
instance.save(update_fields=["head", "head_appointed_at"])
return instance
def partial_update(self, request, *args, **kwargs) -> Response:
"""Частичный апдейт отдела через сервисный слой (LDAP → DB)."""
"""Частичный апдейт отдела."""
instance = self.get_object()
data: Dict[str, Any] = request.data
ldap_enabled = _is_ldap_enabled()
svc = DirectoryService() if ldap_enabled else None
# --- HEAD ---
if any(k in data for k in ("head", "head_id")):
@ -264,22 +236,9 @@ class DepartmentViewSet(viewsets.ModelViewSet):
changes[k] = data.get(k)
if changes:
if ldap_enabled:
try:
instance = svc.update_department(instance, changes)
except DirectoryLdapError as e:
return Response(
{"detail": str(e)}, status=status.HTTP_502_BAD_GATEWAY
)
except DirectoryDbError as e:
return Response(
{"detail": str(e)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
else:
for k, v in changes.items():
setattr(instance, k, v)
instance.save(update_fields=list(changes.keys()))
for k, v in changes.items():
setattr(instance, k, v)
instance.save(update_fields=list(changes.keys()))
return Response(self.get_serializer(instance).data, status=status.HTTP_200_OK)
@ -288,7 +247,7 @@ class DepartmentViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=["post"])
@transaction.atomic
def set_head(self, request, pk: str | None = None) -> Response:
"""Назначение/снятие руководителя отдела через сервисный слой (LDAP → DB)."""
"""Назначение/снятие руководителя отдела."""
dept = self.get_object()
payload = SetHeadInput(data=request.data)
@ -339,29 +298,8 @@ class DepartmentViewSet(viewsets.ModelViewSet):
if link:
# Сотрудник — член отдела: обновляем роль в линке
svc = DirectoryService() if _is_ldap_enabled() else None
if svc:
try:
svc.set_member_role(dept, employee, role)
except (
DirectoryLdapError,
DirectoryDbError,
DirectoryServiceError,
) as e:
return Response(
{"detail": str(e)},
status=(
502
if isinstance(e, DirectoryLdapError)
else 400
if isinstance(e, DirectoryServiceError)
else 500
),
)
else:
link.role = role
link.save(update_fields=["role"])
link.role = role
link.save(update_fields=["role"])
# Также создаём/обновляем RoleAssignment для консистентности
if role:
@ -379,49 +317,16 @@ class DepartmentViewSet(viewsets.ModelViewSet):
via_assignment = True
if role:
from employees.ldap.services.department_service import \
DepartmentService
from employees.ldap.services.group_service import GroupService
from employees.ldap.services.user_service import UserService
if _is_ldap_enabled():
try:
group_service = GroupService()
user_service = UserService(group_service)
dept_service = DepartmentService(
group_service, user_service)
dept_service.assign_role(employee, role, request.user)
except Exception as e:
return Response({"detail": str(e)}, status=400)
else:
RoleAssignment.objects.update_or_create(
employee=employee,
role=role,
defaults={"is_active": True,
"assigned_by": request.user},
)
RoleAssignment.objects.update_or_create(
employee=employee,
role=role,
defaults={"is_active": True, "assigned_by": request.user},
)
else:
from employees.ldap.services.department_service import \
DepartmentService
from employees.ldap.services.group_service import GroupService
from employees.ldap.services.user_service import UserService
active_assignments = RoleAssignment.objects.filter(
employee_id=emp_id, role__department=dept, is_active=True
).select_related("role")
if _is_ldap_enabled():
try:
group_service = GroupService()
user_service = UserService(group_service)
dept_service = DepartmentService(
group_service, user_service)
for assignment in active_assignments:
dept_service.revoke_role(employee, assignment.role)
except Exception as e:
return Response({"detail": str(e)}, status=400)
else:
active_assignments.update(is_active=False)
)
active_assignments.update(is_active=False)
return Response(
{
@ -501,7 +406,7 @@ class DepartmentViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=["post"], url_path="add_member")
@transaction.atomic
def add_member(self, request, pk: int | None = None):
"""Добавляет сотрудника в отдел: MOVE в OU → активирует линк (LDAP → DB)."""
"""Добавляет сотрудника в отдел."""
dept = self.get_object()
payload = AddMemberInput(data=request.data)
payload.is_valid(raise_exception=True)
@ -511,63 +416,26 @@ class DepartmentViewSet(viewsets.ModelViewSet):
employee_model = Department._meta.get_field("head").remote_field.model
employee = get_object_or_404(employee_model, id=emp_id)
ldap_enabled = _is_ldap_enabled()
link, created = EmployeeDepartment.objects.get_or_create(
employee_id=emp_id, department_id=dept.id, defaults={"is_active": True}
)
if not created and not link.is_active:
link.is_active = True
link.save(update_fields=["is_active"])
if ldap_enabled:
svc = DirectoryService()
try:
svc.add_member(dept, employee)
link = (
EmployeeDepartment.objects.filter(
employee_id=emp_id, department_id=dept.id
)
.only("role_id", "is_active")
.first()
)
return Response(
{
"employee_id": emp_id,
"is_active": True,
"role_id": getattr(link, "role_id", None) if link else None,
},
status=status.HTTP_200_OK,
)
except (DirectoryLdapError, DirectoryDbError, DirectoryServiceError) as e:
return Response(
{"detail": str(e)},
status=(
status.HTTP_502_BAD_GATEWAY
if isinstance(e, DirectoryLdapError)
else (
status.HTTP_400_BAD_REQUEST
if isinstance(e, DirectoryServiceError)
else status.HTTP_500_INTERNAL_SERVER_ERROR
)
),
)
else:
link, created = EmployeeDepartment.objects.get_or_create(
employee_id=emp_id, department_id=dept.id, defaults={
"is_active": True}
)
if not created and not link.is_active:
link.is_active = True
link.save(update_fields=["is_active"])
return Response(
{
"employee_id": emp_id,
"is_active": True,
"role_id": link.role_id,
},
status=status.HTTP_200_OK,
)
return Response(
{
"employee_id": emp_id,
"is_active": True,
"role_id": link.role_id,
},
status=status.HTTP_200_OK,
)
@action(detail=True, methods=["post"], url_path="remove_member")
@transaction.atomic
def remove_member(self, request, pk: int | None = None):
"""Удаляет члена отдела: MOVE в Users OU → удаляет линк (LDAP → DB)."""
"""Удаляет члена отдела."""
dept = self.get_object()
payload = RemoveMemberInput(data=request.data)
@ -582,43 +450,21 @@ class DepartmentViewSet(viewsets.ModelViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
ldap_enabled = _is_ldap_enabled()
if ldap_enabled:
svc = DirectoryService()
try:
svc.remove_member(dept, employee)
return Response(
{"employee_id": emp_id, "removed": True},
status=status.HTTP_200_OK,
)
except (DirectoryLdapError, DirectoryDbError, DirectoryServiceError) as e:
return Response(
{"detail": str(e)},
status=(
502
if isinstance(e, DirectoryLdapError)
else 400
if isinstance(e, DirectoryServiceError)
else 500
),
)
else:
try:
link = EmployeeDepartment.objects.get(
employee_id=emp_id, department_id=dept.id
)
link.is_active = False
link.save(update_fields=["is_active"])
return Response(
{"employee_id": emp_id, "removed": True},
status=status.HTTP_200_OK,
)
except EmployeeDepartment.DoesNotExist:
return Response(
{"detail": "Employee is not a member of this department."},
status=status.HTTP_404_NOT_FOUND,
)
try:
link = EmployeeDepartment.objects.get(
employee_id=emp_id, department_id=dept.id
)
link.is_active = False
link.save(update_fields=["is_active"])
return Response(
{"employee_id": emp_id, "removed": True},
status=status.HTTP_200_OK,
)
except EmployeeDepartment.DoesNotExist:
return Response(
{"detail": "Employee is not a member of this department."},
status=status.HTTP_404_NOT_FOUND,
)
@action(detail=False, methods=["get"], url_path="my-departments")
def my_departments(self, request) -> Response:
@ -639,61 +485,26 @@ class DepartmentViewSet(viewsets.ModelViewSet):
return Response(data)
def create(self, request, *args, **kwargs):
"""Создание отдела: сначала LDAP OU → затем запись Department."""
"""Создание отдела."""
ser = self.get_serializer(data=request.data)
ser.is_valid(raise_exception=True)
head = None
head_id = ser.validated_data.get(
"head") or ser.validated_data.get("head_id")
head_id = ser.validated_data.get("head") or ser.validated_data.get("head_id")
if head_id:
employee_model = Department._meta.get_field(
"head").remote_field.model
employee_model = Department._meta.get_field("head").remote_field.model
head = get_object_or_404(employee_model, id=head_id)
ldap_enabled = _is_ldap_enabled()
if ldap_enabled:
dto = DirectoryDepartmentDTO(
name=ser.validated_data["name"],
description=ser.validated_data.get("description", ""),
head=head,
)
svc = DirectoryService()
try:
dept = svc.create_department(dto)
return Response(
self.get_serializer(
dept).data, status=status.HTTP_201_CREATED
)
except DirectoryLdapError as e:
return Response({"detail": str(e)}, status=502)
except DirectoryDbError as e:
return Response({"detail": str(e)}, status=500)
else:
dept = Department.objects.create(
name=ser.validated_data["name"],
description=ser.validated_data.get("description", ""),
head=head,
)
return Response(
self.get_serializer(dept).data, status=status.HTTP_201_CREATED
)
dept = Department.objects.create(
name=ser.validated_data["name"],
description=ser.validated_data.get("description", ""),
head=head,
)
return Response(
self.get_serializer(dept).data, status=status.HTTP_201_CREATED
)
def destroy(self, request, *args, **kwargs):
"""Удаляет отдел: сначала в LDAP → затем из БД."""
"""Удаляет отдел."""
dept = self.get_object()
ldap_enabled = _is_ldap_enabled()
if ldap_enabled:
svc = DirectoryService()
try:
svc.delete_department(dept)
return Response(status=204)
except DirectoryLdapError as e:
return Response({"detail": str(e)}, status=502)
except DirectoryDbError as e:
return Response({"detail": str(e)}, status=500)
else:
dept.delete()
return Response(status=204)
dept.delete()
return Response(status=204)