Browse Source

Feat(Proposição): Adiciona editor OnlyOffice na tela de confirmação

- Adiciona botão "Editar com OnlyOffice" na página de confirmação de proposição
- Permite que usuários com permissão de protocolo editem o documento durante a confirmação
- Adiciona validação de hash para segurança do link de confirmação
- Corrige permissão de download para permitir acesso a usuários do protocolo
pull/3858/head
rangelbruno 8 months ago
parent
commit
22d0ab9068
  1. 130
      sapl/materia/onlyoffice_views.py
  2. 12
      sapl/materia/urls.py
  3. 1
      sapl/materia/views.py
  4. 3
      sapl/templates/materia/confirmar_proposicao.html
  5. 123
      sapl/templates/materia/onlyoffice_confirmar_editor.html

130
sapl/materia/onlyoffice_views.py

@ -106,7 +106,11 @@ def onlyoffice_download(request, pk):
proposicao = get_object_or_404(Proposicao, pk=pk) proposicao = get_object_or_404(Proposicao, pk=pk)
# Verificação de permissão apenas se houver usuário autenticado # Verificação de permissão apenas se houver usuário autenticado
if request.user.is_authenticated and not proposicao.autor.operadores.filter(id=request.user.id).exists(): # Permite: operadores do autor OU usuários com permissão de protocolo
if request.user.is_authenticated:
is_operator = proposicao.autor.operadores.filter(id=request.user.id).exists()
has_protocol_perm = request.user.has_perm('protocoloadm.add_documentoadministrativo')
if not is_operator and not has_protocol_perm:
return HttpResponse("Sem permissão", status=403) return HttpResponse("Sem permissão", status=403)
# Se já tem arquivo, retorna ele # Se já tem arquivo, retorna ele
@ -291,3 +295,127 @@ def onlyoffice_editor(request, pk):
} }
return render(request, 'materia/onlyoffice_editor.html', context) return render(request, 'materia/onlyoffice_editor.html', context)
@login_required
@require_http_methods(["GET"])
def onlyoffice_confirmar_config(request, pk):
"""
Retorna a configuração JSON para o editor OnlyOffice na página de confirmação
"""
proposicao = get_object_or_404(Proposicao, pk=pk)
# Verifica se o usuário tem permissão de protocolo
if not request.user.has_perm('protocoloadm.add_documentoadministrativo'):
return JsonResponse({"error": "Sem permissão"}, status=403)
# Verifica se a proposição está em estado de confirmação
# (enviada mas não recebida)
if not proposicao.data_envio or proposicao.data_recebimento:
return JsonResponse({"error": "Proposição não está em confirmação"}, status=400)
# URLs para o OnlyOffice acessar o documento
download_url = request.build_absolute_uri(
reverse('sapl.materia:onlyoffice_download', kwargs={'pk': pk})
)
callback_url = request.build_absolute_uri(
reverse('sapl.materia:onlyoffice_callback', kwargs={'pk': pk})
)
# Configuração do documento
document_config = {
"fileType": "docx",
"key": generate_file_key(proposicao.pk, request.user.pk),
"title": f"Proposicao_{proposicao.pk}.docx",
"url": download_url,
"permissions": {
"edit": True,
"download": True,
"print": True,
"review": True,
"comment": True,
},
}
# Configuração do editor - permite edição na confirmação
editor_config = {
"mode": "edit",
"lang": "pt-BR",
"callbackUrl": callback_url,
"user": {
"id": str(request.user.pk),
"name": request.user.get_full_name() or request.user.username,
},
"customization": {
"autosave": True,
"forcesave": True,
"comments": True,
"chat": False,
},
}
config = {
"documentType": "word",
"document": document_config,
"editorConfig": editor_config,
"height": "600px",
"width": "100%",
}
# Adiciona JWT se estiver habilitado
if settings.ONLYOFFICE_JWT_ENABLED and settings.ONLYOFFICE_JWT_SECRET:
import jwt
token = jwt.encode(config, settings.ONLYOFFICE_JWT_SECRET, algorithm='HS256')
config['token'] = token
return JsonResponse(config)
@login_required
def onlyoffice_confirmar_editor(request, pk, hash):
"""
Renderiza a página com o editor OnlyOffice para proposição em confirmação
"""
from django.shortcuts import render, redirect
from django.contrib import messages
from sapl.materia.forms import SEPARADOR_HASH_PROPOSICAO
from sapl.utils import gerar_hash_arquivo
proposicao = get_object_or_404(Proposicao, pk=pk)
# Verifica se o usuário tem permissão de protocolo
if not request.user.has_perm('protocoloadm.add_documentoadministrativo'):
messages.error(request, 'Você não tem permissão para editar esta proposição.')
return redirect('sapl.materia:proposicao-confirmar', hash=hash, pk=pk)
# Verifica se a proposição está em estado de confirmação
if not proposicao.data_envio or proposicao.data_recebimento:
messages.warning(request, 'Esta proposição não está em estado de confirmação.')
return redirect('sapl.materia:proposicao-confirmar', hash=hash, pk=pk)
# Verifica o hash (mesma lógica de ConfirmarProposicao)
if proposicao.texto_articulado.exists():
ta = proposicao.texto_articulado.first()
hasher = 'P' + ta.hash() + SEPARADOR_HASH_PROPOSICAO + str(proposicao.pk)
else:
hasher = gerar_hash_arquivo(
proposicao.texto_original.path,
str(proposicao.pk)) if proposicao.texto_original else None
expected_hash = 'P%s%s%s' % (hash, SEPARADOR_HASH_PROPOSICAO, proposicao.pk)
if hasher != expected_hash:
messages.error(request, 'Link de confirmação inválido.')
return redirect('sapl.materia:proposicao-confirmar', hash=hash, pk=pk)
# URL do OnlyOffice acessível pelo navegador do usuário
onlyoffice_url = settings.ONLYOFFICE_URL
context = {
'proposicao': proposicao,
'hash': hash,
'onlyoffice_url': onlyoffice_url,
'config_url': reverse('sapl.materia:onlyoffice_confirmar_config', kwargs={'pk': pk}),
'voltar_url': reverse('sapl.materia:proposicao-confirmar', kwargs={'hash': hash, 'pk': pk}),
}
return render(request, 'materia/onlyoffice_confirmar_editor.html', context)

