Skip to content

Impede presenças duplicadas de parlamentar na mesma sessão - #3850

Open
joaohortsenado wants to merge 1 commit into
3.1.xfrom
fix/presencas-duplicadas-sessao
Open

Impede presenças duplicadas de parlamentar na mesma sessão#3850
joaohortsenado wants to merge 1 commit into
3.1.xfrom
fix/presencas-duplicadas-sessao

Conversation

@joaohortsenado

Copy link
Copy Markdown
Contributor

Descrição

O relatório de presença exibia percentuais acima de 100%. A contagem não estava errada: a
base tinha mesmo duas linhas de presença do mesmo parlamentar na mesma sessão, e o relatório
contava linhas com Count('id').

A origem está em PresencaView.post e PresencaOrdemDiaView.post. presentes_banco vem do
banco como inteiros e marcados vem do POST como strings, então
set(presentes_banco) - set(marcados) nunca casava nada: o efeito era apagar todas as
presenças da sessão a cada salvamento e recriá-las em seguida
. Sequencialmente o resultado
final é correto, mas duas submissões concorrentes do formulário — um duplo clique em Salvar —
atravessam juntas a janela entre o apagar e o recriar e gravam duas linhas por parlamentar.
Nenhum dos dois modelos tinha restrição de unicidade para impedir isso.

Os ids gravados confirmam o mecanismo: em Capanema, a sessão nº 22 de 30/06/2025 tem
3921/3922 André Drebes, 3923/3924 Brizolinha, 3925/3926 Dirceu Alchieri… pares
adjacentes intercalados na ordem alfabética da tela, que é a assinatura de dois laços
concorrentes percorrendo a mesma lista. A base de Agudo/RS tem o mesmo padrão.

Não é um caso isolado — é o que acontece em qualquer Casa em que alguém deu duplo clique em
Salvar:

base presença em sessão presença na ordem do dia
Capanema/PR 625 linhas excedentes em 313 sessões 526 linhas em 283 sessões
Agudo/RS 9 linhas em 1 sessão nenhuma

A correção age em quatro frentes:

  1. sapl/sessao/views.py — compara os ids com o mesmo tipo e passa a criar apenas quem
    ainda não tem presença registrada, sem apagar e recriar quem permanece marcado. Além de
    corrigir o defeito, para de trocar os ids das presenças a cada salvamento.
  2. sapl/sessao/views.pybulk_create(ignore_conflicts=True), para que a submissão
    concorrente descarte a inserção repetida em vez de estourar IntegrityError na cara do
    operador.
  3. sapl/sessao/models.py + migrations/0070unique_together (sessao_plenaria, parlamentar) nos dois modelos. É essa a proteção efetiva contra
    concorrência; a lógica da view sozinha não fecha a janela. A migração remove as duplicatas
    existentes antes de criar a restrição.
  4. sapl/relatorios/views.py — conta sessões distintas em vez de linhas, para que bases
    ainda não migradas não exibam percentuais impossíveis.

Ponto que merece atenção na revisão

A migração apaga linhas em produção de todas as Casas hospedadas. O que sustenta essa
decisão: presença é um sim/não, então as linhas repetidas não carregam informação adicional; e
data_sessao, único campo que poderia diferenciá-las, é campo morto — não é lido nem escrito
em nenhum ponto do código (ainda assim, a migração prefere preservar uma linha com
data_sessao preenchida, caso exista). É destrutivo por natureza e vale um olhar antes do
merge.

Issue Relacionada

OSTicket #212222 — Câmara Municipal de Capanema/PR.

Motivação e Contexto

A Casa reportou que um vereador aparecia com 46 sessões ordinárias em um período de 45
(102,22%) e 45 ordens do dia em 44 (102,27%). Percentual de presença acima de 100% num
relatório oficial não é um detalhe cosmético: é o número que a Casa publica e que os órgãos de
controle leem.

O problema segue acontecendo — nos dados atuais de Capanema já apareceu outro caso, uma
vereadora com 54 ordens do dia num total de 44 (122,73%), posterior ao print enviado no
chamado.

Como Isso Foi Testado?

