admin: единое управление ролями в «Группы доступа»

Две сущности (auth.Group + PermissionGroup-профиль) слиты в одну страницу
админки Group: имя, разделы портала (curated), прочие права, описание,
активна, ссылка на матрицу доступа. Отдельный пункт «Группы разрешений»
убран из меню; модель PermissionGroup остаётся профилем под капотом.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 15:17:57 +03:00
parent 2a1571ef73
commit 385fa07eef
2 changed files with 64 additions and 53 deletions
-5
View File
@@ -267,11 +267,6 @@ UNFOLD = {
"icon": "shield_person",
"link": reverse_lazy("admin:auth_group_changelist"),
},
{
"title": "Группы разрешений",
"icon": "admin_panel_settings",
"link": reverse_lazy("admin:employees_permissiongroup_changelist"),
},
{
"title": "Индивидуальные права",
"icon": "key",
+64 -48
View File
@@ -1,7 +1,7 @@
from django import forms
from django.contrib import admin, messages
from django.contrib.auth import password_validation
from django.contrib.auth.models import Permission
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.html import format_html
@@ -430,76 +430,92 @@ class EmployeeDefaultsAdmin(ModelAdmin):
return False
class PermissionGroupAdminForm(forms.ModelForm):
class GroupAdminForm(forms.ModelForm):
"""Единая форма роли: имя + портальный доступ + прочие права + профиль."""
description = forms.CharField(
label=_('Описание'), required=False,
widget=forms.Textarea(attrs={'rows': 2}),
)
is_active = forms.BooleanField(label=_('Активна'), required=False, initial=True)
portal_permissions = forms.ModelMultipleChoiceField(
label=_('Разделы портала'),
queryset=Permission.objects.none(),
required=False,
widget=admin.widgets.FilteredSelectMultiple(_('разделы'), is_stacked=False),
help_text=_('Базовый набор доступов к разделам для этой роли. Доп. права сотруднику — через overrides.'),
help_text=_('Доступ к разделам портала. Прочие права (кассовые и т.п.) — в списке «Права».'),
)
class Meta:
model = PermissionGroup
fields = ('group', 'description', 'is_active')
model = Group
fields = ('name', 'permissions')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['portal_permissions'].queryset = _portal_permissions_qs()
if self.instance and self.instance.pk and self.instance.group_id:
portal_pks = set(_portal_permissions_qs().values_list('pk', flat=True))
self.fields['portal_permissions'].initial = [
p for p in self.instance.group.permissions.all() if p.pk in portal_pks
]
portal_qs = _portal_permissions_qs()
self._portal_ids = set(portal_qs.values_list('pk', flat=True))
self.fields['portal_permissions'].queryset = portal_qs
# Портальные права не дублируем в общем списке — ими управляет виджет выше.
self.fields['permissions'].queryset = (
self.fields['permissions'].queryset.exclude(pk__in=self._portal_ids)
)
if self.instance and self.instance.pk:
profile = getattr(self.instance, 'profile', None)
if profile:
self.fields['description'].initial = profile.description
self.fields['is_active'].initial = profile.is_active
self.fields['portal_permissions'].initial = list(
self.instance.permissions.filter(pk__in=self._portal_ids)
)
def save_portal_permissions(self, obj):
portal_qs = list(_portal_permissions_qs())
selected = self.cleaned_data.get('portal_permissions')
if selected is None:
return
group = obj.group
group.permissions.remove(*portal_qs)
group.permissions.add(*selected)
def _save_m2m(self):
# set() для permissions снимет портальные (их нет в queryset) — вернём по виджету.
super()._save_m2m()
self.instance.permissions.add(*(self.cleaned_data.get('portal_permissions') or []))
PermissionGroup.objects.update_or_create(
group=self.instance,
defaults={
'description': self.cleaned_data.get('description', ''),
'is_active': self.cleaned_data.get('is_active', True),
},
)
@admin.register(PermissionGroup)
class PermissionGroupAdmin(ModelAdmin):
form = PermissionGroupAdminForm
list_display = ('group', 'is_active', 'description', 'portal_matrix_link')
list_filter = ('is_active',)
search_fields = ('group__name', 'description')
if admin.site.is_registered(Group):
admin.site.unregister(Group)
@admin.register(Group)
class GroupAdmin(ModelAdmin):
"""Роль = группа доступа: имя, портальные разделы, прочие права, описание/активна."""
form = GroupAdminForm
filter_horizontal = ('permissions',)
search_fields = ('name',)
list_display = ('name', 'active_display', 'description_display', 'portal_matrix_link')
fieldsets = (
(None, {
'fields': ('group', 'description', 'is_active', 'portal_permissions'),
}),
(None, {'fields': ('name', 'description', 'is_active')}),
(_('Доступ к разделам портала'), {'fields': ('portal_permissions',)}),
(_('Прочие права'), {'fields': ('permissions',), 'classes': ('collapse',)}),
)
@admin.display(description=_('Матрица в портале'))
@admin.display(description=_('Активна'), boolean=True)
def active_display(self, obj):
p = getattr(obj, 'profile', None)
return p.is_active if p else True
@admin.display(description=_('Описание'))
def description_display(self, obj):
p = getattr(obj, 'profile', None)
return (p.description if p else '') or '—'
@admin.display(description=_('Матрица доступа'))
def portal_matrix_link(self, obj):
if not obj or not obj.group_id:
if not obj or not obj.pk:
return '—'
from django.urls import reverse
from django.utils.html import format_html
url = reverse('user_group_access', kwargs={'group_id': obj.group_id})
url = reverse('user_group_access', kwargs={'group_id': obj.pk})
return format_html('<a href="{}" target="_blank">{}</a>', url, _('Открыть'))
def get_readonly_fields(self, request, obj=None):
if request.user.is_superuser:
return ()
return ('group', 'description', 'is_active', 'portal_permissions')
def has_add_permission(self, request):
return request.user.is_superuser
def has_delete_permission(self, request, obj=None):
return request.user.is_superuser
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
if request.user.is_superuser:
form.save_portal_permissions(obj)
@admin.register(EmployeePermissionOverride)
class EmployeePermissionOverrideAdmin(ModelAdmin):
list_display = (