12
sapl/materia/urls.py

@ -33,7 +33,8 @@ from sapl.materia.views import (AcompanhamentoConfirmarView,
configEtiquetaMateriaLegislativaCrud, configEtiquetaMateriaLegislativaCrud,
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)
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,
@ -194,6 +195,11 @@ urlpatterns_proposicao = [
name='proposicao-recebida'), name='proposicao-recebida'),
url(r'^proposicao/devolvida/', ProposicaoDevolvida.as_view(), url(r'^proposicao/devolvida/', ProposicaoDevolvida.as_view(),
name='proposicao-devolvida'), name='proposicao-devolvida'),
# OnlyOffice endpoints para confirmação de proposição (DEVE vir antes de proposicao-confirmar)
url(r'^proposicao/confirmar/P(?P<hash>[0-9A-Fa-f]+)/(?P<pk>\d+)/onlyoffice/editor$',
onlyoffice_confirmar_editor,
name='onlyoffice_confirmar_editor'),
url(r'^proposicao/confirmar/P(?P<hash>[0-9A-Fa-f]+)/' url(r'^proposicao/confirmar/P(?P<hash>[0-9A-Fa-f]+)/'
r'(?P<pk>\d+)', ConfirmarProposicao.as_view(), r'(?P<pk>\d+)', ConfirmarProposicao.as_view(),
name='proposicao-confirmar'), name='proposicao-confirmar'),
@ -221,6 +227,10 @@ urlpatterns_proposicao = [
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+)/onlyoffice/confirmar/config$',
onlyoffice_confirmar_config,
name='onlyoffice_confirmar_config'),
] ]
urlpatterns_sistema = [ urlpatterns_sistema = [

1
sapl/materia/views.py

@ -755,6 +755,7 @@ class ConfirmarProposicao(PermissionRequiredForAppCrudMixin, UpdateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['subnav_template_name'] = '' context['subnav_template_name'] = ''
context['hash'] = self.kwargs.get('hash', '')
return context return context
def get_form(self, form_class=None): def get_form(self, form_class=None):

3
sapl/templates/materia/confirmar_proposicao.html

@ -13,6 +13,9 @@
{% endif %} {% endif %}
{% if object.texto_original %} {% if object.texto_original %}
<a class="btn btn-outline-primary" href="{{ object.texto_original.url }}">{% trans "Texto Original da Proposição" %}</a> <a class="btn btn-outline-primary" href="{{ object.texto_original.url }}">{% trans "Texto Original da Proposição" %}</a>
<a class="btn btn-outline-success" href="{% url 'sapl.materia:onlyoffice_confirmar_editor' hash=hash pk=object.pk %}">
<i class="fa fa-edit"></i> {% trans "Editar com OnlyOffice" %}
</a>
{% endif %} {% endif %}
</div> </div>
{% endblock actions%} {% endblock actions%}

123
sapl/templates/materia/onlyoffice_confirmar_editor.html

@ -0,0 +1,123 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% block base_content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h2>{% trans "Editor de Texto - Confirmação de Proposição" %}</h2>
<hr>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="alert alert-info">
<strong>{% trans "Tipo:" %}</strong> {{ proposicao.tipo }}<br>
<strong>{% trans "Ementa:" %}</strong> {{ proposicao.descricao }}<br>
<strong>{% trans "Autor:" %}</strong> {{ proposicao.autor }}
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div id="onlyoffice-placeholder" style="height: 600px; border: 1px solid #ccc;">
<div class="text-center" style="padding-top: 250px;">
<i class="fa fa-spinner fa-spin fa-3x"></i>
<p>{% trans "Carregando editor..." %}</p>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<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>
</div>
</div>
</div>
<script type="text/javascript" src="{{ onlyoffice_url }}/web-apps/apps/api/documents/api.js"></script>
<script type="text/javascript">
var docEditor = null;
var documentSaved = false;
var returnUrl = "{{ voltar_url }}";
function salvarEVoltar() {
var btn = document.getElementById('btn-salvar-voltar');
btn.disabled = true;
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Salvando...';
if (docEditor) {
// Força o salvamento do documento
docEditor.requestClose();
}
// Aguarda um tempo para o callback ser processado e redireciona
setTimeout(function() {
window.location.href = returnUrl;
}, 3000);
}
// Carrega a configuração do OnlyOffice
fetch('{{ config_url }}')
.then(response => response.json())
.then(config => {
console.log('OnlyOffice Config:', config);
// Adiciona eventos à configuração
config.events = {
'onDocumentReady': function() {
console.log('Documento pronto para edição');
},
'onError': function(event) {
console.error('Erro no OnlyOffice:', event);
alert('Ocorreu um erro ao carregar o editor. Por favor, tente novamente.');
},
'onWarning': function(event) {
console.warn('Aviso do OnlyOffice:', event);
},
'onDocumentStateChange': function(event) {
// event.data = true quando há alterações não salvas
console.log('Estado do documento:', event.data ? 'Alterações pendentes' : 'Salvo');
documentSaved = !event.data;
},
'onRequestClose': function() {
console.log('Editor fechado, redirecionando...');
}
};
// Inicializa o editor
docEditor = new DocsAPI.DocEditor("onlyoffice-placeholder", config);
})
.catch(error => {
console.error('Erro ao carregar configuração:', error);
document.getElementById('onlyoffice-placeholder').innerHTML =
'<div class="alert alert-danger m-5">' +
'<strong>Erro:</strong> Não foi possível carregar o editor. ' +
'Verifique se o servidor OnlyOffice está rodando.' +
'</div>';
});
// Aviso antes de sair se houver alterações não salvas
window.addEventListener('beforeunload', function(e) {
if (!documentSaved && docEditor) {
e.preventDefault();
e.returnValue = 'Você tem alterações não salvas. Deseja realmente sair?';
return e.returnValue;
}
});
</script>
{% endblock %}
Loading…
Cancel
Save