dd21bea141
Co-authored-by: Cursor <cursoragent@cursor.com>
1201 lines
47 KiB
Python
1201 lines
47 KiB
Python
"""Виджет «Документы» аптеки — HTMX-фрагменты поверх Nextcloud WebDAV.
|
|
|
|
Каждый эндпоинт: (1) резолвит аптеку по pk, (2) проверяет can_access_pharmacy_docs,
|
|
(3) выводит путь из объекта аптеки на сервере, (4) имя файла — через safe_filename.
|
|
"""
|
|
import json
|
|
import logging
|
|
from urllib.parse import quote
|
|
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.http import (
|
|
HttpResponse, HttpResponseForbidden, HttpResponseBadRequest, Http404, JsonResponse,
|
|
)
|
|
from django.shortcuts import get_object_or_404, render
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.views.decorators.http import require_POST
|
|
|
|
from apps.locations.models import Pharmacy
|
|
from apps.nextcloud_connector.access import (
|
|
acl_section_key, additional_share_targets,
|
|
can_access_pharmacy_docs, can_edit_pharmacy_docs, pharmacy_folder_relpath,
|
|
safe_filename, get_section, safe_relpath, resolve_department,
|
|
)
|
|
from apps.nextcloud_connector.doc_acl import (
|
|
filter_entries, grant_owner_edit, rekey_grants_on_rename, resolve_access,
|
|
user_can_create_in, user_can_manage_acl, user_can_manage_path_acl,
|
|
)
|
|
from apps.nextcloud_connector.nextcloud_service import (
|
|
NextcloudConfigError, NextcloudServiceError,
|
|
)
|
|
from apps.nextcloud_connector.blank_office import empty_docx, empty_xlsx
|
|
from apps.nextcloud_connector.onlyoffice import (
|
|
OnlyOfficeConfigError,
|
|
annotate_files,
|
|
build_editor_config,
|
|
can_edit_file,
|
|
decode_file_token,
|
|
document_key,
|
|
encode_file_token,
|
|
fetch_saved_file,
|
|
get_onlyoffice_settings,
|
|
is_allowed_ds_download_url,
|
|
is_office_file,
|
|
is_previewable_file,
|
|
office_content_type,
|
|
onlyoffice_is_ready,
|
|
preview_content_type,
|
|
preview_kind,
|
|
verified_ds_payload,
|
|
)
|
|
from apps.nextcloud_connector.webdav_service import NextcloudWebDAVService
|
|
from core.utils.htmx import render_page
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
MAX_UPLOAD_BYTES = 100 * 1024 * 1024 # 100 МБ
|
|
|
|
|
|
def _get_pharmacy_or_403(request, pk):
|
|
"""Возвращает (pharmacy, None) или (None, HttpResponseForbidden)."""
|
|
from core.location_access import accessible_pharmacies
|
|
pharmacy = get_object_or_404(accessible_pharmacies(request.user), pk=pk)
|
|
if not can_access_pharmacy_docs(request.user, pharmacy):
|
|
return None, HttpResponseForbidden('Нет доступа к документам этой аптеки.')
|
|
return pharmacy, None
|
|
|
|
|
|
def _require_pharmacy_edit(request, pharmacy):
|
|
"""None или HttpResponseForbidden, если нет права менять документы аптеки."""
|
|
if not can_edit_pharmacy_docs(request.user, pharmacy):
|
|
return HttpResponseForbidden('Нет права редактировать документы этой аптеки.')
|
|
return None
|
|
|
|
|
|
def _render_documents(request, pharmacy, message=None, message_type=None):
|
|
"""Рендер фрагмента списка документов (общий для list и upload/delete)."""
|
|
relpath = pharmacy_folder_relpath(pharmacy)
|
|
files, error = None, None
|
|
try:
|
|
dav = NextcloudWebDAVService()
|
|
files = dav.list_dir(relpath)
|
|
except NextcloudConfigError:
|
|
error = 'not_configured'
|
|
except NextcloudServiceError as e:
|
|
error = str(e)
|
|
logger.warning(f'Nextcloud docs (аптека {pharmacy.pk}): {e}')
|
|
return render(request, 'partials/pharmacy-documents.html', {
|
|
'pharmacy': pharmacy,
|
|
'files': annotate_files(files),
|
|
'error': error,
|
|
'message': message,
|
|
'message_type': message_type,
|
|
'onlyoffice_ready': onlyoffice_is_ready(),
|
|
'can_edit_docs': can_edit_pharmacy_docs(request.user, pharmacy),
|
|
})
|
|
|
|
|
|
@login_required
|
|
def pharmacy_documents(request, pk):
|
|
pharmacy, forbidden = _get_pharmacy_or_403(request, pk)
|
|
if forbidden:
|
|
return forbidden
|
|
return _render_documents(request, pharmacy)
|
|
|
|
|
|
def _file_response(content: bytes, name: str, ctype: str | None, *, inline: bool) -> HttpResponse:
|
|
"""Отдача файла: attachment (скачать) или inline (просмотрщик / iframe)."""
|
|
media = preview_content_type(name, ctype) if inline else (ctype or 'application/octet-stream')
|
|
resp = HttpResponse(content, content_type=media)
|
|
disp = 'inline' if inline else 'attachment'
|
|
resp['Content-Disposition'] = f"{disp}; filename*=UTF-8''{quote(name)}"
|
|
resp['X-Content-Type-Options'] = 'nosniff'
|
|
if inline:
|
|
# иначе XFrameOptionsMiddleware ставит DENY и PDF не открывается в модалке
|
|
resp['X-Frame-Options'] = 'SAMEORIGIN'
|
|
return resp
|
|
|
|
|
|
@login_required
|
|
def pharmacy_document_download(request, pk):
|
|
pharmacy, forbidden = _get_pharmacy_or_403(request, pk)
|
|
if forbidden:
|
|
return forbidden
|
|
name = safe_filename(request.GET.get('name'))
|
|
if not name:
|
|
return HttpResponseBadRequest('Некорректное имя файла.')
|
|
relpath = f'{pharmacy_folder_relpath(pharmacy)}/{name}'
|
|
try:
|
|
content, ctype = NextcloudWebDAVService().download(relpath)
|
|
except (NextcloudConfigError, NextcloudServiceError) as e:
|
|
logger.warning(f'Nextcloud download (аптека {pharmacy.pk}, {name}): {e}')
|
|
raise Http404('Файл недоступен.')
|
|
inline = request.GET.get('inline') == '1' and is_previewable_file(name)
|
|
return _file_response(content, name, ctype, inline=inline)
|
|
|
|
|
|
@login_required
|
|
@require_POST
|
|
def pharmacy_document_upload(request, pk):
|
|
pharmacy, forbidden = _get_pharmacy_or_403(request, pk)
|
|
if forbidden:
|
|
return forbidden
|
|
denied = _require_pharmacy_edit(request, pharmacy)
|
|
if denied:
|
|
return denied
|
|
f = request.FILES.get('file')
|
|
if not f:
|
|
return _render_documents(request, pharmacy, 'Файл не выбран.', 'error')
|
|
if f.size > MAX_UPLOAD_BYTES:
|
|
return _render_documents(request, pharmacy, 'Файл больше 100 МБ.', 'error')
|
|
name = safe_filename(f.name)
|
|
if not name:
|
|
return _render_documents(request, pharmacy, 'Недопустимое имя файла.', 'error')
|
|
relpath = f'{pharmacy_folder_relpath(pharmacy)}/{name}'
|
|
try:
|
|
NextcloudWebDAVService().upload(relpath, f.read(), f.content_type)
|
|
except (NextcloudConfigError, NextcloudServiceError) as e:
|
|
logger.warning(f'Nextcloud upload (аптека {pharmacy.pk}, {name}): {e}')
|
|
return _render_documents(request, pharmacy, 'Не удалось загрузить файл.', 'error')
|
|
return _render_documents(request, pharmacy, f'Файл «{name}» загружен.', 'success')
|
|
|
|
|
|
@login_required
|
|
@require_POST
|
|
def pharmacy_document_delete(request, pk):
|
|
pharmacy, forbidden = _get_pharmacy_or_403(request, pk)
|
|
if forbidden:
|
|
return forbidden
|
|
denied = _require_pharmacy_edit(request, pharmacy)
|
|
if denied:
|
|
return denied
|
|
name = safe_filename(request.POST.get('name'))
|
|
if not name:
|
|
return _render_documents(request, pharmacy, 'Некорректное имя файла.', 'error')
|
|
relpath = f'{pharmacy_folder_relpath(pharmacy)}/{name}'
|
|
try:
|
|
NextcloudWebDAVService().delete(relpath)
|
|
except (NextcloudConfigError, NextcloudServiceError) as e:
|
|
logger.warning(f'Nextcloud delete (аптека {pharmacy.pk}, {name}): {e}')
|
|
return _render_documents(request, pharmacy, 'Не удалось удалить файл.', 'error')
|
|
return _render_documents(request, pharmacy, f'Файл «{name}» удалён.', 'success')
|
|
|
|
|
|
@login_required
|
|
@require_POST
|
|
def pharmacy_document_mkdir(request, pk):
|
|
pharmacy, forbidden = _get_pharmacy_or_403(request, pk)
|
|
if forbidden:
|
|
return forbidden
|
|
denied = _require_pharmacy_edit(request, pharmacy)
|
|
if denied:
|
|
return denied
|
|
name = safe_filename(request.POST.get('name'))
|
|
if not name:
|
|
return _render_documents(request, pharmacy, 'Некорректное имя папки.', 'error')
|
|
try:
|
|
NextcloudWebDAVService().mkdir(f'{pharmacy_folder_relpath(pharmacy)}/{name}')
|
|
except (NextcloudConfigError, NextcloudServiceError) as e:
|
|
logger.warning(f'Nextcloud mkdir (аптека {pharmacy.pk}, {name}): {e}')
|
|
return _render_documents(request, pharmacy, 'Не удалось создать папку.', 'error')
|
|
return _render_documents(request, pharmacy, f'Папка «{name}» создана.', 'success')
|
|
|
|
|
|
@login_required
|
|
@require_POST
|
|
def pharmacy_document_rename(request, pk):
|
|
pharmacy, forbidden = _get_pharmacy_or_403(request, pk)
|
|
if forbidden:
|
|
return forbidden
|
|
denied = _require_pharmacy_edit(request, pharmacy)
|
|
if denied:
|
|
return denied
|
|
name = safe_filename(request.POST.get('name'))
|
|
new_name = safe_filename(
|
|
request.POST.get('new_name') or request.headers.get('HX-Prompt')
|
|
)
|
|
if not name or not new_name:
|
|
return _render_documents(request, pharmacy, 'Некорректное имя.', 'error')
|
|
if name == new_name:
|
|
return _render_documents(request, pharmacy)
|
|
base = pharmacy_folder_relpath(pharmacy)
|
|
try:
|
|
NextcloudWebDAVService().move(f'{base}/{name}', f'{base}/{new_name}')
|
|
except (NextcloudConfigError, NextcloudServiceError) as e:
|
|
logger.warning(f'Nextcloud rename (аптека {pharmacy.pk}, {name}): {e}')
|
|
return _render_documents(request, pharmacy, 'Не удалось переименовать.', 'error')
|
|
return _render_documents(
|
|
request, pharmacy, f'«{name}» переименован в «{new_name}».', 'success'
|
|
)
|
|
|
|
|
|
# ── Общий браузер документов (страница /documents/) ───────────────────────────
|
|
|
|
def _resolve_section_path(request, method='GET'):
|
|
"""(section, sub) или (None, HttpResponse-ошибка). sub — безопасный подпуть."""
|
|
src = request.GET if method == 'GET' else request.POST
|
|
section = get_section(request.user, src.get('section', ''))
|
|
if not section:
|
|
return None, HttpResponseForbidden('Раздел недоступен.')
|
|
sub = safe_relpath(src.get('path', ''))
|
|
if sub is None:
|
|
return None, HttpResponseBadRequest('Некорректный путь.')
|
|
if section['key'] == 'mine':
|
|
# «Мой отдел» = папка своего отдела (ACL как org) + доп. шары в корне
|
|
from apps.nextcloud_connector.services.sync_service import dept_mountpoint
|
|
dept = resolve_department(getattr(request.user, 'department_id', None))
|
|
if dept is not None:
|
|
section = {
|
|
**section,
|
|
'acl_key': 'org',
|
|
'relpath': dept_mountpoint(dept),
|
|
'department_id': dept.pk,
|
|
'department_name': dept.name,
|
|
'label': dept.name,
|
|
'show_shares': not bool(sub), # доп. доступ — только в корне вкладки
|
|
}
|
|
else:
|
|
section = {
|
|
**section,
|
|
'acl_key': 'org',
|
|
'relpath': '',
|
|
'department_id': None,
|
|
'department_name': '',
|
|
'label': 'Мой отдел',
|
|
'show_shares': True,
|
|
'shares_only': True,
|
|
}
|
|
elif section['key'] == 'org':
|
|
raw_dept = src.get('dept')
|
|
if raw_dept:
|
|
dept = resolve_department(raw_dept)
|
|
if dept is None:
|
|
return None, HttpResponseBadRequest('Відділ не знайдено.')
|
|
from apps.nextcloud_connector.services.sync_service import dept_mountpoint
|
|
section = {
|
|
**section,
|
|
'relpath': dept_mountpoint(dept),
|
|
'department_id': dept.pk,
|
|
'department_name': dept.name,
|
|
'label': dept.name,
|
|
}
|
|
else:
|
|
section = {**section, 'department_id': None}
|
|
return (section, sub), None
|
|
|
|
|
|
def _section_relpath(section, sub, name=None):
|
|
relpath = section['relpath'] + (f'/{sub}' if sub else '')
|
|
return f'{relpath}/{name}' if name else relpath
|
|
|
|
|
|
def _folder_access(user, section, sub):
|
|
return resolve_access(
|
|
user,
|
|
section_key=acl_section_key(section),
|
|
path=sub or '',
|
|
department_id=section.get('department_id'),
|
|
)
|
|
|
|
|
|
def _item_path(sub, name=None):
|
|
path = sub or ''
|
|
if name:
|
|
path = f'{path}/{name}' if path else name
|
|
return path
|
|
|
|
|
|
def _require_edit(request, section, sub, name=None):
|
|
path = _item_path(sub, name)
|
|
dec = resolve_access(
|
|
request.user,
|
|
section_key=acl_section_key(section),
|
|
path=path,
|
|
department_id=section.get('department_id'),
|
|
)
|
|
if not dec.can_edit:
|
|
return HttpResponseForbidden('Немає права редагування.')
|
|
return None
|
|
|
|
|
|
def _require_create(request, section, sub):
|
|
"""Завантаження/створення в каталозі (не плутати з edit чужого файлу)."""
|
|
if user_can_create_in(
|
|
request.user,
|
|
section_key=acl_section_key(section),
|
|
path=sub or '',
|
|
department_id=section.get('department_id'),
|
|
):
|
|
return None
|
|
return HttpResponseForbidden('Немає права створювати файли тут.')
|
|
|
|
|
|
def _require_view(request, section, sub, name=None):
|
|
path = _item_path(sub, name)
|
|
dec = resolve_access(
|
|
request.user,
|
|
section_key=acl_section_key(section),
|
|
path=path,
|
|
department_id=section.get('department_id'),
|
|
)
|
|
if not dec.can_view:
|
|
return HttpResponseForbidden('Немає права перегляду.')
|
|
return None
|
|
|
|
|
|
def _grant_creator(request, section, sub, name, *, is_folder=False):
|
|
grant_owner_edit(
|
|
request.user,
|
|
section_key=acl_section_key(section),
|
|
path=_item_path(sub, name),
|
|
department_id=section.get('department_id'),
|
|
is_folder=is_folder,
|
|
)
|
|
|
|
|
|
def _virtual_org_children(parent_id):
|
|
"""Дочірні відділи Directory як віртуальні папки в браузері."""
|
|
from core.models import Directory
|
|
qs = Directory.objects.filter(
|
|
directory_type__code='department', is_active=True, parent_id=parent_id,
|
|
).order_by('_path', 'name')
|
|
return [
|
|
{
|
|
'name': d.name,
|
|
'is_dir': True,
|
|
'is_virtual_dept': True,
|
|
'dept_id': d.pk,
|
|
'path': '',
|
|
'size': None,
|
|
'can_view': True,
|
|
'can_edit': False,
|
|
}
|
|
for d in qs
|
|
]
|
|
|
|
|
|
def _render_browser(request, section, sub, message=None, message_type=None):
|
|
files, error = None, None
|
|
folder_dec = _folder_access(request.user, section, sub)
|
|
can_create_here = False
|
|
share_entries = []
|
|
if section.get('show_shares'):
|
|
share_entries = annotate_files(additional_share_targets(request.user)) or []
|
|
|
|
# Корінь «Структура» без dept — лише дерево Directory
|
|
if section['key'] == 'org' and not section.get('department_id'):
|
|
files = annotate_files(_virtual_org_children(None))
|
|
can_edit_here = False
|
|
elif section.get('shares_only'):
|
|
# Нет своего отдела — только дополнительные шары
|
|
files = share_entries
|
|
can_edit_here = False
|
|
folder_dec = resolve_access(
|
|
request.user, section_key='org', path='', department_id=None,
|
|
)
|
|
elif not folder_dec.can_view:
|
|
error = 'access_denied'
|
|
files = []
|
|
can_edit_here = False
|
|
else:
|
|
can_create_here = user_can_create_in(
|
|
request.user,
|
|
section_key=acl_section_key(section),
|
|
path=sub or '',
|
|
department_id=section.get('department_id'),
|
|
)
|
|
# колонка дій / підсвітка: edit каталогу або право створювати (свій відділ)
|
|
can_edit_here = folder_dec.can_edit or can_create_here
|
|
# Під відділом: спочатку дочірні департаменти, потім файли WebDAV
|
|
virtual = []
|
|
if (
|
|
section['key'] in ('org', 'mine')
|
|
and section.get('department_id')
|
|
and not sub
|
|
and section['key'] == 'org'
|
|
):
|
|
# В «Мой отдел» дочерние отделы не показываем — только файлы своего узла
|
|
virtual = _virtual_org_children(section['department_id'])
|
|
try:
|
|
if not section.get('relpath'):
|
|
listed = []
|
|
else:
|
|
listed = NextcloudWebDAVService().list_dir(_section_relpath(section, sub))
|
|
for f in listed:
|
|
f['path'] = f'{sub}/{f["name"]}' if sub else f['name']
|
|
listed = filter_entries(
|
|
request.user, listed,
|
|
section_key=acl_section_key(section),
|
|
parent_path=sub or '',
|
|
department_id=section.get('department_id'),
|
|
)
|
|
files = annotate_files(virtual + listed)
|
|
except NextcloudConfigError:
|
|
error = 'not_configured'
|
|
files = annotate_files(virtual) if virtual else None
|
|
except NextcloudServiceError as e:
|
|
error = str(e)
|
|
logger.warning(f'Nextcloud docs ({section["key"]}/{sub}): {e}')
|
|
files = annotate_files(virtual) if virtual else None
|
|
|
|
crumbs, acc = [], ''
|
|
for part in (sub.split('/') if sub else []):
|
|
acc = f'{acc}/{part}' if acc else part
|
|
crumbs.append({'name': part, 'path': acc})
|
|
return render(request, 'partials/documents-browser.html', {
|
|
'section': section, 'sub': sub, 'crumbs': crumbs,
|
|
'files': files, 'error': error,
|
|
'message': message, 'message_type': message_type,
|
|
'onlyoffice_ready': onlyoffice_is_ready(),
|
|
'can_edit_here': can_edit_here,
|
|
'can_create_here': can_create_here,
|
|
'can_manage_acl': user_can_manage_path_acl(
|
|
request.user,
|
|
section_key=acl_section_key(section),
|
|
path=sub or '',
|
|
department_id=section.get('department_id'),
|
|
),
|
|
'folder_access': folder_dec.code or 'none',
|
|
'share_entries': share_entries if section.get('show_shares') and not section.get('shares_only') else [],
|
|
})
|
|
|
|
|
|
@login_required
|
|
def documents_browse(request):
|
|
resolved, err = _resolve_section_path(request)
|
|
if err:
|
|
return err
|
|
section, sub = resolved
|
|
return _render_browser(request, section, sub)
|
|
|
|
|
|
@login_required
|
|
def document_download(request):
|
|
resolved, err = _resolve_section_path(request)
|
|
if err:
|
|
return err
|
|
section, sub = resolved
|
|
name = safe_filename(request.GET.get('name'))
|
|
if not name:
|
|
return HttpResponseBadRequest('Некорректное имя файла.')
|
|
denied = _require_view(request, section, sub, name)
|
|
if denied:
|
|
return denied
|
|
try:
|
|
content, ctype = NextcloudWebDAVService().download(_section_relpath(section, sub, name))
|
|
except (NextcloudConfigError, NextcloudServiceError) as e:
|
|
logger.warning(f'Nextcloud download ({section["key"]}/{sub}/{name}): {e}')
|
|
raise Http404('Файл недоступен.')
|
|
inline = request.GET.get('inline') == '1' and is_previewable_file(name)
|
|
return _file_response(content, name, ctype, inline=inline)
|
|
|
|
|
|
@login_required
|
|
@require_POST
|
|
def document_upload(request):
|
|
resolved, err = _resolve_section_path(request, 'POST')
|
|
if err:
|
|
return err
|
|
section, sub = resolved
|
|
denied = _require_create(request, section, sub)
|
|
if denied:
|
|
return denied
|
|
f = request.FILES.get('file')
|
|
if not f:
|
|
return _render_browser(request, section, sub, 'Файл не выбран.', 'error')
|
|
if f.size > MAX_UPLOAD_BYTES:
|
|
return _render_browser(request, section, sub, 'Файл больше 100 МБ.', 'error')
|
|
name = safe_filename(f.name)
|
|
if not name:
|
|
return _render_browser(request, section, sub, 'Недопустимое имя файла.', 'error')
|
|
try:
|
|
NextcloudWebDAVService().upload(_section_relpath(section, sub, name), f.read(), f.content_type)
|
|
except (NextcloudConfigError, NextcloudServiceError) as e:
|
|
logger.warning(f'Nextcloud upload ({section["key"]}/{sub}/{name}): {e}')
|
|
return _render_browser(request, section, sub, 'Не удалось загрузить файл.', 'error')
|
|
_grant_creator(request, section, sub, name, is_folder=False)
|
|
return _render_browser(request, section, sub, f'Файл «{name}» загружен.', 'success')
|
|
|
|
|
|
@login_required
|
|
@require_POST
|
|
def document_mkdir(request):
|
|
resolved, err = _resolve_section_path(request, 'POST')
|
|
if err:
|
|
return err
|
|
section, sub = resolved
|
|
denied = _require_create(request, section, sub)
|
|
if denied:
|
|
return denied
|
|
name = safe_filename(request.POST.get('name'))
|
|
if not name:
|
|
return _render_browser(request, section, sub, 'Некорректное имя папки.', 'error')
|
|
try:
|
|
NextcloudWebDAVService().mkdir(_section_relpath(section, sub, name))
|
|
except (NextcloudConfigError, NextcloudServiceError) as e:
|
|
logger.warning(f'Nextcloud mkdir ({section["key"]}/{sub}/{name}): {e}')
|
|
return _render_browser(request, section, sub, 'Не удалось создать папку.', 'error')
|
|
_grant_creator(request, section, sub, name, is_folder=True)
|
|
return _render_browser(request, section, sub, f'Папка «{name}» создана.', 'success')
|
|
|
|
|
|
@login_required
|
|
@require_POST
|
|
def document_create(request):
|
|
"""Пустой Word/Excel, который OnlyOffice реально открывает (не «новый файл» с диска)."""
|
|
resolved, err = _resolve_section_path(request, 'POST')
|
|
if err:
|
|
return err
|
|
section, sub = resolved
|
|
denied = _require_create(request, section, sub)
|
|
if denied:
|
|
return denied
|
|
kind = (request.POST.get('type') or '').lower()
|
|
if kind not in ('folder', 'docx', 'xlsx'):
|
|
return _render_browser(request, section, sub, 'Можно создать папку, Word или Excel.', 'error')
|
|
raw_name = (request.POST.get('name') or '').strip()
|
|
if kind == 'folder':
|
|
name = safe_filename(raw_name or 'Новая папка')
|
|
if not name:
|
|
return _render_browser(request, section, sub, 'Некорректное имя папки.', 'error')
|
|
try:
|
|
NextcloudWebDAVService().mkdir(_section_relpath(section, sub, name))
|
|
except (NextcloudConfigError, NextcloudServiceError) as e:
|
|
logger.warning(f'Nextcloud mkdir ({section["key"]}/{sub}/{name}): {e}')
|
|
return _render_browser(request, section, sub, 'Не удалось создать папку.', 'error')
|
|
_grant_creator(request, section, sub, name, is_folder=True)
|
|
return _render_browser(request, section, sub, f'Папка «{name}» создана.', 'success')
|
|
if not raw_name:
|
|
raw_name = 'Новый документ' if kind == 'docx' else 'Новая таблица'
|
|
if not raw_name.lower().endswith(f'.{kind}'):
|
|
raw_name = f'{raw_name}.{kind}'
|
|
name = safe_filename(raw_name)
|
|
if not name:
|
|
return _render_browser(request, section, sub, 'Некорректное имя файла.', 'error')
|
|
payload = empty_xlsx() if kind == 'xlsx' else empty_docx()
|
|
try:
|
|
NextcloudWebDAVService().upload(
|
|
_section_relpath(section, sub, name),
|
|
payload,
|
|
office_content_type(name),
|
|
)
|
|
except (NextcloudConfigError, NextcloudServiceError) as e:
|
|
logger.warning(f'Nextcloud create ({section["key"]}/{sub}/{name}): {e}')
|
|
return _render_browser(request, section, sub, 'Не удалось создать документ.', 'error')
|
|
_grant_creator(request, section, sub, name, is_folder=False)
|
|
return _render_browser(request, section, sub, f'Файл «{name}» создан.', 'success')
|
|
|
|
|
|
@login_required
|
|
@require_POST
|
|
def document_rename(request):
|
|
resolved, err = _resolve_section_path(request, 'POST')
|
|
if err:
|
|
return err
|
|
section, sub = resolved
|
|
name = safe_filename(request.POST.get('name'))
|
|
new_name = safe_filename(
|
|
request.POST.get('new_name') or request.headers.get('HX-Prompt')
|
|
)
|
|
denied = _require_edit(request, section, sub, name)
|
|
if denied:
|
|
return denied
|
|
if not name or not new_name:
|
|
return _render_browser(request, section, sub, 'Некорректное имя.', 'error')
|
|
if name == new_name:
|
|
return _render_browser(request, section, sub)
|
|
try:
|
|
NextcloudWebDAVService().move(
|
|
_section_relpath(section, sub, name),
|
|
_section_relpath(section, sub, new_name),
|
|
)
|
|
except (NextcloudConfigError, NextcloudServiceError) as e:
|
|
logger.warning(f'Nextcloud rename ({section["key"]}/{sub}/{name}): {e}')
|
|
return _render_browser(request, section, sub, 'Не удалось переименовать.', 'error')
|
|
rekey_grants_on_rename(
|
|
section_key=acl_section_key(section),
|
|
department_id=section.get('department_id'),
|
|
old_path=_item_path(sub, name),
|
|
new_path=_item_path(sub, new_name),
|
|
)
|
|
return _render_browser(
|
|
request, section, sub, f'«{name}» переименован в «{new_name}».', 'success'
|
|
)
|
|
|
|
|
|
@login_required
|
|
@require_POST
|
|
def document_delete(request):
|
|
resolved, err = _resolve_section_path(request, 'POST')
|
|
if err:
|
|
return err
|
|
section, sub = resolved
|
|
name = safe_filename(request.POST.get('name'))
|
|
if not name:
|
|
return _render_browser(request, section, sub, 'Некорректное имя файла.', 'error')
|
|
denied = _require_edit(request, section, sub, name)
|
|
if denied:
|
|
return denied
|
|
try:
|
|
NextcloudWebDAVService().delete(_section_relpath(section, sub, name))
|
|
except (NextcloudConfigError, NextcloudServiceError) as e:
|
|
logger.warning(f'Nextcloud delete ({section["key"]}/{sub}/{name}): {e}')
|
|
return _render_browser(request, section, sub, 'Не удалось удалить.', 'error')
|
|
return _render_browser(request, section, sub, f'«{name}» удалён.', 'success')
|
|
|
|
|
|
# ── Просмотр PDF / изображений ────────────────────────────────────────────────
|
|
|
|
|
|
def _preview_payload_from_request(request):
|
|
"""Резолв PDF/картинки для встроенного просмотрщика (те же ACL, что download)."""
|
|
if request.GET.get('pharmacy'):
|
|
raw = request.GET.get('pharmacy', '')
|
|
if not raw.isdigit():
|
|
return None, HttpResponseBadRequest('Некорректная аптека.')
|
|
pharmacy, forbidden = _get_pharmacy_or_403(request, int(raw))
|
|
if forbidden:
|
|
return None, forbidden
|
|
name = safe_filename(request.GET.get('name'))
|
|
if not name:
|
|
return None, HttpResponseBadRequest('Некорректное имя файла.')
|
|
kind = preview_kind(name)
|
|
if not kind:
|
|
return None, HttpResponseBadRequest('Этот тип файла нельзя открыть в просмотрщике.')
|
|
return {
|
|
'filename': name,
|
|
'kind': kind,
|
|
'content_url': (
|
|
f'/pharmacies/{pharmacy.pk}/documents/download/'
|
|
f'?name={quote(name)}&inline=1'
|
|
),
|
|
'download_url': (
|
|
f'/pharmacies/{pharmacy.pk}/documents/download/'
|
|
f'?name={quote(name)}'
|
|
),
|
|
'back': {
|
|
'kind': 'pharmacy',
|
|
'pharmacy_id': pharmacy.pk,
|
|
'label': 'Документы аптеки',
|
|
},
|
|
}, None
|
|
|
|
resolved, err = _resolve_section_path(request)
|
|
if err:
|
|
return None, err
|
|
section, sub = resolved
|
|
name = safe_filename(request.GET.get('name'))
|
|
if not name:
|
|
return None, HttpResponseBadRequest('Некорректное имя файла.')
|
|
kind = preview_kind(name)
|
|
if not kind:
|
|
return None, HttpResponseBadRequest('Этот тип файла нельзя открыть в просмотрщике.')
|
|
denied = _require_view(request, section, sub, name)
|
|
if denied:
|
|
return None, denied
|
|
q = [
|
|
f'section={quote(section["key"])}',
|
|
f'path={quote(sub)}',
|
|
f'name={quote(name)}',
|
|
]
|
|
if section.get('department_id'):
|
|
q.append(f'dept={section["department_id"]}')
|
|
qs = '&'.join(q)
|
|
return {
|
|
'filename': name,
|
|
'kind': kind,
|
|
'content_url': f'/documents/download/?{qs}&inline=1',
|
|
'download_url': f'/documents/download/?{qs}',
|
|
'back': {
|
|
'kind': 'section',
|
|
'section': section['key'],
|
|
'path': sub,
|
|
'dept': section.get('department_id'),
|
|
'label': section['label'],
|
|
},
|
|
}, None
|
|
|
|
|
|
@login_required
|
|
def document_preview_view(request):
|
|
"""Встроенный просмотр PDF и изображений (браузерный, без OnlyOffice)."""
|
|
resolved, err = _preview_payload_from_request(request)
|
|
if err:
|
|
return err
|
|
return render_page(request, 'documents-preview.html', {
|
|
'filename': resolved['filename'],
|
|
'preview_kind': resolved['kind'],
|
|
'content_url': resolved['content_url'],
|
|
'download_url': resolved['download_url'],
|
|
'back': resolved['back'],
|
|
})
|
|
|
|
|
|
@login_required
|
|
def pharmacy_document_preview(request, pk):
|
|
"""Просмотр PDF/картинки аптеки — URL под /pharmacies/ (без гейта раздела documents)."""
|
|
from django.http import QueryDict
|
|
q = request.GET.copy()
|
|
q['pharmacy'] = str(pk)
|
|
request.GET = q
|
|
return document_preview_view(request)
|
|
|
|
|
|
# ── OnlyOffice ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def _office_payload_from_request(request):
|
|
"""Резолв файла для редактора из query (раздел или аптека)."""
|
|
if request.GET.get('report_doc'):
|
|
from apps.reports.documents import (
|
|
report_document_relpath,
|
|
user_can_edit_report_document,
|
|
user_can_view_report_document,
|
|
)
|
|
from apps.reports.models import ReportDocument
|
|
|
|
raw = request.GET.get('report_doc', '')
|
|
if not raw.isdigit():
|
|
return None, HttpResponseBadRequest('Некорректный документ.')
|
|
doc = get_object_or_404(ReportDocument, pk=int(raw))
|
|
if not user_can_view_report_document(request.user, doc):
|
|
return None, HttpResponseForbidden()
|
|
name = doc.filename
|
|
if not is_office_file(name):
|
|
return None, HttpResponseBadRequest('Этот тип файла OnlyOffice не открывает.')
|
|
return {
|
|
'filename': name,
|
|
'relpath': report_document_relpath(doc),
|
|
'writable': user_can_edit_report_document(request.user, doc) and can_edit_file(name),
|
|
'back': {'kind': 'report_doc', 'label': 'Отчёты'},
|
|
'token_payload': {
|
|
'kind': 'report_doc',
|
|
'doc_id': doc.pk,
|
|
'name': name,
|
|
'uid': request.user.pk,
|
|
},
|
|
}, None
|
|
|
|
if request.GET.get('pharmacy'):
|
|
raw = request.GET.get('pharmacy', '')
|
|
if not raw.isdigit():
|
|
return None, HttpResponseBadRequest('Некорректная аптека.')
|
|
pharmacy, forbidden = _get_pharmacy_or_403(request, int(raw))
|
|
if forbidden:
|
|
return None, forbidden
|
|
name = safe_filename(request.GET.get('name'))
|
|
if not name:
|
|
return None, HttpResponseBadRequest('Некорректное имя файла.')
|
|
if not is_office_file(name):
|
|
return None, HttpResponseBadRequest('Этот тип файла OnlyOffice не открывает.')
|
|
relpath = f'{pharmacy_folder_relpath(pharmacy)}/{name}'
|
|
writable = can_edit_pharmacy_docs(request.user, pharmacy) and can_edit_file(name)
|
|
back = {
|
|
'kind': 'pharmacy',
|
|
'pharmacy_id': pharmacy.pk,
|
|
'label': 'Документы аптеки',
|
|
}
|
|
token_payload = {
|
|
'kind': 'pharmacy',
|
|
'pharmacy_id': pharmacy.pk,
|
|
'name': name,
|
|
'uid': request.user.pk,
|
|
}
|
|
return {
|
|
'filename': name,
|
|
'relpath': relpath,
|
|
'writable': writable,
|
|
'back': back,
|
|
'token_payload': token_payload,
|
|
}, None
|
|
|
|
resolved, err = _resolve_section_path(request)
|
|
if err:
|
|
return None, err
|
|
section, sub = resolved
|
|
name = safe_filename(request.GET.get('name'))
|
|
if not name:
|
|
return None, HttpResponseBadRequest('Некорректное имя файла.')
|
|
if not is_office_file(name):
|
|
return None, HttpResponseBadRequest('Этот тип файла OnlyOffice не открывает.')
|
|
denied = _require_view(request, section, sub, name)
|
|
if denied:
|
|
return None, denied
|
|
relpath = _section_relpath(section, sub, name)
|
|
acl_key = acl_section_key(section)
|
|
can_write = (
|
|
resolve_access(
|
|
request.user,
|
|
section_key=acl_key,
|
|
path=f'{sub}/{name}' if sub else name,
|
|
department_id=section.get('department_id'),
|
|
).can_edit
|
|
and can_edit_file(name)
|
|
)
|
|
# В токене — реальный раздел хранилища (mine → org), чтобы callback резолвился
|
|
token_section = acl_key if section['key'] == 'mine' else section['key']
|
|
token_payload = {
|
|
'kind': 'section',
|
|
'section': token_section,
|
|
'path': sub,
|
|
'dept': section.get('department_id'),
|
|
'name': name,
|
|
'uid': request.user.pk,
|
|
}
|
|
return {
|
|
'filename': name,
|
|
'relpath': relpath,
|
|
'writable': can_write,
|
|
'back': {
|
|
'kind': 'section',
|
|
'section': section['key'],
|
|
'path': sub,
|
|
'dept': section.get('department_id'),
|
|
'label': section['label'],
|
|
},
|
|
'token_payload': token_payload,
|
|
}, None
|
|
|
|
|
|
def _resolve_token_relpath(payload: dict) -> tuple[str, str, bool] | None:
|
|
"""(relpath, filename, writable) или None."""
|
|
kind = payload.get('kind')
|
|
name = safe_filename(payload.get('name'))
|
|
if not name or not is_office_file(name):
|
|
return None
|
|
if kind == 'pharmacy':
|
|
from django.contrib.auth import get_user_model
|
|
from core.location_access import accessible_pharmacies
|
|
user = get_user_model().objects.filter(pk=payload.get('uid'), is_active=True).first()
|
|
if user is None:
|
|
return None
|
|
pharmacy = accessible_pharmacies(user).filter(pk=payload.get('pharmacy_id')).first()
|
|
if pharmacy is None or not can_access_pharmacy_docs(user, pharmacy):
|
|
return None
|
|
writable = can_edit_pharmacy_docs(user, pharmacy) and can_edit_file(name)
|
|
return f'{pharmacy_folder_relpath(pharmacy)}/{name}', name, writable
|
|
if kind == 'report_doc':
|
|
from apps.reports.documents import (
|
|
report_document_relpath,
|
|
user_can_edit_report_document,
|
|
user_can_view_report_document,
|
|
)
|
|
from apps.reports.models import ReportDocument
|
|
from django.contrib.auth import get_user_model
|
|
|
|
user = get_user_model().objects.filter(pk=payload.get('uid')).first()
|
|
doc = ReportDocument.objects.filter(pk=payload.get('doc_id')).first()
|
|
if user is None or doc is None or not user_can_view_report_document(user, doc):
|
|
return None
|
|
if doc.filename != name:
|
|
return None
|
|
writable = user_can_edit_report_document(user, doc) and can_edit_file(name)
|
|
return report_document_relpath(doc), name, writable
|
|
if kind == 'section':
|
|
from django.contrib.auth import get_user_model
|
|
from apps.nextcloud_connector.services.sync_service import dept_mountpoint
|
|
user = get_user_model().objects.filter(pk=payload.get('uid')).first()
|
|
if user is None:
|
|
return None
|
|
section = get_section(user, payload.get('section', ''))
|
|
if not section:
|
|
return None
|
|
sub = safe_relpath(payload.get('path', ''))
|
|
if sub is None:
|
|
return None
|
|
dept_id = payload.get('dept')
|
|
if section['key'] in ('org', 'mine') and dept_id:
|
|
dept = resolve_department(dept_id)
|
|
if dept is None:
|
|
return None
|
|
section = {
|
|
**section,
|
|
'acl_key': 'org' if section['key'] == 'mine' else section.get('acl_key'),
|
|
'relpath': dept_mountpoint(dept),
|
|
'department_id': dept.pk,
|
|
}
|
|
path = f'{sub}/{name}' if sub else name
|
|
dec = resolve_access(
|
|
user, section_key=acl_section_key(section), path=path,
|
|
department_id=section.get('department_id'),
|
|
)
|
|
if not dec.can_view:
|
|
return None
|
|
return (
|
|
_section_relpath(section, sub, name),
|
|
name,
|
|
dec.can_edit and can_edit_file(name),
|
|
)
|
|
return None
|
|
|
|
|
|
# ── ACL: видача прав на папку/файл ────────────────────────────────────────────
|
|
|
|
def _acl_panel_context(request, section, sub, name):
|
|
from apps.nextcloud_connector.access import people_picker_payload
|
|
from apps.nextcloud_connector.models import DocumentAccessGrant
|
|
from django.contrib.auth.models import Group
|
|
|
|
path = f'{sub}/{name}' if name and sub else (name or sub or '')
|
|
grants = DocumentAccessGrant.objects.filter(
|
|
section_key=acl_section_key(section),
|
|
department_id=section.get('department_id'),
|
|
path=path,
|
|
is_active=True,
|
|
).select_related('employee', 'grant_department', 'group')
|
|
ctx = {
|
|
'section': section,
|
|
'sub': sub,
|
|
'path': path,
|
|
'name': name or '',
|
|
'is_folder': not bool(name),
|
|
'grants': grants,
|
|
'empty_ids': set(),
|
|
'group_options': list(Group.objects.order_by('name').values('pk', 'name')),
|
|
}
|
|
ctx.update(people_picker_payload())
|
|
return ctx
|
|
|
|
|
|
def _require_acl_manage(request, section, sub, name=None):
|
|
path = _item_path(sub, name)
|
|
if user_can_manage_path_acl(
|
|
request.user,
|
|
section_key=acl_section_key(section),
|
|
path=path,
|
|
department_id=section.get('department_id'),
|
|
):
|
|
return None
|
|
return HttpResponseForbidden('Немає права керувати доступом.')
|
|
|
|
|
|
@login_required
|
|
def document_acl_panel(request):
|
|
"""Фрагмент зі списком прав + формою (staff / автор / керівник з edit)."""
|
|
resolved, err = _resolve_section_path(request)
|
|
if err:
|
|
return err
|
|
section, sub = resolved
|
|
name = safe_filename(request.GET.get('name')) if request.GET.get('name') else None
|
|
denied = _require_acl_manage(request, section, sub, name)
|
|
if denied:
|
|
return denied
|
|
return render(
|
|
request, 'partials/documents-acl.html',
|
|
_acl_panel_context(request, section, sub, name),
|
|
)
|
|
|
|
|
|
@login_required
|
|
@require_POST
|
|
def document_acl_grant(request):
|
|
resolved, err = _resolve_section_path(request, 'POST')
|
|
if err:
|
|
return err
|
|
section, sub = resolved
|
|
name = safe_filename(request.POST.get('name')) if request.POST.get('name') else None
|
|
denied = _require_acl_manage(request, section, sub, name)
|
|
if denied:
|
|
return denied
|
|
path = f'{sub}/{name}' if name and sub else (name or sub or '')
|
|
level = request.POST.get('level') or 'view'
|
|
if level not in ('view', 'edit'):
|
|
level = 'view'
|
|
from apps.nextcloud_connector.models import DocumentAccessGrant
|
|
from django.contrib.auth.models import Group
|
|
from django.contrib.auth import get_user_model
|
|
|
|
note = (request.POST.get('note') or '')[:255]
|
|
base = dict(
|
|
section_key=acl_section_key(section),
|
|
department_id=section.get('department_id'),
|
|
path=path,
|
|
is_folder=not bool(name),
|
|
level=level,
|
|
note=note,
|
|
)
|
|
subject = request.POST.get('subject_type') or 'employee'
|
|
created = 0
|
|
if subject == 'employee':
|
|
raw_ids = request.POST.getlist('employee_ids') or []
|
|
if not raw_ids and request.POST.get('employee_id'):
|
|
raw_ids = [request.POST.get('employee_id')]
|
|
employees = list(
|
|
get_user_model().objects.filter(pk__in=raw_ids, is_active=True)
|
|
)
|
|
if not employees:
|
|
return HttpResponseBadRequest('Оберіть хоча б одного співробітника.')
|
|
for emp in employees:
|
|
g = DocumentAccessGrant(employee=emp, **base)
|
|
g.full_clean()
|
|
g.save()
|
|
created += 1
|
|
elif subject == 'department':
|
|
dept = resolve_department(request.POST.get('grant_department_id'))
|
|
if not dept:
|
|
return HttpResponseBadRequest('Відділ не знайдено.')
|
|
g = DocumentAccessGrant(grant_department=dept, **base)
|
|
g.full_clean()
|
|
g.save()
|
|
created = 1
|
|
elif subject == 'group':
|
|
group = Group.objects.filter(pk=request.POST.get('group_id')).first()
|
|
if not group:
|
|
return HttpResponseBadRequest('Групу не знайдено.')
|
|
g = DocumentAccessGrant(group=group, **base)
|
|
g.full_clean()
|
|
g.save()
|
|
created = 1
|
|
else:
|
|
return HttpResponseBadRequest('Невірний тип отримувача.')
|
|
if created < 1:
|
|
return HttpResponseBadRequest('Нічого не видано.')
|
|
from django.http import QueryDict
|
|
q = QueryDict(mutable=True)
|
|
q['section'] = section['key']
|
|
q['path'] = sub or ''
|
|
if section.get('department_id'):
|
|
q['dept'] = str(section['department_id'])
|
|
if name:
|
|
q['name'] = name
|
|
request.GET = q
|
|
return document_acl_panel(request)
|
|
|
|
|
|
@login_required
|
|
@require_POST
|
|
def document_acl_revoke(request):
|
|
from apps.nextcloud_connector.models import DocumentAccessGrant
|
|
from django.http import QueryDict
|
|
grant = DocumentAccessGrant.objects.filter(pk=request.POST.get('grant_id')).first()
|
|
if not grant:
|
|
return HttpResponseBadRequest('Право не знайдено.')
|
|
if not user_can_manage_path_acl(
|
|
request.user,
|
|
section_key=grant.section_key,
|
|
path=grant.path or '',
|
|
department_id=grant.department_id,
|
|
):
|
|
return HttpResponseForbidden('Немає права керувати доступом.')
|
|
grant.is_active = False
|
|
grant.save(update_fields=['is_active', 'updated_at'])
|
|
# після revoke показати панель для того ж шляху (з POST форми)
|
|
q = QueryDict(mutable=True)
|
|
q['section'] = request.POST.get('section') or grant.section_key
|
|
q['path'] = request.POST.get('path') or ''
|
|
if request.POST.get('dept') or grant.department_id:
|
|
q['dept'] = request.POST.get('dept') or str(grant.department_id)
|
|
if request.POST.get('name'):
|
|
q['name'] = request.POST.get('name')
|
|
request.GET = q
|
|
return document_acl_panel(request)
|
|
|
|
|
|
@login_required
|
|
def document_editor_view(request):
|
|
try:
|
|
settings = get_onlyoffice_settings()
|
|
except OnlyOfficeConfigError as exc:
|
|
return HttpResponseBadRequest(str(exc))
|
|
if not settings:
|
|
return HttpResponseBadRequest('OnlyOffice пока не настроен.')
|
|
|
|
resolved, err = _office_payload_from_request(request)
|
|
if err:
|
|
return err
|
|
mode = 'edit' if resolved['writable'] else 'view'
|
|
token = encode_file_token({**resolved['token_payload'], 'mode': mode})
|
|
size, modified = None, None
|
|
try:
|
|
meta = NextcloudWebDAVService().stat(resolved['relpath'])
|
|
if meta:
|
|
size, modified = meta.get('size'), meta.get('modified')
|
|
except (NextcloudConfigError, NextcloudServiceError) as e:
|
|
logger.warning(f'OnlyOffice stat ({resolved["relpath"]}): {e}')
|
|
key = document_key(resolved['relpath'], size=size, modified=modified)
|
|
config = build_editor_config(
|
|
request=request,
|
|
settings=settings,
|
|
filename=resolved['filename'],
|
|
relpath=resolved['relpath'],
|
|
token=token,
|
|
mode=mode,
|
|
key=key,
|
|
user=request.user,
|
|
)
|
|
return render_page(request, 'documents-editor.html', {
|
|
'editor_config': json.dumps(config, ensure_ascii=False),
|
|
'api_js': settings['api_js'],
|
|
'filename': resolved['filename'],
|
|
'mode': mode,
|
|
'back': resolved['back'],
|
|
})
|
|
|
|
|
|
@csrf_exempt
|
|
def onlyoffice_content_view(request, token):
|
|
try:
|
|
settings = get_onlyoffice_settings()
|
|
except OnlyOfficeConfigError:
|
|
return HttpResponseForbidden()
|
|
if not settings:
|
|
return HttpResponseForbidden()
|
|
if verified_ds_payload(request, settings) is None:
|
|
return HttpResponseForbidden('Нужна подпись Document Server.')
|
|
try:
|
|
payload = decode_file_token(token)
|
|
except Exception:
|
|
return HttpResponseForbidden('Сессия редактора истекла.')
|
|
resolved = _resolve_token_relpath(payload)
|
|
if not resolved:
|
|
return HttpResponseForbidden()
|
|
relpath, name, _writable = resolved
|
|
try:
|
|
content, ctype = NextcloudWebDAVService().download(relpath)
|
|
except (NextcloudConfigError, NextcloudServiceError) as e:
|
|
logger.warning(f'OnlyOffice content ({relpath}): {e}')
|
|
raise Http404('Файл недоступен.')
|
|
resp = HttpResponse(content, content_type=office_content_type(name, ctype))
|
|
resp['Content-Disposition'] = f'attachment; filename*=UTF-8\'\'{quote(name)}'
|
|
resp['Content-Length'] = str(len(content))
|
|
return resp
|
|
|
|
|
|
@csrf_exempt
|
|
@require_POST
|
|
def onlyoffice_callback_view(request, token):
|
|
try:
|
|
settings = get_onlyoffice_settings()
|
|
except OnlyOfficeConfigError:
|
|
return JsonResponse({'error': 1})
|
|
if not settings:
|
|
return JsonResponse({'error': 1})
|
|
ds_payload = verified_ds_payload(request, settings)
|
|
if ds_payload is None:
|
|
return JsonResponse({'error': 1})
|
|
try:
|
|
file_token = decode_file_token(token)
|
|
except Exception:
|
|
return JsonResponse({'error': 1})
|
|
resolved = _resolve_token_relpath(file_token)
|
|
if not resolved:
|
|
return JsonResponse({'error': 1})
|
|
relpath, _name, writable = resolved
|
|
|
|
status = ds_payload.get('status')
|
|
# 2 — сохранить и закрыть, 6 — принудительное сохранение
|
|
if status not in (2, 6):
|
|
return JsonResponse({'error': 0})
|
|
if not writable:
|
|
return JsonResponse({'error': 1})
|
|
file_url = ds_payload.get('url')
|
|
if not file_url or not is_allowed_ds_download_url(file_url, settings):
|
|
return JsonResponse({'error': 1})
|
|
try:
|
|
content = fetch_saved_file(file_url, settings)
|
|
NextcloudWebDAVService().upload(relpath, content)
|
|
except Exception as e:
|
|
logger.warning(f'OnlyOffice callback save ({relpath}): {e}')
|
|
return JsonResponse({'error': 1})
|
|
return JsonResponse({'error': 0})
|