Browse Source

Oculta sessões plenárias não iniciadas da consulta pública (#3845)

* Oculta sessões plenárias não iniciadas da consulta pública

Sessões cadastradas com antecedência ficavam visíveis no portal mesmo com
"Publicar pauta?", "Sessão iniciada?" e "Sessão finalizada?" em Não: nenhum
desses campos filtrava queryset público. Só publicar_pauta filtrava, e apenas
nos dois pontos de entrada da pauta.

Passa a esconder do usuário anônimo as sessões marcadas explicitamente como
não iniciadas e sem pauta publicada. Usuário autenticado continua vendo todas.
A regra fica centralizada em SessaoPlenaria.objects.visiveis_para(user) e é
aplicada na pesquisa de sessões, no detalhe, no resumo e no resumo/ata.

iniciada nulo conta como visível: o campo nasceu sem default e o default=True
só chegou na migração 0027, que não preencheu as linhas existentes, então
bases anteriores a out/2018 têm sessões realizadas com o campo em NULL.

Também restringe PautaSessaoDetailView, que não filtrava publicar_pauta e
expunha por URL a pauta de sessões não publicadas.

* Troca custom manager por função utilitária de visibilidade

Atende à revisão do PR: substitui a subclasse SessaoPlenariaQuerySet e o
método .visiveis_para() por uma função restringe_sessoes_visiveis(qs, user),
chamada como wrapper nos pontos de acesso público. Mantém a mesma regra de
filtragem e o comportamento de fundir as cadeias de filter em um único SQL.

---------

Co-authored-by: Edward <9326037+edwardoliveira@users.noreply.github.com>
pull/3842/merge
joaohortsenado 2 days ago
committed by GitHub
parent
commit
85079b492d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 20
      sapl/sessao/models.py
  2. 92
      sapl/sessao/tests/test_sessao_view.py
  3. 21
      sapl/sessao/views.py

20
sapl/sessao/models.py

@ -153,6 +153,26 @@ def anexo_upload_path(instance, filename):
# return get_sessao_media_path(instance, 'anexo', filename) # return get_sessao_media_path(instance, 'anexo', filename)
def restringe_sessoes_visiveis(qs, user):
"""Restringe o queryset às sessões visíveis na consulta pública.
Usuário autenticado enxerga todas. Para o público, ficam ocultas as
sessões cadastradas com antecedência, ou seja, as marcadas como não
iniciadas e sem pauta publicada.
`iniciada` nulo conta como visível: o default do campo surgiu na
migração 0027, que não preencheu as linhas existentes, então bases
antigas têm sessões realizadas com o campo em NULL.
"""
if user.is_authenticated:
return qs
return qs.filter(
Q(publicar_pauta=True) |
Q(iniciada=True) |
Q(iniciada__isnull=True))
class SessaoPlenaria(models.Model): class SessaoPlenaria(models.Model):
# TODO trash??? Seems to have been a FK in the past. Would be: # TODO trash??? Seems to have been a FK in the past. Would be:
# andamento_sessao = models.ForeignKey( # andamento_sessao = models.ForeignKey(

92
sapl/sessao/tests/test_sessao_view.py

@ -1,4 +1,7 @@
import pytest import pytest
from datetime import date
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from model_bakery import baker from model_bakery import baker
@ -8,7 +11,8 @@ from sapl.sessao.models import (SessaoPlenaria, TipoSessaoPlenaria,
IntegranteMesa, SessaoPlenariaPresenca, IntegranteMesa, SessaoPlenariaPresenca,
JustificativaAusencia, ExpedienteSessao, JustificativaAusencia, ExpedienteSessao,
TipoExpediente, ExpedienteMateria, TipoExpediente, ExpedienteMateria,
Orador, OcorrenciaSessao) Orador, OcorrenciaSessao,
restringe_sessoes_visiveis)
from sapl.parlamentares.models import Parlamentar, CargoMesa, Filiacao from sapl.parlamentares.models import Parlamentar, CargoMesa, Filiacao
@ -146,4 +150,88 @@ class TestResumoView():
ocorrencia = baker.make(OcorrenciaSessao, sessao_plenaria=self.sessao_plenaria) ocorrencia = baker.make(OcorrenciaSessao, sessao_plenaria=self.sessao_plenaria)
resultado_get_ocorrencia = get_ocorrencias_da_sessao(self.sessao_plenaria) resultado_get_ocorrencia = get_ocorrencias_da_sessao(self.sessao_plenaria)
assert resultado_get_ocorrencia['ocorrencias_da_sessao'][0] == ocorrencia assert resultado_get_ocorrencia['ocorrencias_da_sessao'][0] == ocorrencia
@pytest.mark.django_db(transaction=False)
def test_visiveis_para_oculta_do_anonimo_apenas_a_sessao_previa():
previa = baker.make(SessaoPlenaria, iniciada=False, publicar_pauta=False)
com_pauta = baker.make(SessaoPlenaria, iniciada=False, publicar_pauta=True)
iniciada = baker.make(SessaoPlenaria, iniciada=True, publicar_pauta=False)
# Sessões anteriores à migração 0027 ficaram com `iniciada` em NULL.
legada = baker.make(SessaoPlenaria, iniciada=None, publicar_pauta=False)
visiveis = restringe_sessoes_visiveis(
SessaoPlenaria.objects.all(), AnonymousUser())
assert previa not in visiveis
assert com_pauta in visiveis
assert iniciada in visiveis
assert legada in visiveis
@pytest.mark.django_db(transaction=False)
def test_visiveis_para_nao_oculta_nada_de_usuario_autenticado():
previa = baker.make(SessaoPlenaria, iniciada=False, publicar_pauta=False)
operador = baker.make(get_user_model())
assert previa in restringe_sessoes_visiveis(
SessaoPlenaria.objects.all(), operador)
@pytest.mark.django_db(transaction=False)
def test_pesquisar_sessao_nao_lista_sessao_previa_para_anonimo(client):
previa = baker.make(SessaoPlenaria, iniciada=False, publicar_pauta=False,
data_inicio=date(2025, 11, 5))
iniciada = baker.make(SessaoPlenaria, iniciada=True, publicar_pauta=False,
data_inicio=date(2025, 11, 5))
response = client.get(reverse('sapl.sessao:pesquisar_sessao'),
{'data_inicio__year': '2025'})
assert response.status_code == 200
assert previa not in response.context['object_list']
assert iniciada in response.context['object_list']
@pytest.mark.django_db(transaction=False)
def test_pesquisar_sessao_lista_sessao_previa_para_autenticado(admin_client):
previa = baker.make(SessaoPlenaria, iniciada=False, publicar_pauta=False,
data_inicio=date(2025, 11, 5))
response = admin_client.get(reverse('sapl.sessao:pesquisar_sessao'),
{'data_inicio__year': '2025'})
assert response.status_code == 200
assert previa in response.context['object_list']
@pytest.mark.django_db(transaction=False)
def test_detail_sessao_previa_indisponivel_para_anonimo(client):
previa = baker.make(SessaoPlenaria, iniciada=False, publicar_pauta=False)
response = client.get(reverse('sapl.sessao:sessaoplenaria_detail',
kwargs={'pk': previa.pk}))
assert response.status_code == 404
@pytest.mark.django_db(transaction=False)
def test_resumo_de_sessao_previa_indisponivel_para_anonimo(client):
previa = baker.make(SessaoPlenaria, iniciada=False, publicar_pauta=False)
response = client.get(reverse('sapl.sessao:resumo',
kwargs={'pk': previa.pk}))
assert response.status_code == 404
@pytest.mark.django_db(transaction=False)
def test_pauta_nao_publicada_indisponivel_para_anonimo(client):
sem_pauta = baker.make(SessaoPlenaria, iniciada=True, publicar_pauta=False)
response = client.get(reverse('sapl.sessao:pauta_sessao_detail',
kwargs={'pk': sem_pauta.pk}))
assert response.status_code == 404

