diff --git a/docker/config/nginx/sapl.conf b/docker/config/nginx/sapl.conf index a55731d02..a8cc5363b 100644 --- a/docker/config/nginx/sapl.conf +++ b/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/; diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 66c788241..95f81876f 100644 --- a/docker/docker-compose.yaml +++ b/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 diff --git a/docker/startup_scripts/start.sh b/docker/startup_scripts/start.sh index bd98bdfc0..4a4ace44e 100755 --- a/docker/startup_scripts/start.sh +++ b/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;" } diff --git a/requirements/requirements.txt b/requirements/requirements.txt index aea9f52db..bae0c65de 100644 --- a/requirements/requirements.txt +++ b/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 diff --git a/sapl/asgi.py b/sapl/asgi.py new file mode 100644 index 000000000..60388a808 --- /dev/null +++ b/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//", PainelConsumer.as_asgi()), + ])), +}) diff --git a/sapl/painel/consumers.py b/sapl/painel/consumers.py new file mode 100644 index 000000000..71aa53f19 --- /dev/null +++ b/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 "") diff --git a/sapl/painel/urls.py b/sapl/painel/urls.py index 0795d0a35..087804f92 100644 --- a/sapl/painel/urls.py +++ b/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'), ] diff --git a/sapl/painel/views.py b/sapl/painel/views.py index a4c9e2da3..5d7cf9a4e 100644 --- a/sapl/painel/views.py +++ b/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: ' + ('''
  • %s
  • ''' % ( - 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: ' + ('''
  • %s
  • ''' % ( 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}) diff --git a/sapl/sessao/models.py b/sapl/sessao/models.py index 613068afe..ebdb27cda 100644 --- a/sapl/sessao/models.py +++ b/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) \ No newline at end of file diff --git a/sapl/settings.py b/sapl/settings.py index 20ff48ad7..57c9a95d2 100644 --- a/sapl/settings.py +++ b/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 diff --git a/sapl/templates/painel/painel_v2.html b/sapl/templates/painel/painel_v2.html new file mode 100644 index 000000000..16c33ba60 --- /dev/null +++ b/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 %} + + + + + + + + + + {% block head_title %}{% trans 'SAPL - Sistema de Apoio ao Processo Legislativo' %}{% endblock %} + + {% block webpack_loader_css %} + {% render_chunk_vendors 'css' %} + {% render_bundle 'global' 'css' %} + {% render_bundle 'painel' 'css' %} + {% endblock webpack_loader_css %} + + + + + + + +
    +

    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +

    +
    + +
    +
    +
    +
    + + +
    +
    +
    +
    +

    Parlamentares

    + +
    +
    +
    +
    +

    Oradores

    + +
    + +
    +
    +

    Cronômetros

    + + +
    +
    + Discurso:
    + Aparte:
    + Questão de Ordem:
    + Considerações Finais: +
    +
    + +
    +
    +

    Resultado

    + + +
    +
    + + +
    +
    + +
    +

    Matéria em Votação

    + +
    + +
    + +
    + + +
    +
    +
    + + + {% 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 %} + + \ No newline at end of file diff --git a/scripts/websockets.sh b/scripts/websockets.sh new file mode 100644 index 000000000..0e95dc74b --- /dev/null +++ b/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 +