Skip to content

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

Open
joaohortsenado wants to merge 1 commit into
3.1.xfrom
fix/sessao-nao-iniciada-consulta-publica
Open

Oculta sessões plenárias não iniciadas da consulta pública#3845
joaohortsenado wants to merge 1 commit into
3.1.xfrom
fix/sessao-nao-iniciada-consulta-publica

Conversation

@joaohortsenado

Copy link
Copy Markdown
Contributor

Descrição

Sessões plenárias cadastradas com antecedência ficavam visíveis na consulta pública mesmo com Publicar pauta? = Não, Sessão iniciada? = Não e Sessão finalizada? = Não. Nenhum desses campos filtrava queryset público: publicar_pauta só era aplicado nos dois pontos de entrada da pauta (PautaSessaoView e PesquisarPautaSessaoView), e iniciada/finalizada serviam apenas para liberar o fluxo operacional de painel/votação.

A regra passa a ser: o usuário anônimo só vê a sessão se publicar_pauta=True ou se iniciada não estiver explicitamente marcada como Não. Usuário autenticado continua vendo todas.

Ficou centralizada em SessaoPlenaria.objects.visiveis_para(user) (sapl/sessao/models.py) e aplicada nos quatro pontos públicos:

View Situação anterior
PesquisarSessaoPlenariaView FilterView sem mixin de permissão e queryset aberto
SessaoCrud.DetailView public = [RP_DETAIL], qualquer pk acessível por URL
ResumoView / ResumoAtaView DetailView sem filtro — ata de sessão nunca iniciada era legível
PautaSessaoDetailView não filtrava publicar_pauta; expunha por URL a pauta de sessão não publicada

PesquisarPautaSessaoView não muda de comportamento: ela já filtrava publicar_pauta=True, o que satisfaz o OR, então o AND resultante é equivalente.

iniciada nulo conta como visível

Decisão deliberada, e não detalhe de implementação. Em 0001_initial.py o campo nasceu NullBooleanField sem default; o default=True só chegou na migração 0027 (out/2018), que é um AlterField e portanto não preencheu as linhas existentes. Toda sessão anterior a out/2018 está com iniciada = NULL em produção. Um filter(iniciada=True) literal faria sumir da consulta pública todas as sessões pré-2018 de qualquer casa que roda SAPL desde antes disso.

Ponto de atenção para revisão

O default de iniciada é True. Uma sessão recém-cadastrada continua pública a menos que o operador marque "Sessão iniciada? Não" explicitamente — que é o fluxo já adotado pela casa que abriu o chamado. Se a intenção for que cadastro prévio seja privado por padrão, o default precisa mudar, o que exige migração e afeta todas as casas. Fora do escopo deste PR.

Issue Relacionada

Não há issue no GitHub. A origem é o chamado de suporte #600935 (Câmara Municipal de Tamarana/PR), no osTicket do Interlegis: https://suporte.interlegis.leg.br/scp/tickets.php?id=59932

Motivação e Contexto

A casa cadastra as sessões e as matérias da ordem do dia antes da data de realização e precisa que elas fiquem restritas ao uso interno até lá. Hoje o SAPL não tem nenhum campo que controle a visibilidade pública de uma sessão: ela é pública desde o instante em que é salva, e os parâmetros que o operador ajusta não têm o efeito que a interface sugere.

Como Isso Foi Testado?

Ambiente: container a partir da imagem sapl:dev (Django 2.2.28, Python 3.12), Postgres 10.5, USE_SOLR=False.

Foram adicionados 7 testes em sapl/sessao/tests/test_sessao_view.py, cobrindo:

  • os quatro estados relevantes do registro (iniciada=False, iniciada=True, iniciada=NULL, publicar_pauta=True) em visiveis_para;
  • anônimo vs. autenticado na pesquisa de sessões, via URL real e assert sobre object_list;
  • 404 para anônimo em detalhe, resumo e pauta não publicada.

Resultado em sapl/sessao + sapl/api: 22 passaram, 8 falharam. As 8 falhas são pré-existentes e não têm relação com esta mudança — confirmado rodando a mesma suíte na árvore limpa via git stash, onde falham igual:

  • TestResumoView (7 testes) usa def setup(self) no estilo nose, removido no pytest 8;
  • test_numero_duplicado_sessao_plenaria_form espera uma mensagem de erro que o form não emite mais.

./manage.py makemigrations --check confirma que o manager customizado não requer migração. flake8 limpo nas linhas adicionadas.

Observação à parte: pytest-django não consta de nenhum arquivo em requirements/, embora o pytest.ini dependa dele (DJANGO_SETTINGS_MODULE, --reuse-db). Foi preciso instalá-lo à mão para rodar a suíte.

Capturas de Tela (se apropriado):

n/a

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)

Marcado também como disruptiva por transparência: sessões hoje visíveis publicamente e marcadas como não iniciadas deixarão de aparecer para o público. É exatamente o efeito pretendido, mas é uma mudança de comportamento visível ao cidadão.

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.

Os testes novos passam. O item acima fica desmarcado apenas por causa das 8 falhas pré-existentes descritas em "Como Isso Foi Testado?", que já ocorrem na branch base.

Fora do escopo

O filtro cobre a consulta web. Continuam devolvendo todas as sessões para anônimo, e podem ser alinhados com o mesmo visiveis_para num PR seguinte:

  • o endpoint auto-gerado /api/sessao/sessaoplenaria/ (GET público pelo sistema de regras);
  • o legado de e-Cidadania (sapl/api/deprecated.py, AllowAny) — não alterado aqui porque o app pode legitimamente precisar listar sessão interativa antes de iniciar;
  • o índice Solr (SessaoPlenariaIndex).

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.

@edwardoliveira edwardoliveira left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread sapl/sessao/models.py

class SessaoPlenariaQuerySet(models.QuerySet):

def visiveis_para(self, user):

@edwardoliveira edwardoliveira Jul 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sinceramente, eu estou na dúvida se deveríamos criar uma subclasse de QuerySet para verificar se usuário está logado e aplicar os filtros. Eu entendo a mudança, mas me parece que essa nova subclasse "doesn't pull its own weight", pois adiciona um ponto a mais de complexidade...

IMO, poderíamos ter uma função utilitária do tipo:

def restringe_sessoes_visiveis(qs, user):
        if user.is_authenticated:
            return self

        return qs.filter(
            Q(publicar_pauta=True) |
            Q(iniciada=True) |
            Q(iniciada__isnull=True))

a qual chamaríamos como um wrapper nos pontos chave... Meio que já fazemos isso neste PR, mas mudaria disso:

            if not SessaoPlenaria.objects.visiveis_para(
                    request.user).filter(pk=kwargs.get('pk')).exists():
                raise Http404()

para isso:

            if not restringe_sessoes_visiveis(
                      SessaoPlenaria.objects.filter(pk=kwargs.get('pk')).exists():
                raise Http404()

AFAIK, o ORM do Django faz a fusão da cadeia de filters (um dentro de restringe_sessoes_visiveis e outro por fora) em um comando SQL único.

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.

2 participants