ae54d93442
Section-based portal access, matrix прав по группам, аккаунтинг-портал, управление группами пользователей, справочники, дашборд и общий каркас навигации/стилей. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
258 lines
10 KiB
Python
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)
|