"""Портал: группы пользователей и сводная таблица доступа.""" from __future__ import annotations from django.contrib.auth.decorators import login_required from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType from django.db import transaction from django.shortcuts import get_object_or_404, redirect from django.views.decorators.http import require_POST from apps.employees.models import Employee, EmployeePermissionOverride, GroupSectionAccess from core.models import PortalAccess from core.portal_access import ( matrix_section_keys, matrix_section_label, section_required, ) from core.utils.htmx import render_page def _portal_access_perms_by_key() -> dict[str, Permission]: ct = ContentType.objects.get_for_model(PortalAccess, for_concrete_model=False) perms = Permission.objects.filter(content_type=ct, codename__startswith='access_') return {p.codename[len('access_'):]: p for p in perms} def _view_all_reports_perm() -> Permission | None: return Permission.objects.filter( content_type__app_label='reports', codename='view_all_departments', ).first() def group_can_view_all_reports(group: Group) -> bool: return group.permissions.filter( content_type__app_label='reports', codename='view_all_departments', ).exists() def _sync_view_all_reports(group: Group, enabled: bool) -> None: perm = _view_all_reports_perm() if perm is None: return if enabled: group.permissions.add(perm) else: group.permissions.remove(perm) def employee_has_individual_view_all_reports(employee) -> bool: perm = _view_all_reports_perm() if perm is None: return False if employee.user_permissions.filter(pk=perm.pk).exists(): return True return EmployeePermissionOverride.objects.filter( employee=employee, permission=perm, override_type='grant', is_active=True, ).exists() def grant_individual_view_all_reports(employee) -> None: perm = _view_all_reports_perm() if perm is None: raise ValueError('Право «Видеть отчёты всех отделов» ещё не создано. Примените миграции.') EmployeePermissionOverride.objects.filter( employee=employee, permission=perm, override_type='revoke', ).update(is_active=False) row = EmployeePermissionOverride.objects.filter( employee=employee, permission=perm, override_type='grant', ).first() if row: row.is_active = True if not row.comment: row.comment = 'Видеть отчёты всех отделов' row.save(update_fields=['is_active', 'comment', 'updated_at']) else: EmployeePermissionOverride.objects.create( employee=employee, permission=perm, override_type='grant', comment='Видеть отчёты всех отделов', ) def revoke_individual_view_all_reports(employee) -> None: perm = _view_all_reports_perm() if perm is None: return EmployeePermissionOverride.objects.filter( employee=employee, permission=perm, override_type='grant', ).update(is_active=False) employee.user_permissions.remove(perm) def individual_view_all_report_employees(): perm = _view_all_reports_perm() if perm is None: return Employee.objects.none() from django.db.models import Q return ( Employee.objects.filter(is_active=True) .filter( Q(user_permissions=perm) | Q( permission_overrides__permission=perm, permission_overrides__override_type='grant', permission_overrides__is_active=True, ) ) .distinct() .order_by('last_name', 'first_name', 'username') ) def _group_view_keys(group: Group) -> set[str]: ct = ContentType.objects.get_for_model(PortalAccess, for_concrete_model=False) codenames = group.permissions.filter( content_type=ct, codename__startswith='access_' ).values_list('codename', flat=True) return {c[len('access_'):] for c in codenames} def _matrix_rows(group: Group) -> list[dict]: view_keys = _group_view_keys(group) cud = { row.section_key: row for row in GroupSectionAccess.objects.filter(group=group) } rows = [] for key in matrix_section_keys(): access = cud.get(key) can_view = key in view_keys rows.append({ 'key': key, 'label': matrix_section_label(key), 'can_view': can_view, 'can_create': bool(access.can_create) if access and can_view else (can_view), 'can_update': bool(access.can_update) if access and can_view else (can_view), 'can_delete': bool(access.can_delete) if access and can_view else (can_view), }) return rows def apply_group_access_matrix(group: Group, posted: dict) -> None: """Синхронизировать access_* и GroupSectionAccess из POST-данных матрицы.""" perms_by_key = _portal_access_perms_by_key() keys = matrix_section_keys() with transaction.atomic(): for key in keys: perm = perms_by_key.get(key) if perm is None: continue view_on = posted.get(f'view_{key}') in ('1', 'on', 'yes', 'view') if view_on: group.permissions.add(perm) GroupSectionAccess.objects.update_or_create( group=group, section_key=key, defaults={ 'can_create': posted.get(f'create_{key}') in ('1', 'on', 'yes'), 'can_update': posted.get(f'update_{key}') in ('1', 'on', 'yes'), 'can_delete': posted.get(f'delete_{key}') in ('1', 'on', 'yes'), }, ) else: group.permissions.remove(perm) GroupSectionAccess.objects.filter(group=group, section_key=key).delete() _sync_view_all_reports( group, posted.get('view_all_reports') in ('1', 'on', 'yes'), ) 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() keys = matrix_section_keys() source_views = _group_view_keys(source) source_cud = { r.section_key: r for r in GroupSectionAccess.objects.filter(group=source) } with transaction.atomic(): for key in keys: perm = perms_by_key.get(key) if perm is None: continue if key in source_views: target.permissions.add(perm) src = source_cud.get(key) GroupSectionAccess.objects.update_or_create( group=target, section_key=key, defaults={ 'can_create': True if src is None else src.can_create, 'can_update': True if src is None else src.can_update, 'can_delete': True if src is None else src.can_delete, }, ) else: target.permissions.remove(perm) GroupSectionAccess.objects.filter(group=target, section_key=key).delete() _sync_view_all_reports(target, group_can_view_all_reports(source)) @login_required @section_required('user_groups') def user_groups_list_view(request): groups = list( Group.objects.select_related('profile').order_by('name') ) for g in groups: profile = getattr(g, 'profile', None) g.profile_description = getattr(profile, 'description', '') or '' g.profile_active = getattr(profile, 'is_active', True) if profile else True g.access_count = g.permissions.filter(codename__startswith='access_').count() context = { 'groups': groups, 'individual_report_viewers': list(individual_view_all_report_employees()), 'employee_choices': list( Employee.objects.filter(is_active=True) .order_by('last_name', 'first_name', 'username') .only('id', 'last_name', 'first_name', 'username') ), 'ug_message': request.session.pop('ug_message', ''), 'ug_error': request.session.pop('ug_error', ''), } return render_page(request, 'user-groups.html', context) @login_required @section_required('user_groups') @require_POST def user_group_individual_reports_grant_view(request): raw_id = (request.POST.get('employee_id') or '').strip() if not raw_id.isdigit(): request.session['ug_error'] = 'Выберите сотрудника.' return redirect('user_groups') employee = get_object_or_404(Employee, pk=int(raw_id), is_active=True) try: grant_individual_view_all_reports(employee) except ValueError as exc: request.session['ug_error'] = str(exc) return redirect('user_groups') request.session['ug_message'] = ( f'«{employee.get_full_name() or employee.username}» может видеть отчёты всех отделов.' ) return redirect('user_groups') @login_required @section_required('user_groups') @require_POST def user_group_individual_reports_revoke_view(request, employee_id: int): employee = get_object_or_404(Employee, pk=employee_id) revoke_individual_view_all_reports(employee) request.session['ug_message'] = ( f'Индивидуальный доступ к отчётам всех отделов снят у «{employee.get_full_name() or employee.username}».' ) return redirect('user_groups') @login_required @section_required('user_groups') def user_group_access_view(request, group_id: int): group = get_object_or_404(Group.objects.select_related('profile'), pk=group_id) other_groups = Group.objects.exclude(pk=group.pk).order_by('name') context = { 'group': group, 'matrix_rows': _matrix_rows(group), 'view_all_reports': group_can_view_all_reports(group), 'other_groups': other_groups, 'ug_message': request.session.pop('ug_message', ''), 'ug_error': request.session.pop('ug_error', ''), } return render_page(request, 'user-group-access.html', context) @login_required @section_required('user_groups') @require_POST def user_group_access_save_view(request, group_id: int): group = get_object_or_404(Group, pk=group_id) apply_group_access_matrix(group, request.POST) request.session['ug_message'] = 'Доступ сохранён.' return redirect('user_group_access', group_id=group.pk) @login_required @section_required('user_groups') @require_POST def user_group_access_copy_view(request, group_id: int): source = get_object_or_404(Group, pk=group_id) target_id = (request.POST.get('target_group_id') or '').strip() if not target_id.isdigit(): request.session['ug_error'] = 'Выберите группу-получателя.' return redirect('user_group_access', group_id=source.pk) target = get_object_or_404(Group, pk=int(target_id)) if target.pk == source.pk: request.session['ug_error'] = 'Нельзя копировать группу в саму себя.' return redirect('user_group_access', group_id=source.pk) copy_group_access(source, target) request.session['ug_message'] = f'Доступ скопирован в «{target.name}».' return redirect('user_group_access', group_id=target.pk)