diff --git a/sapl/materia/onlyoffice_materia_views.py b/sapl/materia/onlyoffice_materia_views.py index 0a21307d3..ebe8e4be7 100644 --- a/sapl/materia/onlyoffice_materia_views.py +++ b/sapl/materia/onlyoffice_materia_views.py @@ -197,6 +197,20 @@ def materia_onlyoffice_callback(request, pk): # Status 2 ou 6 significa que o documento foi salvo if status in [2, 6] and download_url: + logger.info(f"URL original recebida: {download_url}") + + # Substitui o host da URL pelo ONLYOFFICE_URL configurado + # O OnlyOffice retorna URLs com seu próprio hostname que pode não ser acessível + import re + from urllib.parse import urlparse + onlyoffice_parsed = urlparse(settings.ONLYOFFICE_URL) + onlyoffice_base = f"{onlyoffice_parsed.scheme}://{onlyoffice_parsed.netloc}" + download_url = re.sub( + r'https?://[^/]+', + onlyoffice_base, + download_url + ) + logger.info(f"URL substituída para: {download_url}") materia = get_object_or_404(MateriaLegislativa, pk=pk) @@ -204,6 +218,7 @@ def materia_onlyoffice_callback(request, pk): import requests try: response = requests.get(download_url, timeout=30) + logger.info(f"Download response: status={response.status_code}, size={len(response.content)}") except Exception as e: logger.error(f"Erro ao fazer requisição de download: {e}") return JsonResponse({"error": 1}) @@ -215,7 +230,9 @@ def materia_onlyoffice_callback(request, pk): # Remove arquivo antigo se existir if materia.texto_original: + old_file = materia.texto_original.name materia.texto_original.delete(save=False) + logger.info(f"Arquivo antigo removido: {old_file}") try: materia.texto_original.save( @@ -239,6 +256,56 @@ def materia_onlyoffice_callback(request, pk): return JsonResponse({"error": 1}) +@login_required +@require_http_methods(["GET"]) +def materia_check_doc(request, pk): + """ + Verifica se a matéria já possui documento salvo. + Usado pelo frontend para polling após forcesave do OnlyOffice. + """ + materia = get_object_or_404(MateriaLegislativa, pk=pk) + has_document = bool(materia.texto_original) + return JsonResponse({"has_document": has_document}) + + +@login_required +@require_http_methods(["POST"]) +def materia_forcesave(request, pk): + """ + Força o salvamento do documento via OnlyOffice Command Service. + """ + get_object_or_404(MateriaLegislativa, pk=pk) + + body = json.loads(request.body.decode('utf-8')) + doc_key = body.get('key') + + if not doc_key: + return JsonResponse({"error": "key é obrigatório"}, status=400) + + command_url = f"{settings.ONLYOFFICE_URL}/coauthoring/CommandService.ashx" + + payload = { + "c": "forcesave", + "key": doc_key + } + + if settings.ONLYOFFICE_JWT_ENABLED and settings.ONLYOFFICE_JWT_SECRET: + import jwt + token = jwt.encode(payload, settings.ONLYOFFICE_JWT_SECRET, algorithm='HS256') + payload['token'] = token + + try: + import requests as http_requests + logger.info(f"Forcesave para matéria {pk}: key={doc_key}") + resp = http_requests.post(command_url, json=payload, timeout=10) + result = resp.json() + logger.info(f"Forcesave response para matéria {pk}: {result}") + return JsonResponse(result) + except Exception as e: + logger.error(f"Erro ao chamar forcesave para matéria {pk}: {e}") + return JsonResponse({"error": str(e)}, status=500) + + @login_required def materia_onlyoffice_editor(request, pk): """ @@ -264,6 +331,8 @@ def materia_onlyoffice_editor(request, pk): 'onlyoffice_url': onlyoffice_url, 'config_url': reverse('sapl.materia:materia_onlyoffice_config', kwargs={'pk': pk}), 'voltar_url': reverse('sapl.materia:materialegislativa_detail', kwargs={'pk': pk}), + 'check_url': reverse('sapl.materia:materia_check_doc', kwargs={'pk': pk}), + 'forcesave_url': reverse('sapl.materia:materia_forcesave', kwargs={'pk': pk}), } return render(request, 'onlyoffice/onlyoffice_editor.html', context) @@ -439,6 +508,20 @@ def docacessorio_onlyoffice_callback(request, pk): # Status 2 ou 6 significa que o documento foi salvo if status in [2, 6] and download_url: + logger.info(f"URL original recebida: {download_url}") + + # Substitui o host da URL pelo ONLYOFFICE_URL configurado + # O OnlyOffice retorna URLs com seu próprio hostname que pode não ser acessível + import re + from urllib.parse import urlparse + onlyoffice_parsed = urlparse(settings.ONLYOFFICE_URL) + onlyoffice_base = f"{onlyoffice_parsed.scheme}://{onlyoffice_parsed.netloc}" + download_url = re.sub( + r'https?://[^/]+', + onlyoffice_base, + download_url + ) + logger.info(f"URL substituída para: {download_url}") documento = get_object_or_404(DocumentoAcessorio, pk=pk) @@ -446,6 +529,7 @@ def docacessorio_onlyoffice_callback(request, pk): import requests try: response = requests.get(download_url, timeout=30) + logger.info(f"Download response: status={response.status_code}, size={len(response.content)}") except Exception as e: logger.error(f"Erro ao fazer requisição de download: {e}") return JsonResponse({"error": 1}) @@ -457,7 +541,9 @@ def docacessorio_onlyoffice_callback(request, pk): # Remove arquivo antigo se existir if documento.arquivo: + old_file = documento.arquivo.name documento.arquivo.delete(save=False) + logger.info(f"Arquivo antigo removido: {old_file}") try: documento.arquivo.save( @@ -481,6 +567,56 @@ def docacessorio_onlyoffice_callback(request, pk): return JsonResponse({"error": 1}) +@login_required +@require_http_methods(["GET"]) +def docacessorio_check_doc(request, pk): + """ + Verifica se o documento acessório já possui arquivo salvo. + Usado pelo frontend para polling após forcesave do OnlyOffice. + """ + documento = get_object_or_404(DocumentoAcessorio, pk=pk) + has_document = bool(documento.arquivo) + return JsonResponse({"has_document": has_document}) + + +@login_required +@require_http_methods(["POST"]) +def docacessorio_forcesave(request, pk): + """ + Força o salvamento do documento via OnlyOffice Command Service. + """ + get_object_or_404(DocumentoAcessorio, pk=pk) + + body = json.loads(request.body.decode('utf-8')) + doc_key = body.get('key') + + if not doc_key: + return JsonResponse({"error": "key é obrigatório"}, status=400) + + command_url = f"{settings.ONLYOFFICE_URL}/coauthoring/CommandService.ashx" + + payload = { + "c": "forcesave", + "key": doc_key + } + + if settings.ONLYOFFICE_JWT_ENABLED and settings.ONLYOFFICE_JWT_SECRET: + import jwt + token = jwt.encode(payload, settings.ONLYOFFICE_JWT_SECRET, algorithm='HS256') + payload['token'] = token + + try: + import requests as http_requests + logger.info(f"Forcesave para documento acessório {pk}: key={doc_key}") + resp = http_requests.post(command_url, json=payload, timeout=10) + result = resp.json() + logger.info(f"Forcesave response para documento acessório {pk}: {result}") + return JsonResponse(result) + except Exception as e: + logger.error(f"Erro ao chamar forcesave para documento acessório {pk}: {e}") + return JsonResponse({"error": str(e)}, status=500) + + @login_required def docacessorio_onlyoffice_editor(request, pk): """ @@ -506,6 +642,8 @@ def docacessorio_onlyoffice_editor(request, pk): 'onlyoffice_url': onlyoffice_url, 'config_url': reverse('sapl.materia:docacessorio_onlyoffice_config', kwargs={'pk': pk}), 'voltar_url': reverse('sapl.materia:documentoacessorio_detail', kwargs={'pk': documento.materia.pk, 'zpk': pk}), + 'check_url': reverse('sapl.materia:docacessorio_check_doc', kwargs={'pk': pk}), + 'forcesave_url': reverse('sapl.materia:docacessorio_forcesave', kwargs={'pk': pk}), } return render(request, 'onlyoffice/onlyoffice_editor.html', context) diff --git a/sapl/materia/urls.py b/sapl/materia/urls.py index 12586d8e0..a90fb7398 100644 --- a/sapl/materia/urls.py +++ b/sapl/materia/urls.py @@ -39,8 +39,10 @@ from sapl.materia.onlyoffice_views import (onlyoffice_config, onlyoffice_downloa from sapl.materia.onlyoffice_materia_views import ( materia_onlyoffice_editor, materia_onlyoffice_config, materia_onlyoffice_download, materia_onlyoffice_callback, + materia_check_doc, materia_forcesave, docacessorio_onlyoffice_editor, docacessorio_onlyoffice_config, docacessorio_onlyoffice_download, docacessorio_onlyoffice_callback, + docacessorio_check_doc, docacessorio_forcesave, materia_gerar_pdf_assinatura ) from sapl.materia.views_assinatura import ( @@ -153,6 +155,10 @@ urlpatterns_materia = [ name='materia_onlyoffice_download'), url(r'^materia/(?P\d+)/onlyoffice/callback$', materia_onlyoffice_callback, name='materia_onlyoffice_callback'), + url(r'^materia/(?P\d+)/check-doc$', materia_check_doc, + name='materia_check_doc'), + url(r'^materia/(?P\d+)/forcesave$', materia_forcesave, + name='materia_forcesave'), url(r'^materia/(?P\d+)/pdf-assinatura$', materia_gerar_pdf_assinatura, name='materia_pdf_assinatura'), @@ -181,6 +187,10 @@ urlpatterns_materia = [ name='docacessorio_onlyoffice_download'), url(r'^materia/documentoacessorio/(?P\d+)/onlyoffice/callback$', docacessorio_onlyoffice_callback, name='docacessorio_onlyoffice_callback'), + url(r'^materia/documentoacessorio/(?P\d+)/check-doc$', docacessorio_check_doc, + name='docacessorio_check_doc'), + url(r'^materia/documentoacessorio/(?P\d+)/forcesave$', docacessorio_forcesave, + name='docacessorio_forcesave'), ] diff --git a/sapl/templates/onlyoffice/onlyoffice_editor.html b/sapl/templates/onlyoffice/onlyoffice_editor.html index 28d711aa1..399959d47 100644 --- a/sapl/templates/onlyoffice/onlyoffice_editor.html +++ b/sapl/templates/onlyoffice/onlyoffice_editor.html @@ -33,6 +33,18 @@
+ {% if forcesave_url %} + + + {% trans "Voltar sem Salvar" %} + +
+ + {% trans "Use o botão 'Salvar e Voltar' para garantir que o documento seja salvo antes de sair. Se usar Ctrl+S, aguarde a confirmação de salvamento antes de sair." %} +
+ {% else %} {% trans "Voltar" %} @@ -40,6 +52,7 @@ {% trans "O documento é salvo automaticamente enquanto você edita. Ao terminar, feche o editor e volte para a página do documento." %}
+ {% endif %}
@@ -47,17 +60,95 @@ {% endblock %}