diff --git a/RUNBOOK.txt b/RUNBOOK.txt new file mode 100644 index 000000000..110dc8018 --- /dev/null +++ b/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 diff --git a/frontend/src/__apps/painel/main.js b/frontend/src/__apps/painel/main.js index fdf4e5dbd..48ef1ffee 100644 --- a/frontend/src/__apps/painel/main.js +++ b/frontend/src/__apps/painel/main.js @@ -1 +1,14 @@ 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'); } +}) diff --git a/sapl/painel/consumers.py b/sapl/painel/consumers.py index 71aa53f19..71a62b13f 100644 --- a/sapl/painel/consumers.py +++ b/sapl/painel/consumers.py @@ -10,24 +10,53 @@ 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 + PresencaOrdemDia, SessaoPlenariaPresenca, OradorExpediente, VotoParlamentar, AbstractOrdemDia, SessaoPresencaView, \ + SessaoOradorView, SessaoMateriaVotacaoView log = logging.getLogger(__name__) def get_dados_painel(pk: int) -> dict: + app_config = AppConfig.objects.first() 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", + brasao = casa.logotipo.url \ + if app_config.mostrar_brasao_painel else None + + # 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 = { + "type": "data", + "data": {}, # legacy + "sessao": { "status_painel": sessao.painel_aberto, "brasao": brasao, "mostrar_voto": app_config.mostrar_voto, @@ -37,260 +66,20 @@ def get_dados_painel(pk: int) -> dict: "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.')}) - + "status_painel": False, # TODO: recover from DB **and** move status to other place. + }, + "presentes": presentes, + "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 + } + + print(json.dumps(dados_sessao, indent=4)) 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): @@ -313,12 +102,15 @@ class PainelConsumer(AsyncJsonWebsocketConsumer): self.controller_id = controller_id self.group = f"controller_{controller_id}" + print(self.group) await self.channel_layer.group_add(self.group, self.channel_name) await self.accept() # await self.send_json({"type": "data", # "text": "Connection established!"}) + print("SENDING DATA DO CONSUMER ") + print(get_dados_painel(controller_id)) await self.send_json(get_dados_painel(controller_id)) async def disconnect(self, code): @@ -328,7 +120,7 @@ class PainelConsumer(AsyncJsonWebsocketConsumer): # Called by server via channel_layer.group_send async def notify(self, event): # 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): data = json.loads(text_data or "{}") @@ -337,7 +129,7 @@ class PainelConsumer(AsyncJsonWebsocketConsumer): print("PING") await self.send_json({"type": "pong", "ts": time.time()}) return - await self.send_json({"type": "data", + await self.send_json({"type": "echo", "text": f"Echo: {data}"}) @database_sync_to_async diff --git a/sapl/painel/urls.py b/sapl/painel/urls.py index 087804f92..4db4ab916 100644 --- a/sapl/painel/urls.py +++ b/sapl/painel/urls.py @@ -3,7 +3,7 @@ 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, websocket_view) + switch_painel, verifica_painel, votante_view, websocket_view, painel_controller_view) from django.urls import path @@ -29,4 +29,5 @@ urlpatterns = [ # url(r'^painel', websocket_view, name='painel_websocket'), path("painel/v2", websocket_view, name='painel_websocket'), + path("painel/controller", painel_controller_view, name='painel_controller'), ] diff --git a/sapl/painel/views.py b/sapl/painel/views.py index 5d7cf9a4e..5c628fbb2 100644 --- a/sapl/painel/views.py +++ b/sapl/painel/views.py @@ -647,13 +647,37 @@ def websocket_view(request): 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 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!"}} - ) + print("called") # LOG! + + command = request.GET["stopwatch"] + print("command", command) + + if command: + layer = get_channel_layer() + controller_id = 4984 # TODO: recover from template call + group = f"controller_{controller_id}" + + print(group) + + async_to_sync(layer.group_send)( + group, {"type": "notify", "stopwatch": command} + ) + # await self.channel_layer.group_add(self.group, self.channel_name) + 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}) diff --git a/sapl/sessao/models.py b/sapl/sessao/models.py index ebdb27cda..6770b573c 100644 --- a/sapl/sessao/models.py +++ b/sapl/sessao/models.py @@ -1,6 +1,7 @@ from operator import xor from django.core.exceptions import ValidationError +from django.contrib.postgres.fields.jsonb import JSONField from django.db import models from django.db.models import Q, F from django.utils import timezone, formats @@ -1052,13 +1053,94 @@ class Correspondencia(models.Model): 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: managed = False 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): - return '{}/{}'.format(self.materia, self.tramitacao) \ No newline at end of file + return f"{self.sessao_plenaria} - {self.etapa_sessao} - {self.tipo_votacao_descricao} - {self.total_votos} - {self.votos_parlamentares}" diff --git a/sapl/templates/painel/painel_v2.html b/sapl/templates/painel/painel_v2.html index 16c33ba60..5bd8fae6c 100644 --- a/sapl/templates/painel/painel_v2.html +++ b/sapl/templates/painel/painel_v2.html @@ -3,8 +3,10 @@ {% load render_bundle from webpack_loader %} {% load webpack_static from webpack_loader %} + - + @@ -13,12 +15,13 @@