AD office-only scope, mapping page, auto-rebuild of directory tree

- ad_connector: restrict provisioning to office employees
  (user_location_type='office') in sync service, signals and command;
  disable AD account on transfer out of office, re-enable on return
- ad_connector: /ad-mapping/ page (staff only) — match office employees
  to AD accounts with suggestions by email/login/full name, link/unlink/
  create actions, list of unmatched domain accounts; sidebar nav item
- core: auto-rebuild treenode Directory tree (_path/_depth) after
  import-export imports and post_migrate; "rebuild tree" button in
  DirectoryAdmin and rebuild_directory_tree management command

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 16:48:38 +03:00
parent 5008ba737f
commit bfd67241c4
14 changed files with 470 additions and 14 deletions
+18 -1
View File
@@ -1,9 +1,11 @@
from django.contrib import admin
from django.contrib import admin, messages
from django.contrib.admin import SimpleListFilter
from django.shortcuts import redirect
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from unfold.admin import ModelAdmin
from unfold.decorators import action
from treenode.forms import TreeNodeForm
from django.forms import TextInput, Textarea
from django.db.models import Count
@@ -132,6 +134,7 @@ class DirectoryAdmin(ModelAdmin, ImportExportModelAdmin):
list_display_links = ['name_with_hierarchy']
list_filter = [DirectoryTypeFilter, ActiveFilter]
ordering = ('_path',)
actions_list = ['rebuild_tree_action']
fieldsets = (
(_('Основная информация'), {
@@ -156,6 +159,20 @@ class DirectoryAdmin(ModelAdmin, ImportExportModelAdmin):
tn_children_count=Count('children', distinct=True)
).order_by('_path')
@action(description=_('🔄 Перестроить дерево'), url_path='rebuild-tree',
permissions=['change'])
def rebuild_tree_action(self, request):
"""Ручная перестройка дерева (пересчёт _path/_depth из parent)"""
Directory.update_tree()
messages.success(request, _('Дерево справочников перестроено'))
return redirect(reverse('admin:core_directory_changelist'))
def process_result(self, result, request):
"""Импорт пишет записи в обход treenode — перестраиваем дерево после него"""
if not result.has_errors():
Directory.update_tree()
return super().process_result(result, request)
def name_with_hierarchy(self, obj):
"""Имя с отступом по уровню вложенности. Глубина из _path (точки)."""
path = getattr(obj, '_path', '') or ''