From 707424447994c06ffb2caf2fc464e8a292c1c935 Mon Sep 17 00:00:00 2001 From: lokses Date: Mon, 21 Sep 2026 15:33:25 +0300 Subject: [PATCH] =?UTF-8?q?admin:=20=D0=BF=D0=BE=D0=BB=D0=BD=D0=B0=D1=8F?= =?UTF-8?q?=20=D0=BC=D0=B0=D1=82=D1=80=D0=B8=D1=86=D0=B0=20=D0=B4=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D1=83=D0=BF=D0=B0=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF?= =?UTF-8?q?=20=D0=B2=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Кнопка «Матрица доступа» на списке групп (unfold actions_list) открывает сетку: строки — разделы портала, колонки — все группы, галочки просмотра. Сохранение через apply_full_access_matrix (CUD не трогает — они на карточке группы). Логика переиспользована из user_groups_portal. Смоук-тесты рендера и сохранения. Co-Authored-By: Claude Opus 4.8 --- apps/employees/admin.py | 22 ++++++++++ core/tests_user_groups.py | 26 ++++++++++++ core/user_groups_portal.py | 42 +++++++++++++++++++ templates/admin/group_access_matrix.html | 51 ++++++++++++++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 templates/admin/group_access_matrix.html diff --git a/apps/employees/admin.py b/apps/employees/admin.py index 5c38804..0c0a0ab 100644 --- a/apps/employees/admin.py +++ b/apps/employees/admin.py @@ -8,6 +8,7 @@ from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from unfold.admin import ModelAdmin, TabularInline +from unfold.decorators import action from core.models import Directory, PortalAccess from core.security import filter_portal_permissions_for_user, log_security_event @@ -491,6 +492,27 @@ class GroupAdmin(ModelAdmin): filter_horizontal = ('permissions',) search_fields = ('name',) list_display = ('name', 'active_display', 'description_display', 'portal_matrix_link') + actions_list = ['access_matrix_action'] + + @action(description=_('Матрица доступа'), url_path='access-matrix') + def access_matrix_action(self, request): + from django.core.exceptions import PermissionDenied + from django.shortcuts import redirect + from django.template.response import TemplateResponse + from core.user_groups_portal import apply_full_access_matrix, full_access_matrix + + if not request.user.has_perm('auth.change_group'): + raise PermissionDenied + if request.method == 'POST': + apply_full_access_matrix(request.POST) + self.message_user(request, _('Матрица доступа сохранена.'), level=messages.SUCCESS) + return redirect(request.path) + ctx = { + **self.admin_site.each_context(request), + 'title': _('Матрица доступа'), + **full_access_matrix(), + } + return TemplateResponse(request, 'admin/group_access_matrix.html', ctx) fieldsets = ( (None, {'fields': ('name', 'description', 'is_active')}), diff --git a/core/tests_user_groups.py b/core/tests_user_groups.py index b382437..f801f9d 100644 --- a/core/tests_user_groups.py +++ b/core/tests_user_groups.py @@ -179,3 +179,29 @@ class UserGroupsMatrixTests(TestCase): self.assertEqual(revoke.status_code, 302) person = Employee.objects.get(pk=person.pk) self.assertFalse(user_can_view_all_department_reports(person)) + + +class GroupAccessMatrixAdminTests(TestCase): + """Полная сетка доступа в админке (/admin/auth/group/access-matrix/).""" + + def setUp(self): + self.admin = make_employee(username='ug.admin', is_staff=True, is_superuser=True) + login_2fa(self.client, self.admin) + self.group = Group.objects.create(name='Тест-роль') + + def test_matrix_renders(self): + resp = self.client.get('/admin/auth/group/access-matrix/') + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, 'Тест-роль') + + def test_matrix_save_grants_view(self): + # включаем раздел 'reports' для группы через сетку + self.client.post('/admin/auth/group/access-matrix/', { + f'g{self.group.id}_reports': 'on', + }) + self.assertTrue(user_group_has_reports_access(self.group)) + + +def user_group_has_reports_access(group): + from core.user_groups_portal import _group_view_keys + return 'reports' in _group_view_keys(group) diff --git a/core/user_groups_portal.py b/core/user_groups_portal.py index 986c802..ed99930 100644 --- a/core/user_groups_portal.py +++ b/core/user_groups_portal.py @@ -183,6 +183,48 @@ def apply_group_access_matrix(group: Group, posted: dict) -> None: ) +def full_access_matrix() -> dict: + """Данные полной сетки: разделы (строки) × группы (колонки), галочки просмотра.""" + groups = list(Group.objects.all().order_by('name')) + group_views = {g.id: _group_view_keys(g) for g in groups} + rows = [] + for key in matrix_section_keys(): + rows.append({ + 'key': key, + 'label': matrix_section_label(key), + 'cells': [{'group_id': g.id, 'checked': key in group_views[g.id]} for g in groups], + }) + rows.append({ + 'key': 'view_all_reports', + 'label': 'Отчёты по всем отделам', + 'cells': [{'group_id': g.id, 'checked': group_can_view_all_reports(g)} for g in groups], + }) + return {'groups': groups, 'rows': rows} + + +def apply_full_access_matrix(posted: dict) -> None: + """Сохранить просмотр (access_*) по сетке для всех групп. + + CUD (создание/изменение/удаление) здесь не трогаем — они настраиваются + на карточке конкретной группы; сетка управляет только доступом на просмотр. + """ + perms_by_key = _portal_access_perms_by_key() + groups = list(Group.objects.all()) + keys = matrix_section_keys() + with transaction.atomic(): + for g in groups: + for key in keys: + perm = perms_by_key.get(key) + if perm is None: + continue + if posted.get(f'g{g.id}_{key}') is not None: + g.permissions.add(perm) + else: + g.permissions.remove(perm) + GroupSectionAccess.objects.filter(group=g, section_key=key).delete() + _sync_view_all_reports(g, posted.get(f'g{g.id}_view_all_reports') is not None) + + def copy_group_access(source: Group, target: Group) -> None: """Скопировать просмотр (access_*) и CUD с source на target.""" perms_by_key = _portal_access_perms_by_key() diff --git a/templates/admin/group_access_matrix.html b/templates/admin/group_access_matrix.html new file mode 100644 index 0000000..bf71c15 --- /dev/null +++ b/templates/admin/group_access_matrix.html @@ -0,0 +1,51 @@ +{% extends "admin/base_site.html" %} +{% load i18n %} + +{% block content %} +
+

+ {% trans "Строки — разделы портала, колонки — группы доступа. Галочка = доступ на просмотр раздела. Детальные права (создание/изменение/удаление) настраиваются на карточке конкретной группы." %} +

+
+ +
+ {% csrf_token %} +
+ + + + + {% for g in groups %} + + {% endfor %} + + + + {% for row in rows %} + + + {% for cell in row.cells %} + + {% endfor %} + + {% endfor %} + +
+ {% trans "Раздел" %} + + {{ g.name }} +
+ {{ row.label }} + + +
+
+ +
+ + {% trans "Назад к группам" %} +
+
+{% endblock %}