From 57fa1e603dc55544e8ec216150e498338d5c8cba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moraes?= Date: Thu, 16 Jul 2026 13:59:53 -0300 Subject: [PATCH] =?UTF-8?q?Oculta=20sess=C3=B5es=20plen=C3=A1rias=20n?= =?UTF-8?q?=C3=A3o=20iniciadas=20da=20consulta=20p=C3=BAblica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- sapl/sessao/models.py | 24 ++++++++ sapl/sessao/tests/test_sessao_view.py | 87 ++++++++++++++++++++++++++- sapl/sessao/views.py | 17 ++++++ 3 files changed, 127 insertions(+), 1 deletion(-) diff --git a/sapl/sessao/models.py b/sapl/sessao/models.py index 613068afe..054c8cf62 100644 --- a/sapl/sessao/models.py +++ b/sapl/sessao/models.py @@ -153,6 +153,28 @@ def anexo_upload_path(instance, filename): # return get_sessao_media_path(instance, 'anexo', filename) +class SessaoPlenariaQuerySet(models.QuerySet): + + def visiveis_para(self, user): + """Restringe às sessões que o usuário pode ver 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 só surgiu na + migração 0027, que não preencheu as linhas já existentes, então bases + antigas têm sessões realizadas com o campo em NULL. + """ + if user.is_authenticated: + return self + + return self.filter( + Q(publicar_pauta=True) | + Q(iniciada=True) | + Q(iniciada__isnull=True)) + + class SessaoPlenaria(models.Model): # TODO trash??? Seems to have been a FK in the past. Would be: # andamento_sessao = models.ForeignKey( @@ -255,6 +277,8 @@ class SessaoPlenaria(models.Model): ) ) + objects = SessaoPlenariaQuerySet.as_manager() + class Meta: verbose_name = _('Sessão Plenária') verbose_name_plural = _('Sessões Plenárias') diff --git a/sapl/sessao/tests/test_sessao_view.py b/sapl/sessao/tests/test_sessao_view.py index 00284465b..267903843 100644 --- a/sapl/sessao/tests/test_sessao_view.py +++ b/sapl/sessao/tests/test_sessao_view.py @@ -1,4 +1,7 @@ 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.utils.translation import ugettext_lazy as _ from model_bakery import baker @@ -146,4 +149,86 @@ class TestResumoView(): ocorrencia = baker.make(OcorrenciaSessao, sessao_plenaria=self.sessao_plenaria) resultado_get_ocorrencia = get_ocorrencias_da_sessao(self.sessao_plenaria) - assert resultado_get_ocorrencia['ocorrencias_da_sessao'][0] == ocorrencia \ No newline at end of file + 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 = SessaoPlenaria.objects.visiveis_para(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 SessaoPlenaria.objects.visiveis_para(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 diff --git a/sapl/sessao/views.py b/sapl/sessao/views.py index ab30c2c6a..14813ab90 100755 --- a/sapl/sessao/views.py +++ b/sapl/sessao/views.py @@ -1348,6 +1348,12 @@ class SessaoCrud(Crud): class DetailView(Crud.DetailView): + def get(self, request, *args, **kwargs): + if not SessaoPlenaria.objects.visiveis_para( + request.user).filter(pk=kwargs.get('pk')).exists(): + raise Http404() + return super().get(request, *args, **kwargs) + @property def layout_key(self): sessao = self.object @@ -2327,6 +2333,9 @@ class ResumoView(DetailView): model = SessaoPlenaria logger = logging.getLogger(__name__) + def get_queryset(self): + return SessaoPlenaria.objects.visiveis_para(self.request.user) + def get_context(self, *args, **kwargs): self.object = self.get_object() context = self.get_context_data(object=self.object) @@ -3833,6 +3842,12 @@ class PautaSessaoDetailView(PautaMultiFormatOutputMixin, DetailView): ('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): return ','.join(obj['autor']) @@ -4028,6 +4043,8 @@ class PesquisarSessaoPlenariaView(MultiFormatOutputMixin, FilterView): qs = self.get_queryset().select_related( 'tipo', 'sessao_legislativa', 'legislatura') + qs = qs.visiveis_para(self.request.user) + qs = qs.distinct().order_by( '-legislatura__numero', '-data_inicio', '-hora_inicio')