mirror of https://github.com/interlegis/sapl.git
Browse Source
Management command para cron na VPS (refinamento §5.1): a conversao DOCX->PDF acontece UMA vez, no SAPL (dono do OnlyOffice), fora do caminho quente das requisicoes. Reusa _gerar_pdf_da_materia da sprint em vez de duplicar — PDF copia os bytes, DOCX converte. - varre materias protocoladas com texto_original; sem alvo gera, com alvo em dia pula (idempotente), hash_origem divergente e RETIFICACAO - retificacao ZERA o processo de assinatura (decisao do arquiteto, 19/08/2026): regenera o alvo e limpa pdf_assinado, assinatura_info, assinado_em, assinado_por e codigo_autenticacao — mesmo efeito da rotina remover-assinatura. Assinar texto retificado e impossivel por construcao. - falha de conversao de uma materia loga e segue: OnlyOffice fora do ar nao trava o ciclo, a pendencia aparece no ciclo seguinte Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>pull/3858/head
3 changed files with 134 additions and 0 deletions
@ -0,0 +1,134 @@ |
|||||
|
import hashlib |
||||
|
import logging |
||||
|
import os |
||||
|
|
||||
|
from django.conf import settings |
||||
|
from django.core.files.base import ContentFile |
||||
|
from django.core.management.base import BaseCommand |
||||
|
from django.db import transaction |
||||
|
|
||||
|
from sapl.integracao_hub.models import DocumentoParaAssinatura |
||||
|
from sapl.materia.models import MateriaLegislativa |
||||
|
|
||||
|
logger = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
class _RequisicaoDeSistema: |
||||
|
"""Substituto mínimo de request para reusar `_gerar_pdf_da_materia` fora do HTTP. |
||||
|
|
||||
|
A rotina da sprint só usa o request em dois pontos do caminho DOCX: |
||||
|
`build_onlyoffice_url` (que prefere `SAPL_INTERNAL_URL` e só cai no request |
||||
|
sem ela — aqui caímos em `SITE_URL`) e `generate_file_key` (pk do usuário, |
||||
|
que num cron não existe — 0 identifica o sistema). O caminho PDF não toca |
||||
|
o request. Reuso em vez de cópia: a conversão OnlyOffice tem UM dono (§5.1). |
||||
|
""" |
||||
|
|
||||
|
class _UsuarioDeSistema: |
||||
|
pk = 0 |
||||
|
|
||||
|
user = _UsuarioDeSistema() |
||||
|
|
||||
|
def build_absolute_uri(self, caminho): |
||||
|
base = getattr(settings, 'SITE_URL', '') or '' |
||||
|
return base.rstrip('/') + caminho |
||||
|
|
||||
|
|
||||
|
class Command(BaseCommand): |
||||
|
help = ('Materializa o PDF-alvo da assinatura (refinamento §5/§5.1): varre ' |
||||
|
'matérias protocoladas com texto_original, gera o PDF uma única ' |
||||
|
'vez e, em retificação do texto, regenera o alvo e ZERA o processo ' |
||||
|
'de assinatura (decisão do arquiteto, 19/08/2026). Idempotente — ' |
||||
|
'feito para cron.') |
||||
|
|
||||
|
def handle(self, *args, **options): |
||||
|
materias = (MateriaLegislativa.objects |
||||
|
.filter(numero_protocolo__isnull=False, |
||||
|
texto_original__isnull=False) |
||||
|
.exclude(texto_original='') |
||||
|
.order_by('id')) |
||||
|
|
||||
|
gerados = retificados = pulados = falhas = 0 |
||||
|
for materia in materias.iterator(): |
||||
|
try: |
||||
|
resultado = self._materializar(materia) |
||||
|
except Exception as exc: # noqa — uma matéria não trava as demais (§5.1) |
||||
|
logger.exception( |
||||
|
'materializar_pdfs: falha inesperada na matéria %s: %s', |
||||
|
materia.pk, exc) |
||||
|
falhas += 1 |
||||
|
continue |
||||
|
if resultado == 'gerado': |
||||
|
gerados += 1 |
||||
|
elif resultado == 'retificado': |
||||
|
retificados += 1 |
||||
|
elif resultado == 'falha': |
||||
|
falhas += 1 |
||||
|
else: |
||||
|
pulados += 1 |
||||
|
|
||||
|
self.stdout.write( |
||||
|
'materializar_pdfs: %s gerados, %s retificados, %s em dia, ' |
||||
|
'%s falhas' % (gerados, retificados, pulados, falhas)) |
||||
|
|
||||
|
def _materializar(self, materia): |
||||
|
materia.texto_original.open('rb') |
||||
|
try: |
||||
|
conteudo_origem = materia.texto_original.read() |
||||
|
finally: |
||||
|
materia.texto_original.close() |
||||
|
hash_origem = hashlib.sha256(conteudo_origem).hexdigest() |
||||
|
|
||||
|
alvo = DocumentoParaAssinatura.objects.filter(materia=materia).first() |
||||
|
if alvo is not None and alvo.hash_origem == hash_origem: |
||||
|
return 'em dia' # idempotência: nada mudou desde a geração |
||||
|
|
||||
|
# Reuso da rotina da sprint: PDF copia os bytes, DOCX converte no |
||||
|
# OnlyOffice — a conversão acontece UMA vez, aqui, fora do caminho |
||||
|
# quente das requisições (§5.1). |
||||
|
from sapl.materia.views_assinatura import _gerar_pdf_da_materia |
||||
|
pdf_bytes, erro = _gerar_pdf_da_materia( |
||||
|
materia, _RequisicaoDeSistema()) |
||||
|
if erro: |
||||
|
logger.error( |
||||
|
'materializar_pdfs: matéria %s não convertida (%s) — segue ' |
||||
|
'visível só na tela do SAPL até o próximo ciclo', materia.pk, |
||||
|
erro) |
||||
|
return 'falha' |
||||
|
|
||||
|
nome = 'materia_%s_alvo.pdf' % materia.pk |
||||
|
hash_alvo = hashlib.sha256(pdf_bytes).hexdigest() |
||||
|
|
||||
|
with transaction.atomic(): |
||||
|
if alvo is None: |
||||
|
alvo = DocumentoParaAssinatura( |
||||
|
materia=materia, hash_sha256=hash_alvo, |
||||
|
hash_origem=hash_origem) |
||||
|
alvo.arquivo.save(nome, ContentFile(pdf_bytes), save=True) |
||||
|
logger.info( |
||||
|
'materializar_pdfs: PDF-alvo da matéria %s gerado (%s)', |
||||
|
materia.pk, hash_alvo) |
||||
|
return 'gerado' |
||||
|
|
||||
|
# RETIFICAÇÃO (§5.1, decisão do arquiteto 19/08): texto_original |
||||
|
# mudou depois da conversão → o alvo está defasado. Regenera E zera |
||||
|
# o processo de assinatura — mesmo efeito da rotina |
||||
|
# `materia_remover_assinatura` que o SAPL já tem. Assinatura sobre |
||||
|
# texto retificado é impossível por construção. |
||||
|
alvo.arquivo.delete(save=False) |
||||
|
alvo.hash_sha256 = hash_alvo |
||||
|
alvo.hash_origem = hash_origem |
||||
|
alvo.arquivo.save(nome, ContentFile(pdf_bytes), save=True) |
||||
|
|
||||
|
if materia.pdf_assinado: |
||||
|
materia.pdf_assinado.delete(save=False) |
||||
|
materia.pdf_assinado = None |
||||
|
materia.assinatura_info = None |
||||
|
materia.assinado_em = None |
||||
|
materia.assinado_por = None |
||||
|
materia.codigo_autenticacao = None |
||||
|
materia.save() |
||||
|
|
||||
|
logger.info( |
||||
|
'materializar_pdfs: matéria %s RETIFICADA — alvo regenerado (%s) ' |
||||
|
'e assinaturas zeradas', materia.pk, hash_alvo) |
||||
|
return 'retificado' |
||||
Loading…
Reference in new issue