Browse Source

Fix(OnlyOffice): Salvar e Voltar em Matéria e Doc Acessório + fix callback URL

- Adiciona botão "Salvar e Voltar" com forcesave no editor de Matéria e Doc Acessório
- Corrige callbacks que não reescreviam a URL de download do OnlyOffice (causava falha no salvamento em ambiente Docker)
- Adiciona endpoints check-doc e forcesave para Matéria e Doc Acessório
- Template genérico agora suporta forcesave condicionalmente
pull/3858/head
rangelbruno 7 months ago
parent
commit
98f6b05872
  1. 138
      sapl/materia/onlyoffice_materia_views.py
  2. 10
      sapl/materia/urls.py
  3. 115
      sapl/templates/onlyoffice/onlyoffice_editor.html

138
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 # Status 2 ou 6 significa que o documento foi salvo
if status in [2, 6] and download_url: 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) materia = get_object_or_404(MateriaLegislativa, pk=pk)
@ -204,6 +218,7 @@ def materia_onlyoffice_callback(request, pk):
import requests import requests
try: try:
response = requests.get(download_url, timeout=30) response = requests.get(download_url, timeout=30)
logger.info(f"Download response: status={response.status_code}, size={len(response.content)}")
except Exception as e: except Exception as e:
logger.error(f"Erro ao fazer requisição de download: {e}") logger.error(f"Erro ao fazer requisição de download: {e}")
return JsonResponse({"error": 1}) return JsonResponse({"error": 1})
@ -215,7 +230,9 @@ def materia_onlyoffice_callback(request, pk):
# Remove arquivo antigo se existir # Remove arquivo antigo se existir
if materia.texto_original: if materia.texto_original:
old_file = materia.texto_original.name
materia.texto_original.delete(save=False) materia.texto_original.delete(save=False)
logger.info(f"Arquivo antigo removido: {old_file}")
try: try:
materia.texto_original.save( materia.texto_original.save(
@ -239,6 +256,56 @@ def materia_onlyoffice_callback(request, pk):
return JsonResponse({"error": 1}) return JsonResponse({"error": 1})
@login_required
@require_http_methods(["GET"])
def materia_check_doc(request, pk):
"""
Verifica se a matéria 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 @login_required
def materia_onlyoffice_editor(request, pk): def materia_onlyoffice_editor(request, pk):
""" """
@ -264,6 +331,8 @@ def materia_onlyoffice_editor(request, pk):
'onlyoffice_url': onlyoffice_url, 'onlyoffice_url': onlyoffice_url,
'config_url': reverse('sapl.materia:materia_onlyoffice_config', kwargs={'pk': pk}), 'config_url': reverse('sapl.materia:materia_onlyoffice_config', kwargs={'pk': pk}),
'voltar_url': reverse('sapl.materia:materialegislativa_detail', 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) 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 # Status 2 ou 6 significa que o documento foi salvo
if status in [2, 6] and download_url: 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) documento = get_object_or_404(DocumentoAcessorio, pk=pk)
@ -446,6 +529,7 @@ def docacessorio_onlyoffice_callback(request, pk):
import requests import requests
try: try:
response = requests.get(download_url, timeout=30) response = requests.get(download_url, timeout=30)
logger.info(f"Download response: status={response.status_code}, size={len(response.content)}")
except Exception as e: except Exception as e:
logger.error(f"Erro ao fazer requisição de download: {e}") logger.error(f"Erro ao fazer requisição de download: {e}")
return JsonResponse({"error": 1}) return JsonResponse({"error": 1})
@ -457,7 +541,9 @@ def docacessorio_onlyoffice_callback(request, pk):
# Remove arquivo antigo se existir # Remove arquivo antigo se existir
if documento.arquivo: if documento.arquivo:
old_file = documento.arquivo.name
documento.arquivo.delete(save=False) documento.arquivo.delete(save=False)
logger.info(f"Arquivo antigo removido: {old_file}")
try: try:
documento.arquivo.save( documento.arquivo.save(
@ -481,6 +567,56 @@ def docacessorio_onlyoffice_callback(request, pk):
return JsonResponse({"error": 1}) return JsonResponse({"error": 1})
@login_required
@require_http_methods(["GET"])
def docacessorio_check_doc(request, pk):
"""
Verifica se o documento acessório 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 @login_required
def docacessorio_onlyoffice_editor(request, pk): def docacessorio_onlyoffice_editor(request, pk):
""" """
@ -506,6 +642,8 @@ def docacessorio_onlyoffice_editor(request, pk):
'onlyoffice_url': onlyoffice_url, 'onlyoffice_url': onlyoffice_url,
'config_url': reverse('sapl.materia:docacessorio_onlyoffice_config', kwargs={'pk': pk}), '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}), '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) return render(request, 'onlyoffice/onlyoffice_editor.html', context)

10
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 ( from sapl.materia.onlyoffice_materia_views import (
materia_onlyoffice_editor, materia_onlyoffice_config, materia_onlyoffice_editor, materia_onlyoffice_config,
materia_onlyoffice_download, materia_onlyoffice_callback, materia_onlyoffice_download, materia_onlyoffice_callback,
materia_check_doc, materia_forcesave,
docacessorio_onlyoffice_editor, docacessorio_onlyoffice_config, docacessorio_onlyoffice_editor, docacessorio_onlyoffice_config,
docacessorio_onlyoffice_download, docacessorio_onlyoffice_callback, docacessorio_onlyoffice_download, docacessorio_onlyoffice_callback,
docacessorio_check_doc, docacessorio_forcesave,
materia_gerar_pdf_assinatura materia_gerar_pdf_assinatura
) )
from sapl.materia.views_assinatura import ( from sapl.materia.views_assinatura import (
@ -153,6 +155,10 @@ urlpatterns_materia = [
name='materia_onlyoffice_download'), name='materia_onlyoffice_download'),
url(r'^materia/(?P<pk>\d+)/onlyoffice/callback$', materia_onlyoffice_callback, url(r'^materia/(?P<pk>\d+)/onlyoffice/callback$', materia_onlyoffice_callback,
name='materia_onlyoffice_callback'), name='materia_onlyoffice_callback'),
url(r'^materia/(?P<pk>\d+)/check-doc$', materia_check_doc,
name='materia_check_doc'),
url(r'^materia/(?P<pk>\d+)/forcesave$', materia_forcesave,
name='materia_forcesave'),
url(r'^materia/(?P<pk>\d+)/pdf-assinatura$', materia_gerar_pdf_assinatura, url(r'^materia/(?P<pk>\d+)/pdf-assinatura$', materia_gerar_pdf_assinatura,
name='materia_pdf_assinatura'), name='materia_pdf_assinatura'),
@ -181,6 +187,10 @@ urlpatterns_materia = [
name='docacessorio_onlyoffice_download'), name='docacessorio_onlyoffice_download'),
url(r'^materia/documentoacessorio/(?P<pk>\d+)/onlyoffice/callback$', docacessorio_onlyoffice_callback, url(r'^materia/documentoacessorio/(?P<pk>\d+)/onlyoffice/callback$', docacessorio_onlyoffice_callback,
name='docacessorio_onlyoffice_callback'), name='docacessorio_onlyoffice_callback'),
url(r'^materia/documentoacessorio/(?P<pk>\d+)/check-doc$', docacessorio_check_doc,
name='docacessorio_check_doc'),
url(r'^materia/documentoacessorio/(?P<pk>\d+)/forcesave$', docacessorio_forcesave,
name='docacessorio_forcesave'),
] ]

