igor_nadein 1f0858b085 refactor(ldap): fix architectural issues in views
DirectoryService:
- Added public methods: assign_role, revoke_role, create_role, update_role, delete_role
- Added public method: move_user_to_base (replaces private _user_service._move_user_to_base)
- Fixed method signatures to match DepartmentService API

roles.py:
- Replaced direct DepartmentService usage with DirectoryService facade (5 methods)
- Removed manual GroupService/UserService instantiation
- All CRUD operations now go through facade layer

actions.py:
- Replaced private API svc._user_service._move_user_to_base() with public svc.move_user_to_base()
- Simplified LDAP connection handling (2 occurrences)
- Removed manual _ldap() and ensure_container_exists calls

manage.py check: System check identified no issues
2026-02-16 16:38:01 +03:00

241 lines
11 KiB
Python
Raw 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.

"""EmployeeActionViewSet — кадровые события (приём, увольнение и т.д.)."""
from __future__ import annotations
import logging
from django.conf import settings
from django.db import transaction
from django.utils import timezone
from employees.constants import ACTION_DISMISSED
from employees.ldap.directory_service import DirectoryService
from employees.ldap.errors import (DirectoryDbError, DirectoryLdapError,
DirectoryServiceError)
from employees.models import EmployeeAction, EmployeeDepartment, LdapSyncState
from rest_framework import filters, viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from ...permissions import AdminOrActionOrModelPerms
from ..serializers import EmployeeActionSerializer
from ._helpers import HistoryActionMixin, _is_ldap_enabled
logger = logging.getLogger(__name__)
class EmployeeActionViewSet(HistoryActionMixin, viewsets.ModelViewSet):
"""
/api/v1/employee-actions/
- GET list/retrieve — IsAuthenticated
- POST create — staff/superuser ИЛИ perm employees.add_employeeaction
- PATCH/PUT update — staff/superuser ИЛИ perm employees.change_employeeaction
- DELETE destroy — staff/superuser ИЛИ perm employees.delete_employeeaction
Фильтры: ?employee=<id> ?date_from=ISO ?date_to=ISO
"""
serializer_class = EmployeeActionSerializer
permission_classes = [IsAuthenticated]
filter_backends = [filters.OrderingFilter]
ordering_fields = ["date", "id"]
ordering = ["-date"]
pagination_class = None
history_diff_fields = ["action", "date", "comment", "extra", "employee_id"]
def get_queryset(self):
qs = EmployeeAction.objects.select_related(
"employee").order_by(*self.ordering)
qp = self.request.query_params
emp = qp.get("employee")
if emp:
try:
emp_id = int(emp)
qs = qs.filter(employee_id=emp_id)
except (TypeError, ValueError):
return EmployeeAction.objects.none()
df = qp.get("date_from")
dt = qp.get("date_to")
if df:
try:
qs = qs.filter(date__gte=df)
except Exception:
pass
if dt:
try:
qs = qs.filter(date__lte=dt)
except Exception:
pass
return qs
def get_permissions(self):
if self.action == "create":
self.required_perm_code = "employees.add_employeeaction"
return [IsAuthenticated(), AdminOrActionOrModelPerms()]
if self.action in ("update", "partial_update"):
self.required_perm_code = "employees.change_employeeaction"
return [IsAuthenticated(), AdminOrActionOrModelPerms()]
if self.action == "destroy":
self.required_perm_code = "employees.delete_employeeaction"
return [IsAuthenticated(), AdminOrActionOrModelPerms()]
self.required_perm_code = None
return super().get_permissions()
# --- бизнес-эффекты после записи события ---
def _apply_effects(self, action_obj: EmployeeAction):
emp = action_obj.employee
ldap_enabled = _is_ldap_enabled()
if action_obj.action == ACTION_DISMISSED:
# Сначала деактивируем сотрудника
if emp.is_active:
emp.is_active = False
emp.save(update_fields=["is_active"])
# Деактивируем связи с отделами в БД
EmployeeDepartment.objects.filter(employee=emp, is_active=True).update(
is_active=False, date_to=timezone.now().date()
)
# Синхронизируем с LDAP
if ldap_enabled:
try:
svc = DirectoryService()
has_ldap_dn = LdapSyncState.objects.filter(
model="employee", object_pk=str(emp.pk), ldap_dn__isnull=False
).exists()
if has_ldap_dn:
svc.update_user(emp, changes={"is_active": False})
active_departments = EmployeeDepartment.objects.filter(
employee=emp
).select_related("department")
departments_processed = False
for emp_dept in active_departments:
try:
svc.remove_member(emp_dept.department, emp)
departments_processed = True
except (
DirectoryLdapError,
DirectoryDbError,
DirectoryServiceError,
) as e:
logger.error(
f"Failed to remove dismissed employee from department: "
f"employee_id={emp.id}, department_id={emp_dept.department.id}, error={e}"
)
if not departments_processed:
try:
from employees.ldap.utils.ldap_utils import \
get_base_dn_for_employee
sync_state = LdapSyncState.objects.filter(
model="employee", object_pk=str(emp.pk)
).first()
if sync_state and sync_state.ldap_dn:
target_base = get_base_dn_for_employee(emp)
current_dn = sync_state.ldap_dn
if not current_dn.lower().endswith(
target_base.lower()
):
try:
new_dn = svc.move_user_to_base(
emp, target_base
)
logger.info(
f"Dismissed employee without department moved to OU=Dismissed: "
f"employee_id={emp.id}, new_dn={new_dn}"
)
except Exception as move_err:
logger.error(
f"Failed to move dismissed employee: {move_err}"
)
except Exception as e:
logger.error(
f"Failed to move dismissed employee without department to OU=Dismissed: "
f"employee_id={emp.id}, error={e}"
)
except (
DirectoryLdapError,
DirectoryDbError,
DirectoryServiceError,
) as e:
logger.error(
f"Failed to disable user in LDAP during dismissal: "
f"employee_id={emp.id}, error={e}"
)
else:
# Любое иное событие делает сотрудника активным
was_inactive = not emp.is_active
if was_inactive:
emp.is_active = True
emp.save(update_fields=["is_active"])
if ldap_enabled:
try:
svc = DirectoryService()
has_ldap_dn = LdapSyncState.objects.filter(
model="employee", object_pk=str(emp.pk), ldap_dn__isnull=False
).exists()
if has_ldap_dn:
svc.update_user(emp, changes={"is_active": True})
if was_inactive:
try:
sync_state = LdapSyncState.objects.get(
model="employee", object_pk=str(emp.pk)
)
current_dn = sync_state.ldap_dn
dismissed_base = getattr(
settings, "LDAP_DISMISSED_BASE", ""
)
if dismissed_base and current_dn.lower().endswith(
dismissed_base.lower()
):
users_base = getattr(
settings, "LDAP_USERS_BASE", None
) or getattr(settings, "LDAP_USER_BASE", None)
if users_base:
try:
new_dn = svc.move_user_to_base(
emp, users_base
)
logger.info(
f"Restored employee moved from Dismissed to Users: "
f"employee_id={emp.id}, new_dn={new_dn}"
)
except Exception as move_err:
logger.error(
f"Failed to move restored employee: {move_err}"
)
except Exception as e:
logger.error(
f"Failed to move restored employee from Dismissed to Users: "
f"employee_id={emp.id}, error={e}"
)
except (
DirectoryLdapError,
DirectoryDbError,
DirectoryServiceError,
) as e:
logger.error(
f"Failed to enable user in LDAP during action: "
f"employee_id={emp.id}, action={action_obj.action}, error={e}"
)
@transaction.atomic
def perform_create(self, serializer):
obj = serializer.save()
self._apply_effects(obj)
@transaction.atomic
def perform_update(self, serializer):
obj = serializer.save()
self._apply_effects(obj)