admin: нормализовать карточку сотрудника — меньше вкладок, inline tabs

Убрана пустая «графік»-вкладка; Mattermost/AD сгруппированы с подсказкой
про ручную привязку; группы через filter_horizontal.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 13:53:03 +03:00
parent c543ef6567
commit 6b28779e00
+94 -56
View File
@@ -144,10 +144,13 @@ class WorkScheduleInline(TabularInline):
model = WorkSchedule
fk_name = 'employee'
extra = 0
tab = True
fields = ('schedule_template', 'location', 'start_date', 'end_date', 'is_primary', 'is_active')
readonly_fields = ('is_active',)
ordering = ('-start_date',)
show_change_link = True
verbose_name = _('График')
verbose_name_plural = _('График работы')
def has_add_permission(self, request, obj=None):
return False
@@ -156,10 +159,11 @@ class WorkScheduleInline(TabularInline):
class EmployeePermissionOverrideInline(TabularInline):
model = EmployeePermissionOverride
extra = 0
tab = True
fields = ('permission', 'override_type', 'valid_from', 'valid_until', 'is_active', 'comment')
autocomplete_fields = []
verbose_name = _('Доп. право на раздел портала')
verbose_name_plural = _('Доп. права на разделы портала')
verbose_name = _('Доп. право')
verbose_name_plural = _('Доп. права на разделы')
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'permission':
@@ -183,10 +187,13 @@ class AttendanceLogInline(TabularInline):
fk_name = 'employee'
extra = 0
max_num = 0
tab = True
fields = ('timestamp', 'direction', 'location', 'device_id', 'synced')
readonly_fields = ('timestamp', 'direction', 'location', 'device_id', 'synced')
ordering = ('-timestamp',)
show_change_link = True
verbose_name = _('Отметка')
verbose_name_plural = _('Посещаемость')
def has_add_permission(self, request, obj=None):
return False
@@ -199,6 +206,10 @@ class AttendanceLogInline(TabularInline):
class EmployeeAdmin(ModelAdmin):
form = EmployeeAdminForm
inlines = [EmployeePermissionOverrideInline, WorkScheduleInline, AttendanceLogInline]
filter_horizontal = ('groups', 'user_permissions')
autocomplete_fields = (
'manager', 'user_pharmacy', 'user_location', 'user_organization', 'post',
)
class Media:
css = {'all': ('users/css/employee_admin.css',)}
@@ -215,7 +226,12 @@ class EmployeeAdmin(ModelAdmin):
]
search_fields = ['username', 'email', 'last_name', 'first_name', 'middle_name']
list_select_related = ['user_pharmacy', 'department', 'post']
readonly_fields = ('photo_preview', 'effective_access_summary')
readonly_fields = (
'photo_preview', 'effective_access_summary', 'date_joined', 'last_seen',
'mattermost_sync_status', 'mattermost_last_sync',
'ad_sync_status', 'ad_sync_error', 'ad_last_sync',
'totp_enabled',
)
actions = ['reset_2fa']
actions_list = ['password_invites_action']
@@ -315,63 +331,71 @@ class EmployeeAdmin(ModelAdmin):
self.message_user(request, f'2FA сброшена у {n} сотрудник(ов).')
fieldsets = (
(_('Аутентификация'), {
'fields': (
'username', 'email', 'groups', 'is_active',
'new_password', 'new_password_confirm', 'sync_ad_password',
),
(_('Основное'), {
'classes': ('tab',),
'fields': (
('last_name', 'first_name', 'middle_name'),
('username', 'email'),
'is_active',
'groups',
('photo', 'photo_preview'),
),
}),
(_('Доступ (сводка)'), {
(_('Работа'), {
'classes': ('tab',),
'fields': (
'user_location_type',
('user_organization', 'user_pharmacy', 'user_location'),
('department', 'post', 'manager'),
('hire_date', 'probation_end_date', 'termination_date'),
'it_work_channel',
'it_responsibility_zone',
),
}),
(_('Контакты и личное'), {
'classes': ('tab',),
'fields': (
('phone', 'internal_number'),
('personal_phone', 'personal_email'),
('emergency_contact_name', 'emergency_contact_phone', 'emergency_contact_relation'),
('birth_date', 'inn_code'),
),
}),
(_('Пароль и 2FA'), {
'classes': ('tab',),
'fields': (
'new_password', 'new_password_confirm', 'sync_ad_password',
'totp_enabled',
),
'description': _(
'Оставьте пароль пустым, чтобы не менять. '
'2FA сбрасывается действием в списке сотрудников.'
),
}),
(_('Доступ к порталу'), {
'classes': ('tab',),
'fields': ('effective_access_summary',),
'description': _(
'Итог по разделам: группы + индивидуальные доп. права. '
'Группы — на вкладке «Основное»; точечные права — вкладка «Доп. права».'
),
}),
(_('Интеграции'), {
'classes': ('tab',),
'description': _(
'Итоговые разрешения на разделы портала (группы + индивидуальные '
'override). Изменить: поле «Группы» выше или блок «Доп. права на '
'разделы портала» ниже.'
'Mattermost: автосвязка только по email. Чтобы привязать уже '
'существующую учётку — укажите её User ID и логин вручную, '
'лишнюю новую учётку деактивируйте в Mattermost.'
),
}),
(_('ФИО'), {
'fields': ('last_name', 'first_name', 'middle_name'),
'classes': ('tab',),
}),
(_('Рабочая информация'), {
'fields': (
'user_location_type', 'user_organization',
'user_pharmacy', 'user_location',
'department', 'post', 'manager',
'it_work_channel', 'it_responsibility_zone',
'mattermost_sync_enabled',
('mattermost_user_id', 'mattermost_username'),
('mattermost_sync_status', 'mattermost_last_sync'),
'ad_sync_enabled',
'ad_user_dn',
('ad_sync_status', 'ad_last_sync'),
'ad_sync_error',
),
'classes': ('tab',),
}),
(_('Даты'), {
'fields': ('hire_date', 'termination_date', 'probation_end_date', 'date_joined'),
'classes': ('tab',),
}),
(_('Личные данные'), {
'fields': ('birth_date', 'inn_code', 'photo', 'photo_preview'),
'classes': ('tab',),
}),
(_('Контакты'), {
'fields': (
'phone', 'internal_number', 'personal_phone', 'personal_email',
'emergency_contact_name', 'emergency_contact_phone', 'emergency_contact_relation',
),
'classes': ('tab',),
}),
('Mattermost', {
'fields': ('mattermost_user_id', 'mattermost_username', 'mattermost_sync_status',
'mattermost_last_sync', 'mattermost_sync_enabled'),
'classes': ('tab', 'collapse'),
}),
('Active Directory', {
'fields': ('ad_user_dn', 'ad_sync_status', 'ad_sync_error', 'ad_last_sync', 'ad_sync_enabled'),
'classes': ('tab', 'collapse'),
}),
(_('Графік та відвідуваність'), {
'fields': (),
'classes': ('tab',),
'description': _('Графіки роботи та записи відвідуваності відображаються нижче.'),
}),
)
@@ -379,11 +403,14 @@ class EmployeeAdmin(ModelAdmin):
fieldsets = list(super().get_fieldsets(request, obj))
if request.user.is_superuser:
fieldsets.append((
_('Системный доступ'),
_('Система'),
{
'fields': ('is_staff', 'user_permissions'),
'classes': ('tab', 'collapse'),
'description': _('Только для IT-администраторов. is_staff — вход в Django admin.'),
'fields': (
'is_staff', 'user_permissions',
('date_joined', 'last_seen'),
),
'classes': ('tab',),
'description': _('Только для IT: вход в Django admin и сырые права.'),
},
))
return fieldsets
@@ -398,6 +425,17 @@ class EmployeeAdmin(ModelAdmin):
])
return readonly
def formfield_for_dbfield(self, db_field, request, **kwargs):
formfield = super().formfield_for_dbfield(db_field, request, **kwargs)
if db_field.name == 'mattermost_user_id' and formfield is not None:
formfield.help_text = _(
'ID пользователя в Mattermost (System Console → Users). '
'Нужен для ручной привязки существующей учётки.'
)
if db_field.name == 'mattermost_username' and formfield is not None:
formfield.help_text = _('Логин в Mattermost (без @).')
return formfield
def save_model(self, request, obj, form, change):
if not request.user.is_superuser and change:
previous = Employee.objects.filter(pk=obj.pk).prefetch_related('groups').first()