Initial commit — ACORE pharmacy network management system
Django project: employees, locations, visit_manager, it_manager, mattermost_connector and stub connectors (ad, mail, portal).
This commit is contained in:
+233
@@ -0,0 +1,233 @@
|
||||
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(
|
||||
'<a href="{}" title="Перейти к записям">{}</a>',
|
||||
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(
|
||||
'<span style="display:inline-block;margin-left:{}px;white-space:nowrap;">'
|
||||
'{}<a href="{}">{} {}</a></span>',
|
||||
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('<span style="color: #999;">Все типы</span>')
|
||||
|
||||
types_html = ', '.join([
|
||||
f'<span style="background-color: #e3f2fd; padding: 2px 6px; border-radius: 3px; font-size: 11px;">{lt.name}</span>'
|
||||
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(
|
||||
'<span style="color: #0066cc; font-weight: bold;">{}</span>',
|
||||
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(
|
||||
'<span style="color:{}; font-weight:bold;">{}</span>',
|
||||
color, obj.get_connector_type_display()
|
||||
)
|
||||
connector_type_display.short_description = _('Коннектор')
|
||||
connector_type_display.admin_order_field = 'connector_type'
|
||||
Reference in New Issue
Block a user