Browse Source

Fix(OnlyOffice): Salvar e Voltar na proposição usando forcesave via Command Service

Corrige o problema onde o botão Salvar e Voltar no editor OnlyOffice da
proposição redirecionava antes do documento ser salvo, mostrando
"Documento não criado". A solução usa o Command Service do OnlyOffice
para forçar o salvamento (forcesave) e polling server-side para
confirmar que o documento foi persistido antes de redirecionar.

Adiciona endpoints check-doc e forcesave para proposição.
pull/3858/head
rangelbruno 7 months ago
parent
commit
ae9f26af03
  1. 56
      sapl/materia/onlyoffice_views.py
  2. 7
      sapl/materia/urls.py
  3. 85
      sapl/templates/materia/onlyoffice_editor.html

56
sapl/materia/onlyoffice_views.py

@ -228,7 +228,6 @@ def onlyoffice_callback(request, pk):
) )
logger.info(f"URL substituída para: {download_url}") logger.info(f"URL substituída para: {download_url}")
logger.info(f"Iniciando download do documento de: {download_url}")
proposicao = get_object_or_404(Proposicao, pk=pk) proposicao = get_object_or_404(Proposicao, pk=pk)
# Baixa o documento do OnlyOffice # Baixa o documento do OnlyOffice
@ -245,7 +244,6 @@ def onlyoffice_callback(request, pk):
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
filename = f"proposicao_{pk}_{int(time.time())}.docx" filename = f"proposicao_{pk}_{int(time.time())}.docx"
logger.info(f"Salvando arquivo: {filename}")
# Remove arquivo antigo se existir # Remove arquivo antigo se existir
if proposicao.texto_original: if proposicao.texto_original:
@ -265,7 +263,6 @@ def onlyoffice_callback(request, pk):
from sapl.utils import gerar_hash_arquivo from sapl.utils import gerar_hash_arquivo
proposicao.hash_code = gerar_hash_arquivo( proposicao.hash_code = gerar_hash_arquivo(
proposicao.texto_original.path, str(proposicao.pk)) proposicao.texto_original.path, str(proposicao.pk))
logger.info(f"Hash code atualizado: {proposicao.hash_code}")
proposicao.save() proposicao.save()
logger.info(f"Documento salvo com sucesso: {filename}") logger.info(f"Documento salvo com sucesso: {filename}")
@ -285,6 +282,59 @@ def onlyoffice_callback(request, pk):
return JsonResponse({"error": 1}) return JsonResponse({"error": 1})
@login_required
@require_http_methods(["GET"])
def proposicao_check_doc(request, pk):
"""
Verifica se a proposição possui documento salvo.
Usado pelo frontend para polling após forcesave do OnlyOffice.
"""
proposicao = get_object_or_404(Proposicao, pk=pk)
has_document = bool(proposicao.texto_original)
return JsonResponse({"has_document": has_document})
@login_required
@require_http_methods(["POST"])
def proposicao_forcesave(request, pk):
"""
Força o salvamento do documento via OnlyOffice Command Service.
Chama o endpoint /coauthoring/CommandService.ashx com o comando forcesave.
"""
proposicao = get_object_or_404(Proposicao, 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)
# Monta a URL do Command Service do OnlyOffice
command_url = f"{settings.ONLYOFFICE_URL}/coauthoring/CommandService.ashx"
payload = {
"c": "forcesave",
"key": doc_key
}
# Adiciona JWT se habilitado
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 proposição {pk}: key={doc_key}")
resp = http_requests.post(command_url, json=payload, timeout=10)
result = resp.json()
logger.info(f"Forcesave response para proposição {pk}: {result}")
return JsonResponse(result)
except Exception as e:
logger.error(f"Erro ao chamar forcesave para proposição {pk}: {e}")
return JsonResponse({"error": str(e)}, status=500)
@login_required @login_required
def onlyoffice_editor(request, pk): def onlyoffice_editor(request, pk):
""" """

7
sapl/materia/urls.py

