Browse Source

Websockets with pure JS

websockets-2025
Edward Ribeiro 2 months ago
parent
commit
acdb813c19
  1. 3
      RUNBOOK.txt
  2. 13
      frontend/src/__apps/painel/main.js
  3. 306
      sapl/painel/consumers.py
  4. 3
      sapl/painel/urls.py
  5. 30
      sapl/painel/views.py
  6. 90
      sapl/sessao/models.py
  7. 345
      sapl/templates/painel/painel_v2.html
  8. 12
      vue.config.js

3
RUNBOOK.txt

@ -0,0 +1,3 @@
docker run --rm -p 6379:6379 redis:7-alpine redis-server --save "" --appendonly no
daphne -b 127.0.0.1 -p 8000 sapl.asgi:application

13
frontend/src/__apps/painel/main.js

@ -1 +1,14 @@
import './scss/painel.scss' import './scss/painel.scss'
import Vue from 'vue'
new Vue({
el: '#painel',
delimiters: ['[[', ']]'],
data() {
return {
parlamentares: [{ nome: 'zé' }, { nome: 'maria' }, { nome: 'antonio' }]
}
},
mounted() { console.log('Vue mounted'); }
})

306
sapl/painel/consumers.py

@ -10,24 +10,53 @@ from django.db.models import Q
from sapl.base.models import CasaLegislativa, AppConfig from sapl.base.models import CasaLegislativa, AppConfig
from sapl.sessao.models import SessaoPlenaria, OrdemDia, ExpedienteMateria, RegistroVotacao, RegistroLeitura, \ from sapl.sessao.models import SessaoPlenaria, OrdemDia, ExpedienteMateria, RegistroVotacao, RegistroLeitura, \
PresencaOrdemDia, SessaoPlenariaPresenca, OradorExpediente, VotoParlamentar, AbstractOrdemDia PresencaOrdemDia, SessaoPlenariaPresenca, OradorExpediente, VotoParlamentar, AbstractOrdemDia, SessaoPresencaView, \
SessaoOradorView, SessaoMateriaVotacaoView
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def get_dados_painel(pk: int) -> dict: def get_dados_painel(pk: int) -> dict:
app_config = AppConfig.objects.first()
sessao = SessaoPlenaria.objects.get(id=pk) sessao = SessaoPlenaria.objects.get(id=pk)
casa = CasaLegislativa.objects.first() casa = CasaLegislativa.objects.first()
app_config = AppConfig.objects.first()
if casa and app_config and (bool(casa.logotipo)):
brasao = casa.logotipo.url \ brasao = casa.logotipo.url \
if app_config.mostrar_brasao_painel else None 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() # FILIACAO
# { 1: { "id": 1, "nome": "fulano", "filiacao": "aquela"}, ...}
# [ { "id": 1, "nome": "fulano", "filiacao": "aquela"}, ... ]
# Painel
presentes = SessaoPresencaView.objects.filter(sessao_plenaria_id=4984,
etapa_sessao='expediente').values_list('parlamentar_id',
'nome_parlamentar',
'filiacao', )
presentes = [dict(zip(['parlamentar_id', 'nome_parlamentar', 'filiacao'], p)) for p in presentes]
oradores = SessaoOradorView.objects.filter(sessao_plenaria_id=4983,
etapa_sessao='expediente').values_list('ordem_pronunciamento',
'nome_parlamentar',
)
oradores = [dict(zip(['ordem_pronunciamento', 'nome_parlamentar'], o)) for o in oradores]
votos = SessaoMateriaVotacaoView.objects.get(sessao_plenaria_id=4984, etapa_sessao='ordemdia', materia_id=4148)
# TODO: recover stopwatch state from DB/Cache
stopwatch = {
"type": "stopwatch.state",
"id": "sw:main",
"status": "running", # "running" | "paused" | "stopped"
"started_at_ms": 1699990000123, # epoch ms when (re)started
"elapsed_ms": 5320
}
dados_sessao = { dados_sessao = {
"type": "data", "type": "data",
"data": {}, # legacy
"sessao": {
"status_painel": sessao.painel_aberto, "status_painel": sessao.painel_aberto,
"brasao": brasao, "brasao": brasao,
"mostrar_voto": app_config.mostrar_voto, "mostrar_voto": app_config.mostrar_voto,
@ -37,260 +66,20 @@ def get_dados_painel(pk: int) -> dict:
"sessao_solene": sessao.tipo.nome == "Solene", "sessao_solene": sessao.tipo.nome == "Solene",
"sessao_finalizada": sessao.finalizada, "sessao_finalizada": sessao.finalizada,
"tema_solene": sessao.tema_solene, "tema_solene": sessao.tema_solene,
# "cronometro_aparte": get_cronometro_status(request, "aparte"), "status_painel": False, # TODO: recover from DB **and** move status to other place.
# "cronometro_discurso": get_cronometro_status(request, "discurso"), },
# "cronometro_ordem": get_cronometro_status(request, "ordem"), "presentes": presentes,
# "cronometro_consideracoes": get_cronometro_status(request, "consideracoes"), "oradores": oradores,
"votacao": votos.total_votos, # TODO unify into single json
"votos_parlamentar": votos.votos_parlamentares, # TODO: unify into single JSON
"materia_legislativa_ementa": votos.materia.ementa,
"stopwatch": stopwatch, # TODO: array of stopwatches
} }
# Caso tenha alguma matéria com votação aberta, ela é mostrada no painel print(json.dumps(dados_sessao, indent=4))
# 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 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): class PainelConsumer(AsyncJsonWebsocketConsumer):
# def __init__(self): # def __init__(self):
@ -313,12 +102,15 @@ class PainelConsumer(AsyncJsonWebsocketConsumer):
self.controller_id = controller_id self.controller_id = controller_id
self.group = f"controller_{controller_id}" self.group = f"controller_{controller_id}"
print(self.group)
await self.channel_layer.group_add(self.group, self.channel_name) await self.channel_layer.group_add(self.group, self.channel_name)
await self.accept() await self.accept()
# await self.send_json({"type": "data", # await self.send_json({"type": "data",
# "text": "Connection established!"}) # "text": "Connection established!"})
print("SENDING DATA DO CONSUMER ")
print(get_dados_painel(controller_id))
await self.send_json(get_dados_painel(controller_id)) await self.send_json(get_dados_painel(controller_id))
async def disconnect(self, code): async def disconnect(self, code):
@ -328,7 +120,7 @@ class PainelConsumer(AsyncJsonWebsocketConsumer):
# Called by server via channel_layer.group_send # Called by server via channel_layer.group_send
async def notify(self, event): async def notify(self, event):
# event: {"type": "notify", "data": {...}} # event: {"type": "notify", "data": {...}}
await self.send_json(event["data"]) await self.send_json(event)
async def receive(self, text_data=None, bytes_data=None): async def receive(self, text_data=None, bytes_data=None):
data = json.loads(text_data or "{}") data = json.loads(text_data or "{}")
@ -337,7 +129,7 @@ class PainelConsumer(AsyncJsonWebsocketConsumer):
print("PING") print("PING")
await self.send_json({"type": "pong", "ts": time.time()}) await self.send_json({"type": "pong", "ts": time.time()})
return return
await self.send_json({"type": "data", await self.send_json({"type": "echo",
"text": f"Echo: {data}"}) "text": f"Echo: {data}"})
@database_sync_to_async @database_sync_to_async

3
sapl/painel/urls.py

@ -3,7 +3,7 @@ from django.conf.urls import url
from .apps import AppConfig from .apps import AppConfig
from .views import (cronometro_painel, get_dados_painel, painel_mensagem_view, from .views import (cronometro_painel, get_dados_painel, painel_mensagem_view,
painel_parlamentar_view, painel_view, painel_votacao_view, painel_parlamentar_view, painel_view, painel_votacao_view,
switch_painel, verifica_painel, votante_view, websocket_view) switch_painel, verifica_painel, votante_view, websocket_view, painel_controller_view)
from django.urls import path from django.urls import path
@ -29,4 +29,5 @@ urlpatterns = [
# url(r'^painel', websocket_view, name='painel_websocket'), # url(r'^painel', websocket_view, name='painel_websocket'),
path("painel/v2", websocket_view, name='painel_websocket'), path("painel/v2", websocket_view, name='painel_websocket'),
path("painel/controller", painel_controller_view, name='painel_controller'),
] ]

