Substitui o polling HTTP do painel legislativo por push via WebSockets
(um PainelConsumer por sessão, broadcast de snapshot completo a partir
de build_dados_painel()) e cobre com páginas Vue 2 + Pinia as telas de
exibição pública do painel, votação nominal, voto individual, votação
simbólica e leitura de matéria — todas consumindo o mesmo broadcast via
normalizePainelData()/painelStore, sem componentes Vuex remanescentes.
- Backend: PainelConsumer/ASGI (Channels + Redis), views Postgres
(SessaoPresencasView/SessaoMateriasVotacoesView) para eliminar N+1,
cronômetro com estado compartilhado em cache, migrations 0070-0073
como cadeia única.
- Máscara de voto preservada: o broadcast público continua sempre
mascarado por mostrar_voto; estado por-operador e por-eleitor vem de
endpoints HTTP dedicados (votante_status, _status_json), nunca do
broadcast compartilhado.
- Infra: docker-compose e nginx atualizados para /ws/, requirements
(channels/daphne/channels-redis) e vue.config.js com as novas entries
de webpack.
- Testes: test_broadcasts.py, test_consumers.py, test_votacao_nominal.py
e test_sessao_view.py cobrindo o novo backend.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H85beqPY6uKTmSdCEovbPa
Painel: remove polling fallback, WS-only with connection error UI
Documents local-dev setup requirements in sapl/sessao/README.md and
finishes the polling->push migration by removing the HTTP polling that
was still running as a fallback everywhere: the three legacy templates
(painel/index.html, voto_individual.html, sessao/votacao/nominal.html)
and voto-individual/main.js. Every painel/votação screen — legacy and
Vue — is now WebSocket-only.
- Reconnect: exponential backoff with jitter (was flat 500ms in Vue),
and connections now distinguish PainelConsumer's permanent close codes
(4401/4403/4404 — auth/permission/no-such-sessão) from transient drops
instead of retrying forever. New frontend/src/__apps/painel/ws/
painelSocket.js + WsStatusBanner.vue share this across the five Vue
apps; the three legacy templates get an equivalent vanilla-JS banner.
- Broadcast coverage: without polling to mask it, every write path that
changes the painel payload now has to call broadcast_dados_painel()
itself. Added it to PresencaView/PresencaOrdemDiaView, the three
Orador*Crud create/update/delete views (via a new
BroadcastPainelOnSaveMixin — covered by a new end-to-end test, since
self.kwargs['pk'] means the parent sessão in CreateView but the
Orador's own pk in Update/DeleteView), LeituraEmBloco, and
VotacaoEmBlocoSimbolicaView/VotacaoEmBlocoNominalView.
- voto_individual.html now opens its WebSocket whenever a sessão is
open (not just when a matéria is), gated on sessao_id being present
rather than on materia — otherwise a page with no votação aberta at
all had no sessao_id to build a WS URL from.
- Dead code removed: painel_mensagem/painel_parlamentar/painel_votacao
(views, urls, templates) had no references anywhere and predate this
work. dados_painel/get_dados_painel (the HTTP polling endpoint) and
its ETag helper are also removed now that nothing polls them, along
with the tests pinning their HTTP-specific behavior — build_dados_painel()
itself is untouched and still backs every WS broadcast.
- scripts/verify_painel_websockets.sh now runs a single Daphne process
instead of Daphne+runserver on separate ports: WS-only clients need
HTTP and WS on the same origin, which the old split-port setup never
actually provided (it only appeared to work via the polling fallback
that's now gone).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H85beqPY6uKTmSdCEovbPa
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>
* Corrige polling excessivo em /voto-individual/
- Adiciona @login_required em votante_view para que usuários
anônimos sejam redirecionados ao login em vez de receberem o
template com auto-reload.
- Corrige bug do relógio: setInterval recursivo dentro de
startTime() acumulava timers exponencialmente. Trocado por
setTimeout, mantendo apenas um timer pendente.
- Move o script de reload/relógio para dentro do bloco
{% if not error_message %} para que a variante de erro não
agende reloads de 30s.
Reduz drasticamente o volume de requisições que disparavam o
rate limiter (RATE_LIMITER_RATE=35/m) quando vários parlamentares
mantinham a tela aberta atrás do mesmo IP NAT.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Bloqueia /voto-individual/ para usuários sem Votante/can_vote
Antes, usuários autenticados sem registro de Votante ou sem a
permissão parlamentares.can_vote recebiam HTTP 200 com um template
de erro (e, antes do commit anterior, ainda traziam o JS de auto-
reload). Agora a view exige as duas condições no início e retorna
HTTP 403 caso falhe:
- @permission_required('parlamentares.can_vote', raise_exception=True)
garante a permissão (com raise_exception para 403 ao invés do
redirect padrão pra LOGIN_URL).
- Checagem inline de Votante.objects.filter(user=...).exists() pega
o caso de superusers (que passam pelo bypass automático de
permissões do Django mas não têm cadastro de Votante) e de
qualquer custom group que conceda can_vote sem o cadastro.
Mantém HTTP 200 com mensagem amigável para falhas de estado que
um votante legítimo pode encontrar (nenhuma matéria aberta,
parlamentar não presente na sessão).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: joao <joao@mezzoplanejamento.com.br>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refact: cria método get_proximo_numero
* feat: impl numeração automática em cadastros via API
* Update sapl/materia/models.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update sapl/api/serializers.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update sapl/api/views_materia.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update sapl/materia/models.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update sapl/materia/models.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Add transaction atomic no endpoint create
* add validação de tipo se tipo não é objeto do model TipoMateriaLegislativa
* refact: aplica solicitações de reviewer e cria testes
* fix: altera numero_preferido para numero_candidato
* fix: remove espaços entre classes
* fix: corrige uso de transaction e ausencia dele
* fix: corrige testes devido mudança de norme de variável
* fix: altera seleção para select_for_update
* fix: mudança de nome remanescente para numero_candidato
* fix: retorna decorator na view function recuperar_materia
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* feat: impl HEADER LastModifiedDecorator na api
* fix: ajuste no frontend para evitar cache automático devido a LastModified sem tratamento adequado
* rebuild frontend
* fix: corrige last_modified_func para retorna sob retrieve
* refact: impl LastModified diretamente nos models que possuem campos específicos
* Seleciona tipo de votação para múltiplas matérias ao incluir na Ordem do Dia ou Expediente
* Ajustes solicitados na seleção de múltiplas Matérias para Ordem/Expediente
* Update adicionar_varias_materias_expediente.html
* Fix read-only mount on k8s
* Fix recibo proposição e adiciona rate limiter em matéria e norma
* Update forms.py
Alteração do nome do campo todos, conforme solicitação.
* Update adicionar_varias_materias_expediente.html
Ajuste no template por alteração do nome do campo "todos".
* Fix recibo proposição e adiciona rate limiter em matéria e norma
---------
Co-authored-by: root <root@info38.camaranh>
Co-authored-by: Edward Oliveira <edwardr@senado.gov.br>
Co-authored-by: Edward <9326037+edwardoliveira@users.noreply.github.com>
* Fix recibo proposição e adiciona rate limiter em matéria e norma
* Conserta bug na pesquisa do Relatório de Votações Nominais
Na pesquisa, ao selecionar filtro por Tipo de Matéria, Número e/ou Ano da Matéria, o sistema retorna erro 500.
* Fix recibo proposição e adiciona rate limiter em matéria e norma
---------
Co-authored-by: Edward Oliveira <edwardr@senado.gov.br>
* feat: impl filtro m2m com lookup '__in' para buscas com multiplos ids
* fix: remove alteração na criação de rotas da api
A classe DrfautoapiRouter removida nesse commit bem como a alteração no classmethod router é uma aleração necessária para que a api rode no django 5.2. Por outro lado, esta alteração quebra os links gerados pela template tag url e exigiria refatoração das mesmas. Esta alteração não é necessária para o propósito do PR.
* Implementa Relatório de Votações Nominais
* Alterações solicitadas - Relatório de Votações Nominais
* Apply suggestions from code review
Todas as sugestões de alteração acatadas.
Co-authored-by: Edward <9326037+edwardoliveira@users.noreply.github.com>
* Update views.py
Conforme observação sobre o retorno da QuerySet, escolha da opção 2 - colocar o qs dentro dos if's. Também houve a alteração na view, utilizando diretamente a classe genérica MultiFormatOutputMixin. Com a refatoração efetuada, não foi necessário definir uma especificação da mesma.
---------
Co-authored-by: root <root@info38.camaranh>
Co-authored-by: Edward <9326037+edwardoliveira@users.noreply.github.com>