From f16db4abb3a71b0e6d1516046fef298cb804f845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Tue, 2 Dec 2025 10:13:17 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=84=D0=B8=D0=BB=D1=8F=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B4=D0=BB=D1=8F=20LDAP:=201.=20=D0=A3=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D1=8B=20=D1=80=D0=B5=D0=B4=D0=B8=D1=80=D0=B5?= =?UTF-8?q?=D0=BA=D1=82=D1=8B=20=D0=B8=D0=B7=20employee=5Fedit=20=D0=B8=20?= =?UTF-8?q?employee=5Fedit=5Fme=20-=20=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C?= =?UTF-8?q?=20=D0=B2=D1=81=D0=B5=D0=B3=D0=B4=D0=B0=20=D0=B2=D0=BE=D0=B7?= =?UTF-8?q?=D0=B2=D1=80=D0=B0=D1=89=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20JSON?= =?UTF-8?q?=202.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D1=87=D0=B8=D1=8F=20ldap=5Fdn=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=D0=BC=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20LDA?= =?UTF-8?q?P=20=D0=B2=20=D0=BE=D0=B1=D0=BE=D0=B8=D1=85=20=D0=BC=D0=B5?= =?UTF-8?q?=D1=82=D0=BE=D0=B4=D0=B0=D1=85=20(me=20=D0=B8=20partial=5Fupdat?= =?UTF-8?q?e)=203.=20=D0=97=D0=B0=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D1=8C=20LdapSyncState=20=D0=B2=20=D0=B0?= =?UTF-8?q?=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA=D0=B5=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=81=D0=BC=D0=BE=D1=82=D1=80=D0=B0=20=D1=81?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D1=8F=20=D1=81=D0=B8?= =?UTF-8?q?=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/employees/admin.py | 43 +++++++++++++ backend/employees/views_front.py | 107 ++----------------------------- 2 files changed, 47 insertions(+), 103 deletions(-) diff --git a/backend/employees/admin.py b/backend/employees/admin.py index 330755d9..81ca5eaa 100644 --- a/backend/employees/admin.py +++ b/backend/employees/admin.py @@ -17,6 +17,7 @@ from .models import ( EmployeeDepartment, Position, Skill, + LdapSyncState, ) # ========================= @@ -278,3 +279,45 @@ class PermissionAdmin(admin.ModelAdmin): list_display = ("name", "codename", "content_type") list_filter = ("content_type",) search_fields = ("name", "codename") + + +@admin.register(LdapSyncState) +class LdapSyncStateAdmin(admin.ModelAdmin): + """Админка для просмотра состояния синхронизации LDAP.""" + + list_display = ( + "model", + "object_pk", + "ldap_dn_short", + "ldap_guid", + "last_sync_dir", + "updated_at", + ) + list_filter = ("model", "last_sync_dir") + search_fields = ("object_pk", "ldap_dn", "ldap_guid") + readonly_fields = ( + "model", + "object_pk", + "ldap_dn", + "ldap_guid", + "last_ldap_modify_ts", + "last_django_modify_ts", + "last_sync_dir", + "data_hash", + "updated_at", + ) + + def ldap_dn_short(self, obj): + """Показываем сокращенную версию DN для удобства.""" + if obj.ldap_dn: + return obj.ldap_dn[:50] + "..." if len(obj.ldap_dn) > 50 else obj.ldap_dn + return "-" + ldap_dn_short.short_description = "LDAP DN" + + def has_add_permission(self, request): + """Запрещаем создание вручную.""" + return False + + def has_delete_permission(self, request, obj=None): + """Разрешаем удаление для очистки.""" + return True diff --git a/backend/employees/views_front.py b/backend/employees/views_front.py index a4ed6588..9ba927a4 100644 --- a/backend/employees/views_front.py +++ b/backend/employees/views_front.py @@ -546,58 +546,8 @@ def employee_edit(request, pk: int): except Exception: data = {"detail": resp.text} - accepts = (request.headers.get("Accept") or "").lower() - is_ajax = request.headers.get("X-Requested-With") == "XMLHttpRequest" - wants_json = "application/json" in accepts or is_ajax - - if wants_json: - return JsonResponse(data or {}, status=resp.status) - - if resp.ok: - messages.success(request, "Профиль сотрудника обновлён") - redirect_url = request.META.get("HTTP_REFERER") or reverse( - "employees:employee_profile", - args=(pk,), - ) - return redirect(redirect_url) - else: - # При ошибке валидации показываем детали - if resp.status == 400 and isinstance(data, dict): - # Собираем все ошибки валидации - error_messages = [] - for field, errors in data.items(): - if field == 'detail': - error_messages.append(str(errors)) - elif isinstance(errors, list): - field_name = field.replace('_', ' ').title() - for error in errors: - if isinstance(error, dict): - err_str = error.get('string', error) - error_messages.append(f"{field_name}: {err_str}") - else: - error_messages.append(f"{field_name}: {error}") - else: - field_name = field.replace('_', ' ').title() - error_messages.append(f"{field_name}: {errors}") - - if error_messages: - for msg in error_messages: - messages.error(request, msg) - else: - messages.error(request, "Ошибка валидации данных") - else: - detail = "" - if isinstance(data, dict): - detail = data.get("detail") or data.get("error") or "" - error_msg = ( - detail - or f"Не удалось обновить сотрудника (HTTP {resp.status})" - ) - messages.error(request, error_msg) - - # Возвращаем на страницу профиля сотрудника - profile_url = reverse("employees:employee_detail", args=(pk,)) - return redirect(profile_url) + # Всегда возвращаем JSON без редиректа + return JsonResponse(data or {}, status=resp.status) # ---------- EDIT ME ---------- @@ -692,57 +642,8 @@ def employee_edit_me(request): except Exception: data = {"detail": resp.text} - accepts = (request.headers.get("Accept") or "").lower() - is_ajax = request.headers.get("X-Requested-With") == "XMLHttpRequest" - wants_json = "application/json" in accepts or is_ajax - - if wants_json: - return JsonResponse(data or {}, status=resp.status) - - if resp.ok: - messages.success(request, "Профиль обновлён") - redirect_url = ( - request.META.get("HTTP_REFERER") - or reverse("employees:profile") - ) - return redirect(redirect_url) - else: - # При ошибке валидации показываем детали - if resp.status == 400 and isinstance(data, dict): - # Собираем все ошибки валидации - error_messages = [] - for field, errors in data.items(): - if field == 'detail': - error_messages.append(str(errors)) - elif isinstance(errors, list): - field_name = field.replace('_', ' ').title() - for error in errors: - if isinstance(error, dict): - err_str = error.get('string', error) - error_messages.append(f"{field_name}: {err_str}") - else: - error_messages.append(f"{field_name}: {error}") - else: - field_name = field.replace('_', ' ').title() - error_messages.append(f"{field_name}: {errors}") - - if error_messages: - for msg in error_messages: - messages.error(request, msg) - else: - messages.error(request, "Ошибка валидации данных") - else: - detail = "" - if isinstance(data, dict): - detail = data.get("detail") or data.get("error") or "" - error_msg = ( - detail or f"Не удалось обновить профиль (HTTP {resp.status})" - ) - messages.error(request, error_msg) - - # Возвращаем на страницу профиля (где есть форма редактирования) - profile_url = reverse("employees:profile") - return redirect(profile_url) + # Всегда возвращаем JSON без редиректа + return JsonResponse(data or {}, status=resp.status) # ---------- CREATE (staff) ----------