@ -34,7 +34,8 @@ from sapl.materia.views import (AcompanhamentoConfirmarView,
PesquisarStatusTramitacaoView, HistoricoProposicaoView) PesquisarStatusTramitacaoView, HistoricoProposicaoView)
from sapl.materia.onlyoffice_views import (onlyoffice_config, onlyoffice_download, from sapl.materia.onlyoffice_views import (onlyoffice_config, onlyoffice_download,
onlyoffice_callback, onlyoffice_editor, onlyoffice_callback, onlyoffice_editor,
onlyoffice_confirmar_config, onlyoffice_confirmar_editor) onlyoffice_confirmar_config, onlyoffice_confirmar_editor,
proposicao_check_doc, proposicao_forcesave)
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,
@ -226,6 +227,10 @@ urlpatterns_proposicao = [
name='onlyoffice_download'), name='onlyoffice_download'),
url(r'^proposicao/(?P<pk>\d+)/onlyoffice/callback$', onlyoffice_callback, url(r'^proposicao/(?P<pk>\d+)/onlyoffice/callback$', onlyoffice_callback,
name='onlyoffice_callback'), name='onlyoffice_callback'),
url(r'^proposicao/(?P<pk>\d+)/check-doc$', proposicao_check_doc,
name='proposicao_check_doc'),
url(r'^proposicao/(?P<pk>\d+)/forcesave$', proposicao_forcesave,
name='proposicao_forcesave'),
url(r'^proposicao/(?P<pk>\d+)/onlyoffice/confirmar/config$', url(r'^proposicao/(?P<pk>\d+)/onlyoffice/confirmar/config$',
onlyoffice_confirmar_config, onlyoffice_confirmar_config,

85
sapl/templates/materia/onlyoffice_editor.html

@ -52,22 +52,82 @@
<script type="text/javascript"> <script type="text/javascript">
var docEditor = null; var docEditor = null;
var documentSaved = false; var documentSaved = false;
var isSaving = false;
var documentKey = null;
var returnUrl = "{% url 'sapl.materia:proposicao_detail' proposicao.pk %}"; var returnUrl = "{% url 'sapl.materia:proposicao_detail' proposicao.pk %}";
var checkUrl = "{% url 'sapl.materia:proposicao_check_doc' proposicao.pk %}";
var forcesaveUrl = "{% url 'sapl.materia:proposicao_forcesave' proposicao.pk %}";
var csrfToken = "{{ csrf_token }}";
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() { function salvarEVoltar() {
if (isSaving) return;
isSaving = true;
var btn = document.getElementById('btn-salvar-voltar'); var btn = document.getElementById('btn-salvar-voltar');
btn.disabled = true; btn.disabled = true;
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Salvando...'; btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Salvando...';
if (docEditor) { // Chama o forcesave via Command Service do OnlyOffice (server-side).
// Força o salvamento do documento // NÃO destroi o editor - o redirect natural encerra a sessão.
docEditor.requestClose(); 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) {
// Forcesave aceito pelo OnlyOffice, aguardar callback salvar o documento
iniciarPolling();
} else if (data.error === 4) {
// Sem alterações para salvar
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);
// Tenta polling mesmo assim (pode ser que o callback já tenha sido chamado)
iniciarPolling();
} }
})
// Aguarda um tempo para o callback ser processado e redireciona .catch(function(err) {
setTimeout(function() { console.error('Erro na requisição forcesave:', err);
window.location.href = returnUrl; iniciarPolling();
}, 3000); });
} }
// Carrega a configuração do OnlyOffice // Carrega a configuração do OnlyOffice
@ -76,6 +136,10 @@
.then(config => { .then(config => {
console.log('OnlyOffice Config:', config); console.log('OnlyOffice Config:', config);
// Guarda a chave do documento para uso no forcesave
documentKey = config.document.key;
console.log('Document key:', documentKey);
// Adiciona eventos à configuração // Adiciona eventos à configuração
config.events = { config.events = {
'onDocumentReady': function() { 'onDocumentReady': function() {
@ -92,9 +156,6 @@
// event.data = true quando há alterações não salvas // event.data = true quando há alterações não salvas
console.log('Estado do documento:', event.data ? 'Alterações pendentes' : 'Salvo'); console.log('Estado do documento:', event.data ? 'Alterações pendentes' : 'Salvo');
documentSaved = !event.data; documentSaved = !event.data;
},
'onRequestClose': function() {
console.log('Editor fechado, redirecionando...');
} }
}; };
@ -112,7 +173,7 @@
// Aviso antes de sair se houver alterações não salvas // Aviso antes de sair se houver alterações não salvas
window.addEventListener('beforeunload', function(e) { window.addEventListener('beforeunload', function(e) {
if (!documentSaved && docEditor) { if (!documentSaved && docEditor && !isSaving) {
e.preventDefault(); e.preventDefault();
e.returnValue = 'Você tem alterações não salvas. Deseja realmente sair?'; e.returnValue = 'Você tem alterações não salvas. Deseja realmente sair?';
return e.returnValue; return e.returnValue;

Loading…
Cancel
Save