- Replaced feed-* inline styles in base.html with feed-cards.css component (~60 lines) - Replaced ios-search inline styles in 3 templates with ios-search.css component * employees/employees_list.html * search/results.html * employees/department_list.html - Eliminated ~150 lines of duplicate CSS across templates - All 17 tests passing
349 lines
12 KiB
Python
349 lines
12 KiB
Python
"""DEPRECATED: Этот модуль сохранён для обратной совместимости.
|
||
|
||
Используйте вместо него:
|
||
from employees.ldap.services.sync_service import SyncService
|
||
|
||
Функции в этом модуле являются обёртками над новым SyncService.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from typing import Iterable, Tuple
|
||
|
||
from django.conf import settings
|
||
from ldap3 import BASE
|
||
|
||
from ..models import Employee, EmployeeDepartment, LdapSyncState
|
||
from .config import SyncConfig
|
||
from .infrastructure.connections import _ldap
|
||
from .utils.dn_utils import (
|
||
_ensure_user_dn,
|
||
_move_to_department,
|
||
_target_department_ou_dn,
|
||
extract_department_from_dn,
|
||
)
|
||
from .utils.group_utils import (
|
||
_desired_group_cns_for_employee,
|
||
sync_user_groups_by_cns,
|
||
)
|
||
from .repositories.ldap_repository import (
|
||
is_taken,
|
||
read_attrs,
|
||
modify_user_attrs,
|
||
)
|
||
|
||
from .utils.ldap_utils import (
|
||
|
||
build_logins_for_user,
|
||
|
||
)
|
||
from .utils.image_utils import normalize_avatar_to_jpeg
|
||
from .services.sync_service import SyncService
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Создаём глобальный экземпляр для backward compatibility
|
||
_sync_service = SyncService()
|
||
|
||
|
||
# ==================== WRAPPER FUNCTIONS ====================
|
||
|
||
|
||
def import_departments(*, cfg: SyncConfig) -> Tuple[int, int, int]:
|
||
"""DEPRECATED: Используйте SyncService().import_departments().
|
||
|
||
Импорт OU отделов из LDAP в Django.
|
||
|
||
Args:
|
||
cfg: Конфигурация синхронизации.
|
||
|
||
Returns:
|
||
Tuple[int, int, int]: (created, updated, deleted).
|
||
"""
|
||
return _sync_service.import_departments(cfg)
|
||
|
||
|
||
def import_users(*, cfg: SyncConfig) -> Tuple[int, int, int]:
|
||
"""DEPRECATED: Используйте SyncService().import_users().
|
||
|
||
Импорт пользователей из LDAP в Django.
|
||
|
||
Args:
|
||
cfg: Конфигурация синхронизации.
|
||
|
||
Returns:
|
||
Tuple[int, int, int]: (created, updated, deleted).
|
||
"""
|
||
return _sync_service.import_users(cfg)
|
||
|
||
|
||
def export_users(*, cfg: SyncConfig) -> Tuple[int, int, int, int, int]:
|
||
"""DEPRECATED: Используйте SyncService().export_users().
|
||
|
||
Полный экспорт пользователей из Django в LDAP.
|
||
|
||
Args:
|
||
cfg: Конфигурация синхронизации.
|
||
|
||
Returns:
|
||
Tuple[int, int, int, int, int]:
|
||
(logins_set, moved, avatars_set, groups_added, groups_removed).
|
||
"""
|
||
return _export_users_legacy(cfg=cfg)
|
||
|
||
|
||
def export_users_delete(
|
||
*, cfg: SyncConfig, employees: Iterable[Employee]
|
||
) -> int:
|
||
"""DEPRECATED: Используйте SyncService().export_users_delete().
|
||
|
||
Удаляет учётные записи пользователей в LDAP.
|
||
|
||
Args:
|
||
cfg: Конфигурация синхронизации.
|
||
employees: Итератор пользователей для удаления.
|
||
|
||
Returns:
|
||
int: Количество успешно удалённых DN.
|
||
"""
|
||
return _sync_service.export_users_delete(cfg, employees)
|
||
|
||
|
||
# ==================== LEGACY IMPLEMENTATIONS ====================
|
||
# Эти функции сохранены для сложной логики export_users,
|
||
# которая ещё не полностью перенесена в новые сервисы
|
||
|
||
|
||
def _employees_with_dn_qs():
|
||
"""Возвращает QuerySet сотрудников с LDAP DN."""
|
||
emp_ids = (
|
||
LdapSyncState.objects.filter(model="employee")
|
||
.exclude(ldap_dn="")
|
||
.values_list("object_pk", flat=True)
|
||
)
|
||
return Employee.objects.filter(pk__in=list(emp_ids)).select_related()
|
||
|
||
|
||
def _ensure_and_persist_user_dn(conn, emp, *, do_write):
|
||
"""Обеспечивает наличие DN и сохраняет в LdapSyncState."""
|
||
user_dn = _ensure_user_dn(conn, emp)
|
||
if do_write:
|
||
st, _ = LdapSyncState.objects.get_or_create(
|
||
model="employee", object_pk=str(emp.pk)
|
||
)
|
||
if (st.ldap_dn or "") != user_dn:
|
||
st.touch(ldap_dn=user_dn, sync_dir="ldap")
|
||
return user_dn
|
||
|
||
|
||
def _active_dept_name(emp: Employee) -> str | None:
|
||
"""Имя активного отдела сотрудника."""
|
||
link = (
|
||
EmployeeDepartment.objects.filter(employee=emp, is_active=True)
|
||
.select_related("department")
|
||
.first()
|
||
)
|
||
return link.department.name if link and link.department_id else None
|
||
|
||
|
||
def export_users_create_attrs(*, cfg: SyncConfig) -> int:
|
||
"""Создаёт недостающие sAMAccountName/UPN и базовые ФИО-атрибуты."""
|
||
upn_suffix: str = getattr(settings, "LDAP_UPN_SUFFIX", "robotail.local")
|
||
do_write = not cfg.dry_run
|
||
assigned = 0
|
||
|
||
with _ldap() as conn:
|
||
for emp in _employees_with_dn_qs():
|
||
try:
|
||
user_dn = _ensure_and_persist_user_dn(
|
||
conn, emp, do_write=do_write
|
||
)
|
||
except RuntimeError as e:
|
||
logger.warning("[WARN] %s", e)
|
||
continue
|
||
|
||
cur = read_attrs(
|
||
conn, user_dn, ["sAMAccountName", "userPrincipalName"]
|
||
)
|
||
if cur["sAMAccountName"] and cur["userPrincipalName"]:
|
||
continue
|
||
|
||
guid = (
|
||
LdapSyncState.objects.filter(
|
||
model="employee", object_pk=str(emp.pk)
|
||
)
|
||
.values_list("ldap_guid", flat=True)
|
||
.first()
|
||
)
|
||
|
||
sam, upn = build_logins_for_user(
|
||
first_name=emp.first_name or "",
|
||
last_name=emp.last_name or "",
|
||
email=emp.email or "",
|
||
upn_suffix=upn_suffix,
|
||
is_taken_sam=lambda s: is_taken(
|
||
conn, attributes={"sAMAccountName": s}
|
||
),
|
||
is_taken_upn=lambda u: is_taken(
|
||
conn, attributes={"userPrincipalName": u}
|
||
),
|
||
guid=guid,
|
||
)
|
||
modify_user_attrs(
|
||
conn,
|
||
user_dn,
|
||
{
|
||
"sAMAccountName": sam,
|
||
"userPrincipalName": upn,
|
||
"givenName": emp.first_name or None,
|
||
"sn": emp.last_name or None,
|
||
"displayName": (
|
||
f"{emp.first_name} {emp.last_name}".strip() or None
|
||
),
|
||
},
|
||
do_write=do_write,
|
||
)
|
||
assigned += 1
|
||
|
||
return assigned
|
||
|
||
|
||
def export_users_update_profile(
|
||
*, cfg: SyncConfig
|
||
) -> Tuple[int, int]:
|
||
"""Обновляет профильные данные: перемещение между отделами и аватары."""
|
||
do_write = not cfg.dry_run
|
||
moved = avatars_set = 0
|
||
|
||
with _ldap() as conn:
|
||
for emp in _employees_with_dn_qs():
|
||
try:
|
||
user_dn = _ensure_and_persist_user_dn(
|
||
conn, emp, do_write=do_write
|
||
)
|
||
except RuntimeError as e:
|
||
logger.warning("[WARN] %s", e)
|
||
continue
|
||
|
||
# MOVE между отделами
|
||
current_dept = extract_department_from_dn(user_dn)
|
||
target_dept = _active_dept_name(emp)
|
||
if target_dept and current_dept != target_dept:
|
||
if do_write:
|
||
_move_to_department(conn, user_dn, target_dept)
|
||
else:
|
||
target_ou = _target_department_ou_dn(target_dept)
|
||
if not (
|
||
conn.search(
|
||
target_ou,
|
||
"(objectClass=organizationalUnit)",
|
||
BASE,
|
||
attributes=["ou"],
|
||
)
|
||
and conn.entries
|
||
):
|
||
logger.warning(
|
||
"[WARN] Целевой OU не найден: %s", target_ou
|
||
)
|
||
continue
|
||
moved += 1
|
||
|
||
# avatar -> thumbnailPhoto
|
||
avatar_bytes: bytes | None = None
|
||
avatar_field = getattr(emp, "avatar", None)
|
||
if avatar_field and hasattr(avatar_field, "read"):
|
||
avatar_bytes = avatar_field.read()
|
||
if avatar_bytes:
|
||
try:
|
||
avatar_bytes = normalize_avatar_to_jpeg(avatar_bytes)
|
||
except Exception as exc:
|
||
logger.warning(
|
||
"[WARN] avatar: не удалось нормализовать для %s: %s",
|
||
user_dn,
|
||
exc,
|
||
)
|
||
else:
|
||
modify_user_attrs(
|
||
conn,
|
||
user_dn,
|
||
{"thumbnailPhoto": avatar_bytes},
|
||
do_write=do_write,
|
||
)
|
||
avatars_set += 1
|
||
|
||
return moved, avatars_set
|
||
|
||
|
||
def export_users_sync_groups(*, cfg: SyncConfig) -> Tuple[int, int]:
|
||
"""Приводит членства пользователей в LDAP-группах к целевому набору."""
|
||
do_write = not cfg.dry_run
|
||
groups_added = groups_removed = 0
|
||
|
||
with _ldap() as conn:
|
||
for emp in _employees_with_dn_qs():
|
||
try:
|
||
user_dn = _ensure_and_persist_user_dn(
|
||
conn, emp, do_write=do_write
|
||
)
|
||
except RuntimeError as e:
|
||
logger.warning("[WARN] %s", e)
|
||
continue
|
||
|
||
current_dept = extract_department_from_dn(user_dn)
|
||
target_dept = _active_dept_name(emp)
|
||
|
||
desired_cns = _desired_group_cns_for_employee(emp)
|
||
|
||
extra_bases: list[str] = []
|
||
dept_for_roles = target_dept or current_dept
|
||
if dept_for_roles:
|
||
dept_base = getattr(settings, "LDAP_DEPARTMENTS_BASE", "")
|
||
extra_bases.append(f"OU=Roles,OU={dept_for_roles},{dept_base}")
|
||
|
||
added, removed = sync_user_groups_by_cns(
|
||
conn,
|
||
user_dn,
|
||
desired_cns,
|
||
extra_bases=extra_bases,
|
||
do_write=do_write,
|
||
)
|
||
groups_added += added
|
||
groups_removed += removed
|
||
|
||
return groups_added, groups_removed
|
||
|
||
|
||
def _export_users_legacy(*, cfg: SyncConfig) -> Tuple[int, int, int, int, int]:
|
||
"""Legacy реализация export_users через старые функции."""
|
||
from django.utils import timezone
|
||
|
||
logins_set = export_users_create_attrs(cfg=cfg)
|
||
moved, avatars_set = export_users_update_profile(cfg=cfg)
|
||
groups_added, groups_removed = export_users_sync_groups(cfg=cfg)
|
||
|
||
do_write = not cfg.dry_run
|
||
with _ldap() as conn:
|
||
from django.db import transaction
|
||
|
||
with transaction.atomic():
|
||
now = timezone.now()
|
||
for emp in _employees_with_dn_qs().only("pk"):
|
||
try:
|
||
user_dn = _ensure_and_persist_user_dn(
|
||
conn, emp, do_write=do_write
|
||
)
|
||
except RuntimeError as e:
|
||
logger.warning("[WARN] state.touch пропущен: %s", e)
|
||
continue
|
||
state, _ = LdapSyncState.objects.get_or_create(
|
||
model="employee", object_pk=str(emp.pk)
|
||
)
|
||
state.touch(
|
||
ldap_dn=user_dn,
|
||
last_django_modify_ts=now,
|
||
sync_dir="django",
|
||
)
|
||
|
||
return logins_set, moved, avatars_set, groups_added, groups_removed
|