From bb692c2c1cc381e909d689676c79190c65a1bb9c 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:32:56 +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=B0=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA=D0=B0=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BC=D0=BE=D0=BD=D0=B8=D1=82=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D0=B3=D0=B0=20LDAP=20=D1=81=D0=B8=D0=BD=D1=85=D1=80?= =?UTF-8?q?=D0=BE=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8:=201.=20=D0=94?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BB=D0=BE=D0=BD=D0=BA=D0=B0=20ldap=5Fsync=5Fstatus=20=D0=B2?= =?UTF-8?q?=20=D1=81=D0=BF=D0=B8=D1=81=D0=BE=D0=BA=20=D1=81=D0=BE=D1=82?= =?UTF-8?q?=D1=80=D1=83=D0=B4=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2=20=D1=81=20?= =?UTF-8?q?=D0=B8=D0=BA=D0=BE=D0=BD=D0=BA=D0=B0=D0=BC=D0=B8=20=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D1=82=D1=83=D1=81=D0=B0=20(=E2=9C=85/=E2=9A=A0=EF=B8=8F/?= =?UTF-8?q?=E2=9D=8C)=202.=20=D0=92=20=D0=B4=D0=B5=D1=82=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20ldap=5Fsync=5Finfo=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BA=D0=BB=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=D0=B1=D0=B5=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D1=81=D1=81?= =?UTF-8?q?=D1=8B=D0=BB=D0=BA=D0=B0=20=D0=BD=D0=B0=20=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D1=8C=20LdapSyncState=203.=20=D0=A1=D0=BE=D0=B7?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=20=D0=BA=D0=B0=D1=81=D1=82=D0=BE=D0=BC=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=20LdapSynce?= =?UTF-8?q?dFilter=20=D0=B4=D0=BB=D1=8F=20=D0=B1=D1=8B=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B9=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D1=81=D0=BE=D1=82=D1=80=D1=83=D0=B4=D0=BD=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2=20=D0=BF=D0=BE=20=D0=BD=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D1=87=D0=B8=D1=8E=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B8=20?= =?UTF-8?q?=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=204.=20=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=87=D0=B8=D1=82=D0=B0=D0=B5=D0=BC=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=8C=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=D0=BE=D0=B2?= =?UTF-8?q?=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D1=81=20=D0=BF=D0=BE=D0=B4=D1=81=D0=BA?= =?UTF-8?q?=D0=B0=D0=B7=D0=BA=D0=B0=D0=BC=D0=B8=20=D0=BF=D1=80=D0=B8=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/employees/admin.py | 92 +++++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/backend/employees/admin.py b/backend/employees/admin.py index 1af30f88..93b1a0fa 100644 --- a/backend/employees/admin.py +++ b/backend/employees/admin.py @@ -24,6 +24,45 @@ from .models import ( # Формы для кастомного User (Employee) # ========================= + +class LdapSyncedFilter(admin.SimpleListFilter): + """Фильтр для отображения сотрудников по состоянию LDAP синхронизации.""" + + title = "Статус LDAP синхронизации" + parameter_name = "ldap_synced" + + def lookups(self, request, model_admin): + return ( + ("yes", "Есть запись синхронизации"), + ("no", "Нет записи синхронизации"), + ) + + def queryset(self, request, queryset): + from django.db.models import Exists, OuterRef + + if self.value() == "yes": + # Показываем только тех, у кого есть запись синхронизации + return queryset.filter( + Exists( + LdapSyncState.objects.filter( + model="employee", + object_pk=OuterRef("pk") + ) + ) + ) + elif self.value() == "no": + # Показываем только тех, у кого нет записи синхронизации + return queryset.exclude( + Exists( + LdapSyncState.objects.filter( + model="employee", + object_pk=OuterRef("pk") + ) + ) + ) + return queryset + + class EmployeeCreationForm(forms.ModelForm): """ Создание сотрудника в админке: логин — email, без username. @@ -158,6 +197,7 @@ class EmployeeAdmin(DjangoUserAdmin): "is_active", "email_verified", "is_ldap_managed", + "ldap_sync_status", "is_staff", ) list_filter = ( @@ -166,6 +206,7 @@ class EmployeeAdmin(DjangoUserAdmin): "is_staff", "is_superuser", "is_ldap_managed", + LdapSyncedFilter, "position", "groups", ) @@ -183,6 +224,42 @@ class EmployeeAdmin(DjangoUserAdmin): "email_activation_code", "sms_activation_code", ) + + def ldap_sync_status(self, obj): + """Краткий статус LDAP синхронизации для списка.""" + if not obj.pk or not obj.is_ldap_managed: + return "-" + + try: + sync_state = LdapSyncState.objects.filter( + model="employee", + object_pk=str(obj.pk) + ).first() + + if not sync_state: + return "❌ Нет записи" + + from django.utils.html import format_html + + # Показываем GUID и дату последнего обновления + if sync_state.ldap_guid and sync_state.updated_at: + return format_html( + '✅ {}', + sync_state.ldap_guid, + sync_state.updated_at.strftime("%d.%m.%Y %H:%M") + ) + elif sync_state.updated_at: + return format_html( + '⚠️ {}', + sync_state.updated_at.strftime("%d.%m.%Y %H:%M") + ) + else: + return "⚠️ Неполные данные" + except Exception: + return "❌ Ошибка" + + ldap_sync_status.short_description = "LDAP статус" + ldap_sync_status.admin_order_field = "id" # Сортировка по ID fieldsets = ( (None, {"fields": ("email", "password")}), @@ -236,8 +313,21 @@ class EmployeeAdmin(DjangoUserAdmin): return "Нет записи синхронизации" from django.utils.html import format_html + from django.urls import reverse + + # Ссылка на запись синхронизации в админке + sync_url = reverse( + 'admin:employees_ldapsyncstate_change', + args=[sync_state.pk] + ) + + info = [ + format_html( + '📋 Открыть запись синхронизации', + sync_url + ) + ] - info = [] if sync_state.ldap_dn: info.append(f"DN: {sync_state.ldap_dn}") if sync_state.ldap_guid: