"""Views раздела «Отчёты».""" from __future__ import annotations import json import re from django.contrib.auth.decorators import login_required from django.http import ( Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect, ) from django.shortcuts import get_object_or_404 from django.views.decorators.clickjacking import xframe_options_sameorigin from django.views.decorators.http import require_POST from apps.reports.access import user_can_edit_template, user_can_manage_templates from apps.reports.engine import TemplateRenderError, render_report from apps.reports.documents import ( publish_report_document, report_document_relpath, unique_filename, unique_upload_filename, unpublish_report_document, user_can_create_report_document, user_can_edit_report_document, user_can_open_reports_hub, user_can_view_report_document, visible_report_documents, ) from apps.reports.it_monthly import ( available_report_months, build_monthly_report, parse_month_id, user_can_manage_it_monthly_report, user_can_view_it_monthly_report, ) from apps.reports.models import ReportDocument, ReportSignature, ReportTemplate from apps.reports.publication import ( available_months_for_user, publication_for_user_month, published_snapshot_for_month, report_list_rows, unpublish_month_and_snapshots, user_can_access_it_monthly_month, ) from apps.reports.registry import visible_reports from apps.reports.signing import ( build_unsigned_pdf, complete_signature, parse_cert_meta, sha256_hex, signed_file_relpath, ) from core.portal_access import deny_section_access from core.utils.htmx import render_page _SLUG_RE = re.compile(r'^[a-z0-9]+(?:-[a-z0-9]+)*$') def _can_open_reports_hub(user) -> bool: return user_can_open_reports_hub(user) def _default_it_template() -> ReportTemplate | None: """Шаблон по умолчанию для IT (без выбора в UI отчёта).""" return ReportTemplate.resolve(ReportTemplate.REPORT_IT_MONTHLY) def _onlyoffice_ready() -> bool: from apps.nextcloud_connector.onlyoffice import onlyoffice_is_ready return onlyoffice_is_ready() def _resolve_it_month(request, month_raw: str | None) -> tuple[int, int] | HttpResponse: """Выбрать месяц с учётом публикации; 403 если нет доступа к периоду.""" months = available_months_for_user(request.user) year, month = parse_month_id(month_raw) mid = f'{year:04d}-{month:02d}' if user_can_manage_it_monthly_report(request.user): return year, month if not months: return HttpResponseForbidden() if mid not in months: year, month = parse_month_id(months[0]) if not user_can_access_it_monthly_month(request.user, year, month): return HttpResponseForbidden() return year, month def _it_monthly_page_context(request, year: int, month: int) -> dict: from apps.reports.it_monthly.builder import user_can_view_all_department_reports from apps.reports.models import ReportDocument, ReportPublication from apps.reports.publication import is_month_published can_manage = user_can_manage_it_monthly_report(request.user) can_all = user_can_view_all_department_reports(request.user) dept_id = getattr(request.user, 'department_id', None) mid = f'{year:04d}-{month:02d}' pub = publication_for_user_month(request.user, year, month) if dept_id else None if pub and pub.is_published: report_is_published = True report_published_at = pub.published_at elif can_all: report_is_published = is_month_published( ReportPublication.REPORT_IT_MONTHLY, year, month, ) report_published_at = None else: report_is_published = False report_published_at = None published_doc = None if dept_id: published_doc = published_snapshot_for_month( department_id=dept_id, year=year, month=month, ) if published_doc is None and can_all: published_doc = ( ReportDocument.objects .filter( kind=ReportDocument.KIND_SNAPSHOT, source_month=mid, status=ReportDocument.STATUS_PUBLISHED, ) .order_by('-published_at', '-id') .first() ) show_live = can_manage kep_signature = published_doc.latest_signature if published_doc else None context = { 'can_manage_templates': user_can_manage_templates(request.user), 'can_publish_report': can_manage, 'can_prepare_word': can_manage and user_can_create_report_document(request.user), 'report_is_published': report_is_published, 'report_published_at': report_published_at, 'published_document': published_doc, 'kep_signature': kep_signature, 'show_live_report': show_live, 'onlyoffice_ready': _onlyoffice_ready(), } if show_live: report = build_monthly_report(year, month) report['available_months'] = available_months_for_user(request.user) or [report['id']] context['report'] = report else: title = published_doc.title if published_doc else mid context['report'] = { 'id': mid, 'title': title, 'period_from': '', 'period_to': '', 'available_months': available_months_for_user(request.user) or [mid], 'total_tasks': 0, 'people_active': 0, 'objects_count': 0, 'pharmacies': [], 'other_points': [], 'summary': [], 'employees': [], 'conclusions': [], } return context @login_required def reports_hub_view(request): if not _can_open_reports_hub(request.user): return deny_section_access(request) specs = visible_reports(request.user) docs = list( visible_report_documents(request.user) .prefetch_related('signatures__signer') ) for doc in docs: doc.kep_signature = doc.latest_signature return render_page(request, 'reports.html', { 'report_specs': specs, 'report_rows': report_list_rows(request.user), 'report_documents': docs, 'can_create_report_document': user_can_create_report_document(request.user), 'onlyoffice_ready': _onlyoffice_ready(), 'reports_message': request.session.pop('reports_message', ''), 'reports_error': request.session.pop('reports_error', ''), 'can_manage_templates': user_can_manage_templates(request.user), }) @login_required def it_monthly_view(request): if not user_can_view_it_monthly_report(request.user): return HttpResponseForbidden() resolved = _resolve_it_month( request, request.GET.get('month') or request.GET.get('report_month'), ) if isinstance(resolved, HttpResponse): return resolved year, month = resolved return render_page(request, 'reports-it-monthly.html', { **_it_monthly_page_context(request, year, month), 'reports_message': request.session.pop('reports_message', ''), 'reports_error': request.session.pop('reports_error', ''), }) def _export_with_template(request, month_id, fmt: str): # Живой экспорт — только руководителю; остальные скачивают опубликованный Word. if not user_can_manage_it_monthly_report(request.user): return HttpResponseForbidden() if not user_can_view_it_monthly_report(request.user): return HttpResponseForbidden() year, month = parse_month_id(month_id) if not user_can_access_it_monthly_month(request.user, year, month): return HttpResponseForbidden() report = build_monthly_report(year, month) tpl = _default_it_template() try: payload = render_report(report, tpl, fmt) except TemplateRenderError as exc: return HttpResponseBadRequest(f'Ошибка шаблона: {exc}') if fmt == 'md': response = HttpResponse(payload, content_type='text/markdown; charset=utf-8') response['Content-Disposition'] = f'attachment; filename="it-report-{report["id"]}.md"' return response if fmt == 'docx': response = HttpResponse( payload, content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document', ) response['Content-Disposition'] = f'attachment; filename="IT-report-{report["id"]}.docx"' return response if fmt == 'pdf': response = HttpResponse(payload, content_type='application/pdf') response['Content-Disposition'] = f'attachment; filename="IT-report-{report["id"]}.pdf"' return response return HttpResponseBadRequest('Неизвестный формат') @login_required def it_monthly_md_view(request, month_id): return _export_with_template(request, month_id, 'md') @login_required def it_monthly_docx_view(request, month_id): return _export_with_template(request, month_id, 'docx') @login_required def it_monthly_pdf_view(request, month_id): return _export_with_template(request, month_id, 'pdf') @login_required @xframe_options_sameorigin def it_monthly_preview_view(request, month_id): if not user_can_manage_it_monthly_report(request.user): return HttpResponseForbidden() year, month = parse_month_id(month_id) if not user_can_access_it_monthly_month(request.user, year, month): return HttpResponseForbidden() report = build_monthly_report(year, month) tpl = _default_it_template() try: html = render_report(report, tpl, 'html') except TemplateRenderError as exc: return HttpResponse( f'

Ошибка шаблона: {exc}

', content_type='text/html; charset=utf-8', status=400, ) return HttpResponse(html, content_type='text/html; charset=utf-8') def _prepare_it_monthly_word(request, year: int, month: int) -> HttpResponse: """Собрать Word-черновик снимка и открыть OnlyOffice (без публикации месяца).""" if not user_can_create_report_document(request.user): return HttpResponseForbidden() if not user_can_manage_it_monthly_report(request.user): return HttpResponseForbidden() report = build_monthly_report(year, month) tpl = _default_it_template() try: payload = render_report(report, tpl, 'docx') except TemplateRenderError as exc: request.session['reports_error'] = f'Не удалось собрать Word: {exc}' return HttpResponseRedirect(f'/reports/it/monthly/?month={year:04d}-{month:02d}') title = f'IT-отчёт {report["title"]}' filename = unique_filename(request.user.department_id, title, 'docx') doc = ReportDocument.objects.create( department_id=request.user.department_id, title=title, filename=filename, kind=ReportDocument.KIND_SNAPSHOT, status=ReportDocument.STATUS_DRAFT, source_month=report['id'], created_by=request.user, ) err = _store_report_file( report_document_relpath(doc), payload, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', ) if err: doc.delete() request.session['reports_error'] = err return HttpResponseRedirect(f'/reports/it/monthly/?month={year:04d}-{month:02d}') request.session['reports_message'] = ( f'Word «{title}» создан как черновик. Поправьте файл и опубликуйте ' f'его в «Документах отдела» — тогда отчёт увидит отдел.' ) if _onlyoffice_ready(): return HttpResponseRedirect(f'/documents/edit/?report_doc={doc.pk}') return HttpResponseRedirect('/reports/') @login_required @require_POST def it_monthly_publish_view(request, month_id): """«Сформировать Word» — черновик документа, без флага публикации месяца.""" if not user_can_manage_it_monthly_report(request.user): return HttpResponseForbidden() year, month = parse_month_id(month_id) return _prepare_it_monthly_word(request, year, month) @login_required @require_POST def it_monthly_unpublish_view(request, month_id): if not user_can_manage_it_monthly_report(request.user): return HttpResponseForbidden() year, month = parse_month_id(month_id) try: unpublish_month_and_snapshots(request.user, year, month) except (ValueError, PermissionError) as exc: return HttpResponseForbidden(str(exc)) request.session['reports_message'] = 'Публикация месяца и Word-снимков снята.' return HttpResponseRedirect(f'/reports/it/monthly/?month={year:04d}-{month:02d}') def redirect_legacy_it_report(request, month_id, fmt: str): if fmt == 'preview': return HttpResponseRedirect(f'/reports/it/monthly/{month_id}/preview/') return HttpResponseRedirect(f'/reports/it/monthly/{month_id}.{fmt}') def _store_report_file(relpath: str, content: bytes, content_type: str | None = None) -> str | None: from apps.nextcloud_connector.nextcloud_service import NextcloudConfigError, NextcloudServiceError from apps.nextcloud_connector.webdav_service import NextcloudWebDAVService try: NextcloudWebDAVService().upload(relpath, content, content_type) except NextcloudConfigError: return 'Файловое хранилище (Nextcloud) ещё не настроено.' except NextcloudServiceError: return 'Не удалось сохранить файл в хранилище.' return None @login_required @require_POST def report_document_create_view(request): if not user_can_create_report_document(request.user): return HttpResponseForbidden() title = (request.POST.get('title') or '').strip() ext = (request.POST.get('file_type') or 'docx').strip().lower() if ext not in ('docx', 'xlsx'): ext = 'docx' if not title: request.session['reports_error'] = 'Укажите название документа.' return HttpResponseRedirect('/reports/') from apps.reports.blank_office import empty_docx, empty_xlsx filename = unique_filename(request.user.department_id, title, ext) doc = ReportDocument.objects.create( department_id=request.user.department_id, title=title, filename=filename, kind=ReportDocument.KIND_EXTRA, status=ReportDocument.STATUS_DRAFT, created_by=request.user, ) payload = empty_xlsx() if ext == 'xlsx' else empty_docx() ctype = ( 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' if ext == 'xlsx' else 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ) err = _store_report_file(report_document_relpath(doc), payload, ctype) if err: doc.delete() request.session['reports_error'] = err return HttpResponseRedirect('/reports/') if _onlyoffice_ready(): return HttpResponseRedirect(f'/documents/edit/?report_doc={doc.pk}') request.session['reports_message'] = f'Документ «{title}» создан (черновик). OnlyOffice пока не настроен — откройте файл позже.' return HttpResponseRedirect('/reports/') @login_required @require_POST def report_document_upload_view(request): """Загрузка готового Word/Excel в документы отдела (черновик).""" if not user_can_create_report_document(request.user): return HttpResponseForbidden() f = request.FILES.get('file') if not f: request.session['reports_error'] = 'Выберите файл Word (.docx) или Excel (.xlsx).' return HttpResponseRedirect('/reports/') from apps.nextcloud_connector.views import MAX_UPLOAD_BYTES if f.size > MAX_UPLOAD_BYTES: request.session['reports_error'] = 'Файл слишком большой (лимит 100 МБ).' return HttpResponseRedirect('/reports/') filename = unique_upload_filename(request.user.department_id, f.name) if not filename: request.session['reports_error'] = 'Допустимы только файлы .docx и .xlsx.' return HttpResponseRedirect('/reports/') title = (request.POST.get('title') or '').strip() if not title: title = filename.rsplit('.', 1)[0].replace('-', ' ').replace('_', ' ').strip() or filename ext = filename.rsplit('.', 1)[-1].lower() ctype = ( 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' if ext == 'xlsx' else 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ) content = f.read() doc = ReportDocument.objects.create( department_id=request.user.department_id, title=title[:200], filename=filename, kind=ReportDocument.KIND_EXTRA, status=ReportDocument.STATUS_DRAFT, created_by=request.user, ) err = _store_report_file(report_document_relpath(doc), content, ctype) if err: doc.delete() request.session['reports_error'] = err return HttpResponseRedirect('/reports/') request.session['reports_message'] = ( f'Файл «{doc.title}» загружен как черновик. Опубликуйте, чтобы отдел его увидел.' ) return HttpResponseRedirect('/reports/') @login_required @require_POST def report_document_publish_view(request, pk): doc = get_object_or_404(ReportDocument, pk=pk) try: publish_report_document(request.user, doc) except PermissionError: return HttpResponseForbidden() request.session['reports_message'] = f'Документ «{doc.title}» опубликован.' return HttpResponseRedirect('/reports/') @login_required @require_POST def report_document_unpublish_view(request, pk): doc = get_object_or_404(ReportDocument, pk=pk) try: unpublish_report_document(request.user, doc) except PermissionError: return HttpResponseForbidden() request.session['reports_message'] = f'Документ «{doc.title}» снят с публикации.' return HttpResponseRedirect('/reports/') @login_required @require_POST def report_document_delete_view(request, pk): doc = get_object_or_404(ReportDocument, pk=pk) if not user_can_edit_report_document(request.user, doc): return HttpResponseForbidden() from apps.nextcloud_connector.nextcloud_service import NextcloudConfigError, NextcloudServiceError from apps.nextcloud_connector.webdav_service import NextcloudWebDAVService relpath = report_document_relpath(doc) try: NextcloudWebDAVService().delete(relpath) except (NextcloudConfigError, NextcloudServiceError): pass title = doc.title doc.delete() request.session['reports_message'] = f'Документ «{title}» удалён.' return HttpResponseRedirect('/reports/') @login_required def report_document_download_view(request, pk): doc = get_object_or_404(ReportDocument, pk=pk) if not user_can_view_report_document(request.user, doc): return HttpResponseForbidden() from apps.nextcloud_connector.nextcloud_service import NextcloudConfigError, NextcloudServiceError from apps.nextcloud_connector.webdav_service import NextcloudWebDAVService try: content, ctype = NextcloudWebDAVService().download(report_document_relpath(doc)) except (NextcloudConfigError, NextcloudServiceError): raise Http404('Файл недоступен.') resp = HttpResponse(content, content_type=ctype) resp['Content-Disposition'] = f'attachment; filename*=UTF-8\'\'{doc.filename}' return resp @login_required @require_POST def it_monthly_save_document_view(request, month_id): """Синоним «Сформировать Word» (совместимость со старым URL).""" year, month = parse_month_id(month_id) if not user_can_access_it_monthly_month(request.user, year, month): return HttpResponseForbidden() return _prepare_it_monthly_word(request, year, month) # ── Шаблоны ────────────────────────────────────────────────────────────── @login_required def templates_list_view(request): if not user_can_manage_templates(request.user): return HttpResponseForbidden() templates = ReportTemplate.objects.filter( report_type=ReportTemplate.REPORT_IT_MONTHLY, ).order_by('-is_default', 'name') return render_page(request, 'reports-templates.html', { 'templates': templates, 'can_manage_templates': True, }) @login_required def template_edit_view(request, pk): if not user_can_manage_templates(request.user): return HttpResponseForbidden() tpl = get_object_or_404(ReportTemplate, pk=pk) can_edit = user_can_edit_template(request.user, tpl) error = '' message = '' if request.method == 'POST': if not can_edit: return HttpResponseForbidden('Системный шаблон можно только скопировать.') name = request.POST.get('name', '').strip() description = request.POST.get('description', '').strip() body_html = request.POST.get('body_html', '') body_md = request.POST.get('body_md', '') blocks_raw = request.POST.get('blocks', '').strip() engine = request.POST.get('engine', '').strip() if engine not in (ReportTemplate.ENGINE_BLOCKS_V1, ReportTemplate.ENGINE_DJANGO_HTML): engine = tpl.engine or ReportTemplate.ENGINE_BLOCKS_V1 is_default = bool(request.POST.get('is_default')) is_active = request.POST.get('is_active') == 'on' from apps.reports.engine.blocks_v1 import normalize_blocks blocks = normalize_blocks(tpl.blocks) if blocks_raw: try: parsed = json.loads(blocks_raw) if not isinstance(parsed, dict): raise ValueError('JSON должен быть объектом {...}') blocks = normalize_blocks(parsed) except (json.JSONDecodeError, ValueError) as exc: error = f'Некорректный JSON blocks: {exc}' if not error: if not name: error = 'Укажите название.' elif engine == ReportTemplate.ENGINE_DJANGO_HTML: try: from apps.reports.engine.django_tpl import render_django_string render_django_string(body_html or ' ', { 'report': {}, 'labels': {}, 'company': {}, 'month': {}, 'stats': {}, 'employees': [], 'conclusions': [], 'month_title': '', 'prepared_date': '', }) except TemplateRenderError as exc: error = f'Ошибка HTML-шаблона: {exc}' if not error: tpl.name = name tpl.description = description tpl.engine = engine tpl.body_html = body_html tpl.body_md = body_md tpl.blocks = blocks tpl.is_default = is_default tpl.is_active = is_active tpl.save() message = 'Шаблон сохранён.' can_edit = user_can_edit_template(request.user, tpl) from apps.reports.engine.blocks_v1 import labels_for_ui, normalize_blocks, sections_for_ui blocks_norm = normalize_blocks(tpl.blocks) months = available_report_months() return render_page(request, 'reports-template-edit.html', { 'tpl': tpl, 'can_edit': can_edit, 'blocks_json': json.dumps(blocks_norm, ensure_ascii=False, indent=2), 'label_fields': labels_for_ui(blocks_norm), 'section_rows': sections_for_ui(blocks_norm), 'editor_mode': ( 'advanced' if tpl.engine == ReportTemplate.ENGINE_DJANGO_HTML else 'constructor' ), 'error': error, 'message': message, 'can_manage_templates': True, 'preview_months': months, 'preview_month': months[0] if months else '', }) @login_required @require_POST def template_preview_draft_view(request, pk): """Рендер черновика без сохранения (конструктор или Ace).""" if not user_can_manage_templates(request.user): return HttpResponseForbidden() get_object_or_404(ReportTemplate, pk=pk) from apps.reports.engine.blocks_v1 import normalize_blocks, render_blocks_html from apps.reports.engine.context import build_template_context, load_default_company from apps.reports.engine.django_tpl import render_django_string as render_str engine = (request.POST.get('engine') or ReportTemplate.ENGINE_BLOCKS_V1).strip() body_html = request.POST.get('body_html', '') blocks_raw = (request.POST.get('blocks') or '').strip() blocks = normalize_blocks(None) if blocks_raw: try: parsed = json.loads(blocks_raw) if isinstance(parsed, dict): blocks = normalize_blocks(parsed) except json.JSONDecodeError: pass year, month = parse_month_id(request.POST.get('month')) report = build_monthly_report(year, month) ctx = build_template_context( report, company=load_default_company(), labels=blocks.get('labels'), ) try: if engine == ReportTemplate.ENGINE_DJANGO_HTML: html = render_str(body_html or ' ', ctx) else: html = render_blocks_html(ctx, blocks) except TemplateRenderError as exc: return HttpResponse( f'' f'Ошибка шаблона
{exc}
' f'', content_type='text/html; charset=utf-8', status=400, ) return HttpResponse(html, content_type='text/html; charset=utf-8') @login_required @require_POST def template_copy_view(request, pk): if not user_can_manage_templates(request.user): return HttpResponseForbidden() src = get_object_or_404(ReportTemplate, pk=pk) base = f'{src.code}-copy' code = base n = 2 while ReportTemplate.objects.filter(code=code).exists(): code = f'{base}-{n}' n += 1 if not _SLUG_RE.match(code): code = f'copy-{src.pk}-{n}' from apps.reports.engine.blocks_v1 import normalize_blocks copy = ReportTemplate.objects.create( code=code, report_type=src.report_type, name=f'{src.name} (копия)', description=src.description, # копия всегда в конструкторе — проще править engine=ReportTemplate.ENGINE_BLOCKS_V1, body_html=src.body_html, body_md=src.body_md, blocks=normalize_blocks(src.blocks), is_default=False, is_active=True, is_system=False, created_by=request.user, ) return HttpResponseRedirect(f'/reports/templates/{copy.pk}/') @login_required @require_POST def template_set_default_view(request, pk): if not user_can_manage_templates(request.user): return HttpResponseForbidden() tpl = get_object_or_404(ReportTemplate, pk=pk, is_active=True) tpl.is_default = True tpl.save() return HttpResponseRedirect('/reports/templates/') @login_required def report_document_sign_view(request, pk): """Страница КЕП: скачать PDF для подписи и загрузить подписанный файл.""" doc = get_object_or_404(ReportDocument, pk=pk) if not user_can_view_report_document(request.user, doc): return HttpResponseForbidden() latest = doc.latest_signature history = list( doc.signatures .filter(status=ReportSignature.STATUS_SIGNED) .select_related('signer') .order_by('-signed_at')[:10] ) return render_page(request, 'report-sign.html', { 'document': doc, 'kep_signature': latest, 'kep_history': history, 'sign_message': request.session.pop('reports_message', ''), 'sign_error': request.session.pop('reports_error', ''), }) @login_required def report_document_sign_prepare_view(request, pk): """Скачать исходный PDF для подписи КЕП + sha256 в заголовке.""" doc = get_object_or_404(ReportDocument, pk=pk) if not user_can_view_report_document(request.user, doc): return HttpResponseForbidden() try: pdf, _note = build_unsigned_pdf(doc) except RuntimeError as exc: return HttpResponseBadRequest(str(exc)) except Exception: return HttpResponseBadRequest('Не удалось подготовить PDF для подписи.') digest = sha256_hex(pdf) # Запомним хеш в сессии — complete сверит при желании. request.session[f'report_sign_sha_{doc.pk}'] = digest from urllib.parse import quote resp = HttpResponse(pdf, content_type='application/pdf') name = f'{doc.title[:80] or "report"}-for-sign.pdf' resp['Content-Disposition'] = f"attachment; filename*=UTF-8''{quote(name)}" resp['X-Content-SHA256'] = digest resp['X-Content-Type-Options'] = 'nosniff' return resp @login_required @require_POST def report_document_sign_complete_view(request, pk): """Принять подписанный PDF (браузер EndUser или офлайн-загрузка).""" doc = get_object_or_404(ReportDocument, pk=pk) if not user_can_view_report_document(request.user, doc): return HttpResponseForbidden() f = request.FILES.get('file') or request.FILES.get('signed_file') if not f: request.session['reports_error'] = 'Выберите подписанный PDF-файл.' return HttpResponseRedirect(f'/reports/documents/{pk}/sign/') from apps.nextcloud_connector.views import MAX_UPLOAD_BYTES if f.size > MAX_UPLOAD_BYTES: request.session['reports_error'] = 'Файл слишком большой (лимит 100 МБ).' return HttpResponseRedirect(f'/reports/documents/{pk}/sign/') content = f.read() meta = parse_cert_meta(request.POST.get('cert_meta')) if request.POST.get('cert_subject'): meta['cert_subject'] = request.POST.get('cert_subject', '')[:500] if request.POST.get('cert_serial'): meta['cert_serial'] = request.POST.get('cert_serial', '')[:120] if request.POST.get('cert_issuer'): meta['cert_issuer'] = request.POST.get('cert_issuer', '')[:500] content_sha = (request.POST.get('content_sha256') or '').strip().lower() session_sha = (request.session.get(f'report_sign_sha_{doc.pk}') or '').lower() if not content_sha and session_sha: content_sha = session_sha offline = request.POST.get('offline') == '1' or not request.POST.get('cert_meta') try: sig = complete_signature( document=doc, user=request.user, signed_pdf=content, content_sha256=content_sha, cert_subject=meta.get('cert_subject', ''), cert_serial=meta.get('cert_serial', ''), cert_issuer=meta.get('cert_issuer', ''), signature_format=meta.get('signature_format') or ReportSignature.FORMAT_PADES, offline_upload=offline, ) except ValueError as exc: request.session['reports_error'] = str(exc) return HttpResponseRedirect(f'/reports/documents/{pk}/sign/') except RuntimeError as exc: request.session['reports_error'] = str(exc) return HttpResponseRedirect(f'/reports/documents/{pk}/sign/') request.session.pop(f'report_sign_sha_{doc.pk}', None) who = sig.cert_subject or (request.user.get_full_name() or request.user.username) request.session['reports_message'] = ( f'Документ подписан КЕП ({who}). Назначение: ознакомлен и принял в работу.' ) return HttpResponseRedirect(f'/reports/documents/{pk}/sign/') @login_required def report_document_signed_download_view(request, pk, sig_pk): doc = get_object_or_404(ReportDocument, pk=pk) if not user_can_view_report_document(request.user, doc): return HttpResponseForbidden() sig = get_object_or_404(ReportSignature, pk=sig_pk, document=doc) if sig.status != ReportSignature.STATUS_SIGNED or not sig.signed_filename: raise Http404('Подписанный файл недоступен.') from urllib.parse import quote from apps.nextcloud_connector.nextcloud_service import NextcloudConfigError, NextcloudServiceError from apps.nextcloud_connector.webdav_service import NextcloudWebDAVService try: content, ctype = NextcloudWebDAVService().download( signed_file_relpath(doc, sig.signed_filename), ) except (NextcloudConfigError, NextcloudServiceError): raise Http404('Файл недоступен.') resp = HttpResponse(content, content_type=ctype or 'application/pdf') resp['Content-Disposition'] = ( f"attachment; filename*=UTF-8''{quote(sig.signed_filename)}" ) resp['X-Content-Type-Options'] = 'nosniff' return resp