Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions sapl/relatorios/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2080,17 +2080,22 @@ def get_context_data(self, **kwargs):
parlamentares_id = parlamentares_qs.values_list('id', flat=True)

# Presenças de cada Parlamentar em Sessões
# Conta sessões distintas, e não linhas de presença: bases com
# presenças repetidas para o mesmo parlamentar na mesma sessão
# produziam percentuais acima de 100%.
presenca_sessao = SessaoPlenariaPresenca.objects.filter(
**param0).values_list('parlamentar_id').annotate(sessao_count=Count('id'))
**param0).values_list('parlamentar_id').annotate(
sessao_count=Count('sessao_plenaria_id', distinct=True))

# Presenças de cada Ordem do Dia
presenca_ordem = PresencaOrdemDia.objects.filter(
**param0).values_list('parlamentar_id').annotate(sessao_count=Count('id'))
**param0).values_list('parlamentar_id').annotate(
sessao_count=Count('sessao_plenaria_id', distinct=True))

# Ausencias justificadas
ausencia_justificadas = JustificativaAusencia.objects.filter(
**param0, ausencia=2).values_list('parlamentar_id')\
.annotate(sessao_count=Count('id'))
.annotate(sessao_count=Count('sessao_plenaria_id', distinct=True))

total_ordemdia = PresencaOrdemDia.objects.filter(
**param0).distinct('sessao_plenaria__id').order_by('sessao_plenaria__id').count()
Expand Down
55 changes: 55 additions & 0 deletions sapl/sessao/migrations/0070_presenca_unica_por_sessao.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Generated by Django 2.2.28 on 2026-08-05 16:00

from django.db import migrations
from django.db.models import Count


def remove_presencas_duplicadas(apps, schema_editor):
"""Mantém uma única presença por (sessão, parlamentar).

Presença é um sim/não, então as linhas repetidas não carregam informação
adicional: preserva-se a de menor id e descartam-se as demais. Em
SessaoPlenariaPresenca dá-se preferência a uma linha com data_sessao
preenchida, caso exista, para não perder o único campo que as diferencia.
"""
for model_name, tem_data_sessao in (('SessaoPlenariaPresenca', True),
('PresencaOrdemDia', False)):
model = apps.get_model('sessao', model_name)
# list() para materializar antes de apagar dentro do laço
duplicados = list(model.objects.values_list(
'sessao_plenaria_id', 'parlamentar_id').annotate(
total=Count('id')).filter(total__gt=1))

for sessao_id, parlamentar_id, _total in duplicados:
linhas = list(model.objects.filter(
sessao_plenaria_id=sessao_id,
parlamentar_id=parlamentar_id).order_by('id'))
manter = linhas[0]
if tem_data_sessao:
com_data = [x for x in linhas if x.data_sessao is not None]
if com_data:
manter = com_data[0]
model.objects.filter(
sessao_plenaria_id=sessao_id,
parlamentar_id=parlamentar_id).exclude(
id=manter.id).delete()


class Migration(migrations.Migration):

dependencies = [
('sessao', '0069_auto_20220919_1705'),
]

operations = [
migrations.RunPython(remove_presencas_duplicadas,
migrations.RunPython.noop),
migrations.AlterUniqueTogether(
name='sessaoplenariapresenca',
unique_together={('sessao_plenaria', 'parlamentar')},
),
migrations.AlterUniqueTogether(
name='presencaordemdia',
unique_together={('sessao_plenaria', 'parlamentar')},
),
]
6 changes: 6 additions & 0 deletions sapl/sessao/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,9 @@ class Meta:
verbose_name = _('Presença da Ordem do Dia')
verbose_name_plural = _('Presenças da Ordem do Dia')
ordering = ['parlamentar__nome_parlamentar']
# Presença é um sim/não: mais de uma linha para o mesmo parlamentar
# na mesma sessão infla a contagem dos relatórios.
unique_together = ('sessao_plenaria', 'parlamentar')

def __str__(self):
# FIXME ambigous
Expand Down Expand Up @@ -736,6 +739,9 @@ class Meta:
verbose_name = _('Presença em Sessão Plenária')
verbose_name_plural = _('Presenças em Sessões Plenárias')
ordering = ['parlamentar__nome_parlamentar']
# Presença é um sim/não: mais de uma linha para o mesmo parlamentar
# na mesma sessão infla a contagem dos relatórios.
unique_together = ('sessao_plenaria', 'parlamentar')


