From 385fa07eef09fa99bdc32a5620db25a86b3ec129 Mon Sep 17 00:00:00 2001 From: lokses Date: Mon, 21 Sep 2026 15:17:57 +0300 Subject: [PATCH] =?UTF-8?q?admin:=20=D0=B5=D0=B4=D0=B8=D0=BD=D0=BE=D0=B5?= =?UTF-8?q?=20=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D1=80=D0=BE=D0=BB=D1=8F=D0=BC=D0=B8=20=D0=B2=20=C2=AB=D0=93?= =?UTF-8?q?=D1=80=D1=83=D0=BF=D0=BF=D1=8B=20=D0=B4=D0=BE=D1=81=D1=82=D1=83?= =?UTF-8?q?=D0=BF=D0=B0=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Две сущности (auth.Group + PermissionGroup-профиль) слиты в одну страницу админки Group: имя, разделы портала (curated), прочие права, описание, активна, ссылка на матрицу доступа. Отдельный пункт «Группы разрешений» убран из меню; модель PermissionGroup остаётся профилем под капотом. Co-Authored-By: Claude Opus 4.8 --- acore/settings/base.py | 5 -- apps/employees/admin.py | 112 +++++++++++++++++++++++----------------- 2 files changed, 64 insertions(+), 53 deletions(-) diff --git a/acore/settings/base.py b/acore/settings/base.py index cbb7875..4ffcc57 100644 --- a/acore/settings/base.py +++ b/acore/settings/base.py @@ -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", diff --git a/apps/employees/admin.py b/apps/employees/admin.py index 227ed88..5c38804 100644 --- a/apps/employees/admin.py +++ b/apps/employees/admin.py @@ -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('{}', 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 = (