from django.contrib import admin from django.contrib.admin import SimpleListFilter 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 treenode.forms import TreeNodeForm from django.forms import TextInput, Textarea from django.db.models import Count from core.models import DirectoryType, Directory, ConnectorConfig 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',) fieldsets = ( (_('Основная информация'), { 'fields': ('directory_type', 'parent', 'code', 'name', 'description', 'is_active') }), (_('Должности и локации'), { 'fields': ('location_types', 'ad_group_dn'), 'description': 'location_types — для должностей: в каких локациях доступна. ad_group_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') 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 format_html('Все типы') types_html = ', '.join([ f'{lt.name}' for lt in location_types ]) return format_html(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(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',), 'classes': ('collapse',), }), ) 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'