ORDENACAO_RESUMO = [
Expand Down
102 changes: 102 additions & 0 deletions sapl/sessao/tests/test_sessao_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
from datetime import date
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.db import IntegrityError, transaction
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from model_bakery import baker

from sapl.parlamentares.models import Legislatura, SessaoLegislativa
from sapl.sessao.models import (SessaoPlenaria, TipoSessaoPlenaria,
IntegranteMesa, SessaoPlenariaPresenca,
PresencaOrdemDia,
JustificativaAusencia, ExpedienteSessao,
TipoExpediente, ExpedienteMateria,
Orador, OcorrenciaSessao,
Expand Down Expand Up @@ -235,3 +237,103 @@ def test_pauta_nao_publicada_indisponivel_para_anonimo(client):
kwargs={'pk': sem_pauta.pk}))

assert response.status_code == 404


def cria_sessao_com_parlamentares():
legislatura = baker.make(Legislatura)
sessao_legislativa = baker.make(SessaoLegislativa)
tipo = baker.make(TipoSessaoPlenaria)
sessao = baker.make(SessaoPlenaria,
legislatura=legislatura,
sessao_legislativa=sessao_legislativa,
tipo=tipo)
parlamentares = [baker.make(Parlamentar, ativo=True) for _i in range(3)]
return sessao, parlamentares


@pytest.mark.django_db(transaction=False)
def test_presenca_sessao_preserva_registros_ao_salvar_novamente(admin_client):
# Cada salvamento apagava todas as presenças da sessão e as recriava,
# trocando os ids. Além do desperdício, é essa janela entre apagar e
# recriar que permite a duplicação quando o formulário é submetido duas
# vezes em paralelo. Salvar de novo deve ser inócuo.
sessao, parlamentares = cria_sessao_com_parlamentares()
url = reverse('sapl.sessao:presenca', kwargs={'pk': sessao.pk})
dados = {'presenca_ativos': [str(p.pk) for p in parlamentares]}

admin_client.post(url, dados, follow=True)
presencas = SessaoPlenariaPresenca.objects.filter(sessao_plenaria=sessao)
ids_iniciais = sorted(presencas.values_list('id', flat=True))

admin_client.post(url, dados, follow=True)
admin_client.post(url, dados, follow=True)

assert presencas.count() == len(parlamentares)
assert sorted(presencas.values_list('parlamentar_id', flat=True)) == \
sorted(p.pk for p in parlamentares)
assert sorted(presencas.values_list('id', flat=True)) == ids_iniciais


@pytest.mark.django_db(transaction=False)
def test_presenca_sessao_ignora_registro_ja_existente(admin_client):
# Reproduz o efeito da submissão concorrente: a linha já existe quando a
# view vai gravar. Deve terminar com uma única presença, sem erro.
sessao, parlamentares = cria_sessao_com_parlamentares()
parlamentar = parlamentares[0]
SessaoPlenariaPresenca.objects.create(sessao_plenaria=sessao,
parlamentar=parlamentar)

url = reverse('sapl.sessao:presenca', kwargs={'pk': sessao.pk})
response = admin_client.post(
url, {'presenca_ativos': [str(parlamentar.pk)]}, follow=True)

assert response.status_code == 200
assert SessaoPlenariaPresenca.objects.filter(
sessao_plenaria=sessao, parlamentar=parlamentar).count() == 1


@pytest.mark.django_db(transaction=False)
def test_presenca_sessao_remove_desmarcados(admin_client):
sessao, parlamentares = cria_sessao_com_parlamentares()
url = reverse('sapl.sessao:presenca', kwargs={'pk': sessao.pk})

admin_client.post(url, {'presenca_ativos': [str(p.pk)
for p in parlamentares]},
follow=True)
admin_client.post(url, {'presenca_ativos': [str(parlamentares[0].pk)]},
follow=True)

presencas = SessaoPlenariaPresenca.objects.filter(sessao_plenaria=sessao)
assert list(presencas.values_list('parlamentar_id', flat=True)) == \
[parlamentares[0].pk]


@pytest.mark.django_db(transaction=False)
def test_presenca_ordem_dia_preserva_registros_ao_resalvar(admin_client):
sessao, parlamentares = cria_sessao_com_parlamentares()
url = reverse('sapl.sessao:presencaordemdia', kwargs={'pk': sessao.pk})
dados = {'presenca_ativos': [str(p.pk) for p in parlamentares]}

admin_client.post(url, dados, follow=True)
presencas = PresencaOrdemDia.objects.filter(sessao_plenaria=sessao)
ids_iniciais = sorted(presencas.values_list('id', flat=True))

