Impede presenças duplicadas de parlamentar na mesma sessão - #3850
Open
joaohortsenado wants to merge 1 commit into
Open
Impede presenças duplicadas de parlamentar na mesma sessão#3850joaohortsenado wants to merge 1 commit into
joaohortsenado wants to merge 1 commit into
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.postePresencaOrdemDiaView.post.presentes_bancovem dobanco como inteiros e
marcadosvem do POST como strings, entãoset(presentes_banco) - set(marcados)nunca casava nada: o efeito era apagar todas aspresenç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/3922André Drebes,3923/3924Brizolinha,3925/3926Dirceu Alchieri… paresadjacentes 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:
A correção age em quatro frentes:
sapl/sessao/views.py— compara os ids com o mesmo tipo e passa a criar apenas quemainda 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.
sapl/sessao/views.py—bulk_create(ignore_conflicts=True), para que a submissãoconcorrente descarte a inserção repetida em vez de estourar
IntegrityErrorna cara dooperador.
sapl/sessao/models.py+migrations/0070—unique_together (sessao_plenaria, parlamentar)nos dois modelos. É essa a proteção efetiva contraconcorrê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.
sapl/relatorios/views.py— conta sessões distintas em vez de linhas, para que basesainda 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 escritoem nenhum ponto do código (ainda assim, a migração prefere preservar uma linha com
data_sessaopreenchida, caso exista). É destrutivo por natureza e vale um olhar antes domerge.
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 restauradodo dump de produção da Câmara Municipal de Agudo/RS.
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.
Migração aplicada ao banco de Agudo restaurado —
sessao_sessaoplenariapresencafoi de24.707 para 24.698 linhas, exatamente as 9 excedentes;
sessao_presencaordemdiapermaneceuem 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.
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:
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.
Testes de regressão — 5 novos em
sapl/sessao/tests/test_sessao_view.py. Verificado que3 deles falham sem a correção (
preserva_registros_ao_salvar_novamente,preserva_registros_ao_resalvar,presenca_unica_por_sessao_e_parlamentar) e que todos os5 passam com ela.
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.
flake8sem apontamentos nas linhas alteradas.Escopo executado:
pytest sapl/sessao/ sapl/relatorios/— 29 passam, 8 falham. As 8 falhas sãoanteriores a este PR: confirmei rodando a mesma suíte com todas as alterações revertidas,
que dá exatamente as mesmas 8 (
TestResumoView, cujosetUpnão populaself.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
Checklist:
Sobre o último item: os testes novos passam e nenhum teste existente foi quebrado por este PR,
mas as suítes de
sessaoerelatoriosjá 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.