115
sapl/templates/onlyoffice/onlyoffice_editor.html

@ -33,6 +33,18 @@
<div class="row mt-3"> <div class="row mt-3">
<div class="col-12"> <div class="col-12">
{% if forcesave_url %}
<button id="btn-salvar-voltar" class="btn btn-success" onclick="salvarEVoltar()">
<i class="fa fa-save"></i> {% trans "Salvar e Voltar" %}
</button>
<a href="{{ voltar_url }}" class="btn btn-secondary">
<i class="fa fa-arrow-left"></i> {% trans "Voltar sem Salvar" %}
</a>
<div class="alert alert-info mt-3">
<i class="fa fa-info-circle"></i>
{% 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." %}
</div>
{% else %}
<a href="{{ voltar_url }}" class="btn btn-secondary"> <a href="{{ voltar_url }}" class="btn btn-secondary">
<i class="fa fa-arrow-left"></i> {% trans "Voltar" %} <i class="fa fa-arrow-left"></i> {% trans "Voltar" %}
</a> </a>
@ -40,6 +52,7 @@
<i class="fa fa-info-circle"></i> <i class="fa fa-info-circle"></i>
{% trans "O documento é salvo automaticamente enquanto você edita. Ao terminar, feche o editor e volte para a página do documento." %} {% trans "O documento é salvo automaticamente enquanto você edita. Ao terminar, feche o editor e volte para a página do documento." %}
</div> </div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -47,17 +60,95 @@
<script type="text/javascript" src="{{ onlyoffice_url }}/web-apps/apps/api/documents/api.js"></script> <script type="text/javascript" src="{{ onlyoffice_url }}/web-apps/apps/api/documents/api.js"></script>
<script type="text/javascript"> <script type="text/javascript">
var docEditor = null;
var documentSaved = false;
var isSaving = false;
var documentKey = null;
var returnUrl = "{{ voltar_url }}";
{% if check_url %}var checkUrl = "{{ check_url }}";{% endif %}
{% if forcesave_url %}var forcesaveUrl = "{{ forcesave_url }}";{% endif %}
var csrfToken = "{{ csrf_token }}";
{% if forcesave_url %}
function iniciarPolling() {
var tentativas = 0;
var maxTentativas = 30;
var intervalo = setInterval(function() {
tentativas++;
console.log('Verificando documento... tentativa ' + tentativas);
fetch(checkUrl)
.then(function(r) { return r.json(); })
.then(function(data) {
console.log('check-doc response:', data);
if (data.has_document) {
clearInterval(intervalo);
window.location.href = returnUrl;
} else if (tentativas >= maxTentativas) {
clearInterval(intervalo);
alert('Tempo esgotado aguardando salvamento. Verifique se o documento foi salvo.');
window.location.href = returnUrl;
}
})
.catch(function(err) {
console.error('Erro no check-doc:', err);
if (tentativas >= maxTentativas) {
clearInterval(intervalo);
window.location.href = returnUrl;
}
});
}, 1000);
}
function salvarEVoltar() {
if (isSaving) return;
isSaving = true;
var btn = document.getElementById('btn-salvar-voltar');
btn.disabled = true;
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Salvando...';
fetch(forcesaveUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({key: documentKey})
})
.then(function(r) { return r.json(); })
.then(function(data) {
console.log('Forcesave response:', data);
if (data.error === 0) {
iniciarPolling();
} else if (data.error === 4) {
alert('Nenhuma alteração foi detectada no documento.');
btn.disabled = false;
btn.innerHTML = '<i class="fa fa-save"></i> Salvar e Voltar';
isSaving = false;
} else {
console.error('Erro no forcesave:', data);
iniciarPolling();
}
})
.catch(function(err) {
console.error('Erro na requisição forcesave:', err);
iniciarPolling();
});
}
{% endif %}
// Carrega a configuração do OnlyOffice // Carrega a configuração do OnlyOffice
fetch('{{ config_url }}') fetch('{{ config_url }}')
.then(response => response.json()) .then(response => response.json())
.then(config => { .then(config => {
console.log('OnlyOffice Config:', config); console.log('OnlyOffice Config:', config);
// Inicializa o editor // Guarda a chave do documento para uso no forcesave
var docEditor = new DocsAPI.DocEditor("onlyoffice-placeholder", config); documentKey = config.document.key;
console.log('Document key:', documentKey);
// Eventos do editor // Adiciona eventos à configuração
docEditor.events = { config.events = {
'onDocumentReady': function() { 'onDocumentReady': function() {
console.log('Documento pronto para edição'); console.log('Documento pronto para edição');
}, },
@ -67,8 +158,15 @@
}, },
'onWarning': function(event) { 'onWarning': function(event) {
console.warn('Aviso do OnlyOffice:', event); console.warn('Aviso do OnlyOffice:', event);
},
'onDocumentStateChange': function(event) {
console.log('Estado do documento:', event.data ? 'Alterações pendentes' : 'Salvo');
documentSaved = !event.data;
} }
}; };
// Inicializa o editor
docEditor = new DocsAPI.DocEditor("onlyoffice-placeholder", config);
}) })
.catch(error => { .catch(error => {
console.error('Erro ao carregar configuração:', error); console.error('Erro ao carregar configuração:', error);
@ -78,5 +176,14 @@
'Verifique se o servidor OnlyOffice está rodando.' + 'Verifique se o servidor OnlyOffice está rodando.' +
'</div>'; '</div>';
}); });
// Aviso antes de sair se houver alterações não salvas
window.addEventListener('beforeunload', function(e) {
if (!documentSaved && docEditor && !isSaving) {
e.preventDefault();
e.returnValue = 'Você tem alterações não salvas. Deseja realmente sair?';
return e.returnValue;
}
});
</script> </script>
{% endblock %} {% endblock %}

Loading…
Cancel
Save