admin_client.post(url, dados, follow=True)

assert presencas.count() == len(parlamentares)
assert sorted(presencas.values_list('id', flat=True)) == ids_iniciais


@pytest.mark.django_db(transaction=False)
def test_presenca_unica_por_sessao_e_parlamentar():
# A restrição de unicidade é a proteção contra submissões concorrentes,
# que a lógica da view sozinha não cobre.
sessao, parlamentares = cria_sessao_com_parlamentares()
parlamentar = parlamentares[0]

for model in (SessaoPlenariaPresenca, PresencaOrdemDia):
model.objects.create(sessao_plenaria=sessao, parlamentar=parlamentar)
with pytest.raises(IntegrityError):
with transaction.atomic():
model.objects.create(sessao_plenaria=sessao,
parlamentar=parlamentar)
56 changes: 32 additions & 24 deletions sapl/sessao/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1420,26 +1420,31 @@ def post(self, request, *args, **kwargs):

if form.is_valid():
# Pegar os presentes salvos no banco
presentes_banco = SessaoPlenariaPresenca.objects.filter(
presentes_banco = set(SessaoPlenariaPresenca.objects.filter(
sessao_plenaria_id=self.object.id).values_list(
'parlamentar_id', flat=True).distinct()
'parlamentar_id', flat=True))

# Id dos parlamentares presentes
marcados = request.POST.getlist('presenca_ativos') \
+ request.POST.getlist('presenca_inativos')
marcados = set(int(p) for p in
request.POST.getlist('presenca_ativos')
+ request.POST.getlist('presenca_inativos'))

# Deletar os que foram desmarcados
deletar = set(presentes_banco) - set(marcados)
SessaoPlenariaPresenca.objects.filter(
parlamentar_id__in=deletar,
parlamentar_id__in=presentes_banco - marcados,
sessao_plenaria_id=self.object.id).delete()

for p in marcados:
sessao = SessaoPlenariaPresenca()
sessao.sessao_plenaria = self.object
sessao.parlamentar = Parlamentar.objects.get(id=p)
sessao.save()
username = request.user.username
# Criar apenas quem ainda não tem presença registrada. O
# ignore_conflicts descarta a inserção duplicada quando o
# formulário é submetido duas vezes em paralelo, em vez de
# gravar uma segunda linha para o mesmo parlamentar.
username = request.user.username
novos = marcados - presentes_banco
SessaoPlenariaPresenca.objects.bulk_create(
[SessaoPlenariaPresenca(sessao_plenaria=self.object,
parlamentar_id=p) for p in novos],
ignore_conflicts=True)
for p in novos:
self.logger.info(
"user=" + username + ". SessaoPlenariaPresenca salva com sucesso (parlamentar_id={})!".format(p))
msg = _('Presença em Sessão salva com sucesso!')
Expand Down Expand Up @@ -1535,26 +1540,29 @@ def post(self, request, *args, **kwargs):

if form.is_valid():
# Pegar os presentes salvos no banco
presentes_banco = PresencaOrdemDia.objects.filter(
presentes_banco = set(PresencaOrdemDia.objects.filter(
sessao_plenaria_id=self.object.id).values_list(
'parlamentar_id', flat=True).distinct()
'parlamentar_id', flat=True))

# Id dos parlamentares presentes
marcados = request.POST.getlist('presenca_ativos') \
+ request.POST.getlist('presenca_inativos')
marcados = set(int(p) for p in
request.POST.getlist('presenca_ativos')
+ request.POST.getlist('presenca_inativos'))

# Deletar os que foram desmarcados
deletar = set(presentes_banco) - set(marcados)
PresencaOrdemDia.objects.filter(
parlamentar_id__in=deletar,
parlamentar_id__in=presentes_banco - marcados,
sessao_plenaria_id=self.object.id).delete()

for p in marcados:
ordem = PresencaOrdemDia()
ordem.sessao_plenaria = self.object
ordem.parlamentar = Parlamentar.objects.get(id=p)
ordem.save()
username = request.user.username
# Criar apenas quem ainda não tem presença registrada. Ver
# comentário equivalente em PresencaView.post.
username = request.user.username
novos = marcados - presentes_banco
PresencaOrdemDia.objects.bulk_create(
[PresencaOrdemDia(sessao_plenaria=self.object,
parlamentar_id=p) for p in novos],
ignore_conflicts=True)
for p in novos:
self.logger.info(
'user=' + username + '. PresencaOrdemDia (parlamentar com id={}) salva com sucesso!'.format(p))

Expand Down