Browse Source

Add Websockets to Painel

websockets-2025
Edward Ribeiro 2 months ago
parent
commit
c2efa15336
  1. 17
      docker/config/nginx/sapl.conf
  2. 4
      docker/docker-compose.yaml
  3. 2
      docker/startup_scripts/start.sh
  4. 4
      requirements/requirements.txt
  5. 21
      sapl/asgi.py
  6. 365
      sapl/painel/consumers.py
  7. 7
      sapl/painel/urls.py
  8. 103
      sapl/painel/views.py
  9. 12
      sapl/sessao/models.py
  10. 20
      sapl/settings.py
  11. 423
      sapl/templates/painel/painel_v2.html
  12. 6
      scripts/websockets.sh

17
docker/config/nginx/sapl.conf

@ -1,9 +1,13 @@
upstream sapl_server {
server unix:/var/interlegis/sapl/run/gunicorn.sock fail_timeout=0;
server unix:/var/interlegis/sapl/run/gunicorn.sock fail_timeout=0;
}
upstream sapl_ws {
server unix:/var/interlegis/sapl/run/daphne.sock fail_timeout=0;
}
server {
listen 80;
@ -52,6 +56,17 @@ server {
proxy_pass http://sapl_server;
}
location /ws/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_read_timeout 3600;
proxy_send_timeout 3600;
proxy_pass http://sapl_ws;
}
error_page 500 502 503 504 /500.html;
location = /500.html {
root /var/interlegis/sapl/sapl/static/;

4
docker/docker-compose.yaml

@ -32,6 +32,9 @@ services:
- "8983:8983"
networks:
- sapl-net
saplredis:
image: redis:7-alpine
command: ["redis-server", "--save", "", "--appendonly", "no"]
sapl:
image: interlegis/sapl:3.1.164-RC5
# build:
@ -56,6 +59,7 @@ services:
SOLR_URL: http://solr:solr@saplsolr:8983
IS_ZK_EMBEDDED: 'True'
ENABLE_SAPN: 'False'
REDIS_URL: 'redis://saplredis:6379/0'
TZ: America/Sao_Paulo
volumes:
- sapl_data:/var/interlegis/sapl/data

2
docker/startup_scripts/start.sh

@ -259,6 +259,8 @@ setup_cache_dir() {
start_services() {
log "Starting gunicorn..."
gunicorn -c gunicorn.conf.py &
log "Starting websockets..."
daphne --unix-socket "$RUN_DIR/daphne.sock" sapl.asgi:application &
log "Starting nginx..."
exec /usr/sbin/nginx -g "daemon off;"
}

4
requirements/requirements.txt

@ -26,6 +26,10 @@ reportlab==3.6.13
WeasyPrint==66
trml2pdf==0.6
gunicorn==23.0.0
channels==3.0.3
daphne==3.0.2
channels-redis==3.4.1
asgiref==3.7.2
more-itertools==8.2.0
pysolr==3.6.0
PyPDF4==1.27.0

21
sapl/asgi.py

@ -0,0 +1,21 @@
# sapl/asgi.py
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sapl.settings")
import django
django.setup()
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from channels.http import AsgiHandler # Django 2.2 uses AsgiHandler, not django.core.asgi
from django.urls import path
from sapl.painel.consumers import PainelConsumer, HealthConsumer
application = ProtocolTypeRouter({
"http": AsgiHandler(),
"websocket": AuthMiddlewareStack(URLRouter([
# path("ws/painel/", PainelConsumer.as_asgi()),
path("ws/painel/<int:controller_id>/", PainelConsumer.as_asgi()),
])),
})

365
sapl/painel/consumers.py

@ -0,0 +1,365 @@
import html
import json
import logging
import time
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer, AsyncJsonWebsocketConsumer
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from sapl.base.models import CasaLegislativa, AppConfig
from sapl.sessao.models import SessaoPlenaria, OrdemDia, ExpedienteMateria, RegistroVotacao, RegistroLeitura, \
PresencaOrdemDia, SessaoPlenariaPresenca, OradorExpediente, VotoParlamentar, AbstractOrdemDia
log = logging.getLogger(__name__)
def get_dados_painel(pk: int) -> dict:
sessao = SessaoPlenaria.objects.get(id=pk)
casa = CasaLegislativa.objects.first()
app_config = AppConfig.objects.first()
if casa and app_config and (bool(casa.logotipo)):
brasao = casa.logotipo.url \
if app_config.mostrar_brasao_painel else None
ordem_dia = OrdemDia.objects.filter(sessao_plenaria_id=pk, votacao_aberta=True).last()
expediente = ExpedienteMateria.objects.filter(sessao_plenaria_id=pk, votacao_aberta=True).last()
dados_sessao = {
"type": "data",
"status_painel": sessao.painel_aberto,
"brasao": brasao,
"mostrar_voto": app_config.mostrar_voto,
"sessao_plenaria": str(sessao),
"sessao_plenaria_data": sessao.data_inicio.strftime("%d/%m/%Y"),
"sessao_plenaria_hora_inicio": sessao.hora_inicio,
"sessao_solene": sessao.tipo.nome == "Solene",
"sessao_finalizada": sessao.finalizada,
"tema_solene": sessao.tema_solene,
# "cronometro_aparte": get_cronometro_status(request, "aparte"),
# "cronometro_discurso": get_cronometro_status(request, "discurso"),
# "cronometro_ordem": get_cronometro_status(request, "ordem"),
# "cronometro_consideracoes": get_cronometro_status(request, "consideracoes"),
}
# Caso tenha alguma matéria com votação aberta, ela é mostrada no painel
# com prioridade para Ordem Dia.
if ordem_dia:
dados_sessao.update(get_presentes(pk, ordem_dia))
dados_sessao.update(get_votos(ordem_dia, app_config.mostrar_voto))
elif expediente:
dados_sessao.update(get_presentes(pk, expediente))
dados_sessao.update(get_votos(expediente, app_config.mostrar_voto))
# Caso não tenha nenhuma aberta,
# a matéria a ser mostrada no Painel deve ser a última votada
last_ordem_voto = RegistroVotacao.objects.filter(
ordem__sessao_plenaria=sessao).order_by("data_hora").last()
last_expediente_voto = RegistroVotacao.objects.filter(
expediente__sessao_plenaria=sessao).order_by("data_hora").last()
last_ordem_leitura = RegistroLeitura.objects.filter(
ordem__sessao_plenaria=sessao).order_by("data_hora").last()
last_expediente_leitura = RegistroLeitura.objects.filter(
expediente__sessao_plenaria=sessao).order_by("data_hora").last()
# Obtém última matéria votada, através do timestamp mais recente
ordem_expediente = None
ultimo_timestamp = None
if last_ordem_voto:
ordem_expediente = last_ordem_voto.ordem
ultimo_timestamp = last_ordem_voto.data_hora
if (last_expediente_voto and ultimo_timestamp and last_expediente_voto.data_hora > ultimo_timestamp) or \
(not ultimo_timestamp and last_expediente_voto):
ordem_expediente = last_expediente_voto.expediente
ultimo_timestamp = last_expediente_voto.data_hora
if (last_ordem_leitura and ultimo_timestamp and last_ordem_leitura.data_hora > ultimo_timestamp) or \
(not ultimo_timestamp and last_ordem_leitura):
ordem_expediente = last_ordem_leitura.ordem
ultimo_timestamp = last_ordem_leitura.data_hora
if (last_expediente_leitura and ultimo_timestamp and last_expediente_leitura.data_hora > ultimo_timestamp) or \
(not ultimo_timestamp and last_expediente_leitura):
ordem_expediente = last_expediente_leitura.expediente
ultimo_timestamp = last_expediente_leitura.data_hora
# if ordem_expediente:
# dados_sessao.update(get_presentes(pk, ordem_expediente))
# dados_sessao.update(get_votos(ordem_expediente, app_config.mostrar_voto))
# Retorna que não há nenhuma matéria já votada ou aberta
dados_sessao.update({
'msg_painel': str('Nenhuma matéria disponivel para votação.')})
return dados_sessao
def get_votos(materia: AbstractOrdemDia, mostrar_voto: bool):
logger = logging.getLogger(__name__)
if type(materia) == OrdemDia:
if materia.tipo_votacao != 4:
registro = RegistroVotacao.objects.filter(
ordem=materia, materia=materia.materia).order_by('data_hora').last()
leitura = None
else:
leitura = RegistroLeitura.objects.filter(
ordem=materia, materia=materia.materia).order_by('data_hora').last()
registro = None
tipo = 'ordem'
elif type(materia) == ExpedienteMateria:
if materia.tipo_votacao != 4:
registro = RegistroVotacao.objects.filter(
expediente=materia, materia=materia.materia).order_by('data_hora').last()
leitura = None
else:
leitura = RegistroLeitura.objects.filter(
expediente=materia, materia=materia.materia).order_by('data_hora').last()
registro = None
tipo = 'expediente'
response = {}
if not registro and not leitura:
response.update({
'numero_votos_sim': 0,
'numero_votos_nao': 0,
'numero_abstencoes': 0,
'registro': None,
'total_votos': 0,
'tipo_resultado': 'Ainda não foi votada.',
})
if materia.tipo_votacao == 2:
if tipo == 'ordem':
votos_parlamentares = VotoParlamentar.objects.filter(
ordem_id=materia.id).order_by(
'parlamentar__nome_parlamentar')
else:
votos_parlamentares = VotoParlamentar.objects.filter(
expediente_id=materia.id).order_by(
'parlamentar__nome_parlamentar')
for i, p in enumerate(response.get('presentes', [])):
try:
logger.info("Tentando obter votos do parlamentar (id={}).".format(p['parlamentar_id']))
voto = votos_parlamentares.get(parlamentar_id=p['parlamentar_id']).voto
if voto:
if mostrar_voto:
response['presentes'][i]['voto'] = voto
else:
response['presentes'][i]['voto'] = 'Voto Informado'
except ObjectDoesNotExist:
# logger.error("Votos do parlamentar (id={}) não encontrados. Retornado vazio."
# .format(p['parlamentar_id']))
response['presentes'][i]['voto'] = ''
elif leitura:
response.update({
'numero_votos_sim': 0,
'numero_votos_nao': 0,
'numero_abstencoes': 0,
'registro': True,
'total_votos': 0,
'tipo_resultado': 'Matéria lida.',
})
else:
total = (registro.numero_votos_sim +
registro.numero_votos_nao +
registro.numero_abstencoes)
if materia.tipo_votacao == 2:
votos_parlamentares = VotoParlamentar.objects.filter(
votacao_id=registro.id).order_by(
'parlamentar__nome_parlamentar')
for i, p in enumerate(response.get('presentes', [])):
try:
logger.debug("Tentando obter votos do parlamentar (id={}).".format(p['parlamentar_id']))
response['presentes'][i]['voto'] = votos_parlamentares.get(
parlamentar_id=p['parlamentar_id']).voto
except ObjectDoesNotExist:
logger.error(
"Votos do parlamentar (id={}) não encontrados. Retornado None.".format(p['parlamentar_id']))
response['presentes'][i]['voto'] = None
response.update({
'numero_votos_sim': registro.numero_votos_sim,
'numero_votos_nao': registro.numero_votos_nao,
'numero_abstencoes': registro.numero_abstencoes,
'registro': True,
'total_votos': total,
'tipo_resultado': registro.tipo_resultado_votacao.nome,
})
return response
def filiacao_data(parlamentar, data_inicio, data_fim=None):
from sapl.parlamentares.models import Filiacao
filiacoes_parlamentar = Filiacao.objects.filter(
parlamentar=parlamentar)
filiacoes = filiacoes_parlamentar.filter(Q(
data__lte=data_inicio,
data_desfiliacao__isnull=True) | Q(
data__lte=data_inicio,
data_desfiliacao__gte=data_inicio))
if data_fim:
filiacoes = filiacoes | filiacoes_parlamentar.filter(
data__gte=data_inicio,
data__lte=data_fim)
return ' | '.join([f.partido.sigla for f in filiacoes])
def get_presentes(pk: int, materia: AbstractOrdemDia):
if type(materia) == OrdemDia:
presentes = PresencaOrdemDia.objects.filter(
sessao_plenaria_id=pk)
else:
presentes = SessaoPlenariaPresenca.objects.filter(
sessao_plenaria_id=pk)
sessao = SessaoPlenaria.objects.get(id=pk)
num_presentes = len(presentes)
data_sessao = sessao.data_inicio
oradores = OradorExpediente.objects.filter(
sessao_plenaria_id=pk).order_by('numero_ordem')
oradores_list = []
for o in oradores:
oradores_list.append(
{
'nome': o.parlamentar.nome_parlamentar,
'numero': o.numero_ordem
})
presentes_list = []
for p in presentes:
legislatura = sessao.legislatura
# Recupera os mandatos daquele parlamentar
mandatos = p.parlamentar.mandato_set.filter(legislatura=legislatura)
if p.parlamentar.ativo and mandatos:
filiacao = filiacao_data(p.parlamentar, data_sessao, data_sessao)
if not filiacao:
partido = 'Sem Registro'
else:
partido = filiacao
presentes_list.append(
{'id': p.id,
'parlamentar_id': p.parlamentar.id,
'nome': p.parlamentar.nome_parlamentar,
'partido': partido,
'voto': ''
})
elif not p.parlamentar.ativo or not mandatos:
num_presentes += -1
response = {}
if materia:
if materia.tipo_votacao == 1:
tipo_votacao = 'Simbólica'
elif materia.tipo_votacao == 2:
tipo_votacao = 'Nominal'
elif materia.tipo_votacao == 3:
tipo_votacao = 'Secreta'
elif materia.tipo_votacao == 4:
tipo_votacao = 'Leitura'
response.update({
'tipo_resultado': materia.resultado,
'observacao_materia': html.unescape(materia.observacao),
'tipo_votacao': tipo_votacao,
'materia_legislativa_texto': str(materia.materia),
'materia_legislativa_ementa': str(materia.materia.ementa)
})
# presentes_list = sort_lista_chave(presentes_list, 'nome')
response.update({
'presentes': presentes_list,
'num_presentes': num_presentes,
'oradores': oradores_list,
'msg_painel': str('Votação aberta!'),
})
return response
class PainelConsumer(AsyncJsonWebsocketConsumer):
# def __init__(self):
# self.group = set()
# self.controller_id = None
async def connect(self):
user = self.scope.get("user")
controller_id = self.scope["url_route"]["kwargs"]["controller_id"]
print(f"user: {user}, controller_id: {controller_id}")
if not (user and user.is_authenticated):
log.info(f"{user} is not authenticated user!")
await self.close(code=4401) # explicit, graceful close
return
if not await self.user_can_view(user.id, controller_id):
await self.close(code=4403)
return
self.controller_id = controller_id
self.group = f"controller_{controller_id}"
await self.channel_layer.group_add(self.group, self.channel_name)
await self.accept()
# await self.send_json({"type": "data",
# "text": "Connection established!"})
await self.send_json(get_dados_painel(controller_id))
async def disconnect(self, code):
if hasattr(self, "group"):
await self.channel_layer.group_discard(self.group, self.channel_name)
# Called by server via channel_layer.group_send
async def notify(self, event):
# event: {"type": "notify", "data": {...}}
await self.send_json(event["data"])
async def receive(self, text_data=None, bytes_data=None):
data = json.loads(text_data or "{}")
print("Received from client:", data) # TODO: turn into log messages
if data.get("type") == "ping":
print("PING")
await self.send_json({"type": "pong", "ts": time.time()})
return
await self.send_json({"type": "data",
"text": f"Echo: {data}"})
@database_sync_to_async
def user_can_view(self, user_id, controller_id) -> bool:
# Replace with your ACL check (ORM must be in a sync wrapper)
# return Controller.objects.filter(id=controller_id, owners__id=user_id).exists()
return True
class HealthConsumer(AsyncWebsocketConsumer):
"""
WebSockets consumer that doesn"t require authentication to
debug with wscat (wscat -c ws://127.0.0.1:8000/ws/painel/)
"""
async def connect(self):
try:
await self.accept()
await self.send(json.dumps({"ok": True}))
except Exception as e:
log.exception("connect failed: %s", e)
# Let Channels close with 1011 if we got here
async def receive(self, text_data=None, bytes_data=None):
await self.send(text_data or "")

7
sapl/painel/urls.py

@ -3,7 +3,9 @@ from django.conf.urls import url
from .apps import AppConfig
from .views import (cronometro_painel, get_dados_painel, painel_mensagem_view,
painel_parlamentar_view, painel_view, painel_votacao_view,
switch_painel, verifica_painel, votante_view)
switch_painel, verifica_painel, votante_view, websocket_view)
from django.urls import path
app_name = AppConfig.name
@ -24,4 +26,7 @@ urlpatterns = [
url(r'^voto-individual/$', votante_view,
name='voto_individual'),
# url(r'^painel', websocket_view, name='painel_websocket'),
path("painel/v2", websocket_view, name='painel_websocket'),
]

103
sapl/painel/views.py

@ -58,11 +58,11 @@ def votacao_aberta(request):
kwargs={'pk': v.id}),
v.__str__()))
logger.info('user=' + username + '. Existe mais de uma votações aberta. Elas se encontram '
'nas seguintes Sessões: ' + ', '.join(msg_abertas) + '. '
'Para votar, peça para que o Operador feche-as.')
'nas seguintes Sessões: ' + ', '.join(msg_abertas) + '. '
'Para votar, peça para que o Operador feche-as.')
msg = _('Existe mais de uma votações aberta. Elas se encontram '
'nas seguintes Sessões: ' + ', '.join(msg_abertas) + '. '
'Para votar, peça para que o Operador feche-as.')
'Para votar, peça para que o Operador feche-as.')
messages.add_message(request, messages.INFO, msg)
return None, msg
@ -78,9 +78,9 @@ def votacao_aberta(request):
if numero_materias_abertas > 1:
logger.info('user=' + username + '. Existe mais de uma votação aberta na Sessão: ' +
('''<li><a href="%s">%s</a></li>''' % (
reverse('sapl.sessao:sessaoplenaria_detail',
kwargs={'pk': votacoes_abertas.first().id}),
votacoes_abertas.first().__str__())))
reverse('sapl.sessao:sessaoplenaria_detail',
kwargs={'pk': votacoes_abertas.first().id}),
votacoes_abertas.first().__str__())))
msg = _('Existe mais de uma votação aberta na Sessão: ' +
('''<li><a href="%s">%s</a></li>''' % (
reverse('sapl.sessao:sessaoplenaria_detail',
@ -93,7 +93,7 @@ def votacao_aberta(request):
return votacoes_abertas.first(), None
def votacao(context,context_vars):
def votacao(context, context_vars):
logger = logging.getLogger(__name__)
parlamentar = context_vars['votante'].parlamentar
parlamentar_presente = False
@ -102,8 +102,8 @@ def votacao(context,context_vars):
context_vars.update({'parlamentar': parlamentar})
else:
context.update({'error_message':
'Não há presentes na Sessão com a '
'matéria em votação.'})
'Não há presentes na Sessão com a '
'matéria em votação.'})
if parlamentar_presente:
voto = []
@ -123,13 +123,13 @@ def votacao(context,context_vars):
logger.error("Voto do parlamentar {} não computado.".format(context_vars['parlamentar']))
context.update(
{'voto_parlamentar': 'Voto não '
'computado.'})
'computado.'})
else:
logger.error("Parlamentar com id={} não está presente na "
"Ordem do Dia/Expediente em votação.".format(parlamentar.id))
"Ordem do Dia/Expediente em votação.".format(parlamentar.id))
context.update({'error_message':
'Você não está presente na '
'Ordem do Dia/Expediente em votação.'})
'Você não está presente na '
'Ordem do Dia/Expediente em votação.'})
return context, context_vars
def sessao_votacao(context,context_vars):
@ -143,9 +143,9 @@ def sessao_votacao(context,context_vars):
presentes = []
ordem_dia = get_materia_aberta(pk)
expediente = get_materia_expediente_aberta(pk)
errors_msgs = {'materia':'Não há nenhuma matéria aberta.',
'registro':'A votação para esta matéria já encerrou.',
'tipo':'A matéria aberta não é do tipo votação nominal.'}
errors_msgs = {'materia': 'Não há nenhuma matéria aberta.',
'registro': 'A votação para esta matéria já encerrou.',
'tipo': 'A matéria aberta não é do tipo votação nominal.'}
materia_aberta = None
if ordem_dia:
@ -160,8 +160,8 @@ def sessao_votacao(context,context_vars):
'parlamentar_id', flat=True).distinct()
context_vars.update({'ordem_dia': ordem_dia,
'expediente':expediente,
'presentes': presentes})
'expediente': expediente,
'presentes': presentes})
# Verifica votação aberta
# Se aberta, verifica se é nominal. ID nominal == 2
@ -188,7 +188,7 @@ def can_vote(context, context_vars, request):
# Pega sessão
sessao, msg = votacao_aberta(request)
context_vars.update({'sessao':sessao})
context_vars.update({'sessao': sessao})
if sessao and not msg:
context, context_vars = sessao_votacao(context, context_vars)
elif not sessao and msg:
@ -202,7 +202,7 @@ def can_vote(context, context_vars, request):
def votante_view(request):
logger = logging.getLogger(__name__)
username = request.user.username if request.user.is_authenticated else 'AnonymousUser'
# Pega o votante relacionado ao usuário
template_name = 'painel/voto_nominal.html'
context = {}
@ -215,9 +215,11 @@ def votante_view(request):
else:
raise ObjectDoesNotExist
except ObjectDoesNotExist:
logger.error(f"user={username}. Usuário (user={request.user}) não cadastrado como votante na tela de parlamentares. "
"Contate a administração de sua Casa Legislativa!")
msg = _("Usuário não cadastrado como votante na tela de parlamentares. Contate a administração de sua Casa Legislativa!")
logger.error(
f"user={username}. Usuário (user={request.user}) não cadastrado como votante na tela de parlamentares. "
"Contate a administração de sua Casa Legislativa!")
msg = _(
"Usuário não cadastrado como votante na tela de parlamentares. Contate a administração de sua Casa Legislativa!")
context.update({'error_message': msg})
return render(request, template_name, context)
@ -227,7 +229,8 @@ def votante_view(request):
# Verifica se usuário possui permissão para votar
if 'parlamentares.can_vote' in request.user.get_all_permissions():
context, context_vars = can_vote(context, context_vars, request)
logger.debug("user=" + username + ". Verificando se usuário {} possui permissão para votar.".format(request.user))
logger.debug(
"user=" + username + ". Verificando se usuário {} possui permissão para votar.".format(request.user))
else:
logger.error("user=" + username + ". Usuário {} sem permissão para votar.".format(request.user))
context.update({'permissao': False,
@ -237,14 +240,16 @@ def votante_view(request):
if request.method == 'POST':
if context_vars['ordem_dia']:
try:
logger.info("user=" + username + ". Tentando obter objeto VotoParlamentar para parlamentar={} e ordem={}."
.format(context_vars['parlamentar'], context_vars['ordem_dia']))
logger.info(
"user=" + username + ". Tentando obter objeto VotoParlamentar para parlamentar={} e ordem={}."
.format(context_vars['parlamentar'], context_vars['ordem_dia']))
voto = VotoParlamentar.objects.get(
parlamentar=context_vars['parlamentar'],
ordem=context_vars['ordem_dia'])
except ObjectDoesNotExist:
logger.error("user=" + username + ". Erro ao obter VotoParlamentar para parlamentar={} e ordem={}. Criando objeto."
.format(context_vars['parlamentar'], context_vars['ordem_dia']))
logger.error(
"user=" + username + ". Erro ao obter VotoParlamentar para parlamentar={} e ordem={}. Criando objeto."
.format(context_vars['parlamentar'], context_vars['ordem_dia']))
voto = VotoParlamentar.objects.create(
parlamentar=context_vars['parlamentar'],
voto=request.POST['voto'],
@ -261,14 +266,16 @@ def votante_view(request):
elif context_vars['expediente']:
try:
logger.info("user=" + username + ". Tentando obter objeto VotoParlamentar para parlamentar={} e expediente={}."
.format(context_vars['parlamentar'], context_vars['expediente']))
logger.info(
"user=" + username + ". Tentando obter objeto VotoParlamentar para parlamentar={} e expediente={}."
.format(context_vars['parlamentar'], context_vars['expediente']))
voto = VotoParlamentar.objects.get(
parlamentar=context_vars['parlamentar'],
expediente=context_vars['expediente'])
except ObjectDoesNotExist:
logger.error("user=" + username + ". Erro ao obter VotoParlamentar para parlamentar={} e expediente={}. Criando objeto."
.format(context_vars['parlamentar'], context_vars['expediente']))
logger.error(
"user=" + username + ". Erro ao obter VotoParlamentar para parlamentar={} e expediente={}. Criando objeto."
.format(context_vars['parlamentar'], context_vars['expediente']))
voto = VotoParlamentar.objects.create(
parlamentar=context_vars['parlamentar'],
voto=request.POST['voto'],
@ -276,8 +283,9 @@ def votante_view(request):
ip=get_client_ip(request),
expediente=context_vars['expediente'])
else:
logger.info("user=" + username + ". VotoParlamentar para parlamentar={} e expediente={} obtido com sucesso."
.format(context_vars['parlamentar'], context_vars['expediente']))
logger.info(
"user=" + username + ". VotoParlamentar para parlamentar={} e expediente={} obtido com sucesso."
.format(context_vars['parlamentar'], context_vars['expediente']))
voto.voto = request.POST['voto']
voto.ip = get_client_ip(request)
voto.user = request.user
@ -294,7 +302,7 @@ def painel_view(request, pk):
now = timezone.localtime(timezone.now())
utc_offset = now.utcoffset().total_seconds() / 60
context = {'head_title': str(_('Painel Plenário')), 'sessao_id': pk, 'utc_offset': utc_offset }
context = {'head_title': str(_('Painel Plenário')), 'sessao_id': pk, 'utc_offset': utc_offset}
return render(request, 'painel/index.html', context)
@ -624,3 +632,28 @@ def get_dados_painel(request, pk):
# Retorna que não há nenhuma matéria já votada ou aberta
return response_nenhuma_materia(get_presentes(pk, response, None))
def websocket_view(request):
now = timezone.localtime(timezone.now())
utc_offset = now.utcoffset().total_seconds() / 60
# controller_id == session_id
context = {'head_title': str(_('Painel Plenário')),
'sessao_id': 4984, # TODO: recover from template call
'utc_offset': utc_offset,
'enable_live_ws': True,
'controller_id': 4984, # TODO: unify with sessao_id
}
return render(request, "painel/painel_v2.html", context)
def push_to_me(request):
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
layer = get_channel_layer()
group = f"user_{request.user.pk}"
async_to_sync(layer.group_send)(
group, {"type": "notify", "data": {"text": "server says hi!"}}
)
return JsonResponse({"ok": True})

12
sapl/sessao/models.py

@ -1050,3 +1050,15 @@ class Correspondencia(models.Model):
def __str__(self):
return _('Correspondência: {}').format(self.documento)
class OradoresView(models.Model):
materia = models.ForeignKey(MateriaLegislativa, on_delete=models.DO_NOTHING)
class Meta:
managed = False
db_table = "sessao_presenca_view"
ordering = ('-id',)
def __str__(self):
return '{}/{}'.format(self.materia, self.tramitacao)

20
sapl/settings.py

@ -14,6 +14,7 @@ See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
"""
import logging
import os
import socket
import sys
@ -83,6 +84,8 @@ INSTALLED_APPS = (
'crispy_forms',
'channels',
'waffle',
'drf_spectacular',
@ -102,6 +105,23 @@ INSTALLED_APPS = (
) + SAPL_APPS
# Web-sockets
ASGI_APPLICATION = 'sapl.asgi.application'
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [config("REDIS_URL", cast=str, default="redis://127.0.0.1:6379/0")],
"prefix": f"{host}",
"capacity": 1000,
"expiry": 60,
},
},
}
# FTS = Full Text Search
# Desabilita a indexação textual até encontramos uma solução para a issue
# https://github.com/interlegis/sapl/issues/2055

423
sapl/templates/painel/painel_v2.html

@ -0,0 +1,423 @@
{% load i18n %}
{% load common_tags %}
{% load render_bundle from webpack_loader %}
{% load webpack_static from webpack_loader %}
<!DOCTYPE HTML>
<!--[if IE 8]> <html class="no-js lt-ie9" lang="pt-br"> <![endif]-->
<!--[if gt IE 8]><!-->
<html lang="pt-br">
<!--<![endif]-->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- TODO: does it need this head_title here? -->
<title>{% block head_title %}{% trans 'SAPL - Sistema de Apoio ao Processo Legislativo' %}{% endblock %}</title>
{% block webpack_loader_css %}
{% render_chunk_vendors 'css' %}
{% render_bundle 'global' 'css' %}
{% render_bundle 'painel' 'css' %}
{% endblock webpack_loader_css %}
<style type="text/css">
html, body {
max-width: 100%;
overflow-x: hidden;
}
@media screen {
ul, li {
list-style-type: none;
}
}
</style>
</head>
<body class="painel-principal">
<audio type="hidden" id="audio" src="{% webpack_static 'audio/ring.mp3' %}"></audio>
<div class="d-flex justify-content-center">
<h1 id="sessao_plenaria" class="title text-title"></h1>
</div>
<div class="row ">
<div class="col text-center">
<span id="sessao_plenaria_data" class="text-value"></span>
</div>
<div class="col text-center">
<span id="sessao_plenaria_hora_inicio" class="text-value"></span>
</div>
</div>
<div class="row justify-content-center">
<div class="col-1">
<img src="" id="logo-painel" class="logo-painel" alt=""/>
</div>
</div>
<div class="row justify-content-center">
<h2 class="text-danger"><span id="message"></span></h2>
</div>
<div class="row">
<div class="col text-center"><span class="text-value data-hora" id="date"></span></div>
<div class="col text-center"><span class="text-value data-hora" id="relogio"></span></div>
</div>
<div class="">
<div class="d-flex justify-content-start">
<div class="col-md-4">
<div class="text-center painel">
<h2 class="text-subtitle">Parlamentares</h2>
<span id="parlamentares" class="text-value text-center"></span>
</div>
</div>
<div class="d-flex col-md-8 painels">
<div class="col-md-6 text-center painel" id="aparecer_oradores">
<h2 class="text-subtitle">Oradores</h2>
<span id="orador"></span>
</div>
<div class="col-md-6 text-left painel">
<div class="d-flex align-items-left justify-content-left mb-2">
<h2 class="text-subtitle mb-0">Cronômetros</h2>
<button class="btn btn-sm btn-secondary ms-2" onclick="changeFontSize('box_cronometros', -1)">
A-
</button>
<button class="btn btn-sm btn-secondary ms-2" onclick="changeFontSize('box_cronometros', 1)">
A+
</button>
</div>
<div class="text-value" id="box_cronometros">
Discurso: <span id="cronometro_discurso"></span><br>
Aparte: <span id="cronometro_aparte"></span><br>
Questão de Ordem: <span id="cronometro_ordem"></span><br>
Considerações Finais: <span id="cronometro_consideracoes"></span>
</div>
</div>
<div class="col-md-6 text-left painel" id="resultado_votacao_div">
<div class="d-flex align-items-left justify-content-left mb-2">
<h2 class="text-subtitle mb-0">Resultado</h2>
<button class="btn btn-sm btn-secondary ms-2" onclick="changeFontSize('box_votacao', -1)">
A-
</button>
<button class="btn btn-sm btn-secondary ms-2" onclick="changeFontSize('box_votacao', 1)">
A+
</button>
</div>
<div id="box_votacao">
<span id="votacao" class="text-value"></span>
<span id="resultado_votacao" lass="text-title"></span>
</div>
</div>
<div class="col-md-6 text-center painel" id="obs_materia_div">
<h2 class="text-subtitle" id="mat_em_votacao">Matéria em Votação</h2>
<span id="materia_legislativa_texto" class="text-value"></span>
<br>
<span id="materia_legislativa_ementa" class="text-value"></span>
<br>
<span id="observacao_materia" class="text-value"></span>
</div>
<div class="col-md-6 text-center painel" id="tema_solene_div" style="display: none">
<h2 class="text-subtitle">Tema da Sessão Solene</h2>
<span id="sessao_solene_tema" class="text-value"></span>
</div>
</div>
</div>
</div>
</div>
</body>
{% block webpack_loader_js %}
{% render_chunk_vendors 'js' %}
{% render_bundle 'global' 'js' %}
{% render_bundle 'painel' 'js' %}
{% endblock webpack_loader_js %}
{% block webpack_loader_chunks_js %}
{% endblock webpack_loader_chunks_js %}
<script>
const controllerId = "{{ controller_id }}";
const proto = location.protocol === "https:" ? "wss" : "ws";
let ws, backoff = 500, timer;
const url = `${proto}://${location.host}/ws/painel/${controllerId}/`
function update_view(data) {
$("#sessao_plenaria").text(data["sessao_plenaria"])
$("#sessao_plenaria_data").text("Data Início: " + data["sessao_plenaria_data"])
$("#sessao_plenaria_hora_inicio").text("Hora Início: " + data["sessao_plenaria_hora_inicio"])
$("#sessao_solene_tema").text(data["tema_solene"])
if (data["status_painel"] == false) {
$("#message").text("PAINEL ENCONTRA-SE FECHADO");
}
else {
$("#message").text("");
}
if (data["sessao_solene"]){
$("#resultado_votacao_div").hide();
$("#obs_materia_div").hide();
$('#tema_solene_div').show();
}
if (data["brasao"] != null)
$("#logo-painel").attr("src", data["brasao"]);
var presentes = $("#parlamentares");
var votacao = $("#votacao");
var oradores = $("#orador")
$("#votacao").text('');
presentes.children().remove();
votacao.children().remove();
oradores.children().remove();
var oradores_list = data["oradores"];
var presentes_list = data["presentes"];
if (data["status_painel"] == true) {
mostrar_voto = data["mostrar_voto"];
presentes.append('<table id="parlamentares_list">');
$.each(presentes_list, function (index, parlamentar) {
if (parlamentar.voto == 'Voto Informado' && mostrar_voto == false){
$('#parlamentares_list').append('<tr><td style="padding-right:20px; color:yellow" >' +
parlamentar.nome +
'</td> <td style="padding-right:20px; color:yellow">' +
parlamentar.partido + '</td> <td style="padding-right:20px; color:yellow">'
+ '</td></tr>')
}
else{
$('#parlamentares_list').append(show_voto(parlamentar))
}
});
presentes.append('</table>')
if (data["oradores"].length > 0){
$('#aparecer_oradores').show();
oradores.append('<table id="oradores_list">');
$.each(oradores_list, function (index, orador) {
$('#oradores_list').append('<tr><td style="padding-right:20px; color:white" >' +
orador.numero + 'º &nbsp' +
orador.nome +'</td></tr>')
});
oradores.append('</table>');
}
else {
$('#aparecer_oradores').hide();
}
}
else{
presentes.append('<span style="color:white" id="parlamentares_list">');
$('#parlamentares_list').append(
'<center>A listagem de parlamentares só aparecerá quando o painel estiver aberto.</center>')
presentes.append('</span>');
oradores.append('<span style="color:white" id="oradores_list">');
$('#oradores_list').append(
'<center>A listagem de oradores só aparecerá quando o painel estiver aberto.</center>')
oradores.append('</span>');
votacao.append('<span id="votacao">');
$("#votacao").append('<center>A votação só aparecerá quando o painel estiver aberto</center>');
votacao.append('</span>');
}
if(data["status_painel"]){
if (data['materia_legislativa_texto']){
var votacao = $("#votacao");
votacao.append("<li>Sim: " + data["numero_votos_sim"] + "</li>");
votacao.append("<li>Não: " + data["numero_votos_nao"] + "</li>");
votacao.append("<li>Abstenções: " + data["numero_abstencoes"] + "</li>");
votacao.append("<li>Presentes: " + data["num_presentes"] + "</li>");
votacao.append("<li>Total votos: " + data["total_votos"] + "</li>");
}
else{
$("#votacao").append('<center>Não há votação, pois não há nenhuma matéria aberta ou já votada.</center>');
}
}
var discurso_current = data["cronometro_discurso"];
if (!discurso_previous){
discurso_previous = ''
}
if (discurso_current != discurso_previous) {
$('#cronometro_discurso').runner(discurso_current);
discurso_previous = discurso_current;
}
var aparte_current = data["cronometro_aparte"];
if (!aparte_previous){
aparte_previous = ''
}
if (aparte_current != aparte_previous) {
$('#cronometro_aparte').runner(aparte_current);
aparte_previous = aparte_current;
}
var ordem_current = data["cronometro_ordem"];
if (!ordem_previous){
ordem_previous = ''
}
if (ordem_current != ordem_previous) {
$('#cronometro_ordem').runner(ordem_current);
ordem_previous = ordem_current;
}
var consideracoes_current = data["cronometro_consideracoes"];
if (!consideracoes_previous){
consideracoes_previous = ''
}
if (consideracoes_current != consideracoes_previous) {
$('#cronometro_consideracoes').runner(consideracoes_current);
consideracoes_previous = consideracoes_current;
}
if($('#cronometro_discurso').runner('info').formattedTime == "00:00:30") {
audioAlertFinish.play();
}
if($('#cronometro_aparte').runner('info').formattedTime == "00:00:30") {
audioAlertFinish.play();
}
if($('#cronometro_ordem').runner('info').formattedTime == "00:00:30") {
audioAlertFinish.play();
}
if($('#cronometro_consideracoes').runner('info').formattedTime == "00:00:30") {
audioAlertFinish.play();
}
if(data['sessao_finalizada']){
$("#obs_materia_div").hide();
$("#resultado_votacao_div").hide();
}
else if (data['materia_legislativa_texto']){
if (data["status_painel"] == true){
$("#materia_legislativa_texto").text(data["materia_legislativa_texto"]);
$("#materia_legislativa_ementa").text(data["materia_legislativa_ementa"]);
}
else{
$("#materia_legislativa_texto").text('A Matéria em votação só aparecerá quando o painel estiver aberto');
}
}
else{
$("#materia_legislativa_texto").text('Não há nenhuma matéria votada ou para votação.');
}
if (data['observacao_materia'] && data["status_painel"] == true){
var texto = data['observacao_materia'];
if(texto.length > 151) {
$("#observacao_materia").text(texto.substr(0, 145).concat('(...)'));
}
else{
$("#observacao_materia").text(texto);
}
}
else{
$("#observacao_materia").text('');
}
if (data['tipo_resultado'] && data['status_painel'] == true){
if(data['tipo_votacao'] != 'Leitura' && !data['sessao_finalizada'] && !data["sessao_solene"]){
$("#resultado_votacao").css("color", "#45919D");
$("#mat_em_votacao").text("Matéria em Votação");
$("#resultado_votacao_div").show();
}
else{
$("#resultado_votacao_div").hide();
$("#mat_em_votacao").text("Matéria em Leitura");
}
console.log(data["tipo_resultado"], data['tipo_votacao']);
$("#resultado_votacao").text(data["tipo_resultado"]);
var resultado_votacao_upper = $("#resultado_votacao").text().toUpperCase();
console.log(resultado_votacao_upper, data['tipo_resultado']);
if (resultado_votacao_upper.search("APROV") != -1){
$("#resultado_votacao").css("color", "#7CFC00");
$("#mat_em_votacao").text("Matéria Votada");
}
else if (resultado_votacao_upper.search("REJEIT") != -1){
$("#resultado_votacao").css("color", "red");
$("#mat_em_votacao").text("Matéria Votada");
}
else if (resultado_votacao_upper.search("LIDA") != -1){
$("#mat_em_votacao").text("Matéria Lida");
}
}
else{
$("#resultado_votacao").text('');
if(data['tipo_votacao'] != 'Leitura')
$("#mat_em_votacao").text("Matéria em Votação");
else{
$("#mat_em_votacao").text("Matéria em Leitura");
}
}
}
function connect() {
console.log(`Connecting to ${url}...`);
const ws = new WebSocket(url);
let lastPong = Date.now(), pingTimer;
// When the connection is established
ws.onopen = function () {
console.log("✅ WebSocket connected");
// Optionally send an initial message to the server
ws.send(JSON.stringify({ type: "hello", message: "Client connected" }));
// Ping keep-alive
pingTimer = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({type:"ping", ts:Date.now()}));
if (Date.now() - lastPong > 75000) ws.close(); // force reconnect
}, 30000);
};
ws.onmessage = function (event) {
try {
const data = JSON.parse(event.data);
console.log(data);
if (data.type === "pong") lastPong = Date.now();
if (data.type === "data") {
console.log("📩 Message from server:", data);
update_view(data);
//if (data.text) {
// document.getElementById("output").textContent = data.text;
//}
}
} catch (e) {
console.error(e);
}
};
// When the connection closes
ws.onclose = function (event) {
console.log("❌ WebSocket closed:", event);
clearInterval(pingTimer);
clearTimeout(timer);
// retry with capped exponential backoff
timer = setTimeout(connect, Math.min(backoff, 10000));
backoff *= 2;
};
// When an error occurs
ws.onerror = function (error) {
console.error("⚠️ WebSocket error:", event);
};
}
// ENTRYPOINT
connect();
</script>
</html>

6
scripts/websockets.sh

@ -0,0 +1,6 @@
# DEBUG
daphne -b 127.0.0.1 -p 8000 sapl.asgi:application
# Redis-CLI
redis-cli -h localhost -p 6379
Loading…
Cancel
Save