21
sapl/sessao/views.py

@ -64,7 +64,7 @@ from .models import (Bancada, CargoBancada, CargoMesa,
SessaoPlenaria, SessaoPlenariaPresenca, TipoExpediente, SessaoPlenaria, SessaoPlenariaPresenca, TipoExpediente,
TipoResultadoVotacao, TipoSessaoPlenaria, VotoParlamentar, TipoRetiradaPauta, TipoResultadoVotacao, TipoSessaoPlenaria, VotoParlamentar, TipoRetiradaPauta,
RetiradaPauta, TipoJustificativa, JustificativaAusencia, OradorOrdemDia, RetiradaPauta, TipoJustificativa, JustificativaAusencia, OradorOrdemDia,
ORDENACAO_RESUMO, RegistroLeitura) ORDENACAO_RESUMO, RegistroLeitura, restringe_sessoes_visiveis)
TipoSessaoCrud = CrudAux.build(TipoSessaoPlenaria, 'tipo_sessao_plenaria') TipoSessaoCrud = CrudAux.build(TipoSessaoPlenaria, 'tipo_sessao_plenaria')
TipoJustificativaCrud = CrudAux.build(TipoJustificativa, 'tipo_justificativa') TipoJustificativaCrud = CrudAux.build(TipoJustificativa, 'tipo_justificativa')
@ -1348,6 +1348,13 @@ class SessaoCrud(Crud):
class DetailView(Crud.DetailView): class DetailView(Crud.DetailView):
def get(self, request, *args, **kwargs):
if not restringe_sessoes_visiveis(
SessaoPlenaria.objects.filter(pk=kwargs.get('pk')),
request.user).exists():
raise Http404()
return super().get(request, *args, **kwargs)
@property @property
def layout_key(self): def layout_key(self):
sessao = self.object sessao = self.object
@ -2327,6 +2334,10 @@ class ResumoView(DetailView):
model = SessaoPlenaria model = SessaoPlenaria
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_queryset(self):
return restringe_sessoes_visiveis(
SessaoPlenaria.objects.all(), self.request.user)
def get_context(self, *args, **kwargs): def get_context(self, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
context = self.get_context_data(object=self.object) context = self.get_context_data(object=self.object)
@ -3833,6 +3844,12 @@ class PautaSessaoDetailView(PautaMultiFormatOutputMixin, DetailView):
('situacao', 'Situação') ('situacao', 'Situação')
) )
def get_queryset(self):
qs = SessaoPlenaria.objects.all()
if not self.request.user.is_authenticated:
qs = qs.filter(publicar_pauta=True)
return qs
def hook_autor(self, obj): def hook_autor(self, obj):
return ','.join(obj['autor']) return ','.join(obj['autor'])
@ -4028,6 +4045,8 @@ class PesquisarSessaoPlenariaView(MultiFormatOutputMixin, FilterView):
qs = self.get_queryset().select_related( qs = self.get_queryset().select_related(
'tipo', 'sessao_legislativa', 'legislatura') 'tipo', 'sessao_legislativa', 'legislatura')
qs = restringe_sessoes_visiveis(qs, self.request.user)
qs = qs.distinct().order_by( qs = qs.distinct().order_by(
'-legislatura__numero', '-data_inicio', '-hora_inicio') '-legislatura__numero', '-data_inicio', '-hora_inicio')

Loading…
Cancel
Save