Os parâmetros texto, rotulo, num_ta e ano_ta da busca de dispositivos
eram interpolados diretamente na SQL bruta executada via
Dispositivo.objects.raw(), sem nenhum escape (ano_ta nem sequer era
citado entre aspas). Troca as interpolações por placeholders %s com
params, deixando o driver do banco responsável pela escapagem.
Além disso:
- Renomeia a lista de palavras pesquisadas para texto_valores e monta
os fragmentos '%s' repetidos via ['d.texto ~* %s'] * len(texto_valores),
em vez de reatribuir a mesma variável texto ou usar uma comprehension
com variável de loop não utilizada.
- Adiciona sapl.compilacao.utils.valid_int e aplica a tipo_model,
tipo_ta e ano_ta (colunas/PKs inteiras), rejeitando valores não
numéricos com um erro claro em vez de deixar o filtro inválido
seguir adiante. num_ta não é validado dessa forma porque
ta.numero é CharField pesquisado via regex (~*), então aceitar
valores não numéricos ali é esperado.
Claude-Session: https://claude.ai/code/session_01RZAufy4JQKWAaDgfcQByyt
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
O campo `CasaLegislativa.informacao_geral` é editado por editor de texto
rico: `CasaLegislativaForm` aplica `attrs={'id': 'texto-rico'}` no widget
e `frontend/src/__global/main.js` instancia o TinyMCE sobre esse id. O
valor submetido, portanto, é HTML, e não texto puro.
Com `max_length=100` no model, o MaxLengthValidator derivado pelo Django
contava os caracteres do HTML, tags inclusive. Um endereço de três linhas
com 87 caracteres visíveis vira 106 caracteres depois dos `<p></p>`, e o
salvamento era recusado com "no máximo 100 caracteres (ele possui 106)"
sem que o usuário tivesse digitado 100 caracteres. Quanto mais formatação
o texto tem, mais cedo o erro aparece — e não há como o operador
descobrir sozinho que o limite está sendo consumido por tags que ele não
vê.
O limite nunca teve efeito no banco: `TextField` gera coluna `text` no
PostgreSQL, sem restrição de tamanho. A migration é um no-op em SQL e
serve apenas para manter o state do Django coerente com o model.
Adiciona teste que trava o comportamento e comentário no model
registrando por que o campo não deve receber `max_length` de volta.
OSTicket #824090
* 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>
O PDF do Resumo da Sessão ordenava os expedientes alfabeticamente pelo
nome do tipo, ignorando o campo `ordenacao` de TipoExpediente. Numa Casa
que cadastre vários tipos, isso embaralha o documento: "Grande
Expediente" sai antes de "Leitura e Aprovação da Ata" e o encerramento
pode ser impresso logo após a abertura.
A tela do Resumo e o Extrato passam por sapl.sessao.views.get_expedientes,
que já ordena por `tipo__ordenacao`; apenas o relatório em PDF, que usa
get_sessao_plenaria, ficou para trás. O order_by('tipo__nome') havia sido
introduzido em 25be37eb justamente para acompanhar o Resumo, mas quando
2eb0b796 (#2920) criou o campo `ordenacao` somente get_expedientes foi
atualizado, e os dois caminhos divergiram em silêncio.
Alinha get_sessao_plenaria à mesma ordenação e adiciona testes que travam
a equivalência entre o PDF e o Resumo exibido em tela.
OSTicket #125461
RelatorioNormasPorAutorFilterSet: chaining .filter(autorianorma__primeiro_autor=True)
after the filterset's own autorianorma__autor filter created a second JOIN, causing
each norma to appear once per Autoria with primeiro_autor=True. Removing that filter
leaves a single JOIN so DISTINCT works correctly.
Both filtersets now return none() when no author is selected, preventing a full-table
scan with an unguarded ORDER BY on a related field.
Both views pass the Autor object (not str) into context so the templates can compare
autoria.autor != autor by PK. Both templates now show only co-authors (those different
from the filtered author) in the Coautor(es) column, mirroring each other.
RelatorioMateriasPorAutorView: removed the OrderedDict.fromkeys() deduplication
workaround that was masking the same underlying JOIN issue.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- audiencia/forms.py: also catch ValueError when querying MateriaLegislativa
by numero, so a non-numeric input (e.g. '36 e 61') surfaces as a form
validation error instead of an unhandled 500.
- utils.py: MultiFormatOutputMixin.render_to_response was doing
getattr(request, request.method) which fails for HEAD requests because
Django exposes request.GET/POST but not request.HEAD; remap HEAD → GET.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>