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.safestring import mark_safe 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 from core.models import DirectoryType, Directory, ConnectorConfig, AccessSystem from import_export.admin import ImportExportModelAdmin from unfold.contrib.import_export.forms import ExportForm, ImportForm # Register your models here. class DirectoryTypeFilter(SimpleListFilter): title = _('Тип справочника') parameter_name = 'directory_type' def lookups(self, request, model_admin): return DirectoryType.objects.filter(is_active=True).values_list('id', 'name') def queryset(self, request, queryset): if self.value(): return queryset.filter(directory_type_id=self.value()) return queryset class ActiveFilter(SimpleListFilter): title = _('Статус') parameter_name = 'is_active' def lookups(self, request, model_admin): return ( ('1', _('Активные')), ('0', _('Неактивные')), ) def queryset(self, request, queryset): if self.value() == '1': return queryset.filter(is_active=True) elif self.value() == '0': return queryset.filter(is_active=False) return queryset class HasChildrenFilter(SimpleListFilter): title = _('Наличие потомков') parameter_name = 'has_children' def lookups(self, request, model_admin): return ( ('yes', _('Есть потомки')), ('no', _('Нет потомков')), ) def queryset(self, request, queryset): if self.value() == 'yes': # Фильтруем по наличию детей return queryset.filter(tn_children_count__gt=0) if hasattr(queryset.model, 'tn_children_count') else queryset elif self.value() == 'no': return queryset.filter(tn_children_count=0) if hasattr(queryset.model, 'tn_children_count') else queryset return queryset @admin.register(DirectoryType) class DirectoryTypeAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm list_display = [ 'name', 'code', 'hierarchy_type', 'is_active', 'directories_count', 'created_at' ] def directories_count(self, obj): """Количество записей в справочнике""" count = obj.directory_set.count() if count > 0: url = reverse('admin:core_directory_changelist') + f'?directory_type={obj.id}' return format_html( '{}', url, count ) return count directories_count.short_description = _('Записей') directories_count.admin_order_field = 'directory_count' class DirectoryForm(TreeNodeForm): """Кастомная форма с дополнительными проверками""" class Meta: model = Directory fields = '__all__' widgets = { 'code': TextInput(attrs={'size': '30'}), 'name': TextInput(attrs={'size': '50'}), 'description': Textarea(attrs={'rows': 3, 'cols': 60}), 'extra_data': Textarea(attrs={'rows': 4, 'cols': 60, 'class': 'vLargeTextField'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Фильтруем допустимых родителей по правилам иерархии if 'parent' in self.fields and self.instance.directory_type_id: self.fields['parent'].queryset = self._get_allowed_parents() def _get_allowed_parents(self): """Получить разрешенных родителей для текущего типа""" current_type = self.instance.directory_type # Для корневых элементов можем выбирать из всех типов if not self.instance.parent: return Directory.objects.filter(directory_type=current_type) # Иначе фильтруем по правилам иерархии allowed_parents = Directory.objects.none() for dir_type in DirectoryType.objects.filter(is_active=True): if dir_type.can_have_child_type(current_type): allowed_parents = allowed_parents | Directory.objects.filter(directory_type=dir_type) return allowed_parents.exclude(pk=self.instance.pk) @admin.register(Directory) class DirectoryAdmin(ModelAdmin, ImportExportModelAdmin): """Справочники с иерархией (отступы по уровню вложенности).""" import_form_class = ImportForm export_form_class = ExportForm form = DirectoryForm list_display = ['name_with_hierarchy', 'directory_type', 'code', 'is_active', 'updated_at'] list_display_links = ['name_with_hierarchy'] list_filter = [DirectoryTypeFilter, ActiveFilter] ordering = ('_path',) actions_list = ['rebuild_tree_action'] fieldsets = ( (_('Основная информация'), { 'fields': ('directory_type', 'parent', 'code', 'name', 'description', 'is_active') }), (_('Должности и локации'), { 'fields': ('location_types', 'ad_group_dn', 'ad_ou_dn'), 'description': 'location_types — для должностей: в каких локациях доступна. ' 'ad_group_dn, ad_ou_dn — для отделов.', 'classes': ('collapse',) }), (_('Дополнительные данные'), { 'fields': ('extra_data',), 'classes': ('collapse',) }), ) filter_horizontal = ['location_types'] def get_queryset(self, request): return super().get_queryset(request).select_related( 'parent', 'directory_type' ).annotate( 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 '' depth = path.count('.') # root=0, дочерний=1, внук=2... indent_px = depth * 24 url = reverse('admin:core_directory_change', args=[obj.pk]) has_children = getattr(obj, 'tn_children_count', 0) > 0 icon = '📁' if has_children else '📄' branch = '└ ' if depth else '' return format_html( '' '{}{} {}', indent_px, branch, url, icon, obj.name ) name_with_hierarchy.short_description = _('Название') name_with_hierarchy.admin_order_field = '_path' def location_types_display(self, obj): """Отображение типов локаций для должностей""" if obj.directory_type.code != 'post': return '-' location_types = obj.location_types.all() if not location_types.exists(): return mark_safe('Все типы') types_html = ', '.join([ f'{lt.name}' for lt in location_types ]) return mark_safe(types_html) location_types_display.short_description = _('Типы локаций') def children_count_display(self, obj): """Отображение количества потомков""" count = obj.get_children_count() if count > 0: return format_html( '{}', count ) return count children_count_display.short_description = 'Потомков' @admin.register(AccessSystem) class AccessSystemAdmin(ModelAdmin): """Отдельный раздел для списка систем/баз доступа (1С, почта, CRM…), которые выбираются чекбоксами в заявке на найм.""" list_display = ['name', 'code', 'is_active', 'updated_at'] list_filter = ['is_active'] search_fields = ['name', 'code'] ordering = ['name'] fields = ['name', 'code', 'description', 'is_active'] def get_queryset(self, request): return super().get_queryset(request).filter(directory_type__code='access_system') def save_model(self, request, obj, form, change): if not obj.directory_type_id: obj.directory_type = DirectoryType.objects.get(code='access_system') super().save_model(request, obj, form, change) @admin.register(ConnectorConfig) class ConnectorConfigAdmin(ModelAdmin): list_display = ('connector_type_display', 'is_enabled', 'host', 'port', 'use_ssl', 'updated_at') list_filter = ('is_enabled', 'use_ssl', 'connector_type') fieldsets = ( (_('Основное'), { 'fields': ('connector_type', 'is_enabled', 'notes'), }), (_('Подключение'), { 'fields': ('host', 'port', 'use_ssl'), }), (_('Аутентификация'), { 'fields': ('username', 'password', 'token'), 'description': _('Укажите токен или логин/пароль — в зависимости от типа коннектора'), }), (_('Дополнительные параметры'), { 'fields': ('extra_config',), }), ) def connector_type_display(self, obj): color = '#16a34a' if obj.is_enabled else '#dc2626' return format_html( '{}', color, obj.get_connector_type_display() ) connector_type_display.short_description = _('Коннектор') connector_type_display.admin_order_field = 'connector_type'