30
sapl/painel/views.py

@ -647,13 +647,37 @@ def websocket_view(request):
return render(request, "painel/painel_v2.html", context) return render(request, "painel/painel_v2.html", context)
def push_to_me(request): def painel_controller_view(request):
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer from channels.layers import get_channel_layer
print("called") # LOG!
command = request.GET["stopwatch"]
print("command", command)
if command:
layer = get_channel_layer() layer = get_channel_layer()
group = f"user_{request.user.pk}" controller_id = 4984 # TODO: recover from template call
group = f"controller_{controller_id}"
print(group)
async_to_sync(layer.group_send)( async_to_sync(layer.group_send)(
group, {"type": "notify", "data": {"text": "server says hi!"}} group, {"type": "notify", "stopwatch": command}
) )
# await self.channel_layer.group_add(self.group, self.channel_name)
return JsonResponse({"ok": True}) return JsonResponse({"ok": True})
# 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})

90
sapl/sessao/models.py

@ -1,6 +1,7 @@
from operator import xor from operator import xor
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.contrib.postgres.fields.jsonb import JSONField
from django.db import models from django.db import models
from django.db.models import Q, F from django.db.models import Q, F
from django.utils import timezone, formats from django.utils import timezone, formats
@ -1052,13 +1053,94 @@ class Correspondencia(models.Model):
return _('Correspondência: {}').format(self.documento) return _('Correspondência: {}').format(self.documento)
class OradoresView(models.Model): ##
materia = models.ForeignKey(MateriaLegislativa, on_delete=models.DO_NOTHING) ## Painel V2
##
class SessaoPresencaView(models.Model):
sessao_plenaria = models.ForeignKey(SessaoPlenaria,
on_delete=models.DO_NOTHING,
verbose_name=_('Sessão Plenária'))
parlamentar = models.ForeignKey(Parlamentar,
on_delete=models.DO_NOTHING,
verbose_name=_('Parlamentar'))
etapa_sessao = models.CharField(max_length=15)
nome_parlamentar = models.CharField(max_length=50,
verbose_name=_('Nome Parlamentar'))
filiacao = models.CharField(max_length=50,
verbose_name=_('Filiacao'))
class Meta: class Meta:
managed = False managed = False
db_table = "sessao_presenca_view" db_table = "sessao_presenca_view"
ordering = ('-id',) ordering = ('sessao_plenaria_id', 'etapa_sessao', 'nome_parlamentar')
def __str__(self):
return f"{self.sessao_plenaria} - {self.etapa_sessao} - {self.parlamentar}"
class SessaoOradorView(models.Model):
sessao_plenaria = models.ForeignKey(SessaoPlenaria,
on_delete=models.DO_NOTHING,
verbose_name=_('Sessão Plenária'))
parlamentar = models.ForeignKey(Parlamentar,
on_delete=models.DO_NOTHING,
verbose_name=_('Parlamentar'))
etapa_sessao = models.CharField(max_length=15)
nome_parlamentar = models.CharField(max_length=50,
verbose_name=_('Nome Parlamentar'))
ordem_pronunciamento = models.PositiveIntegerField(verbose_name=_('Ordem Pronunciamento'))
nome_parlamentar = models.CharField(max_length=50, verbose_name=_('Nome Parlamentar'))
filiacao = models.CharField(max_length=20, verbose_name=_('Sigla'))
class Meta:
managed = False
db_table = "sessao_orador_view"
ordering = ('sessao_plenaria_id', 'etapa_sessao', 'ordem_pronunciamento', 'nome_parlamentar')
def __str__(self):
return f"{self.sessao_plenaria} - {self.etapa_sessao} - {self.ordem_pronunciamento} - {self.parlamentar}"
class SessaoMateriaVotacaoView(models.Model):
sessao_plenaria = models.ForeignKey(SessaoPlenaria,
on_delete=models.DO_NOTHING,
verbose_name=_('Sessão Plenária'))
materia = models.ForeignKey(MateriaLegislativa,
on_delete=models.DO_NOTHING,
verbose_name=_('Matéria Legislativa'))
etapa_sessao = models.CharField(max_length=15)
numero_ordem = models.PositiveIntegerField(verbose_name=_('Número de Ordem'))
total_votos = JSONField(null=True, verbose_name=_('Total Votos'))
votos_parlamentares = JSONField(null=True, verbose_name=_('Votos Parlamentares'))
votacao_aberta = models.BooleanField(default=False)
tipo_votacao = models.PositiveIntegerField(verbose_name=_('Tipo Votação'))
tipo_votacao_descricao = models.CharField(max_length=15)
resultado = models.CharField(
max_length=256,
blank=True,
verbose_name=_('Resultado'))
resultado_votacao = models.CharField(
max_length=256,
blank=True,
verbose_name=_('Resultado Votação'))
class Meta:
managed = False
db_table = "sessao_materias_votacoes_view"
ordering = ('sessao_plenaria_id', 'etapa_sessao', 'numero_ordem')
def __str__(self): def __str__(self):
return '{}/{}'.format(self.materia, self.tramitacao) return f"{self.sessao_plenaria} - {self.etapa_sessao} - {self.tipo_votacao_descricao} - {self.total_votos} - {self.votos_parlamentares}"

