From eacfd6ae824ff899cca1fa72d2b91c2c2065d031 Mon Sep 17 00:00:00 2001 From: Edward Oliveira Date: Mon, 28 Sep 2026 15:35:32 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20aplica=20ajustes=20da=20revis=C3=A3o=20d?= =?UTF-8?q?o=20#3855=20na=20vota=C3=A7=C3=A3o=20nominal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - renumera as migrações para 0071/0072, após 0070_presenca_unica_por_sessao - encerramento conta só os presentes e grava 'Não Votou' para quem não votou - fechar outras votações usa update() e zera registro_aberto - abertura simultânea mostra erro em vez de 500; "foram fechadas" só após commit - poll do tablet não acumula mensagens; voto_individual.html exibe messages - poll do tablet recarrega em resposta não-JSON e não sobrepõe requisições - remove timers duplicados de conta_votos e o hidden obsoleto em nominal.html - painel só mostra "Não votou" após o registro da votação - "Registrar Votação" volta a ser GET - 0071 carrega só os grupos duplicados; 0072 zera registro_aberto legado - usa get_or_create diretamente e remove código morto Co-Authored-By: Claude Opus 5.5 --- sapl/painel/tests/tests.py | 46 +++- sapl/painel/views.py | 52 ++-- ...0071_votoparlamentar_unique_constraint.py} | 24 +- ... 0072_votacao_aberta_unique_constraint.py} | 16 +- sapl/sessao/tests/test_votacao_nominal.py | 226 ++++++++++++++++-- sapl/sessao/views.py | 188 +++++++-------- sapl/templates/painel/index.html | 7 +- sapl/templates/painel/voto_individual.html | 25 +- sapl/templates/sessao/votacao/nominal.html | 8 - 9 files changed, 408 insertions(+), 184 deletions(-) rename sapl/sessao/migrations/{0070_votoparlamentar_unique_constraint.py => 0071_votoparlamentar_unique_constraint.py} (68%) rename sapl/sessao/migrations/{0071_votacao_aberta_unique_constraint.py => 0072_votacao_aberta_unique_constraint.py} (65%) diff --git a/sapl/painel/tests/tests.py b/sapl/painel/tests/tests.py index 826bf90de..dd7753d18 100644 --- a/sapl/painel/tests/tests.py +++ b/sapl/painel/tests/tests.py @@ -9,7 +9,8 @@ from sapl.base.models import AppConfig as ConfiguracoesAplicacao from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa from sapl.parlamentares.models import (Legislatura, Mandato, Parlamentar, SessaoLegislativa, Votante) -from sapl.sessao.models import (OrdemDia, PresencaOrdemDia, SessaoPlenaria, +from sapl.sessao.models import (ExpedienteMateria, OrdemDia, + PresencaOrdemDia, SessaoPlenaria, TipoResultadoVotacao, TipoSessaoPlenaria, VotoParlamentar) @@ -158,6 +159,11 @@ def test_post_de_voto_bloqueado_nao_persiste(admin_client, admin_user): assert not VotoParlamentar.objects.filter( ordem=ordem, parlamentar=parlamentar).exists() + # voto_individual.html é avulso (não herda base.html): o erro precisa + # aparecer na própria tela do tablet. + pagina = admin_client.get(response.url) + assert 'A votação não está disponível para novos votos.' in pagina.content.decode() + @pytest.mark.django_db(transaction=False) def test_post_rejeita_valor_de_voto_invalido(admin_client, admin_user): @@ -171,6 +177,9 @@ def test_post_rejeita_valor_de_voto_invalido(admin_client, admin_user): assert not VotoParlamentar.objects.filter( ordem=ordem, parlamentar=parlamentar).exists() + pagina = admin_client.get(response.url) + assert 'Voto inválido.' in pagina.content.decode() + @pytest.mark.django_db(transaction=False) def test_propria_tela_nao_mostra_voto_de_outra_materia(admin_client): @@ -193,7 +202,6 @@ def test_propria_tela_nao_mostra_voto_de_outra_materia(admin_client): ordem_antiga.save() baker.make(OrdemDia, sessao_plenaria=sessao, materia=_materia(), tipo_votacao=NOMINAL, votacao_aberta=True, registro_aberto=False) - baker.make(PresencaOrdemDia, sessao_plenaria=sessao, parlamentar=vereador) status_url = reverse('sapl.painel:voto_individual_status') @@ -228,10 +236,10 @@ def test_get_dados_painel_nao_usa_etag_incompleto(admin_client): @pytest.mark.django_db(transaction=False) def test_painel_exibe_nao_votou_para_parlamentar_sem_voto(admin_client): """ - Regressão: depois que o fechamento deixou de persistir o valor - provisório "Não Votou", parlamentares sem VotoParlamentar passaram a - chegar ao painel como null. O JavaScript não pode renderizar esse null - literalmente no telão. + Quem estava presente e não votou chega ao painel como 'Não Votou' depois + do encerramento. Para dado legado sem VotoParlamentar (voto null), o + JavaScript mostra "Não votou" só com a votação registrada — com ela + aberta, voto vazio significa apenas "ainda não votou". """ baker.make(ConfiguracoesAplicacao, mostrar_voto=True, mostrar_brasao_painel=False) @@ -261,12 +269,12 @@ def test_painel_exibe_nao_votou_para_parlamentar_sem_voto(admin_client): 'sapl.painel:dados_painel', kwargs={'pk': sessao.pk})).json() parlamentar_sem_voto = next( p for p in dados['presentes'] if p['parlamentar_id'] == nao_votou.pk) - assert parlamentar_sem_voto['voto'] is None + assert parlamentar_sem_voto['voto'] == 'Não Votou' painel = admin_client.get(reverse( 'sapl.painel:painel_principal', kwargs={'pk': sessao.pk})) assert painel.status_code == 200 - assert b'if (!parlamentar.voto)' in painel.content + assert b'if (!parlamentar.voto && registrada)' in painel.content assert 'Não votou'.encode() in painel.content @@ -332,3 +340,25 @@ def test_votante_status_reflete_estado_e_nao_exige_permissao_do_painel(): data2 = resposta2.json() assert data2['voto_parlamentar'] == 'Não' assert 'encerramento da votação' in data2['status_message'] + + +@pytest.mark.django_db(transaction=False) +def test_votante_status_com_duas_votacoes_abertas_nao_acumula_mensagens(): + """ + Uma OrdemDia e uma ExpedienteMateria abertas ao mesmo tempo (as + constraints são por tabela): o poll do tablet devolve o erro em + error_message, em texto puro, sem empilhar mensagens na sessão. + """ + sessao, ordem = _ordem_nominal_aberta() + vereador, votante_client = _votante_com_client(sessao) + outra_sessao = _sessao_plenaria() + baker.make(ExpedienteMateria, sessao_plenaria=outra_sessao, + materia=_materia(), tipo_votacao=NOMINAL, votacao_aberta=True) + + status_url = reverse('sapl.painel:voto_individual_status') + for _ in range(2): + data = votante_client.get(status_url).json() + + assert 'mais de uma' in data['error_message'] + assert '<' not in data['error_message'] + assert '_messages' not in votante_client.session diff --git a/sapl/painel/views.py b/sapl/painel/views.py index 68b798af0..67a2016ca 100644 --- a/sapl/painel/views.py +++ b/sapl/painel/views.py @@ -45,6 +45,10 @@ def votacao_aberta(request): Função que verifica se há somente 1 uma matéria aberta ou nenhuma. É utilizada como uma função auxiliar para a view votante_view. + + A mensagem de erro volta como texto puro e é exibida pelo chamador em + error_message (inclusive no poll JSON do tablet), então não é + registrada em messages — senão cada poll empilharia uma cópia na sessão. ''' logger = logging.getLogger(__name__) username = request.user.username @@ -54,19 +58,13 @@ def votacao_aberta(request): Q(expedientemateria__votacao_aberta=True)).distinct() if len(votacoes_abertas) > 1: - msg_abertas = [] - for v in votacoes_abertas: - msg_abertas.append('''
  • %s
  • ''' % ( - reverse('sapl.sessao:sessaoplenaria_detail', - kwargs={'pk': v.id}), - v.__str__())) + sessoes = ', '.join(str(v) for v in votacoes_abertas) 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: ' + sessoes + '. ' + '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.') - messages.add_message(request, messages.INFO, msg) + 'nas seguintes Sessões: ' + sessoes + '. ' + 'Para votar, peça para que o Operador feche-as.') return None, msg elif len(votacoes_abertas) == 1: @@ -79,18 +77,11 @@ def votacao_aberta(request): numero_materias_abertas = len(ordens) + len(expedientes) if numero_materias_abertas > 1: + sessao = str(votacoes_abertas.first()) 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__()))) - msg = _('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__())) + - 'Para votar, peça para que o Operador as feche.') - messages.add_message(request, messages.INFO, msg) + sessao) + msg = _('Existe mais de uma votação aberta na Sessão: ' + sessao + + '. Para votar, peça para que o Operador as feche.') return None, msg return votacoes_abertas.first(), None @@ -314,16 +305,13 @@ def votante_view(request): request, _('A votação não está mais disponível para novos votos.')) else: - try: - with transaction.atomic(): - voto, created = (VotoParlamentar.objects - .select_for_update().get_or_create( - parlamentar=parlamentar, - **fase_sessao)) - except IntegrityError: - voto = VotoParlamentar.objects.select_for_update().get( - parlamentar=parlamentar, **fase_sessao) - + # get_or_create já cria num savepoint e, se der + # IntegrityError, refaz o get() no mesmo queryset + # (com select_for_update). + voto, _created = (VotoParlamentar.objects + .select_for_update().get_or_create( + parlamentar=parlamentar, + **fase_sessao)) voto.voto = voto_submetido voto.ip = get_client_ip(request) voto.user = request.user diff --git a/sapl/sessao/migrations/0070_votoparlamentar_unique_constraint.py b/sapl/sessao/migrations/0071_votoparlamentar_unique_constraint.py similarity index 68% rename from sapl/sessao/migrations/0070_votoparlamentar_unique_constraint.py rename to sapl/sessao/migrations/0071_votoparlamentar_unique_constraint.py index 1d1570b2d..f23ff2cea 100644 --- a/sapl/sessao/migrations/0070_votoparlamentar_unique_constraint.py +++ b/sapl/sessao/migrations/0071_votoparlamentar_unique_constraint.py @@ -1,6 +1,7 @@ # Generated by Django 2.2.28 on 2026-08-16 17:36 from django.db import migrations, models +from django.db.models import Count def remove_votos_duplicados(apps, schema_editor): @@ -8,15 +9,18 @@ def remove_votos_duplicados(apps, schema_editor): VotoParlamentar = apps.get_model('sessao', 'VotoParlamentar') for materia_field in ('ordem_id', 'expediente_id'): - grupos = {} - votos = VotoParlamentar.objects.exclude(**{materia_field: None}) - for voto in votos.iterator(): - chave = (voto.parlamentar_id, getattr(voto, materia_field)) - grupos.setdefault(chave, []).append(voto) - - for votos_duplicados in grupos.values(): - if len(votos_duplicados) < 2: - continue + # order_by() limpa o Meta.ordering ('id'), que no Django 2.2 entraria + # no GROUP BY e deixaria todo grupo com n=1. + duplicadas = (VotoParlamentar.objects + .exclude(**{materia_field: None}) + .order_by() + .values('parlamentar_id', materia_field) + .annotate(n=Count('id')) + .filter(n__gt=1)) + for chave in duplicadas: + votos_duplicados = list(VotoParlamentar.objects.filter( + parlamentar_id=chave['parlamentar_id'], + **{materia_field: chave[materia_field]})) voto_preservado = max( votos_duplicados, key=lambda voto: ( @@ -33,7 +37,7 @@ def remove_votos_duplicados(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('sessao', '0069_auto_20220919_1705'), + ('sessao', '0070_presenca_unica_por_sessao'), ] operations = [ diff --git a/sapl/sessao/migrations/0071_votacao_aberta_unique_constraint.py b/sapl/sessao/migrations/0072_votacao_aberta_unique_constraint.py similarity index 65% rename from sapl/sessao/migrations/0071_votacao_aberta_unique_constraint.py rename to sapl/sessao/migrations/0072_votacao_aberta_unique_constraint.py index 897207077..0fc5e8d48 100644 --- a/sapl/sessao/migrations/0071_votacao_aberta_unique_constraint.py +++ b/sapl/sessao/migrations/0072_votacao_aberta_unique_constraint.py @@ -3,16 +3,22 @@ from django.db import migrations, models -def fecha_matérias_abertas_duplicadas(apps, schema_editor): +def fecha_materias_abertas_duplicadas(apps, schema_editor): """ Pré-requisito para o AddConstraint abaixo: se já existir mais de uma OrdemDia/ExpedienteMateria com votacao_aberta=True (dado deixado por versões anteriores, que não garantiam essa invariante), a constraint - falharia ao ser criada. Mantém aberta apenas a mais recentemente - modificada de cada tabela e fecha as demais. + falharia ao ser criada. Mantém aberta apenas a criada por último (maior + id) de cada tabela e fecha as demais. + + Também zera registro_aberto, que mudou de significado ("a Mesa abriu a + tela de registro" -> "novos votos bloqueados"): o fluxo antigo marcava + True ao abrir a tela e não voltava para False ao encerrar, o que faria + uma matéria reaberta voltar com os tablets bloqueados. """ for model_name in ('OrdemDia', 'ExpedienteMateria'): model = apps.get_model('sessao', model_name) + model.objects.filter(registro_aberto=True).update(registro_aberto=False) abertas = model.objects.filter(votacao_aberta=True).order_by('-id') for materia in abertas[1:]: materia.votacao_aberta = False @@ -22,12 +28,12 @@ def fecha_matérias_abertas_duplicadas(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('sessao', '0070_votoparlamentar_unique_constraint'), + ('sessao', '0071_votoparlamentar_unique_constraint'), ] operations = [ migrations.RunPython( - fecha_matérias_abertas_duplicadas, migrations.RunPython.noop), + fecha_materias_abertas_duplicadas, migrations.RunPython.noop), migrations.AddConstraint( model_name='expedientemateria', constraint=models.UniqueConstraint(condition=models.Q(votacao_aberta=True), fields=('votacao_aberta',), name='sessao_expedientemateria_unique_votacao_aberta'), diff --git a/sapl/sessao/tests/test_votacao_nominal.py b/sapl/sessao/tests/test_votacao_nominal.py index 7256ac09d..6abe22a8e 100644 --- a/sapl/sessao/tests/test_votacao_nominal.py +++ b/sapl/sessao/tests/test_votacao_nominal.py @@ -1,4 +1,8 @@ +import re +from unittest import mock + import pytest +from django.contrib.messages import get_messages from django.db import IntegrityError from django.urls import reverse from model_bakery import baker @@ -63,22 +67,37 @@ def test_get_registrar_votacao_nao_bloqueia_novos_votos(admin_client): @pytest.mark.django_db(transaction=False) -def test_post_sem_acao_reconhecida_apenas_renderiza(admin_client): - """ - O botão "Registrar Votação" agora faz POST (para evitar cache/replay de - GET), mas sem nenhuma chave de ação reconhecida isso deve continuar - sendo pura navegação, sem nenhum efeito colateral. - """ +def test_post_sem_acao_reconhecida_redireciona_para_registro(admin_client): sessao, ordem = _ordem_nominal_aberta() _presente(sessao) - response = admin_client.post(_registrar_url(sessao, ordem), {}) + response = admin_client.post(_registrar_url(sessao, ordem) + '?page=2', {}) - assert response.status_code == 200 + assert response.status_code == 302 + assert response.url == _registrar_url(sessao, ordem) + '?page=2' ordem.refresh_from_db() assert ordem.registro_aberto is False +@pytest.mark.django_db(transaction=False) +def test_botao_registrar_votacao_nominal_usa_get(admin_client): + """ + A tela de registro não tem efeito colateral no GET; com o botão em GET + o ?page vai na URL e é preservado até o redirect final do post(). + """ + sessao, ordem = _ordem_nominal_aberta() + + response = admin_client.get( + reverse('sapl.sessao:ordemdia_list', kwargs={'pk': sessao.pk})) + + conteudo = response.content.decode() + form = re.search( + r'
    ]*>' % re.escape(_registrar_url(sessao, ordem)), + conteudo) + assert form + assert form.group(0) == '' % _registrar_url(sessao, ordem) + + @pytest.mark.django_db(transaction=False) def test_bloquear_e_reabrir_votacao_nao_mexe_em_votos_existentes(admin_client): sessao, ordem = _ordem_nominal_aberta() @@ -145,6 +164,88 @@ def test_salvar_votacao_nao_sobrescreve_voto_ja_registrado(admin_client): assert ordem.votacao_aberta is False assert ordem.registro_aberto is False + # O 'Não Votou' enviado para quem já votou é só o valor padrão do + # ' - % get_token(request)) btn_registrar = ''' - - %s + %s
    ''' % ( - url, metodo, csrf_input, page_number) + url, page_number) else: btn_registrar = '''
    @@ -904,8 +905,7 @@ class MateriaOrdemDiaCrud(MasterDetailCrud): context = super().get_context_data(**kwargs) has_permition = self.request.user.has_module_perms(AppConfig.label) - return customize_link_materia(context, self.kwargs['pk'], has_permition, False, - request=self.request) + return customize_link_materia(context, self.kwargs['pk'], has_permition, False) def recuperar_materia(request): @@ -981,8 +981,7 @@ class ExpedienteMateriaCrud(MasterDetailCrud): context['page'] = self.request.GET.get('page') has_permition = self.request.user.has_module_perms(AppConfig.label) - return customize_link_materia(context, self.kwargs['pk'], has_permition, True, - request=self.request) + return customize_link_materia(context, self.kwargs['pk'], has_permition, True) class CreateView(MasterDetailCrud.CreateView): form_class = ExpedienteMateriaForm @@ -3075,22 +3074,6 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): 'votos': votos, }) - def _get_or_create_voto_parlamentar(self, lookup_field, lookup_value, parlamentar_id): - """ - get_or_create protegido contra a corrida de duas inserções - concorrentes para o mesmo (parlamentar, matéria) — ex.: o tablet do - parlamentar e o formulário em lote do operador chegando ao mesmo - tempo. Usa um savepoint próprio para que um IntegrityError aqui não - derrube a transação inteira do 'Encerrar Votação'. - """ - try: - with transaction.atomic(): - return VotoParlamentar.objects.select_for_update().get_or_create( - parlamentar_id=parlamentar_id, **{lookup_field: lookup_value}) - except IntegrityError: - return VotoParlamentar.objects.select_for_update().get( - parlamentar_id=parlamentar_id, **{lookup_field: lookup_value}), False - def _lock_materia_votacao(self, materia_votacao): model = OrdemDia if self.ordem else ExpedienteMateria return model.objects.select_for_update().get(pk=materia_votacao.pk) @@ -3168,20 +3151,20 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): return self._redirect_lista(kwargs, page) for voto_submetido, parlamentar_id in votos_submetidos: - voto_parlamentar, created = self._get_or_create_voto_parlamentar( - fase_sessao_field, fase_sessao_value, parlamentar_id) + if voto_submetido == 'Não Votou': + # "Não Votou" é só o valor padrão do + # sempre que existe um VotoParlamentar) mesmo quando + # o fechamento falha por falta de votos reais. Os + # presentes que continuarem sem voto recebem + # 'Não Votou' só depois que o RegistroVotacao existe. + continue + voto_parlamentar, created = VotoParlamentar.objects.select_for_update().get_or_create( + parlamentar_id=parlamentar_id, + **{fase_sessao_field: fase_sessao_value}) if created: - if voto_submetido == 'Não Votou': - # "Não Votou" é só o valor padrão do sempre que existe um VotoParlamentar) - # mesmo quando o fechamento falha por falta de - # votos reais, impedindo o operador de corrigir e - # tentar de novo. - voto_parlamentar.delete() - continue voto_parlamentar.voto = voto_submetido voto_parlamentar.user = request.user voto_parlamentar.ip = get_client_ip(request) @@ -3195,7 +3178,8 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): skipped_parlamentares.append(voto_parlamentar.parlamentar) votos_atuais = VotoParlamentar.objects.filter( - **{fase_sessao_field: fase_sessao_value}) + **{fase_sessao_field: fase_sessao_value}, + parlamentar_id__in=presentes.values('parlamentar_id')) votos_sim = votos_atuais.filter(voto='Sim').count() votos_nao = votos_atuais.filter(voto='Não').count() abstencoes = votos_atuais.filter(voto='Abstenção').count() @@ -3207,10 +3191,6 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): 'nenhum voto')) return self.form_invalid(form) - # Remove todas as votação desta matéria, caso existam - RegistroVotacao.objects.filter( - **{fase_sessao_field: fase_sessao_value}).delete() - votacao = RegistroVotacao( numero_votos_sim=votos_sim, numero_votos_nao=votos_nao, @@ -3224,6 +3204,16 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): votacao.save() votos_atuais.update(votacao_id=votacao.id) + VotoParlamentar.objects.bulk_create([ + VotoParlamentar( + parlamentar_id=presenca.parlamentar_id, + voto='Não Votou', + votacao=votacao, + user=request.user, + ip=get_client_ip(request), + **{fase_sessao_field: fase_sessao_value}) + for presenca in presentes.exclude( + parlamentar_id__in=votos_atuais.values('parlamentar_id'))]) materia_votacao.resultado = form.cleaned_data['resultado_votacao'].nome materia_votacao.votacao_aberta = False @@ -3246,10 +3236,8 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): return self.form_valid(form) - # Nenhuma chave de ação reconhecida: navegação simples para a tela - # de registro (botão "Registrar Votação"), sem efeito colateral. - context = self._build_registro_context(materia_votacao, presentes, total) - return self.render_to_response(context) + # Nenhuma ação reconhecida: volta para a tela de registro (GET). + return self._redirect_same_registro(kwargs, page) def form_invalid(self, form): errors_tuple = [(form[e].label, form.errors[e]) diff --git a/sapl/templates/painel/index.html b/sapl/templates/painel/index.html index 1ac72b669..22ef48ecb 100644 --- a/sapl/templates/painel/index.html +++ b/sapl/templates/painel/index.html @@ -331,7 +331,7 @@ + '') } else{ - $('#parlamentares_list').append(show_voto(parlamentar)) + $('#parlamentares_list').append(show_voto(parlamentar, !!data['registro'])) } }); @@ -510,10 +510,11 @@ })(); }); - function show_voto(parlamentar) { + function show_voto(parlamentar, registrada) { color = 'white' - if (!parlamentar.voto) { + // Com a votação ainda aberta, voto vazio só significa "ainda não votou". + if (!parlamentar.voto && registrada) { parlamentar.voto = "Não votou"; } if (parlamentar.voto == "Sim"){ diff --git a/sapl/templates/painel/voto_individual.html b/sapl/templates/painel/voto_individual.html index 2f33026e4..df71296c5 100644 --- a/sapl/templates/painel/voto_individual.html +++ b/sapl/templates/painel/voto_individual.html @@ -46,6 +46,10 @@ + {% for message in messages %} +

    {{ message }}

    + {% endfor %} + {% if not error_message %}

    {{sessao}}

    @@ -213,7 +217,17 @@ function verificaEstado() { fetch("{% url 'sapl.painel:voto_individual_status' %}", {credentials: 'same-origin'}) - .then(function(resp) { return resp.json(); }) + .then(function(resp) { + // Sessão expirada (redirect para o login), 403 ou 500: recarrega + // para o tablet não ficar parado na tela antiga. + var contentType = resp.headers.get('content-type') || ''; + if (!resp.ok || resp.redirected || + contentType.indexOf('application/json') === -1) { + document.location.reload(); + throw new Error('resposta inesperada'); + } + return resp.json(); + }) .then(function(data) { var novo = { materiaId: data.materia_id ? String(data.materia_id) : "", @@ -228,9 +242,14 @@ document.location.reload(); } }) - .catch(function() { /* tenta de novo no próximo poll */ }); + .catch(function() { /* falha de rede: tenta de novo no próximo poll */ }) + .then(function() { + // Agenda o próximo só depois deste terminar, para não sobrepor + // polls quando o servidor está lento. + setTimeout(verificaEstado, 5000); + }); } - setInterval(verificaEstado, 5000); + setTimeout(verificaEstado, 5000); })(); diff --git a/sapl/templates/sessao/votacao/nominal.html b/sapl/templates/sessao/votacao/nominal.html index 30f617c8e..887803ee4 100644 --- a/sapl/templates/sessao/votacao/nominal.html +++ b/sapl/templates/sessao/votacao/nominal.html @@ -35,7 +35,6 @@ {% for parlamentar in parlamentares %}
    {{parlamentar.0.nome_parlamentar}}
    - {% if parlamentar.1 %} {% endif %}