mirror of https://github.com/interlegis/sapl.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
320 lines
12 KiB
320 lines
12 KiB
"""
|
|
Views e utilitários para integração com OnlyOffice Document Server
|
|
"""
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import time
|
|
from django.conf import settings
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.http import JsonResponse, HttpResponse
|
|
from django.shortcuts import get_object_or_404
|
|
from django.urls import reverse
|
|
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
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def generate_file_key(proposicao_id, user_id):
|
|
"""
|
|
Gera uma chave única para o documento no OnlyOffice
|
|
A chave muda a cada edição para forçar o OnlyOffice a recarregar
|
|
"""
|
|
timestamp = str(int(time.time()))
|
|
string_to_hash = f"proposicao_{proposicao_id}_user_{user_id}_{timestamp}"
|
|
return hashlib.md5(string_to_hash.encode()).hexdigest()
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET"])
|
|
def onlyoffice_config(request, pk):
|
|
"""
|
|
Retorna a configuração JSON para inicializar o editor OnlyOffice
|
|
"""
|
|
proposicao = get_object_or_404(Proposicao, pk=pk)
|
|
|
|
# Verifica se o usuário tem permissão para editar
|
|
can_edit = (
|
|
not proposicao.data_envio and
|
|
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(
|
|
reverse('sapl.materia:onlyoffice_download', kwargs={'pk': pk})
|
|
)
|
|
callback_url = request.build_absolute_uri(
|
|
reverse('sapl.materia:onlyoffice_callback', kwargs={'pk': pk})
|
|
)
|
|
|
|
# Substituir localhost/host externo pelo nome do container na rede Docker
|
|
# para que o OnlyOffice consiga acessar
|
|
host = request.get_host()
|
|
# sapl-dev:8000 é o nome do container e porta interna do SAPL
|
|
download_url = download_url.replace(f'http://{host}', 'http://sapl-dev:8000')
|
|
download_url = download_url.replace(f'https://{host}', 'http://sapl-dev:8000')
|
|
callback_url = callback_url.replace(f'http://{host}', 'http://sapl-dev:8000')
|
|
callback_url = callback_url.replace(f'https://{host}', 'http://sapl-dev:8000')
|
|
|
|
# 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,
|
|
}
|
|
|
|
# Configuração do editor
|
|
editor_config = {
|
|
"mode": "edit" if can_edit else "view",
|
|
"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)
|
|
|
|
|
|
@require_http_methods(["GET"])
|
|
def onlyoffice_download(request, pk):
|
|
"""
|
|
Endpoint para o OnlyOffice baixar o documento
|
|
Se não existe arquivo, retorna um documento em branco
|
|
NOTA: Sem @login_required pois o OnlyOffice não tem sessão do Django
|
|
"""
|
|
proposicao = get_object_or_404(Proposicao, pk=pk)
|
|
|
|
# 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():
|
|
return HttpResponse("Sem permissão", status=403)
|
|
|
|
# Se já tem arquivo, retorna ele
|
|
if proposicao.texto_original:
|
|
try:
|
|
with open(proposicao.texto_original.path, 'rb') as f:
|
|
content = f.read()
|
|
response = HttpResponse(content, content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document')
|
|
response['Content-Disposition'] = f'attachment; filename="Proposicao_{pk}.docx"'
|
|
return response
|
|
except Exception as e:
|
|
logger.error(f"Erro ao ler arquivo: {e}")
|
|
|
|
# Tenta criar documento usando template
|
|
try:
|
|
from sapl.utils_template import criar_documento_com_template, criar_documento_em_branco
|
|
|
|
# Busca tipo específico da proposição (TipoProposicao)
|
|
tipo_especifico = proposicao.tipo if proposicao.tipo else None
|
|
|
|
file_stream = criar_documento_com_template(
|
|
'proposicao',
|
|
tipo_especifico,
|
|
{
|
|
'titulo': f'Proposição {proposicao.tipo}',
|
|
'descricao': proposicao.descricao,
|
|
'tipo_display': 'Proposição'
|
|
}
|
|
)
|
|
|
|
# Se não encontrou template, cria documento em branco
|
|
if not file_stream:
|
|
file_stream = criar_documento_em_branco(
|
|
f'Proposição {proposicao.tipo}',
|
|
proposicao.descricao,
|
|
'Proposição'
|
|
)
|
|
|
|
if file_stream:
|
|
response = HttpResponse(
|
|
file_stream.getvalue(),
|
|
content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
)
|
|
response['Content-Disposition'] = f'attachment; filename="Proposicao_{pk}.docx"'
|
|
return response
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erro ao criar documento: {e}")
|
|
|
|
# Fallback: Se python-docx não está instalado ou houve erro
|
|
try:
|
|
from docx import Document
|
|
from io import BytesIO
|
|
|
|
doc = Document()
|
|
doc.add_heading(f'Proposição {proposicao.tipo}', 0)
|
|
doc.add_paragraph(f'Ementa: {proposicao.descricao}')
|
|
doc.add_paragraph('')
|
|
doc.add_paragraph('Digite o texto da proposição abaixo:')
|
|
doc.add_paragraph('')
|
|
|
|
file_stream = BytesIO()
|
|
doc.save(file_stream)
|
|
file_stream.seek(0)
|
|
|
|
response = HttpResponse(
|
|
file_stream.getvalue(),
|
|
content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
)
|
|
response['Content-Disposition'] = f'attachment; filename="Proposicao_{pk}.docx"'
|
|
return response
|
|
|
|
except ImportError:
|
|
logger.error("python-docx não está instalado")
|
|
return HttpResponse("Erro: python-docx não instalado", status=500)
|
|
|
|
|
|
@csrf_exempt
|
|
@require_http_methods(["POST"])
|
|
def onlyoffice_callback(request, pk):
|
|
"""
|
|
Callback chamado pelo OnlyOffice quando o documento é salvo
|
|
|
|
Status codes:
|
|
0 - Nenhum documento com a chave identificada foi encontrado
|
|
1 - Documento está sendo editado
|
|
2 - Documento está pronto para salvar
|
|
3 - Ocorreu erro ao salvar o documento
|
|
4 - Documento está fechado sem alterações
|
|
6 - Documento está sendo editado, mas a versão atual do documento foi salva
|
|
7 - Ocorreu um erro de salvamento forçado ou salvamento automático
|
|
"""
|
|
try:
|
|
body = json.loads(request.body.decode('utf-8'))
|
|
status = body.get('status')
|
|
download_url = body.get('url')
|
|
|
|
logger.info(f"OnlyOffice callback para proposição {pk}: status={status}, url={download_url}, body={body}")
|
|
|
|
# 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 URLs externas por URLs internas da rede Docker
|
|
# O OnlyOffice pode retornar localhost:8001 ou o host externo
|
|
import re
|
|
# Padrão para capturar qualquer host:porta antes do path
|
|
download_url = re.sub(
|
|
r'https?://[^/]+',
|
|
'http://onlyoffice:80',
|
|
download_url
|
|
)
|
|
logger.info(f"URL substituída para rede Docker: {download_url}")
|
|
|
|
logger.info(f"Iniciando download do documento de: {download_url}")
|
|
proposicao = get_object_or_404(Proposicao, pk=pk)
|
|
|
|
# Baixa o documento do OnlyOffice
|
|
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})
|
|
|
|
if response.status_code == 200:
|
|
# Salva o arquivo
|
|
from django.core.files.base import ContentFile
|
|
|
|
filename = f"proposicao_{pk}_{int(time.time())}.docx"
|
|
logger.info(f"Salvando arquivo: {filename}")
|
|
|
|
# Remove arquivo antigo se existir
|
|
if proposicao.texto_original:
|
|
old_file = proposicao.texto_original.name
|
|
proposicao.texto_original.delete(save=False)
|
|
logger.info(f"Arquivo antigo removido: {old_file}")
|
|
|
|
try:
|
|
proposicao.texto_original.save(
|
|
filename,
|
|
ContentFile(response.content),
|
|
save=True
|
|
)
|
|
logger.info(f"Documento salvo com sucesso: {filename}")
|
|
return JsonResponse({"error": 0})
|
|
except Exception as e:
|
|
logger.error(f"Erro ao salvar arquivo: {e}")
|
|
return JsonResponse({"error": 1})
|
|
else:
|
|
logger.error(f"Erro ao baixar documento: status={response.status_code}")
|
|
return JsonResponse({"error": 1})
|
|
|
|
# Para outros status, apenas retorna sucesso
|
|
return JsonResponse({"error": 0})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erro no callback OnlyOffice: {e}")
|
|
return JsonResponse({"error": 1})
|
|
|
|
|
|
@login_required
|
|
def onlyoffice_editor(request, pk):
|
|
"""
|
|
Renderiza a página com o editor OnlyOffice integrado
|
|
"""
|
|
from django.shortcuts import render
|
|
|
|
proposicao = get_object_or_404(Proposicao, pk=pk)
|
|
|
|
# Verifica se o usuário tem permissão
|
|
if not proposicao.autor.operadores.filter(id=request.user.id).exists():
|
|
from django.contrib import messages
|
|
from django.shortcuts import redirect
|
|
messages.error(request, 'Você não tem permissão para editar esta proposição.')
|
|
return redirect('sapl.materia:proposicao_detail', pk=pk)
|
|
|
|
# Verifica se já foi enviada
|
|
if proposicao.data_envio:
|
|
from django.contrib import messages
|
|
from django.shortcuts import redirect
|
|
messages.warning(request, 'Esta proposição já foi enviada e não pode mais ser editada.')
|
|
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"
|
|
|
|
context = {
|
|
'proposicao': proposicao,
|
|
'onlyoffice_url': onlyoffice_url,
|
|
'config_url': reverse('sapl.materia:onlyoffice_config', kwargs={'pk': pk}),
|
|
}
|
|
|
|
return render(request, 'materia/onlyoffice_editor.html', context)
|
|
|