From 172e2df17655147dc93ad4af81d86f8c839384bc Mon Sep 17 00:00:00 2001 From: rangelbruno Date: Mon, 9 Feb 2026 07:41:21 -0300 Subject: [PATCH] =?UTF-8?q?Feat(OnlyOffice):=20Bot=C3=A3o=20editar=20em=20?= =?UTF-8?q?Mat=C3=A9ria=20e=20Doc=20Acess=C3=B3rio=20+=20refatora=20URLs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adiciona botão "Editar com OnlyOffice" na detail de Matéria Legislativa para administradores, operadores de matéria e protocolo - Adiciona botão OnlyOffice na listagem e detail de Documento Acessório - Refatora URLs do OnlyOffice usando SAPL_INTERNAL_URL para comunicação entre containers Docker (evita URLs hardcoded) - Centraliza lógica de URL em build_onlyoffice_url() e get_onlyoffice_browser_url() no utils.py --- .gitignore | 2 ++ docker/Dockerfile.dev | 1 + sapl/base/onlyoffice_template_views.py | 9 +++-- sapl/materia/onlyoffice_materia_views.py | 28 +++++++++++----- sapl/materia/onlyoffice_views.py | 28 +++++++--------- sapl/materia/views.py | 33 +++++++++++++++++++ sapl/norma/onlyoffice_views.py | 9 +++-- sapl/protocoloadm/onlyoffice_views.py | 9 +++-- sapl/settings.py | 4 +++ .../materia/documentoacessorio_detail.html | 2 +- .../materia/materialegislativa_detail.html | 2 +- sapl/utils.py | 28 ++++++++++++++++ 12 files changed, 119 insertions(+), 36 deletions(-) diff --git a/.gitignore b/.gitignore index 501ce48c9..f98b6307d 100644 --- a/.gitignore +++ b/.gitignore @@ -124,3 +124,5 @@ NOTAS.txt # Database dumps *.sql dump_*.sql + +DADOS \ No newline at end of file diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index 174e59d3b..1ff1d9097 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -14,6 +14,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ COPY ./requirements ./requirements/ RUN pip install --upgrade pip && \ + pip install "setuptools<71" && \ pip install -r ./requirements/dev-requirements.txt EXPOSE 8000 diff --git a/sapl/base/onlyoffice_template_views.py b/sapl/base/onlyoffice_template_views.py index ae5f29426..a5c6401bc 100644 --- a/sapl/base/onlyoffice_template_views.py +++ b/sapl/base/onlyoffice_template_views.py @@ -16,6 +16,7 @@ from django.views.decorators.http import require_http_methods from django.contrib import messages from sapl.base.models import DocumentTemplate +from sapl.utils import build_onlyoffice_url, get_onlyoffice_browser_url logger = logging.getLogger(__name__) @@ -40,10 +41,12 @@ def template_onlyoffice_config(request, pk): template = get_object_or_404(DocumentTemplate, pk=pk) # URLs para o OnlyOffice acessar (dentro da rede Docker) - download_url = request.build_absolute_uri( + download_url = build_onlyoffice_url( + request, reverse('sapl.base:template_onlyoffice_download', kwargs={'pk': pk}) ) - callback_url = request.build_absolute_uri( + callback_url = build_onlyoffice_url( + request, reverse('sapl.base:template_onlyoffice_callback', kwargs={'pk': pk}) ) @@ -226,7 +229,7 @@ def template_onlyoffice_editor(request, pk): template = get_object_or_404(DocumentTemplate, pk=pk) # URL do OnlyOffice acessível pelo navegador do usuário - onlyoffice_url = settings.ONLYOFFICE_URL + onlyoffice_url = get_onlyoffice_browser_url(request) context = { 'documento': template, diff --git a/sapl/materia/onlyoffice_materia_views.py b/sapl/materia/onlyoffice_materia_views.py index 32feaebe6..0a21307d3 100644 --- a/sapl/materia/onlyoffice_materia_views.py +++ b/sapl/materia/onlyoffice_materia_views.py @@ -16,6 +16,7 @@ from django.views.decorators.http import require_http_methods from django.contrib import messages from sapl.materia.models import MateriaLegislativa, DocumentoAcessorio +from sapl.utils import build_onlyoffice_url, get_onlyoffice_browser_url logger = logging.getLogger(__name__) @@ -46,10 +47,12 @@ def materia_onlyoffice_config(request, pk): can_edit = request.user.has_perm('materia.change_materialegislativa') # URLs para o OnlyOffice acessar (dentro da rede Docker) - download_url = request.build_absolute_uri( + download_url = build_onlyoffice_url( + request, reverse('sapl.materia:materia_onlyoffice_download', kwargs={'pk': pk}) ) - callback_url = request.build_absolute_uri( + callback_url = build_onlyoffice_url( + request, reverse('sapl.materia:materia_onlyoffice_callback', kwargs={'pk': pk}) ) @@ -244,12 +247,14 @@ def materia_onlyoffice_editor(request, pk): materia = get_object_or_404(MateriaLegislativa, pk=pk) # Verifica se o usuário tem permissão - if not request.user.has_perm('materia.change_materialegislativa'): + # (administrador, operador de matéria, protocolo) + if not (request.user.is_superuser or + request.user.has_perm('materia.change_materialegislativa')): messages.error(request, 'Você não tem permissão para editar esta matéria.') return redirect('sapl.materia:materialegislativa_detail', pk=pk) # URL do OnlyOffice acessível pelo navegador do usuário - onlyoffice_url = settings.ONLYOFFICE_URL + onlyoffice_url = get_onlyoffice_browser_url(request) context = { 'documento': materia, @@ -280,10 +285,12 @@ def docacessorio_onlyoffice_config(request, pk): can_edit = request.user.has_perm('materia.change_documentoacessorio') # URLs para o OnlyOffice acessar (dentro da rede Docker) - download_url = request.build_absolute_uri( + download_url = build_onlyoffice_url( + request, reverse('sapl.materia:docacessorio_onlyoffice_download', kwargs={'pk': pk}) ) - callback_url = request.build_absolute_uri( + callback_url = build_onlyoffice_url( + request, reverse('sapl.materia:docacessorio_onlyoffice_callback', kwargs={'pk': pk}) ) @@ -482,12 +489,14 @@ def docacessorio_onlyoffice_editor(request, pk): documento = get_object_or_404(DocumentoAcessorio, pk=pk) # Verifica se o usuário tem permissão - if not request.user.has_perm('materia.change_documentoacessorio'): + # (administrador, operador de matéria, protocolo) + if not (request.user.is_superuser or + request.user.has_perm('materia.change_documentoacessorio')): messages.error(request, 'Você não tem permissão para editar este documento.') return redirect('sapl.materia:documentoacessorio_detail', pk=documento.materia.pk, zpk=pk) # URL do OnlyOffice acessível pelo navegador do usuário - onlyoffice_url = settings.ONLYOFFICE_URL + onlyoffice_url = get_onlyoffice_browser_url(request) context = { 'documento': documento, @@ -546,7 +555,8 @@ def materia_gerar_pdf_assinatura(request, pk): return redirect('sapl.materia:materialegislativa_detail', pk=pk) # URL do documento para o OnlyOffice (dentro da rede Docker) - download_url = request.build_absolute_uri( + download_url = build_onlyoffice_url( + request, reverse('sapl.materia:materia_onlyoffice_download', kwargs={'pk': pk}) ) diff --git a/sapl/materia/onlyoffice_views.py b/sapl/materia/onlyoffice_views.py index 0bbc48685..fc0d6a3d3 100644 --- a/sapl/materia/onlyoffice_views.py +++ b/sapl/materia/onlyoffice_views.py @@ -15,7 +15,7 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods from sapl.materia.models import Proposicao -from sapl.utils import get_base_url +from sapl.utils import get_base_url, build_onlyoffice_url, get_onlyoffice_browser_url logger = logging.getLogger(__name__) @@ -44,14 +44,14 @@ def onlyoffice_config(request, pk): proposicao.autor.operadores.filter(id=request.user.id).exists() ) - base_url = get_base_url(request) - # URLs para o OnlyOffice acessar (dentro da rede Docker) - # OnlyOffice precisa acessar o container SGVP pelo nome do serviço - download_url = request.build_absolute_uri( + # Usa SAPL_INTERNAL_URL quando configurada para comunicação container-a-container + download_url = build_onlyoffice_url( + request, reverse('sapl.materia:onlyoffice_download', kwargs={'pk': pk}) ) - callback_url = request.build_absolute_uri( + callback_url = build_onlyoffice_url( + request, reverse('sapl.materia:onlyoffice_callback', kwargs={'pk': pk}) ) @@ -309,13 +309,7 @@ def onlyoffice_editor(request, pk): return redirect('sapl.materia:proposicao_detail', pk=pk) # URL do OnlyOffice acessível pelo navegador do usuário - # Se ONLYOFFICE_URL contém 'onlyoffice' (nome do container), substitui pelo host da requisição - onlyoffice_url = settings.ONLYOFFICE_URL - if 'onlyoffice:' in onlyoffice_url or 'onlyoffice/' in onlyoffice_url: - # É a URL interna do Docker, precisa usar a URL externa - protocol = 'https' if request.is_secure() else 'http' - host = request.get_host().split(':')[0] # Remove porta se existir - onlyoffice_url = f"{protocol}://{host}:8001" + onlyoffice_url = get_onlyoffice_browser_url(request) context = { 'proposicao': proposicao, @@ -344,10 +338,12 @@ def onlyoffice_confirmar_config(request, pk): 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( + download_url = build_onlyoffice_url( + request, reverse('sapl.materia:onlyoffice_download', kwargs={'pk': pk}) ) - callback_url = request.build_absolute_uri( + callback_url = build_onlyoffice_url( + request, reverse('sapl.materia:onlyoffice_callback', kwargs={'pk': pk}) ) @@ -437,7 +433,7 @@ def onlyoffice_confirmar_editor(request, pk, hash): 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 + onlyoffice_url = get_onlyoffice_browser_url(request) context = { 'proposicao': proposicao, diff --git a/sapl/materia/views.py b/sapl/materia/views.py index 4e658085f..5552322b9 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -1727,6 +1727,30 @@ class DocumentoAcessorioCrud(MasterDetailCrud): context = super(UpdateView, self).get_context_data(**kwargs) return context + class DetailView(MasterDetailCrud.DetailView): + template_name = "materia/documentoacessorio_detail.html" + + class ListView(MasterDetailCrud.ListView): + + def hook_arquivo(self, obj, default, url): + u = self.request.user + can_edit = u.is_authenticated and ( + u.is_superuser or + u.has_perm('materia.change_documentoacessorio') + ) + html = default + if can_edit: + editor_url = reverse( + 'sapl.materia:docacessorio_onlyoffice_editor', + kwargs={'pk': obj.pk} + ) + html += ( + f' ' + f'' + ) + return html, url + class AutoriaCrud(MasterDetailCrud): model = Autoria @@ -2060,6 +2084,15 @@ class MateriaLegislativaCrud(Crud): break context['is_autor'] = is_autor + # Verifica se o usuário tem permissão de edição da matéria + # (administrador, operador de matéria, protocolo) + context['can_edit_materia'] = ( + self.request.user.is_authenticated and ( + self.request.user.is_superuser or + self.request.user.has_perm('materia.change_materialegislativa') + ) + ) + return context class ListView(Crud.ListView, RedirectView): diff --git a/sapl/norma/onlyoffice_views.py b/sapl/norma/onlyoffice_views.py index ae8a462e1..cf58827fa 100644 --- a/sapl/norma/onlyoffice_views.py +++ b/sapl/norma/onlyoffice_views.py @@ -16,6 +16,7 @@ from django.views.decorators.http import require_http_methods from django.contrib import messages from sapl.norma.models import NormaJuridica +from sapl.utils import build_onlyoffice_url, get_onlyoffice_browser_url logger = logging.getLogger(__name__) @@ -42,10 +43,12 @@ def norma_onlyoffice_config(request, pk): can_edit = request.user.has_perm('norma.change_normajuridica') # URLs para o OnlyOffice acessar (dentro da rede Docker) - download_url = request.build_absolute_uri( + download_url = build_onlyoffice_url( + request, reverse('sapl.norma:norma_onlyoffice_download', kwargs={'pk': pk}) ) - callback_url = request.build_absolute_uri( + callback_url = build_onlyoffice_url( + request, reverse('sapl.norma:norma_onlyoffice_callback', kwargs={'pk': pk}) ) @@ -245,7 +248,7 @@ def norma_onlyoffice_editor(request, pk): return redirect('sapl.norma:normajuridica_detail', pk=pk) # URL do OnlyOffice acessível pelo navegador do usuário - onlyoffice_url = settings.ONLYOFFICE_URL + onlyoffice_url = get_onlyoffice_browser_url(request) context = { 'documento': norma, diff --git a/sapl/protocoloadm/onlyoffice_views.py b/sapl/protocoloadm/onlyoffice_views.py index c2ae4b94f..d540c7376 100644 --- a/sapl/protocoloadm/onlyoffice_views.py +++ b/sapl/protocoloadm/onlyoffice_views.py @@ -16,6 +16,7 @@ from django.views.decorators.http import require_http_methods from django.contrib import messages from sapl.protocoloadm.models import DocumentoAdministrativo +from sapl.utils import build_onlyoffice_url, get_onlyoffice_browser_url logger = logging.getLogger(__name__) @@ -42,10 +43,12 @@ def docadm_onlyoffice_config(request, pk): can_edit = request.user.has_perm('protocoloadm.change_documentoadministrativo') # URLs para o OnlyOffice acessar (dentro da rede Docker) - download_url = request.build_absolute_uri( + download_url = build_onlyoffice_url( + request, reverse('sapl.protocoloadm:docadm_onlyoffice_download', kwargs={'pk': pk}) ) - callback_url = request.build_absolute_uri( + callback_url = build_onlyoffice_url( + request, reverse('sapl.protocoloadm:docadm_onlyoffice_callback', kwargs={'pk': pk}) ) @@ -246,7 +249,7 @@ def docadm_onlyoffice_editor(request, pk): return redirect('sapl.protocoloadm:documentoadministrativo_detail', pk=pk) # URL do OnlyOffice acessível pelo navegador do usuário - onlyoffice_url = settings.ONLYOFFICE_URL + onlyoffice_url = get_onlyoffice_browser_url(request) context = { 'documento': documento, diff --git a/sapl/settings.py b/sapl/settings.py index d15544739..8172ad554 100644 --- a/sapl/settings.py +++ b/sapl/settings.py @@ -308,6 +308,10 @@ EMAIL_RUNNING = None ONLYOFFICE_URL = config('ONLYOFFICE_URL', default=config('ONLYOFFICE_SERVER', default='https://onlyoffice.legisinc.com.br')) ONLYOFFICE_JWT_SECRET = config('ONLYOFFICE_JWT_SECRET', default='') ONLYOFFICE_JWT_ENABLED = config('ONLYOFFICE_JWT_ENABLED', cast=bool, default=False) +# URL interna para o OnlyOffice acessar o SAPL (ex: http://sapl-dev:8000) +# Quando não configurada, usa a URL do request (funciona quando OnlyOffice +# consegue acessar o SAPL pela mesma URL que o navegador) +SAPL_INTERNAL_URL = config('SAPL_INTERNAL_URL', default='') # Integração externa de matérias da sessão SESSAO_MATERIAS_API_URL = config('SESSAO_MATERIAS_API_URL', default='') diff --git a/sapl/templates/materia/documentoacessorio_detail.html b/sapl/templates/materia/documentoacessorio_detail.html index d3ab57225..2ce82bf8c 100644 --- a/sapl/templates/materia/documentoacessorio_detail.html +++ b/sapl/templates/materia/documentoacessorio_detail.html @@ -3,7 +3,7 @@ {% block sub_actions %} {{ block.super }} - {% if perms.materia.change_documentoacessorio %} + {% if perms.materia.change_documentoacessorio or user.is_superuser %}
{% trans "Editar com OnlyOffice" %} diff --git a/sapl/templates/materia/materialegislativa_detail.html b/sapl/templates/materia/materialegislativa_detail.html index a7a9b6ec6..3e52f7452 100644 --- a/sapl/templates/materia/materialegislativa_detail.html +++ b/sapl/templates/materia/materialegislativa_detail.html @@ -8,7 +8,7 @@ Acompanhar Matéria
{% endif %} - {% if is_autor and not object.numero_protocolo %} + {% if is_autor and not object.numero_protocolo or can_edit_materia %}
{% trans "Editar com OnlyOffice" %} diff --git a/sapl/utils.py b/sapl/utils.py index 54212e45a..6dc123b53 100644 --- a/sapl/utils.py +++ b/sapl/utils.py @@ -427,6 +427,34 @@ def get_base_url(request): return "{0}://{1}".format(protocol, current_domain) +def build_onlyoffice_url(request, path): + """ + Constrói URL absoluta para o OnlyOffice acessar o SAPL. + Usa SAPL_INTERNAL_URL quando configurada (ambiente Docker), + caso contrário usa a URL do request. + """ + from django.conf import settings + internal_url = getattr(settings, 'SAPL_INTERNAL_URL', '') + if internal_url: + return internal_url.rstrip('/') + path + return request.build_absolute_uri(path) + + +def get_onlyoffice_browser_url(request): + """ + Retorna a URL do OnlyOffice acessível pelo navegador do usuário. + Se ONLYOFFICE_URL é um nome de container Docker (ex: http://onlyoffice:80), + substitui pelo host da requisição com porta 8001. + """ + from django.conf import settings + onlyoffice_url = settings.ONLYOFFICE_URL + if 'onlyoffice:' in onlyoffice_url or 'onlyoffice/' in onlyoffice_url: + protocol = 'https' if request.is_secure() else 'http' + host = request.get_host().split(':')[0] + onlyoffice_url = f"{protocol}://{host}:8001" + return onlyoffice_url + + def create_barcode(value, width=170, height=50, dpi=72): """ creates a base64 encoded barcode PNG image