Files
ACORE/apps/reports/documents.py
T
lokses fa323d657c locations: недостающие модули области доступа + гейт reports
Делает HEAD целостным: коммит 5366799 уже импортировал эти файлы, но они
не были добавлены в git (свежий checkout падал с ImportError, а migrate не
создавал таблицу GroupLocationAccess).

- core/location_access.py: область данных (accessible_pharmacies/locations,
  scoped_queryset/employees, has_all_locations) — fail-closed
- apps/locations/models/access.py + миграция 0014: GroupLocationAccess,
  право locations.view_all_locations
- core/tests_location_access.py: 15 сценариев scoping
- apps/reports/*: снятие лишнего гейта has_all_locations с IT-отчёта отдела
  (руководитель не обязан иметь доступ ко всей сети) + тест
- apps/rro/tests: чужая аптека теперь 404 (единая область доступа), а не
  «200 с урезанными панелями» — под новую модель

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-21 22:59:47 +03:00

165 lines
6.7 KiB
Python

"""Документы отдела на /reports/: доступ, путь в Nextcloud, создание файлов."""
from __future__ import annotations
from django.utils import timezone
from django.utils.text import slugify
from apps.nextcloud_connector.access import safe_filename
from apps.nextcloud_connector.services.sync_service import safe_component
from apps.reports.it_monthly.builder import (
user_can_manage_it_monthly_report,
user_can_view_all_department_reports,
)
from apps.reports.models import ReportDocument
from apps.reports.registry import visible_reports
from core.portal_access import user_can_access
REPORTS_DAV_ROOT = '06_Отчёты'
ALLOWED_UPLOAD_EXTS = frozenset({'docx', 'xlsx'})
def user_can_open_reports_hub(user) -> bool:
if not getattr(user, 'is_authenticated', False):
return False
if user_can_access(user, 'reports'):
return True
if visible_reports(user):
return True
if user_can_manage_it_monthly_report(user):
return True
# Опубликованные Word-документы отдела — доступ к хабу без живого IT-отчёта.
dept_id = getattr(user, 'department_id', None)
if dept_id and ReportDocument.objects.filter(
department_id=dept_id,
status=ReportDocument.STATUS_PUBLISHED,
).exists():
return True
return False
def user_can_view_report_document(user, doc: ReportDocument) -> bool:
if not user_can_open_reports_hub(user):
return False
# Черновики — только руководителю своего отдела.
if user_can_manage_it_monthly_report(user) and getattr(user, 'department_id', None) == doc.department_id:
return True
# view_all и обычные сотрудники — только опубликованные.
if not doc.is_published:
return False
if user_can_view_all_department_reports(user):
return True
return getattr(user, 'department_id', None) == doc.department_id
def user_can_edit_report_document(user, doc: ReportDocument) -> bool:
if not user_can_open_reports_hub(user):
return False
if user_can_manage_it_monthly_report(user) and getattr(user, 'department_id', None) == doc.department_id:
return True
# view_all может править/снимать только уже опубликованные (черновики чужих отделов не видит).
if user_can_view_all_department_reports(user):
return doc.is_published
return False
def user_can_create_report_document(user) -> bool:
if not user_can_open_reports_hub(user):
return False
if not getattr(user, 'department_id', None):
return False
return user_can_manage_it_monthly_report(user) or user_can_view_all_department_reports(user)
def user_can_manage_report_documents(user) -> bool:
"""Руководитель / view_all — создавать, грузить, публиковать."""
return user_can_create_report_document(user)
def visible_report_documents(user):
if not user_can_open_reports_hub(user):
return ReportDocument.objects.none()
qs = ReportDocument.objects.select_related('department', 'created_by', 'published_by')
dept_id = getattr(user, 'department_id', None)
manage_own = user_can_manage_it_monthly_report(user) and bool(dept_id)
if user_can_view_all_department_reports(user):
# Все отделы — только опубликованные; свои черновики — если ещё и руководитель.
if manage_own:
from django.db.models import Q
return qs.filter(
Q(status=ReportDocument.STATUS_PUBLISHED) | Q(department_id=dept_id)
).distinct()
return qs.filter(status=ReportDocument.STATUS_PUBLISHED)
if not dept_id:
return ReportDocument.objects.none()
qs = qs.filter(department_id=dept_id)
if manage_own:
return qs
return qs.filter(status=ReportDocument.STATUS_PUBLISHED)
def report_document_relpath(doc: ReportDocument) -> str:
folder = safe_component(doc.department.code or doc.department.name)
return f'{REPORTS_DAV_ROOT}/{folder}/{doc.filename}'
def unique_filename(department_id: int, title: str, ext: str) -> str:
base = slugify(title, allow_unicode=True) or 'document'
candidate = safe_filename(f'{base}.{ext}') or f'document.{ext}'
if not ReportDocument.objects.filter(department_id=department_id, filename=candidate).exists():
return candidate
n = 2
while True:
name = safe_filename(f'{base}-{n}.{ext}') or f'document-{n}.{ext}'
if not ReportDocument.objects.filter(department_id=department_id, filename=name).exists():
return name
n += 1
def unique_upload_filename(department_id: int, original_name: str) -> str | None:
"""Сохранить исходное имя файла, при коллизии — суффикс -2, -3…"""
name = safe_filename(original_name)
if not name:
return None
if '.' in name:
stem, ext = name.rsplit('.', 1)
ext = ext.lower()
else:
stem, ext = name, 'docx'
if ext not in ALLOWED_UPLOAD_EXTS:
return None
candidate = safe_filename(f'{stem}.{ext}') or f'document.{ext}'
if not ReportDocument.objects.filter(department_id=department_id, filename=candidate).exists():
return candidate
n = 2
while True:
cand = safe_filename(f'{stem}-{n}.{ext}') or f'document-{n}.{ext}'
if not ReportDocument.objects.filter(department_id=department_id, filename=cand).exists():
return cand
n += 1
def publish_report_document(user, doc: ReportDocument) -> ReportDocument:
if not user_can_edit_report_document(user, doc):
raise PermissionError('Недостаточно прав для публикации.')
doc.status = ReportDocument.STATUS_PUBLISHED
doc.published_at = timezone.now()
doc.published_by = user
doc.save(update_fields=['status', 'published_at', 'published_by', 'updated_at'])
from apps.reports.publication import sync_month_publication_from_snapshot
sync_month_publication_from_snapshot(user, doc)
return doc
def unpublish_report_document(user, doc: ReportDocument) -> ReportDocument:
if not user_can_edit_report_document(user, doc):
raise PermissionError('Недостаточно прав для снятия с публикации.')
doc.status = ReportDocument.STATUS_DRAFT
doc.published_at = None
doc.published_by = None
doc.save(update_fields=['status', 'published_at', 'published_by', 'updated_at'])
from apps.reports.publication import sync_month_publication_from_snapshot
sync_month_publication_from_snapshot(user, doc)
return doc