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.
186 lines
6.3 KiB
186 lines
6.3 KiB
from django.db import models
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
|
|
class EventoRecebido(models.Model):
|
|
"""Dedupe do Componente A (spec §3): uma linha por evento entregue pelo hub.
|
|
|
|
Chave repetida significa reentrega — a resposta devolve a proposição já
|
|
criada, sem efeito colateral. Nada além do mínimo do dedupe persiste aqui
|
|
(spec §6).
|
|
"""
|
|
|
|
chave_idempotencia = models.UUIDField(
|
|
unique=True,
|
|
verbose_name=_('Chave de Idempotência'))
|
|
|
|
proposicao = models.ForeignKey(
|
|
'materia.Proposicao',
|
|
on_delete=models.PROTECT,
|
|
related_name='+',
|
|
verbose_name=_('Proposição'))
|
|
|
|
recebido_em = models.DateTimeField(
|
|
auto_now_add=True,
|
|
verbose_name=_('Recebido em'))
|
|
|
|
class Meta:
|
|
verbose_name = _('Evento Recebido do Hub')
|
|
verbose_name_plural = _('Eventos Recebidos do Hub')
|
|
permissions = (
|
|
('pode_integrar', _('Pode operar a integração com o hub')),
|
|
)
|
|
|
|
def __str__(self):
|
|
return str(self.chave_idempotencia)
|
|
|
|
|
|
def _caminho_anexo(instancia, nome):
|
|
return 'integracao_hub/proposicao_%s/%s' % (instancia.proposicao_id, nome)
|
|
|
|
|
|
def _caminho_pdf_alvo(instancia, nome):
|
|
return 'integracao_hub/materia_%s/%s' % (instancia.materia_id, nome)
|
|
|
|
|
|
class DocumentoParaAssinatura(models.Model):
|
|
"""O PDF-alvo persistido da matéria — o invariante do documento único (§5).
|
|
|
|
O PDF que o SAPL assina, o que o app exibe e o que aparece assinado são o
|
|
MESMO binário: gerado uma única vez pela materialização (management command
|
|
`materializar_pdfs_para_assinatura`) e referenciado por hash em cada salto.
|
|
Vive neste app isolado, não em `MateriaLegislativa`, pelo mesmo racional do
|
|
`AnexoProposicao`: custo zero de rebase do fork (refinamento §10).
|
|
|
|
`hash_origem` é o sha256 do `texto_original` usado na geração — é ele que
|
|
detecta retificação: mudou o texto depois da conversão, o alvo está defasado
|
|
e o processo de assinatura zera (decisão do arquiteto 19/08, §5.1).
|
|
"""
|
|
|
|
materia = models.OneToOneField(
|
|
'materia.MateriaLegislativa',
|
|
on_delete=models.PROTECT,
|
|
related_name='documento_para_assinatura',
|
|
verbose_name=_('Matéria Legislativa'))
|
|
|
|
arquivo = models.FileField(
|
|
upload_to=_caminho_pdf_alvo,
|
|
verbose_name=_('PDF-alvo da assinatura'))
|
|
|
|
hash_sha256 = models.CharField(
|
|
max_length=64,
|
|
verbose_name=_('SHA-256 do PDF-alvo'))
|
|
|
|
hash_origem = models.CharField(
|
|
max_length=64,
|
|
verbose_name=_('SHA-256 do texto_original usado na geração'))
|
|
|
|
gerado_em = models.DateTimeField(
|
|
auto_now=True,
|
|
verbose_name=_('Gerado em'))
|
|
|
|
class Meta:
|
|
verbose_name = _('Documento para Assinatura')
|
|
verbose_name_plural = _('Documentos para Assinatura')
|
|
|
|
def __str__(self):
|
|
return 'PDF-alvo da matéria %s' % self.materia_id
|
|
|
|
|
|
class AssinaturaRecebida(models.Model):
|
|
"""Dedupe do `POST /api/integracao/assinaturas/` — padrão do EventoRecebido.
|
|
|
|
Uma linha por assinatura entregue pelo hub; chave repetida é reentrega e
|
|
devolve a mesma resposta sem efeito colateral. `hash_assinado` fica aqui
|
|
para a reentrega responder o que a primeira entrega respondeu. A linha
|
|
também marca a ORIGEM da gravação (anti-eco do refinamento §5.1): o hub
|
|
correlaciona o que ele mesmo entregou.
|
|
"""
|
|
|
|
chave_idempotencia = models.UUIDField(
|
|
unique=True,
|
|
verbose_name=_('Chave de Idempotência'))
|
|
|
|
materia = models.ForeignKey(
|
|
'materia.MateriaLegislativa',
|
|
on_delete=models.PROTECT,
|
|
related_name='+',
|
|
verbose_name=_('Matéria Legislativa'))
|
|
|
|
hash_assinado = models.CharField(
|
|
max_length=64,
|
|
verbose_name=_('SHA-256 do PDF assinado recebido'))
|
|
|
|
operado_por = models.CharField(
|
|
max_length=150,
|
|
blank=True,
|
|
verbose_name=_('Operado por'),
|
|
help_text=_(
|
|
'Rastro operacional: quem DISPAROU o ato (o próprio vereador ou um '
|
|
'assessor agindo por ele). NÃO é a autoria jurídica — o signatário '
|
|
'é sempre o vereador titular (assinatura é ato pessoal e '
|
|
'indelegável). Hoje o evento do app ainda não carrega a identidade '
|
|
'do assessor logado; até lá recebe o próprio titular.'))
|
|
|
|
recebido_em = models.DateTimeField(
|
|
auto_now_add=True,
|
|
verbose_name=_('Recebido em'))
|
|
|
|
class Meta:
|
|
verbose_name = _('Assinatura Recebida do Hub')
|
|
verbose_name_plural = _('Assinaturas Recebidas do Hub')
|
|
|
|
def __str__(self):
|
|
return str(self.chave_idempotencia)
|
|
|
|
|
|
class AnexoProposicao(models.Model):
|
|
"""Anexo GERAL vindo do app (foto, vídeo, qualquer mídia) — não é o texto oficial.
|
|
|
|
Decisão do arquiteto (17/08/2026): o que o vereador anexa no app é evidência da
|
|
demanda (foto da rua, vídeo da indicação), não o documento legislativo — este o SAPL
|
|
continua gerando pelo fluxo próprio (texto_original). Por ora só GRAVA, sem tela: a
|
|
consulta vem depois. Vive neste app isolado para custo zero de rebase do fork.
|
|
|
|
O hash é calculado AQUI, dos bytes recebidos: é o SAPL dizendo o que ele guardou,
|
|
fechando a cadeia app -> hub -> legislativo (o hub confere o hash do app antes de
|
|
entregar; nós registramos o que chegou).
|
|
"""
|
|
|
|
proposicao = models.ForeignKey(
|
|
'materia.Proposicao',
|
|
on_delete=models.PROTECT,
|
|
related_name='anexos_do_app',
|
|
verbose_name=_('Proposição'))
|
|
|
|
arquivo = models.FileField(
|
|
upload_to=_caminho_anexo,
|
|
verbose_name=_('Arquivo'))
|
|
|
|
nome_original = models.CharField(
|
|
max_length=255,
|
|
verbose_name=_('Nome original'))
|
|
|
|
mime = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
verbose_name=_('Tipo MIME'))
|
|
|
|
tamanho_bytes = models.BigIntegerField(
|
|
verbose_name=_('Tamanho (bytes)'))
|
|
|
|
hash_sha256 = models.CharField(
|
|
max_length=64,
|
|
verbose_name=_('SHA-256 dos bytes recebidos'))
|
|
|
|
recebido_em = models.DateTimeField(
|
|
auto_now_add=True,
|
|
verbose_name=_('Recebido em'))
|
|
|
|
class Meta:
|
|
verbose_name = _('Anexo de Proposição (app)')
|
|
verbose_name_plural = _('Anexos de Proposição (app)')
|
|
|
|
def __str__(self):
|
|
return '%s (%s)' % (self.nome_original, self.proposicao_id)
|
|
|
|
|