345
sapl/templates/painel/painel_v2.html

@ -3,8 +3,10 @@
{% load render_bundle from webpack_loader %} {% load render_bundle from webpack_loader %}
{% load webpack_static from webpack_loader %} {% load webpack_static from webpack_loader %}
<!DOCTYPE HTML> <!DOCTYPE HTML>
<!--[if IE 8]> <html class="no-js lt-ie9" lang="pt-br"> <![endif]--> <!--[if IE 8]>
<html class="no-js lt-ie9" lang="pt-br"> <![endif]-->
<!--[if gt IE 8]><!--> <!--[if gt IE 8]><!-->
<html lang="pt-br"> <html lang="pt-br">
<!--<![endif]--> <!--<![endif]-->
@ -13,6 +15,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- TODO: does it need this head_title here? --> <!-- TODO: does it need this head_title here? -->
<title>{% block head_title %}{% trans 'SAPL - Sistema de Apoio ao Processo Legislativo' %}{% endblock %}</title> <title>{% block head_title %}{% trans 'SAPL - Sistema de Apoio ao Processo Legislativo' %}{% endblock %}</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
{% block webpack_loader_css %} {% block webpack_loader_css %}
{% render_chunk_vendors 'css' %} {% render_chunk_vendors 'css' %}
@ -31,14 +34,23 @@
list-style-type: none; list-style-type: none;
} }
} }
</style> </style>
</head> </head>
<body class="painel-principal"> <body class="painel-principal">
<audio type="hidden" id="audio" src="{% webpack_static 'audio/ring.mp3' %}"></audio>
<!--<div id="display">00:00:00.00</div>-->
<!--<button id="startBtn">Start</button>-->
<!--<button id="stopBtn">Stop</button>-->
<!--<button id="resumeBtn">Resume</button>-->
<!--<button id="resetBtn">Reset</button>-->
<audio type="hidden" id="alarm" src="{% webpack_static 'audio/ring.mp3' %}"></audio>
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<h1 id="sessao_plenaria" class="title text-title"></h1> <h1 id="sessao_plenaria" class="title text-title"></h1>
</div> </div>
<div class="row "> <div class="row ">
<div class="col text-center"> <div class="col text-center">
<span id="sessao_plenaria_data" class="text-value"></span> <span id="sessao_plenaria_data" class="text-value"></span>
@ -48,6 +60,14 @@
</div> </div>
</div> </div>
{% block vue_content %}
<div id="painel" class="col text-center">
<div v-for="parlamentar in parlamentares" class="text-value">
<strong>[[ parlamentar.nome ]]</strong>
</div>
</div>
{% endblock %}
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-1"> <div class="col-1">
<img src="" id="logo-painel" class="logo-painel" alt=""/> <img src="" id="logo-painel" class="logo-painel" alt=""/>
@ -139,229 +159,90 @@
{% block webpack_loader_chunks_js %} {% block webpack_loader_chunks_js %}
{% endblock webpack_loader_chunks_js %} {% endblock webpack_loader_chunks_js %}
<script> <script>
$(document).ready(function() {
const controllerId = "{{ controller_id }}"; const controllerId = "{{ controller_id }}";
const proto = location.protocol === "https:" ? "wss" : "ws"; const proto = location.protocol === "https:" ? "wss" : "ws";
let ws, backoff = 500, timer; let ws, backoff = 500, connTimer;
const url = `${proto}://${location.host}/ws/painel/${controllerId}/` const url = `${proto}://${location.host}/ws/painel/${controllerId}/`
function update_view(data) { function update_view(data) {
$("#sessao_plenaria").text(data["sessao_plenaria"]) console.log(data)
$("#sessao_plenaria_data").text("Data Início: " + data["sessao_plenaria_data"]) let sessao = data.sessao
$("#sessao_plenaria_hora_inicio").text("Hora Início: " + data["sessao_plenaria_hora_inicio"])
$("#sessao_solene_tema").text(data["tema_solene"]) // DADOS SESSAO
if (data["status_painel"] == false) { $("#sessao_plenaria").text(sessao.sessao_plenaria)
$("#sessao_plenaria_data").text("Data Início: " + sessao.sessao_plenaria_data)
$("#sessao_plenaria_hora_inicio").text("Hora Início: " + sessao.sessao_plenaria_hora_inicio)
$("#sessao_solene_tema").text(sessao.tema_solene)
// PANEL CONFIG
if (sessao.status_painel === false) {
$("#message").text("PAINEL ENCONTRA-SE FECHADO"); $("#message").text("PAINEL ENCONTRA-SE FECHADO");
} }
else { else {
$("#message").text(""); $("#message").text("");
} }
if (data["sessao_solene"]){ if (sessao.sessao_solene){
$("#resultado_votacao_div").hide(); $("#resultado_votacao_div").hide();
$("#obs_materia_div").hide(); $("#obs_materia_div").hide();
$('#tema_solene_div').show(); $('#tema_solene_div').show();
} }
if (data["brasao"] != null) if (sessao.brasao != null)
$("#logo-painel").attr("src", data["brasao"]); $("#logo-painel").attr("src", sessao.brasao);
// PARLAMENTARES
var presentes_list = data.presentes;
var presentes = $("#parlamentares"); var presentes = $("#parlamentares");
var votacao = $("#votacao");
var oradores = $("#orador")
$("#votacao").text('');
presentes.children().remove(); presentes.children().remove();
votacao.children().remove(); console.log(presentes_list.length);
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">'); presentes.append('<table id="parlamentares_list">');
$.each(presentes_list, function (index, parlamentar) { $.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" >' + $('#parlamentares_list').append('<tr><td style="padding-right:20px; color:yellow" >' +
parlamentar.nome + parlamentar.nome_parlamentar +
'</td> <td style="padding-right:20px; color:yellow">' + '</td> <td style="padding-right:20px; color:yellow">' +
parlamentar.partido + '</td> <td style="padding-right:20px; color:yellow">' parlamentar.filiacao + '</td> <td style="padding-right:20px; color:yellow">'
+ '</td></tr>') + '</td></tr>');
}
else{
$('#parlamentares_list').append(show_voto(parlamentar))
}
}); });
presentes.append('</table>')
if (data["oradores"].length > 0){
// VOTOS
var votos = data.votacao;
var votacao = $("#votacao");
// retornar o total no JSON
let total_votos = votos["sim"] + votos["não"] + votos["abstencoes"]
votacao.append("<li>Sim: " + votos["sim"] + "</li>");
votacao.append("<li>Não: " + votos["não"] + "</li>");
votacao.append("<li>Abstenções: " + votos["abstencoes"] + "</li>");
votacao.append("<li>Presentes: " + presentes_list.length + "</li>");
votacao.append("<li>Total votos: " + total_votos + "</li>");
// ORADORES
var oradores_list = data.oradores;
var oradores = $("#orador")
oradores.children().remove();
if (oradores_list.length > 0){
$('#aparecer_oradores').show(); $('#aparecer_oradores').show();
oradores.append('<table id="oradores_list">'); oradores.append('<table id="oradores_list">');
$.each(oradores_list, function (index, orador) { $.each(oradores_list, function (index, orador) {
$('#oradores_list').append('<tr><td style="padding-right:20px; color:white" >' + $('#oradores_list').append('<tr><td style="padding-right:20px; color:white" >' +
orador.numero + 'º &nbsp' + orador.ordem_pronunciamento + 'º &nbsp' +
orador.nome +'</td></tr>') orador.nome_parlamentar +'</td></tr>')
}); });
oradores.append('</table>'); oradores.append('</table>');
} }
else { else {
$('#aparecer_oradores').hide(); $('#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) { // Matéria
$('#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_texto").text(data["materia_legislativa_texto"]);
$("#materia_legislativa_ementa").text(data["materia_legislativa_ementa"]); $("#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() { function connect() {
@ -373,7 +254,7 @@
ws.onopen = function () { ws.onopen = function () {
console.log("✅ WebSocket connected"); console.log("✅ WebSocket connected");
// Optionally send an initial message to the server // Optionally send an initial message to the server
ws.send(JSON.stringify({ type: "hello", message: "Client connected" })); ws.send(JSON.stringify({ type: "echo", message: "Client connected" }));
// Ping keep-alive // Ping keep-alive
pingTimer = setInterval(() => { pingTimer = setInterval(() => {
@ -390,9 +271,18 @@
if (data.type === "data") { if (data.type === "data") {
console.log("📩 Message from server:", data); console.log("📩 Message from server:", data);
update_view(data); update_view(data);
//if (data.text) { }
// document.getElementById("output").textContent = data.text; if (data.type == "notify") {
//} console.log("command from backend:", data);
if (data.stopwatch === "start") {
startTimer();
} else if (data.stopwatch === "stop") {
stopTimer();
} else if (data.stopwatch === "resume") {
resumeTimer();
} else if (data.stopwatch === "reset") {
resetTimer();
}
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -404,10 +294,10 @@
console.log("❌ WebSocket closed:", event); console.log("❌ WebSocket closed:", event);
clearInterval(pingTimer); clearInterval(pingTimer);
clearTimeout(timer); clearTimeout(connTimer);
// retry with capped exponential backoff // retry with capped exponential backoff
timer = setTimeout(connect, Math.min(backoff, 10000)); connTimer = setTimeout(connect, Math.min(backoff, 10000));
backoff *= 2; backoff *= 2;
}; };
@ -417,7 +307,78 @@
}; };
} }
let startTime = 0;
let elapsed = 0;
let timer = null;
let durationMs = 1 * 60 * 1000; // <-- initial time (1 minutes)
let remaining = durationMs;
let alarmPlayed = false;
$("#cronometro_discurso").text(formatTime(durationMs));
function formatTime(ms) {
const totalSeconds = Math.floor(ms / 1000);
const hours = String(Math.floor(totalSeconds / 3600)).padStart(2, '0');
const minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, '0');
const seconds = String(totalSeconds % 60).padStart(2, '0');
const centis = String(Math.floor((ms % 1000) / 10)).padStart(2, '0');
return `${hours}:${minutes}:${seconds}.${centis}`;
}
function updateDisplay() {
const now = Date.now();
const diff = remaining - (now - startTime);
const display = Math.max(0, diff);
$("#cronometro_discurso").text(formatTime(display));
if (diff <= 0 && !alarmPlayed) {
alarmPlayed = true;
$("#alarm")[0].play();
clearInterval(timer);
timer = null;
}
}
function startTimer() {
console.log("start");
if (timer) return;
alarmPlayed = false;
startTime = Date.now();
timer = setInterval(updateDisplay, 10);
}
function stopTimer() {
if (!timer) return;
clearInterval(timer);
timer = null;
remaining -= Date.now() - startTime;
}
function resumeTimer() {
if (timer) return;
startTime = Date.now();
timer = setInterval(updateDisplay, 10);
}
function resetTimer() {
clearInterval(timer);
timer = null;
remaining = durationMs;
alarmPlayed = false;
$("#cronometro_discurso").text(formatTime(remaining));
}
// Bind buttons
/*
$("#startBtn").click(startTimer);
$("#stopBtn").click(stopTimer);
$("#resumeBtn").click(resumeTimer);
$("#resetBtn").click(resetTimer);
*/
// ENTRYPOINT // ENTRYPOINT
connect(); connect();
})
</script> </script>
</html> </html>

12
vue.config.js

@ -16,6 +16,18 @@ dotenv.config({
module.exports = { module.exports = {
runtimeCompiler: true, runtimeCompiler: true,
configureWebpack: {
resolve: {
alias: {
// This ensures that imports of 'vue' use the build with the template compiler
'vue$': 'vue/dist/vue.esm.js'
}
}
},
// Optional but recommended if Django serves the final HTML:
// disable source maps in production builds to avoid exposing source
productionSourceMap: false,
publicPath: publicPath:
process.env.NODE_ENV === 'production' process.env.NODE_ENV === 'production'
? '/static/sapl/frontend' ? '/static/sapl/frontend'

Loading…
Cancel
Save