fa323d657c
Делает 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>
165 lines
6.7 KiB
Python
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
|