From 677feaa1113896be7d7592f8dab490c7810d768b 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:35:41 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD-=D0=B4=D0=B5?= =?UTF-8?q?=D0=B9=D1=81=D1=82=D0=B2=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?LDAP=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B5=D0=B9:=201.=20=D0=A3=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D1=88=D0=B5=D0=BD=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=20lda?= =?UTF-8?q?p=5Fsync=5Fstatus=20=D1=81=20=D0=B4=D0=B5=D1=82=D0=B0=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE=D0=B9=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B5=D0=B9:=20=20=20=20-=20=D0=9B=D0=BE?= =?UTF-8?q?=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B8=20=D0=BE?= =?UTF-8?q?=D1=82=D0=BC=D0=B5=D1=87=D0=B5=D0=BD=D1=8B=20=D0=BA=D0=B0=D0=BA?= =?UTF-8?q?=20"=D0=9B=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9"=20(?= =?UTF-8?q?=D1=81=D0=B5=D1=80=D1=8B=D0=B9)=20=20=20=20-=20LDAP=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=20=D1=81=20=D0=BF=D0=BE=D0=BB=D0=BD=D1=8B=D0=BC=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D0=BC=D0=B8:=20=E2=9C=85=20?= =?UTF-8?q?=D1=81=20GUID=20=D0=B8=20DN=20=D0=B2=20=D0=BF=D0=BE=D0=B4=D1=81?= =?UTF-8?q?=D0=BA=D0=B0=D0=B7=D0=BA=D0=B5=20=20=20=20-=20LDAP=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=20=D1=81=20=D0=BD=D0=B5=D0=BF=D0=BE=D0=BB=D0=BD=D1=8B?= =?UTF-8?q?=D0=BC=D0=B8=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D0=BC=D0=B8:=20?= =?UTF-8?q?=E2=9A=A0=EF=B8=8F=20=D1=81=20=D1=83=D0=BA=D0=B0=D0=B7=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=D0=BC=20=D0=BE=D1=82=D1=81=D1=83=D1=82=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D1=83=D1=8E=D1=89=D0=B8=D1=85=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=B5=D0=B9=20=20=20=20-=20LDAP=20=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B8=20=D0=B1=D0=B5?= =?UTF-8?q?=D0=B7=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B8:=20=E2=9D=8C=20?= =?UTF-8?q?=D1=81=20=D0=BF=D0=BE=D1=8F=D1=81=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=D0=BC=202.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=20action=20"=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B8=20LDAP=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"=20=D0=B4=D0=BB=D1=8F=20=D0=BC=D0=B0=D1=81=D1=81=D0=BE?= =?UTF-8?q?=D0=B2=D0=BE=D0=B3=D0=BE=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B5=D0=B9=20Lda?= =?UTF-8?q?pSyncState=203.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20action=20"=D0=9F=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=20LDAP=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"=20=D1=81=20=D0=B4=D0=B5=D1=82=D0=B0=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE=D0=B9=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D0=B8=D0=BA=D0=BE=D0=B9=20=D0=BF=D0=BE=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D0=BD=D0=BD=D1=8B=D0=BC=20=D1=81=D0=BE=D1=82?= =?UTF-8?q?=D1=80=D1=83=D0=B4=D0=BD=D0=B8=D0=BA=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/employees/admin.py | 139 +++++++++++++++++++++++++++++++++---- 1 file changed, 125 insertions(+), 14 deletions(-) diff --git a/backend/employees/admin.py b/backend/employees/admin.py index 93b1a0fa..d8c515b3 100644 --- a/backend/employees/admin.py +++ b/backend/employees/admin.py @@ -227,9 +227,15 @@ class EmployeeAdmin(DjangoUserAdmin): def ldap_sync_status(self, obj): """Краткий статус LDAP синхронизации для списка.""" - if not obj.pk or not obj.is_ldap_managed: + if not obj.pk: return "-" + from django.utils.html import format_html + + # Если не управляется LDAP + if not obj.is_ldap_managed: + return format_html('Локальный') + try: sync_state = LdapSyncState.objects.filter( model="employee", @@ -237,26 +243,45 @@ class EmployeeAdmin(DjangoUserAdmin): ).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( - '✅ {}', + '❌ Нет записи' + ) + + # Проверяем полноту данных + has_guid = bool(sync_state.ldap_guid) + has_dn = bool(sync_state.ldap_dn) + has_updated = bool(sync_state.updated_at) + + # Полные данные + if has_guid and has_dn and has_updated: + return format_html( + '✅ {}', sync_state.ldap_guid, + sync_state.ldap_dn[:80] + '...' if len(sync_state.ldap_dn) > 80 else sync_state.ldap_dn, sync_state.updated_at.strftime("%d.%m.%Y %H:%M") ) - elif sync_state.updated_at: + # Неполные данные + elif has_updated: + missing = [] + if not has_guid: + missing.append("GUID") + if not has_dn: + missing.append("DN") + return format_html( - '⚠️ {}', + '⚠️ {}', + ", ".join(missing), sync_state.updated_at.strftime("%d.%m.%Y %H:%M") ) else: - return "⚠️ Неполные данные" - except Exception: - return "❌ Ошибка" + return format_html( + '⚠️ Неполные данные' + ) + except Exception as e: + return format_html( + '❌ Ошибка', + str(e) + ) ldap_sync_status.short_description = "LDAP статус" ldap_sync_status.admin_order_field = "id" # Сортировка по ID @@ -373,7 +398,93 @@ class EmployeeAdmin(DjangoUserAdmin): }), ) - inlines = [EmployeeDepartmentInline,] + inlines = [EmployeeDepartmentInline] + + actions = ["create_ldap_sync_records", "check_ldap_sync_status"] + + def create_ldap_sync_records(self, request, queryset): + """Создать записи синхронизации для выбранных сотрудников.""" + created_count = 0 + already_exists = 0 + + for employee in queryset: + if not employee.is_ldap_managed: + continue + + sync_state, created = LdapSyncState.objects.get_or_create( + model="employee", + object_pk=str(employee.pk), + defaults={ + "last_sync_dir": "django", + } + ) + + if created: + created_count += 1 + else: + already_exists += 1 + + if created_count: + self.message_user( + request, + f"Создано {created_count} записей синхронизации", + level="success" + ) + if already_exists: + self.message_user( + request, + f"У {already_exists} сотрудников уже есть записи синхронизации", + level="info" + ) + if created_count == 0 and already_exists == 0: + self.message_user( + request, + "Выбраны только локальные пользователи (не управляются LDAP)", + level="warning" + ) + + create_ldap_sync_records.short_description = "Создать записи LDAP синхронизации" + + def check_ldap_sync_status(self, request, queryset): + """Проверить статус LDAP синхронизации для выбранных сотрудников.""" + ldap_managed = queryset.filter(is_ldap_managed=True) + ldap_count = ldap_managed.count() + + if ldap_count == 0: + self.message_user( + request, + "Среди выбранных нет пользователей, управляемых через LDAP", + level="warning" + ) + return + + # Проверяем наличие записей синхронизации + sync_records = LdapSyncState.objects.filter( + model="employee", + object_pk__in=[str(e.pk) for e in ldap_managed] + ) + + has_sync = sync_records.count() + missing_sync = ldap_count - has_sync + + # Проверяем полноту данных + complete = sync_records.filter( + ldap_guid__isnull=False, + ldap_dn__isnull=False + ).exclude(ldap_dn="").count() + + incomplete = has_sync - complete + + message = f"Статус LDAP синхронизации:\n" + message += f"• Управляются LDAP: {ldap_count}\n" + message += f"• Есть запись синхронизации: {has_sync}\n" + message += f"• Нет записи синхронизации: {missing_sync}\n" + message += f"• Полные данные: {complete}\n" + message += f"• Неполные данные: {incomplete}" + + self.message_user(request, message, level="info") + + check_ldap_sync_status.short_description = "Проверить статус LDAP синхронизации" @admin.register(Position)