Files
ACORE/core/directory_portal.py
T
lokses ae54d93442 core: секции портала, контроль доступа и вью страниц
Section-based portal access, matrix прав по группам, аккаунтинг-портал,
управление группами пользователей, справочники, дашборд и общий каркас
навигации/стилей.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-09 18:03:20 +03:00

258 lines
10 KiB
Python

"""Портал «Системные справочники»: просмотр, CRUD записей, порядок."""
from __future__ import annotations
from collections import defaultdict
from django.contrib.auth.decorators import login_required
from django.db.models import Count, Max
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.views.decorators.http import require_POST
from pytils.translit import slugify
from core.models.directories import Directory, DirectoryType
from core.portal_access import section_required, user_can_section_action
from core.utils.htmx import render_page
def _can_create_directories(user) -> bool:
return user_can_section_action(user, 'databases', 'create')
def _can_update_directories(user) -> bool:
return user_can_section_action(user, 'databases', 'update')
def _can_edit_directories(user) -> bool:
"""Создание или обновление записей справочника."""
return _can_create_directories(user) or _can_update_directories(user)
def _items_for_type(dtype: DirectoryType):
items = list(
Directory.objects.filter(directory_type=dtype)
.select_related('parent')
.order_by('_path', 'priority', 'name')
)
for item in items:
item.indent_depth = (getattr(item, '_path', None) or '').count('.')
return items
def _parents_choices(dtype: DirectoryType, exclude_pk=None):
qs = Directory.objects.filter(directory_type=dtype, is_active=True).order_by('_path', 'name')
if exclude_pk:
qs = qs.exclude(pk=exclude_pk)
return qs
def _sort_type_alphabetically(dtype: DirectoryType) -> int:
"""Выставить priority по алфавиту среди соседей и перестроить дерево."""
items = list(Directory.objects.filter(directory_type=dtype).only('pk', 'parent_id', 'name', 'priority'))
by_parent: dict = defaultdict(list)
for item in items:
by_parent[item.parent_id].append(item)
updated = 0
for siblings in by_parent.values():
siblings.sort(key=lambda x: ((x.name or '').casefold(), x.pk))
for i, item in enumerate(siblings):
if item.priority != i:
Directory.objects.filter(pk=item.pk).update(priority=i)
updated += 1
Directory.update_tree()
return updated
@login_required
@section_required('databases')
def databases_view(request):
"""Список типов системных справочников."""
directory_types = list(
DirectoryType.objects
.annotate(records=Count('directory'), last_updated=Max('directory__updated_at'))
.order_by('name')
)
directories = Directory.objects.all()
context = {
'directory_types': directory_types,
'types_count': len(directory_types),
'records_count': directories.count(),
'active_records_count': directories.filter(is_active=True).count(),
'last_updated': max(
(dt.last_updated for dt in directory_types if dt.last_updated),
default=None,
),
'can_edit_directories': _can_edit_directories(request.user),
'can_create_directories': _can_create_directories(request.user),
'can_update_directories': _can_update_directories(request.user),
'dir_message': request.session.pop('dir_message', ''),
'dir_error': request.session.pop('dir_error', ''),
}
return render_page(request, 'databases.html', context)
@login_required
@section_required('databases')
def directory_type_detail_view(request, type_id: int):
dtype = get_object_or_404(DirectoryType, pk=type_id)
items = _items_for_type(dtype)
edit_id = request.GET.get('edit')
edit_item = None
if edit_id and str(edit_id).isdigit():
edit_item = next((i for i in items if i.pk == int(edit_id)), None)
context = {
'dtype': dtype,
'items': items,
'parents': _parents_choices(dtype, exclude_pk=edit_item.pk if edit_item else None),
'edit_item': edit_item,
'can_edit_directories': _can_edit_directories(request.user),
'can_create_directories': _can_create_directories(request.user),
'can_update_directories': _can_update_directories(request.user),
'dir_message': request.session.pop('dir_message', ''),
'dir_error': request.session.pop('dir_error', ''),
}
return render_page(request, 'directories-type.html', context)
@login_required
@section_required('databases')
@require_POST
def directory_rebuild_view(request):
if not _can_update_directories(request.user):
request.session['dir_error'] = 'Нет прав на перестройку.'
return redirect(reverse('databases'))
Directory.update_tree()
request.session['dir_message'] = 'Дерево справочников перестроено.'
type_id = (request.POST.get('type_id') or '').strip()
if type_id.isdigit():
return redirect('directory_type_detail', type_id=int(type_id))
return redirect('databases')
@login_required
@section_required('databases')
@require_POST
def directory_sort_alpha_view(request, type_id: int):
if not _can_update_directories(request.user):
request.session['dir_error'] = 'Нет прав на изменение порядка.'
return redirect('directory_type_detail', type_id=type_id)
dtype = get_object_or_404(DirectoryType, pk=type_id)
n = _sort_type_alphabetically(dtype)
request.session['dir_message'] = f'Порядок по алфавиту обновлён ({n} записей).'
return redirect('directory_type_detail', type_id=type_id)
@login_required
@section_required('databases')
@require_POST
def directory_item_save_view(request, type_id: int):
pk = (request.POST.get('pk') or '').strip()
is_new = not pk.isdigit()
if is_new and not _can_create_directories(request.user):
request.session['dir_error'] = 'Нет прав на создание.'
return redirect('directory_type_detail', type_id=type_id)
if not is_new and not _can_update_directories(request.user):
request.session['dir_error'] = 'Нет прав на редактирование.'
return redirect('directory_type_detail', type_id=type_id)
dtype = get_object_or_404(DirectoryType, pk=type_id)
name = (request.POST.get('name') or '').strip()
code = (request.POST.get('code') or '').strip()
parent_id = (request.POST.get('parent_id') or '').strip()
is_active = request.POST.get('is_active') in ('1', 'on', 'true', 'yes')
description = (request.POST.get('description') or '').strip()
if not name:
request.session['dir_error'] = 'Укажите название.'
return redirect('directory_type_detail', type_id=type_id)
if pk.isdigit():
item = get_object_or_404(Directory, pk=int(pk), directory_type=dtype)
else:
item = Directory(directory_type=dtype)
# Новый — в конец среди корневых/соседей
parent_pk = int(parent_id) if parent_id.isdigit() else None
max_prio = (
Directory.objects.filter(directory_type=dtype, parent_id=parent_pk)
.aggregate(m=Max('priority'))
.get('m')
)
item.priority = (max_prio or 0) + 1
item.name = name
item.code = code or slugify(name)
item.description = description
item.is_active = is_active
if parent_id.isdigit():
parent = Directory.objects.filter(pk=int(parent_id), directory_type=dtype).first()
if parent and parent.pk != item.pk:
item.parent = parent
else:
item.parent = None
else:
item.parent = None
try:
item.full_clean()
item.save()
except Exception as exc:
from django.core.exceptions import ValidationError
if isinstance(exc, ValidationError):
messages = []
if hasattr(exc, 'message_dict'):
for errs in exc.message_dict.values():
messages.extend(errs)
elif hasattr(exc, 'messages'):
messages.extend(exc.messages)
request.session['dir_error'] = '; '.join(str(m) for m in messages) or str(exc)
else:
request.session['dir_error'] = str(exc)
return redirect('directory_type_detail', type_id=type_id)
request.session['dir_message'] = 'Запись сохранена.'
return redirect('directory_type_detail', type_id=type_id)
@login_required
@section_required('databases')
@require_POST
def directory_item_reorder_view(request, type_id: int):
"""Сохранить порядок соседей после drag-and-drop (order=id1,id2,…)."""
if not _can_update_directories(request.user):
request.session['dir_error'] = 'Нет прав на изменение порядка.'
return redirect('directory_type_detail', type_id=type_id)
dtype = get_object_or_404(DirectoryType, pk=type_id)
raw_ids = (request.POST.get('order') or '').strip()
parent_raw = (request.POST.get('parent_id') or '').strip()
parent_id = int(parent_raw) if parent_raw.isdigit() else None
try:
ordered_ids = [int(x) for x in raw_ids.split(',') if x.strip().isdigit()]
except ValueError:
ordered_ids = []
if not ordered_ids:
request.session['dir_error'] = 'Не передан порядок записей.'
return redirect('directory_type_detail', type_id=type_id)
siblings = list(
Directory.objects.filter(
directory_type=dtype,
parent_id=parent_id,
pk__in=ordered_ids,
)
)
by_pk = {s.pk: s for s in siblings}
if len(by_pk) != len(ordered_ids) or any(pk not in by_pk for pk in ordered_ids):
request.session['dir_error'] = 'Некорректный набор записей для сортировки.'
return redirect('directory_type_detail', type_id=type_id)
for i, pk in enumerate(ordered_ids):
Directory.objects.filter(pk=pk).update(priority=i)
Directory.update_tree()
request.session['dir_message'] = 'Порядок обновлён.'
return redirect('directory_type_detail', type_id=type_id)