Sistema de Apoio ao Processo Legislativo
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.
 
 
 
 
 

647 lines
23 KiB

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields.jsonb import JSONField
from django.core.cache import cache
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.db.models.deletion import CASCADE, PROTECT
from django.db.models.signals import post_migrate
from django.db.utils import DEFAULT_DB_ALIAS
from django.utils.translation import ugettext_lazy as _
from sapl.utils import (LISTA_DE_UFS, YES_NO_CHOICES,
get_settings_auth_user_model, models_with_gr_for_model)
TIPO_CONTEUDO_TEMPLATE = (
('proposicao', _('Proposição')),
('materia', _('Matéria Legislativa')),
('docacessorio', _('Documento Acessório')),
('docadm', _('Documento Administrativo')),
('norma', _('Norma Jurídica')),
)
DOC_ADM_OSTENSIVO = 'O'
DOC_ADM_RESTRITIVO = 'R'
TIPO_DOCUMENTO_ADMINISTRATIVO = ((DOC_ADM_OSTENSIVO, _('Ostensiva')),
(DOC_ADM_RESTRITIVO, _('Restritiva')))
RELATORIO_ATOS_ACESSADOS = (('S', _('Sim')),
('N', _('Não')))
SEQUENCIA_NUMERACAO_PROTOCOLO = (('A', _('Sequencial por ano')),
('L', _('Sequencial por legislatura')),
('U', _('Sequencial único')))
SEQUENCIA_NUMERACAO_PROPOSICAO = (('A', _('Sequencial por ano para cada autor')),
('B', _('Sequencial por ano indepententemente do autor')))
ESFERA_FEDERACAO_CHOICES = (('M', _('Municipal')),
('E', _('Estadual')),
('F', _('Federal')),
)
ASSINATURA_ATA_CHOICES = (
('M', _('Mesa Diretora da Sessão')),
('P', _('Apenas o Presidente da Sessão')),
('T', _('Todos os Parlamentares Presentes na Sessão')),
)
TIPO_NOME_ASSINATURA = (
('P', _('Nome Político (Parlamentar)')),
('C', _('Nome Civil (Real)')),
)
ORDENACAO_PESQUISA_MATERIA = (
('S', _('Alfabética por Sigla')),
('R', _('Sequência Regimental')),
)
class CasaLegislativa(models.Model):
# TODO ajustar todos os max_length !!!!
# cod_casa => id (pk)
codigo = models.CharField(max_length=100,
blank=True,
verbose_name=_('Codigo'))
nome = models.CharField(max_length=100, verbose_name=_('Nome'))
sigla = models.CharField(max_length=100, verbose_name=_('Sigla'))
endereco = models.CharField(max_length=100, verbose_name=_('Endereço'))
cep = models.CharField(max_length=100, blank=True, verbose_name=_('CEP'))
municipio = models.CharField(max_length=50, verbose_name=_('Município'))
uf = models.CharField(max_length=2,
choices=LISTA_DE_UFS,
verbose_name=_('UF'))
telefone = models.CharField(
max_length=100, blank=True, verbose_name=_('Telefone'))
fax = models.CharField(
max_length=100, blank=True, verbose_name=_('Fax'))
logotipo = models.ImageField(
blank=True,
upload_to='sapl/public/casa/logotipo/',
verbose_name=_('Logotipo'))
endereco_web = models.URLField(
max_length=100, blank=True, verbose_name=_('HomePage'))
email = models.EmailField(
max_length=100, blank=True, verbose_name=_('E-mail'))
informacao_geral = models.TextField(
max_length=100,
blank=True,
verbose_name=_('Informação Geral'))
class Meta:
verbose_name = _('Casa Legislativa')
verbose_name_plural = _('Casa Legislativa')
ordering = ('id',)
def __str__(self):
return _('Casa Legislativa de %(municipio)s') % {
'municipio': self.municipio}
class AppConfig(models.Model):
POLITICA_PROTOCOLO_CHOICES = (
('O', _('Sempre Gerar Protocolo')),
('C', _('Perguntar se é pra gerar protocolo ao incorporar')),
('N', _('Nunca Protocolar ao incorporar uma proposição')),
)
# MANTENHA A SEQUÊNCIA EQUIVALENTE COM /sapl/templates/base/layout.yaml
# AppConfig:
# CONFIGURAÇÕES GERAIS
# Linha 1 ------------
esfera_federacao = models.CharField(
max_length=1,
blank=True,
default="",
verbose_name=_('Esfera Federação'),
choices=ESFERA_FEDERACAO_CHOICES)
# MÓDULO PARLAMENTARES
# MÓDULO MESA DIRETORA
# MÓDULO COMISSÕES
# MÓDULO BANCADAS PARLAMENTARES
# MÓDULO DOCUMENTOS ADMINISTRATIVOS
# Linha 1 -------------------------
documentos_administrativos = models.CharField(
max_length=1,
verbose_name=_('Visibilidade dos Documentos Administrativos'),
choices=TIPO_DOCUMENTO_ADMINISTRATIVO, default='O')
tramitacao_documento = models.BooleanField(
verbose_name=_(
'Tramitar documentos anexados junto com os documentos principais?'),
choices=YES_NO_CHOICES, default=True)
# Linha 2 -------------------------
protocolo_manual = models.BooleanField(
verbose_name=_('Permitir informe manual de data e hora de protocolo?'),
choices=YES_NO_CHOICES, default=False)
sequencia_numeracao_protocolo = models.CharField(
max_length=1,
verbose_name=_('Sequência de numeração de protocolos'),
choices=SEQUENCIA_NUMERACAO_PROTOCOLO, default='A')
inicio_numeracao_protocolo = models.PositiveIntegerField(
verbose_name=_('Início da numeração de protocolo'),
default=1
)
# Linha 3 -------------------------
identificacao_de_documentos = models.CharField(
max_length=254,
verbose_name=_('Formato da identificação dos documentos'),
default='{sigla}{numero}/{ano}{-}{complemento} - {nome}',
help_text="""
Como mostrar a identificação dos documentos administrativos?
Você pode usar um conjunto de combinações que pretender.
Ao fazer sua edição, será mostrado logo abaixo o último documento cadastrado, como exemplo de resultado de sua edição.
Em caso de erro, nenhum documento será mostrado e aparecerá apenas o formato padrão mínimo, que é este: "{sigla}{numero}/{ano}{-}{complemento} - {nome}".
Muito importante, use as chaves "{}", sem elas, você estará inserindo um texto qualquer e não o valor de um campo.
Você pode combinar as seguintes campos: {sigla} {nome} {numero} {ano} {complemento} {assunto}
Ainda pode ser usado {/}, {-}, {.} se você quiser que uma barra, traço, ou ponto
seja adicionado apenas se o próximo campo que será usado tenha algum conteúdo
(não use dois destes destes condicionais em sequência, somente o último será considerado).
"""
)
# MÓDULO PROPOSIÇÕES
# Linha 1 ----------
sequencia_numeracao_proposicao = models.CharField(
max_length=1,
verbose_name=_('Sequência de numeração de proposições'),
choices=SEQUENCIA_NUMERACAO_PROPOSICAO, default='A')
receber_recibo_proposicao = models.BooleanField(
verbose_name=_('Protocolar proposição somente com recibo?'),
choices=YES_NO_CHOICES, default=True)
proposicao_incorporacao_obrigatoria = models.CharField(
verbose_name=_('Regra de incorporação de proposições e protocolo'),
max_length=1, choices=POLITICA_PROTOCOLO_CHOICES, default='O')
escolher_numero_materia_proposicao = models.BooleanField(
verbose_name=_(
'Indicar número da matéria a ser gerada na proposição?'),
choices=YES_NO_CHOICES, default=False)
revisao_setor_legislativo = models.BooleanField(
verbose_name=_(
'Habilitar etapa de revisão pelo Setor Legislativo?'),
choices=YES_NO_CHOICES, default=False)
# MÓDULO MATÉRIA LEGISLATIVA
# Linha 1 ------------------
tramitacao_origem_fixa = models.BooleanField(
verbose_name=_(
'Fixar origem de novas tramitações como sendo a tramitação de destino da última tramitação?'),
choices=YES_NO_CHOICES,
default=True,
help_text=_('Ao utilizar a opção NÂO, você compreende que os controles '
'de origem e destino das tramitações são anulados, '
'podendo seu operador registrar quaisquer origem e '
'destino para as tramitações. Se você colocar Não, '
'fizer tramitações aleatórias e voltar para SIM, '
'o destino da tramitação mais recente será utilizado '
'para a origem de uma nova inserção!'))
tramitacao_materia = models.BooleanField(
verbose_name=_(
'Tramitar matérias anexadas junto com as matérias principais?'),
choices=YES_NO_CHOICES, default=True)
ordenacao_pesquisa_materia = models.CharField(
max_length=1,
verbose_name=_(
'Ordenação de Pesquisa da Matéria?'),
choices=ORDENACAO_PESQUISA_MATERIA, default='S')
# MÓDULO NORMAS JURÍDICAS
# MÓDULO TEXTOS ARTICULADOS
# Linha 1 -----------------
texto_articulado_proposicao = models.BooleanField(
verbose_name=_('Usar Textos Articulados para Proposições'),
choices=YES_NO_CHOICES, default=False)
texto_articulado_materia = models.BooleanField(
verbose_name=_('Usar Textos Articulados para Matérias'),
choices=YES_NO_CHOICES, default=False)
texto_articulado_norma = models.BooleanField(
verbose_name=_('Usar Textos Articulados para Normas'),
choices=YES_NO_CHOICES, default=True)
# MÓDULO SESSÃO PLENÁRIA
assinatura_ata = models.CharField(
verbose_name=_('Quem deve assinar a ata'),
max_length=1, choices=ASSINATURA_ATA_CHOICES, default='T')
# MÓDULO ASSINATURA DIGITAL
assinatura_nome = models.CharField(
max_length=1,
verbose_name=_('Nome utilizado na assinatura digital'),
choices=TIPO_NOME_ASSINATURA,
default='P')
permite_remover_assinatura = models.BooleanField(
verbose_name=_('Permitir remoção de assinatura digital?'),
choices=YES_NO_CHOICES,
default=False,
help_text=_(
'Quando habilitado, superusuários e usuários com a permissão '
'"Pode remover assinatura digital" poderão remover assinaturas '
'de matérias e documentos acessórios para permitir edições.'
)
)
# MÓDULO PAINEL
cronometro_discurso = models.DurationField(
verbose_name=_('Cronômetro do Discurso'),
blank=True,
null=True)
cronometro_aparte = models.DurationField(
verbose_name=_('Cronômetro do Aparte'),
blank=True,
null=True)
cronometro_ordem = models.DurationField(
verbose_name=_('Cronômetro da Ordem'),
blank=True,
null=True)
cronometro_consideracoes = models.DurationField(
verbose_name=_('Cronômetro de Considerações Finais'),
blank=True,
null=True)
mostrar_brasao_painel = models.BooleanField(
default=False,
verbose_name=_('Mostrar brasão da Casa no painel?'))
mostrar_voto = models.BooleanField(
verbose_name=_(
'Exibir voto do Parlamentar antes de encerrar a votação?'),
choices=YES_NO_CHOICES, default=False)
# MÓDULO ESTATÍSTICAS DE ACESSO
estatisticas_acesso_normas = models.CharField(
max_length=1,
verbose_name=_('Estatísticas de acesso a normas'),
choices=RELATORIO_ATOS_ACESSADOS, default='N')
# MÓDULO SEGURANÇA
# MÓDULO LEXML
# TODO: a ser implementado na versão 3.2
# painel_aberto = models.BooleanField(
# verbose_name=_('Painel aberto para usuário anônimo'),
# choices=YES_NO_CHOICES, default=False)
google_recaptcha_site_key = models.CharField(
verbose_name=_('Chave pública gerada pelo Google Recaptcha'),
max_length=256, default='')
google_recaptcha_secret_key = models.CharField(
verbose_name=_('Chave privada gerada pelo Google Recaptcha'),
max_length=256, default='')
google_analytics_id_metrica = models.CharField(
verbose_name=_('ID da Métrica do Google Analytics'),
max_length=256, default='')
class Meta:
verbose_name = _('Configurações da Aplicação')
verbose_name_plural = _('Configurações da Aplicação')
permissions = (
('menu_sistemas', _('Renderizar Menu Sistemas')),
('view_tabelas_auxiliares', _('Visualizar Parâmetros')),
)
ordering = ('-id',)
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
fields = self._meta.get_fields()
for f in fields:
if f.name != 'id' and not cache.get(f'sapl_{f.name}') is None:
cache.set(f'sapl_{f.name}', getattr(self, f.name), 600)
return models.Model.save(self, force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
@classmethod
def attr(cls, attr):
value = cache.get(f'sapl_{attr}')
if not value is None:
return value
config = AppConfig.objects.first()
if not config:
config = AppConfig()
config.save()
value = getattr(config, attr)
cache.set(f'sapl_{attr}', value, 600)
return value
def __str__(self):
return _('Configurações da Aplicação - %(id)s') % {
'id': self.id}
class TipoAutor(models.Model):
descricao = models.CharField(
max_length=50,
verbose_name=_('Descrição'),
help_text=_(
'Obs: Não crie tipos de autores semelhante aos tipos fixos. ')
)
content_type = models.OneToOneField(
ContentType,
null=True,
default=None,
verbose_name=_('Modelagem no SGVP'),
on_delete=models.PROTECT)
class Meta:
ordering = ['descricao']
verbose_name = _('Tipo de Autor')
verbose_name_plural = _('Tipos de Autor')
def __str__(self):
return self.descricao
class Autor(models.Model):
operadores = models.ManyToManyField(
get_settings_auth_user_model(),
through='OperadorAutor',
through_fields=('autor', 'user'),
symmetrical=False,
related_name='autor_set',
verbose_name='Operadores')
tipo = models.ForeignKey(
TipoAutor,
verbose_name=_('Tipo do Autor'),
on_delete=models.PROTECT)
content_type = models.ForeignKey(
ContentType,
blank=True,
null=True,
default=None,
on_delete=models.PROTECT)
object_id = models.PositiveIntegerField(
blank=True,
null=True,
default=None)
autor_related = GenericForeignKey('content_type', 'object_id')
nome = models.CharField(
max_length=120,
blank=True,
verbose_name=_('Nome do Autor'))
cargo = models.CharField(
max_length=50,
blank=True)
class Meta:
verbose_name = _('Autor')
verbose_name_plural = _('Autores')
unique_together = (('content_type', 'object_id'), )
ordering = ('nome',)
def __str__(self):
if self.autor_related:
return str(self.autor_related)
else:
if self.nome:
if self.cargo:
return '{} - {}'.format(self.nome, self.cargo)
else:
return str(self.nome)
return '?'
class OperadorAutor(models.Model):
user = models.ForeignKey(
get_settings_auth_user_model(),
verbose_name=_('Operador do Autor'),
related_name='operadorautor_set',
on_delete=CASCADE)
autor = models.ForeignKey(
Autor,
related_name='operadorautor_set',
verbose_name=_('Autor'),
on_delete=CASCADE)
@property
def user_name(self):
return '%s - %s' % (
self.autor,
self.user)
class Meta:
verbose_name = _('Operador do Autor')
verbose_name_plural = _('Operadores do Autor')
unique_together = (
('user', 'autor', ),)
def __str__(self):
return self.user_name
class AuditLog(models.Model):
operation = ('C', 'D', 'U')
MAX_DATA_LENGTH = 4096 # 4KB de texto
username = models.CharField(max_length=100,
verbose_name=_('username'),
blank=True,
db_index=True)
operation = models.CharField(max_length=1,
verbose_name=_('operation'),
db_index=True)
timestamp = models.DateTimeField(verbose_name=_('timestamp'),
db_index=True)
# DEPRECATED FIELD! TO BE REMOVED (EVENTUALLY)
object = models.CharField(max_length=MAX_DATA_LENGTH,
blank=True,
verbose_name=_('object'))
data = JSONField(null=True, verbose_name=_('data'))
object_id = models.PositiveIntegerField(verbose_name=_('object_id'),
db_index=True)
model_name = models.CharField(max_length=100,
verbose_name=_('model'),
db_index=True)
app_name = models.CharField(max_length=100,
verbose_name=_('app'),
db_index=True)
class Meta:
verbose_name = _('AuditLog')
verbose_name_plural = _('AuditLogs')
ordering = ('-id', '-timestamp')
def __str__(self):
return "[%s] %s %s.%s %s" % (self.timestamp,
self.operation,
self.app_name,
self.model_name,
self.username,
)
class Metadata(models.Model):
content_type = models.ForeignKey(
ContentType,
blank=True,
null=True,
default=None,
on_delete=models.PROTECT)
object_id = models.PositiveIntegerField(
blank=True,
null=True,
default=None)
content_object = GenericForeignKey('content_type', 'object_id')
metadata = JSONField(
verbose_name=_('Metadados'),
blank=True, null=True, default=None, encoder=DjangoJSONEncoder)
class Meta:
verbose_name = _('Metadado')
verbose_name_plural = _('Metadados')
unique_together = (('content_type', 'object_id'), )
def __str__(self):
return f'Metadata de {self.content_object}'
class DocumentTemplate(models.Model):
"""
Template de documento para ser usado na criação de novos documentos
via OnlyOffice. Permite definir cabeçalho, rodapé e formatação padrão.
"""
nome = models.CharField(
max_length=100,
verbose_name=_('Nome do Template'))
descricao = models.TextField(
blank=True,
verbose_name=_('Descrição'))
tipo_conteudo = models.CharField(
max_length=20,
choices=TIPO_CONTEUDO_TEMPLATE,
verbose_name=_('Tipo de Conteúdo'),
help_text=_('Tipo de documento ao qual este template se aplica'))
# GenericFK para tipo específico (opcional)
# Ex: TipoMateriaLegislativa, TipoProposicao, etc.
content_type = models.ForeignKey(
ContentType,
null=True,
blank=True,
on_delete=PROTECT,
verbose_name=_('Tipo Específico'),
help_text=_('Tipo específico de documento (ex: Projeto de Lei, Requerimento)'))
object_id = models.PositiveIntegerField(
null=True,
blank=True,
verbose_name=_('ID do Tipo Específico'))
tipo_especifico = GenericForeignKey('content_type', 'object_id')
arquivo = models.FileField(
upload_to='templates/',
verbose_name=_('Arquivo do Template'),
help_text=_('Arquivo .docx com o template do documento'))
ativo = models.BooleanField(
default=True,
verbose_name=_('Ativo'))
padrao = models.BooleanField(
default=False,
verbose_name=_('Padrão'),
help_text=_('Se marcado, será o template padrão para este tipo de conteúdo'))
data_criacao = models.DateTimeField(
auto_now_add=True,
verbose_name=_('Data de Criação'))
data_modificacao = models.DateTimeField(
auto_now=True,
verbose_name=_('Data de Modificação'))
class Meta:
verbose_name = _('Template de Documento')
verbose_name_plural = _('Templates de Documentos')
ordering = ['tipo_conteudo', 'nome']
def __str__(self):
if self.tipo_especifico:
return f'{self.nome} ({self.get_tipo_conteudo_display()} - {self.tipo_especifico})'
return f'{self.nome} ({self.get_tipo_conteudo_display()})'
def save(self, *args, **kwargs):
# Se este template está sendo marcado como padrão,
# desmarcar outros templates padrão do mesmo tipo
if self.padrao:
qs = DocumentTemplate.objects.filter(
tipo_conteudo=self.tipo_conteudo,
padrao=True
)
if self.pk:
qs = qs.exclude(pk=self.pk)
# Se tem tipo específico, filtrar apenas os do mesmo tipo
if self.content_type and self.object_id:
qs = qs.filter(
content_type=self.content_type,
object_id=self.object_id
)
else:
# Se não tem tipo específico, desmarcar apenas os genéricos
qs = qs.filter(content_type__isnull=True)
qs.update(padrao=False)
super().save(*args, **kwargs)
@classmethod
def get_template_for(cls, tipo_conteudo, tipo_especifico=None):
"""
Busca o template mais adequado para o tipo de conteúdo.
Prioridade: específico > genérico > None
Args:
tipo_conteudo: string com o tipo (proposicao, materia, etc.)
tipo_especifico: objeto do tipo específico (TipoMateriaLegislativa, etc.)
Returns:
DocumentTemplate ou None
"""
# Primeiro, tenta encontrar template específico
if tipo_especifico:
ct = ContentType.objects.get_for_model(tipo_especifico)
template = cls.objects.filter(
tipo_conteudo=tipo_conteudo,
content_type=ct,
object_id=tipo_especifico.pk,
ativo=True
).order_by('-padrao', '-data_modificacao').first()
if template:
return template
# Se não encontrou específico, busca genérico
template = cls.objects.filter(
tipo_conteudo=tipo_conteudo,
content_type__isnull=True,
ativo=True
).order_by('-padrao', '-data_modificacao').first()
return template