Ambiente: container sapl:dev (Django 2.2.28) contra PostgreSQL 10.5, com o banco restaurado
do dump de produção da Câmara Municipal de Agudo/RS.

  1. Diagnóstico sobre dados reais — as duplicatas foram localizadas e quantificadas pela
    API pública de Capanema (/api/sessao/sessaoplenariapresenca/ e /api/sessao/presencaordemdia/),
    e confirmadas por consulta direta ao banco de Agudo.

  2. Migração aplicada ao banco de Agudo restauradosessao_sessaoplenariapresenca foi de
    24.707 para 24.698 linhas, exatamente as 9 excedentes; sessao_presencaordemdia permaneceu
    em 18.152 (não tinha duplicatas); zero pares duplicados nos dois modelos ao final; as duas
    constraints UNIQUE (sessao_plenaria_id, parlamentar_id) criadas. Na sessão afetada,
    preservou o menor id de cada par.

  3. Reprodução do relatório de Capanema, recalculando a aritmética da view sobre os dados
    da API pública, com e sem a correção:

    antes depois
    Jilmar Jablonski — sessões 46 (102,22%) 45 (100,00%)
    Jilmar Jablonski — ordens do dia 45 (102,27%) 44 (100,00%)

    Os totais do período batem com o print do chamado (45 sessões, 44 ordens do dia), e 45 é o
    número que a Casa aponta como correto.

  4. Testes de regressão — 5 novos em sapl/sessao/tests/test_sessao_view.py. Verificado que
    3 deles falham sem a correção (preserva_registros_ao_salvar_novamente,
    preserva_registros_ao_resalvar, presenca_unica_por_sessao_e_parlamentar) e que todos os
    5 passam com ela.

  5. Smoke test da nova agregação sobre o banco de Agudo, confirmando que nenhum parlamentar
    tem contagem distinta maior que o número de sessões com presença.

  6. flake8 sem apontamentos nas linhas alteradas.

Escopo executado: pytest sapl/sessao/ sapl/relatorios/ — 29 passam, 8 falham. As 8 falhas são
anteriores a este PR: confirmei rodando a mesma suíte com todas as alterações revertidas,
que dá exatamente as mesmas 8 (TestResumoView, cujo setUp não popula self.sessao_plenaria,
e test_numero_duplicado_sessao_plenaria_form). Nenhuma delas toca o código alterado aqui.

Ressalva sobre o item 3: a aritmética do relatório foi reconstruída a partir da API pública, não
executando a view contra o banco de Capanema, ao qual não tenho acesso. As contagens de outros
cinco vereadores já divergem do print de 30/07/2026, sinal de que a Casa vem editando presenças
desde então.

Capturas de Tela (se apropriado):

Não se aplica — não há mudança de interface.

Tipos de Mudanças

  • Bug fix (alteração que corrige uma issue e não altera funcionalidades já existentes)
  • Nova feature (alteração que adiciona uma funcionalidade e não altera funcionalidades já existentes)
  • Alteração disruptiva (Breaking change) (Correção ou funcionalidade que causa alteração nas funcionalidades existentes)

Checklist:

  • Eu li o documento de Contribuição (CONTRIBUTING).
  • Meu código segue o estilo de código deste projeto.
  • Minha alteração requer uma alteração na documentação.
  • Eu atualizei a documentação de acordo.
  • Eu adicionei testes para cobrir minhas mudanças.
  • Todos os testes novos e existentes passaram.

Sobre o último item: os testes novos passam e nenhum teste existente foi quebrado por este PR,
mas as suítes de sessao e relatorios já tinham 8 falhas anteriores a estas alterações —
detalhadas em "Como Isso Foi Testado?". Marquei a caixa como não atendida para não afirmar algo
que a execução não sustenta.

O relatório de presença exibia percentuais acima de 100% — em Capanema/PR,
um vereador aparecia com 46 sessões em um período de 45. A contagem não
estava errada: existiam mesmo duas linhas de presença dele na sessão nº 22
de 30/06/2025, e o relatório contava linhas com Count('id').

A origem é PresencaView/PresencaOrdemDiaView. `presentes_banco` vem do banco
como inteiros e `marcados` vem do POST como strings, então
`set(presentes_banco) - set(marcados)` nunca casava e resultava em apagar
todas as presenças da sessão a cada salvamento, recriando-as em seguida.
Sequencialmente o resultado é correto, mas duas submissões concorrentes do
formulário — um duplo clique em Salvar — passam ambas pela janela entre o
apagar e o recriar e gravam duas linhas por parlamentar. Os ids gravados em
Capanema confirmam: pares adjacentes intercalados na ordem alfabética da
tela, assinatura de dois laços concorrentes.

Nada no banco impedia isso. Não é um caso isolado: Capanema tinha 625 linhas
excedentes em 313 sessões, e a base de Agudo/RS, 9 linhas em 1 sessão.

Corrige em quatro frentes:

- compara os ids com o mesmo tipo e passa a criar apenas quem ainda não tem
  presença, sem apagar e recriar quem permanece marcado;
- usa bulk_create(ignore_conflicts=True), para que a submissão concorrente
  descarte a inserção repetida em vez de estourar IntegrityError;
- adiciona unique_together (sessao_plenaria, parlamentar) nos dois modelos,
  que é a proteção efetiva contra concorrência, com migração que remove as
  duplicatas existentes antes de criar a restrição;
- conta sessões distintas no relatório, em vez de linhas, para que bases
  ainda não migradas não exibam percentuais impossíveis.

OSTicket #212222
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant