diff --git a/sapl/api/serializers.py b/sapl/api/serializers.py index b8bb06b4d..f4a82db35 100644 --- a/sapl/api/serializers.py +++ b/sapl/api/serializers.py @@ -12,7 +12,7 @@ from rest_framework.fields import SerializerMethodField from sapl.base.models import Autor, CasaLegislativa, Metadata from sapl.parlamentares.models import Parlamentar, Mandato, Legislatura -from sapl.sessao.models import OrdemDia, SessaoPlenaria +from sapl.sessao.models import ExpedienteMateria, OrdemDia, SessaoPlenaria class SaplSerializerMixin(DrfAutoApiSerializerMixin): @@ -317,5 +317,25 @@ class SessaoPlenariaECidadaniaSerializer(serializers.ModelSerializer): return self.casa().nome def casa(self): - casa = CasaLegislativa.objects.first() - return casa + return CasaLegislativa.objects.first() + + +class OrdemDiaSerializer(SaplSerializerMixin): + """ + votacao_aberta/registro_aberto só podem ser alterados através do fluxo + de abrir_votacao()/VotacaoNominalAbstract (sapl/sessao/views.py), que + garante a invariante de no máximo uma matéria aberta por vez. Editáveis + aqui, a API contornaria essa garantia por completo. + """ + class Meta: + model = OrdemDia + fields = '__all__' + read_only_fields = ('votacao_aberta', 'registro_aberto') + + +class ExpedienteMateriaSerializer(SaplSerializerMixin): + """Ver OrdemDiaSerializer — mesmo motivo para os mesmos dois campos.""" + class Meta: + model = ExpedienteMateria + fields = '__all__' + read_only_fields = ('votacao_aberta', 'registro_aberto') diff --git a/sapl/api/tests/test_serializers_sessao.py b/sapl/api/tests/test_serializers_sessao.py new file mode 100644 index 000000000..69e8d7259 --- /dev/null +++ b/sapl/api/tests/test_serializers_sessao.py @@ -0,0 +1,19 @@ +import pytest +from model_bakery import baker + +from sapl.api.serializers import SessaoPlenariaECidadaniaSerializer +from sapl.base.models import CasaLegislativa +from sapl.sessao.models import SessaoPlenaria + + +@pytest.mark.django_db(transaction=False) +def test_sessao_ecidadania_serializa_dados_da_casa(): + casa = baker.make(CasaLegislativa, nome='Câmara Municipal', sigla='CM', + endereco='Praça Central') + sessao = baker.make(SessaoPlenaria) + + data = SessaoPlenariaECidadaniaSerializer(sessao).data + + assert data['txtNomeOrgao'] == casa.nome + assert data['txtSiglaOrgao'] == casa.sigla + assert data['txtLocal'] == casa.endereco diff --git a/sapl/painel/tests/tests.py b/sapl/painel/tests/tests.py index a79ca8be5..84b451702 100644 --- a/sapl/painel/tests/tests.py +++ b/sapl/painel/tests/tests.py @@ -1,3 +1,289 @@ -# from django.test import TestCase +import pytest +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission +from django.test import Client +from django.urls import reverse +from model_bakery import baker -# Create your tests here. +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, + TipoResultadoVotacao, TipoSessaoPlenaria, + VotoParlamentar) + +NOMINAL = 2 + + +def _sessao_plenaria(): + legislatura = baker.make(Legislatura) + sessao_legislativa = baker.make(SessaoLegislativa) + tipo = baker.make(TipoSessaoPlenaria) + return baker.make(SessaoPlenaria, legislatura=legislatura, + sessao_legislativa=sessao_legislativa, tipo=tipo, numero=1) + + +def _materia(): + tipo_materia = baker.make(TipoMateriaLegislativa) + return baker.make(MateriaLegislativa, tipo=tipo_materia) + + +def _ordem_nominal_aberta(registro_aberto=False): + sessao = _sessao_plenaria() + materia = _materia() + ordem = baker.make(OrdemDia, sessao_plenaria=sessao, materia=materia, + tipo_votacao=NOMINAL, votacao_aberta=True, + registro_aberto=registro_aberto) + return sessao, ordem + + +def _votante(sessao, admin_user): + parlamentar = baker.make(Parlamentar, ativo=True) + baker.make(PresencaOrdemDia, sessao_plenaria=sessao, parlamentar=parlamentar) + baker.make(Votante, parlamentar=parlamentar, user=admin_user) + return parlamentar + + +def _votante_com_client(sessao): + """ + Como _votante(), mas cria seu próprio usuário (não-superuser, só com a + permissão parlamentares.can_vote) e devolve um Client logado separado — + necessário para testar a corrida entre o operador e o vereador, que + precisam ser duas sessões/usuários distintos. + """ + parlamentar = baker.make(Parlamentar, ativo=True) + baker.make(PresencaOrdemDia, sessao_plenaria=sessao, parlamentar=parlamentar) + user = get_user_model().objects.create_user( + username='votante-{}'.format(parlamentar.pk), password='x') + user.user_permissions.add(Permission.objects.get(codename='can_vote')) + baker.make(Votante, parlamentar=parlamentar, user=user) + client = Client() + client.force_login(user) + return parlamentar, client + + +def _registrar_url(sessao, ordem): + return reverse('sapl.sessao:votacaonominal', + kwargs={'pk': sessao.pk, 'oid': ordem.pk, 'mid': ordem.materia_id}) + + +@pytest.mark.django_db(transaction=False) +def test_votante_view_envia_headers_never_cache(admin_client, admin_user): + sessao, ordem = _ordem_nominal_aberta() + _votante(sessao, admin_user) + + response = admin_client.get(reverse('sapl.painel:voto_individual')) + + assert response.status_code == 200 + assert 'no-store' in response['Cache-Control'] + + +@pytest.mark.django_db(transaction=False) +def test_votante_view_mostra_materia_quando_registro_fechado(admin_client, admin_user): + sessao, ordem = _ordem_nominal_aberta(registro_aberto=False) + _votante(sessao, admin_user) + + response = admin_client.get(reverse('sapl.painel:voto_individual')) + + assert response.status_code == 200 + assert 'error_message' not in response.context + assert response.context['materia'] == ordem.materia + + +@pytest.mark.django_db(transaction=False) +def test_votante_view_mostra_erro_explicito_quando_registro_bloqueado(admin_client, admin_user): + """ + Regressão da causa raiz #1: quando a Mesa bloqueia novos votos + (registro_aberto=True), o vereador que ainda não votou precisa ver uma + mensagem explícita — não uma tela em branco sem explicação. + """ + sessao, ordem = _ordem_nominal_aberta(registro_aberto=True) + _votante(sessao, admin_user) + + response = admin_client.get(reverse('sapl.painel:voto_individual')) + + assert response.status_code == 200 + assert 'Mesa encerrou o recebimento de novos votos' in response.context['error_message'] + + +@pytest.mark.django_db(transaction=False) +def test_voto_do_vereador_prevalece_sobre_lote_do_operador(admin_client): + """ + Invariante: o voto do próprio vereador sempre prevalece sobre qualquer + valor provisório já existente para ele (ex.: de uma tentativa anterior + do operador, ou de qualquer outra origem). Complementa + test_salvar_votacao_nao_sobrescreve_voto_ja_registrado (que cobre a + ordem inversa: o operador não pode sobrescrever um voto real já + registrado) — aqui é o vereador votando por cima de um valor existente + através da view de verdade, não apenas o estado inicial simulado. + """ + sessao, ordem = _ordem_nominal_aberta() + vereador, votante_client = _votante_com_client(sessao) + + # 1) Já existe um valor provisório "Não Votou" para o vereador (ex.: o + # ). Esses registros persistiam + mesmo com o fechamento falhando (o bloco atomic não é revertido, já que + form_invalid retorna normalmente em vez de lançar), e nominal.html + desabilita o ' + % get_token(request)) btn_registrar = ''' -
+ + %s %s
''' % ( - url, page_number) + url, metodo, csrf_input, page_number) else: btn_registrar = '''
@@ -845,7 +904,8 @@ 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) + return customize_link_materia(context, self.kwargs['pk'], has_permition, False, + request=self.request) def recuperar_materia(request): @@ -921,7 +981,8 @@ 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) + return customize_link_materia(context, self.kwargs['pk'], has_permition, True, + request=self.request) class CreateView(MasterDetailCrud.CreateView): form_class = ExpedienteMateriaForm @@ -1570,112 +1631,6 @@ class PresencaOrdemDiaView(FormMixin, PresencaMixin, DetailView): return reverse('sapl.sessao:presencaordemdia', kwargs={'pk': pk}) -class ListMateriaOrdemDiaView(FormMixin, DetailView): - template_name = 'sessao/materia_ordemdia_list.html' - form_class = ListMateriaForm - model = SessaoPlenaria - - def get(self, request, *args, **kwargs): - self.object = self.get_object() - context = self.get_context_data(object=self.object) - - pk = self.kwargs['pk'] - ordem = OrdemDia.objects.filter(sessao_plenaria_id=pk) - - materias_ordem = [] - for o in ordem: - ementa = o.materia.ementa - titulo = o.materia - numero = o.numero_ordem - - autoria = Autoria.objects.filter(materia_id=o.materia_id) - autor = [str(a.autor) for a in autoria] - - mat = {'pk': pk, - 'oid': o.id, - 'ordem_id': o.materia_id, - 'ementa': ementa, - 'titulo': titulo, - 'numero': numero, - 'resultado': o.resultado, - 'autor': autor, - 'votacao_aberta': o.votacao_aberta, - 'tipo_votacao': o.tipo_votacao - } - materias_ordem.append(mat) - - sorted(materias_ordem, key=lambda x: x['numero']) - - context.update({'materias_ordem': materias_ordem}) - - return self.render_to_response(context) - - @method_decorator(permission_required('sessao.change_ordemdia')) - def post(self, request, *args, **kwargs): - self.object = self.get_object() - context = self.get_context_data(object=self.object) - - pk = self.kwargs['pk'] - form = ListMateriaForm(request.POST) - - # TODO: Existe uma forma de atualizar em lote de acordo - # com a forma abaixo, mas como setar o primeiro para "1"? - # OrdemDia.objects.filter(sessao_plenaria_id=pk) - # .order_by('numero_ordem').update(numero_ordem=3) - - if 'materia_reorder' in request.POST: - ordens = OrdemDia.objects.filter(sessao_plenaria_id=pk) - ordem_num = 1 - for o in ordens: - o.numero_ordem = ordem_num - o.save() - ordem_num += 1 - elif 'abrir-votacao' in request.POST: - existe_votacao_aberta = OrdemDia.objects.filter( - sessao_plenaria_id=pk, votacao_aberta=True).exists() - if existe_votacao_aberta: - context = self.get_context_data(object=self.object) - - form._errors = {'error_message': 'error_message'} - context.update({'form': form}) - - pk = self.kwargs['pk'] - ordem = OrdemDia.objects.filter(sessao_plenaria_id=pk) - - materias_ordem = [] - for o in ordem: - ementa = o.materia.ementa - titulo = o.materia - numero = o.numero_ordem - - autoria = Autoria.objects.filter(materia_id=o.materia_id) - autor = [str(a.autor) for a in autoria] - - mat = {'pk': pk, - 'oid': o.id, - 'ordem_id': o.materia_id, - 'ementa': ementa, - 'titulo': titulo, - 'numero': numero, - 'resultado': o.resultado, - 'autor': autor, - 'votacao_aberta': o.votacao_aberta, - 'tipo_votacao': o.tipo_votacao - } - materias_ordem.append(mat) - - sorted(materias_ordem, key=lambda x: x['numero']) - context.update({'materias_ordem': materias_ordem}) - return self.render_to_response(context) - else: - ordem_id = request.POST['ordem_id'] - ordem = OrdemDia.objects.get(id=ordem_id) - ordem.votacao_aberta = True - ordem.registro_aberto = False - ordem.save() - return self.get(self, request, args, kwargs) - - class MesaView(FormMixin, DetailView): template_name = 'sessao/mesa.html' form_class = MesaForm @@ -2977,7 +2932,15 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): logger = logging.getLogger(__name__) - def get(self, request, *args, **kwargs): + def _get_materia_votacao(self, request, kwargs): + """ + Resolve a OrdemDia/ExpedienteMateria sendo registrada, sem nenhum + efeito colateral (não altera registro_aberto nem qualquer outro + estado) — apenas consulta. Retorna + (materia_votacao, presentes, total, redirect); quando a matéria não + pode ser exibida (já votada ou com a votação fechada), os três + primeiros valores são None e `redirect` é a resposta a devolver. + """ username = request.user.username if self.ordem: ordem_id = kwargs['oid'] @@ -2986,33 +2949,27 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): messages.add_message(request, messages.ERROR, msg) self.logger.info( 'user=' + username + '. Matéria (ordem_id={}) já votada!'.format(ordem_id)) - return HttpResponseRedirect(reverse( + return None, None, None, HttpResponseRedirect(reverse( 'sapl.sessao:ordemdia_list', kwargs={'pk': kwargs['pk']})) try: - ordem = OrdemDia.objects.get(id=ordem_id) + materia_votacao = OrdemDia.objects.get(id=ordem_id) except ObjectDoesNotExist: self.logger.error( 'user=' + username + '. Objeto OrdemDia (pk={}) não existe.'.format(ordem_id)) raise Http404() presentes = PresencaOrdemDia.objects.filter( - sessao_plenaria_id=ordem.sessao_plenaria_id) - total = presentes.count() - - materia_votacao = ordem + sessao_plenaria_id=materia_votacao.sessao_plenaria_id) - if not ordem.votacao_aberta: + if not materia_votacao.votacao_aberta: self.logger.error( 'user=' + username + '. A votação para esta OrdemDia (id={}) encontra-se fechada!'.format(ordem_id)) msg = _('A votação para esta matéria encontra-se fechada!') messages.add_message(request, messages.ERROR, msg) - return HttpResponseRedirect(reverse( + return None, None, None, HttpResponseRedirect(reverse( 'sapl.sessao:ordemdia_list', kwargs={'pk': kwargs['pk']})) - ordem.registro_aberto = True - ordem.save() - elif self.expediente: expediente_id = kwargs['oid'] if (RegistroVotacao.objects.filter( @@ -3021,194 +2978,270 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): "user=" + username + ". RegistroVotacao (expediente_id={}) já existe.".format(expediente_id)) msg = _('Esta matéria já foi votada!') messages.add_message(request, messages.ERROR, msg) - return HttpResponseRedirect(reverse( + return None, None, None, HttpResponseRedirect(reverse( 'sapl.sessao:expedientemateria_list', kwargs={'pk': kwargs['pk']})) try: self.logger.debug( "user=" + username + ". Tentando obter Objeto ExpedienteMateria com id={}.".format(expediente_id)) - expediente = ExpedienteMateria.objects.get(id=expediente_id) + materia_votacao = ExpedienteMateria.objects.get(id=expediente_id) except ObjectDoesNotExist: self.logger.error( 'user=' + username + '. Objeto ExpedienteMateria com id={} não existe.'.format(expediente_id)) raise Http404() presentes = SessaoPlenariaPresenca.objects.filter( - sessao_plenaria_id=expediente.sessao_plenaria_id) - total = presentes.count() - - materia_votacao = expediente + sessao_plenaria_id=materia_votacao.sessao_plenaria_id) - if not expediente.votacao_aberta: + if not materia_votacao.votacao_aberta: msg = _( 'A votação para este ExpedienteMateria (id={}) encontra-se fechada!'.format(expediente_id)) messages.add_message(request, messages.ERROR, msg) - return HttpResponseRedirect(reverse( + return None, None, None, HttpResponseRedirect(reverse( 'sapl.sessao:expedientemateria_list', kwargs={'pk': kwargs['pk']})) - expediente.registro_aberto = True - expediente.save() + total = presentes.count() + return materia_votacao, presentes, total, None + def _build_registro_context(self, materia_votacao, presentes, total): materia = {'materia': materia_votacao.materia, 'ementa': sub( ' ', ' ', strip_tags( materia_votacao.materia.ementa))} - context = {'materia': materia, 'object': self.get_object(), - 'parlamentares': self.get_parlamentares(presentes), - 'form': self.get_form(), - 'total': total} + return {'materia': materia, 'object': self.get_object(), + 'parlamentares': self.get_parlamentares(presentes), + 'form': self.get_form(), + 'total': total, + 'registro_aberto': materia_votacao.registro_aberto} + + def _redirect_same_registro(self, kwargs, page): + view = ('sapl.sessao:votacaonominal' if self.ordem + else 'sapl.sessao:votacaonominalexp') + return HttpResponseRedirect(reverse(view, kwargs={ + 'pk': kwargs['pk'], 'oid': kwargs['oid'], 'mid': kwargs['mid']}) + page) + + def _redirect_lista(self, kwargs, page): + view = ('sapl.sessao:ordemdia_list' if self.ordem + else 'sapl.sessao:expedientemateria_list') + return HttpResponseRedirect( + reverse(view, kwargs={'pk': kwargs['pk']}) + page + + "#id{}".format(kwargs['mid'])) + def get(self, request, *args, **kwargs): + if request.GET.get('status') == '1': + return self._status_json(kwargs) + materia_votacao, presentes, total, redirect = self._get_materia_votacao( + request, kwargs) + if redirect: + return redirect + context = self._build_registro_context(materia_votacao, presentes, total) return self.render_to_response(context) + def _status_json(self, kwargs): + """ + Poll leve para a tela de registro (nominal.html) acompanhar, em tempo + real, os votos que chegam pelos tablets — sem os efeitos colaterais + de _get_materia_votacao (mensagens, redirect quando já votada) e sem + a máscara de mostrar_voto do sapl.painel:dados_painel (que é para o + telão público; aqui é a tela da própria Mesa, que precisa do valor + real para não sobrescrever por engano um voto que mudou). + """ + model = OrdemDia if self.ordem else ExpedienteMateria + lookup_field = 'ordem_id' if self.ordem else 'expediente_id' + try: + materia_votacao = model.objects.get(id=kwargs['oid']) + except ObjectDoesNotExist: + raise Http404() + + votos = dict(VotoParlamentar.objects.filter( + **{lookup_field: materia_votacao.id}).values_list( + 'parlamentar_id', 'voto')) + + return JsonResponse({ + 'votacao_aberta': materia_votacao.votacao_aberta, + 'registro_aberto': materia_votacao.registro_aberto, + 'ja_registrada': RegistroVotacao.objects.filter( + **{lookup_field: materia_votacao.id}).exists(), + '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) + def post(self, request, *args, **kwargs): self.object = self.get_object() - form = self.get_form() username = request.user.username page = '' if 'page' in self.request.GET: page = '?page={}'.format(self.request.GET['page']) - if self.ordem: - ordem_id = kwargs['oid'] - try: - self.logger.debug( - "user=" + username + ". Tentando obter objeto OrdemDia com id={}.".format(ordem_id)) - materia_votacao = OrdemDia.objects.get(id=ordem_id) - except ObjectDoesNotExist: - self.logger.error( - 'user=' + username + '. Objeto OrdemDia com id={} não existe.'.format(ordem_id)) - raise Http404() - elif self.expediente: - expediente_id = kwargs['oid'] - try: - self.logger.debug( - "user=" + username + ". Tentando obter ExpedienteMateria com id={}.".format(expediente_id)) - materia_votacao = ExpedienteMateria.objects.get( - id=expediente_id) - except ObjectDoesNotExist: - self.logger.error( - 'user=' + username + '. Objeto ExpedienteMateria com id={} não existe.'.format(expediente_id)) - raise Http404() + materia_votacao, presentes, total, redirect = self._get_materia_votacao( + request, kwargs) + if redirect: + return redirect - if form.is_valid(): - votos_sim = 0 - votos_nao = 0 - abstencoes = 0 - nao_votou = 0 + fase_sessao_field = 'ordem_id' if self.ordem else 'expediente_id' + fase_sessao_value = kwargs['oid'] - if 'cancelar-votacao' in request.POST: + if 'reabrir-votacao' in request.POST: + with transaction.atomic(): + materia_votacao = self._lock_materia_votacao(materia_votacao) + materia_votacao.registro_aberto = False + materia_votacao.save(update_fields=['registro_aberto']) + self.logger.info( + 'user=' + username + '. Reabriu a matéria (id={}) para novos votos.'.format(fase_sessao_value)) + return self._redirect_same_registro(kwargs, page) + + if 'bloquear-registro-votacao' in request.POST: + with transaction.atomic(): + materia_votacao = self._lock_materia_votacao(materia_votacao) + materia_votacao.registro_aberto = True + materia_votacao.save(update_fields=['registro_aberto']) + self.logger.info( + 'user=' + username + '. Bloqueou novos votos para a matéria (id={}).'.format(fase_sessao_value)) + return self._redirect_same_registro(kwargs, page) + + if 'cancelar-votacao' in request.POST: + with transaction.atomic(): + materia_votacao = self._lock_materia_votacao(materia_votacao) fechar_votacao_materia(materia_votacao) - if self.ordem: - return HttpResponseRedirect( - reverse( - 'sapl.sessao:ordemdia_list', - kwargs={'pk': kwargs['pk']} - ) + page + "#id{}".format(self.kwargs['mid']) - ) - else: - return HttpResponseRedirect( - reverse( - 'sapl.sessao:expedientemateria_list', - kwargs={'pk': kwargs['pk']} - ) + page + "#id{}".format(self.kwargs['mid']) - ) - else: - if form.cleaned_data['resultado_votacao'] == None: - form.add_error(None, 'Não é possível finalizar a votação sem ' - 'nenhum resultado da votação') - return self.form_invalid(form) + return self._redirect_lista(kwargs, page) - for votos in request.POST.getlist('voto_parlamentar'): - v = votos.split(':') - voto = v[0] - parlamentar_id = v[1] - - if voto == 'Sim': - votos_sim += 1 - elif voto == 'Não': - votos_nao += 1 - elif voto == 'Abstenção': - abstencoes += 1 - elif voto == 'Não Votou': - nao_votou += 1 - - # Caso todas as opções sejam 'Não votou', fecha a votação - if nao_votou == len(request.POST.getlist('voto_parlamentar')): - self.logger.error('user=' + username + '. Não é possível finalizar a votação sem ' - 'nenhum voto') - form.add_error(None, 'Não é possível finalizar a votação sem ' - 'nenhum voto') + if 'salvar-votacao' in request.POST: + form = self.get_form() + if not form.is_valid(): return self.form_invalid(form) - # Remove todas as votação desta matéria, caso existam - if self.ordem: - RegistroVotacao.objects.filter(ordem_id=ordem_id).delete() - elif self.expediente: + + if form.cleaned_data['resultado_votacao'] is None: + form.add_error(None, _('Não é possível finalizar a votação sem ' + 'nenhum resultado da votação')) + return self.form_invalid(form) + + votos_submetidos = [] + for voto_codificado in request.POST.getlist('voto_parlamentar'): + try: + voto_submetido, parlamentar_id = voto_codificado.split(':', 1) + except ValueError: + form.add_error(None, _('Formato de voto inválido.')) + return self.form_invalid(form) + if voto_submetido not in ('Sim', 'Não', 'Abstenção', 'Não Votou'): + form.add_error(None, _('Voto inválido.')) + return self.form_invalid(form) + votos_submetidos.append((voto_submetido, parlamentar_id)) + + skipped_parlamentares = [] + with transaction.atomic(): + materia_votacao = self._lock_materia_votacao(materia_votacao) + if (not materia_votacao.votacao_aberta or + RegistroVotacao.objects.filter( + **{fase_sessao_field: fase_sessao_value}).exists()): + messages.error( + request, _('A votação já foi encerrada por outra operação.')) + 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 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) + voto_parlamentar.save() + elif voto_parlamentar.voto != voto_submetido: + # Alguém (tipicamente via tablet) já registrou um + # voto diferente do valor deste formulário desde que + # a tela de registro foi carregada. O voto já + # registrado prevalece — não sobrescrevemos com um + # valor obsoleto do formulário em lote. + skipped_parlamentares.append(voto_parlamentar.parlamentar) + + votos_atuais = VotoParlamentar.objects.filter( + **{fase_sessao_field: fase_sessao_value}) + 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() + + if votos_sim + votos_nao + abstencoes == 0: + self.logger.error('user=' + username + '. Não é possível finalizar a votação sem ' + 'nenhum voto') + form.add_error(None, _('Não é possível finalizar a votação sem ' + 'nenhum voto')) + return self.form_invalid(form) + + # Remove todas as votação desta matéria, caso existam RegistroVotacao.objects.filter( - expediente_id=expediente_id).delete() - - votacao = RegistroVotacao() - votacao.numero_votos_sim = votos_sim - votacao.numero_votos_nao = votos_nao - votacao.numero_abstencoes = abstencoes - votacao.observacao = request.POST.get('observacao', None) - votacao.user = request.user - votacao.ip = get_client_ip(request) - - votacao.materia_id = materia_votacao.materia.id - if self.ordem: - votacao.ordem_id = ordem_id - elif self.expediente: - votacao.expediente_id = expediente_id - - votacao.tipo_resultado_votacao = form.cleaned_data['resultado_votacao'] - votacao.save() - - for votos in request.POST.getlist('voto_parlamentar'): - v = votos.split(':') - voto = v[0] - parlamentar_id = v[1] - - if self.ordem: - voto_parlamentar = VotoParlamentar.objects.get_or_create( - parlamentar_id=parlamentar_id, - ordem_id=ordem_id)[0] - elif self.expediente: - voto_parlamentar = VotoParlamentar.objects.get_or_create( - parlamentar_id=parlamentar_id, - expediente_id=expediente_id)[0] - - voto_parlamentar.voto = voto - voto_parlamentar.parlamentar_id = parlamentar_id - voto_parlamentar.votacao_id = votacao.id - voto_parlamentar.user = request.user - voto_parlamentar.ip = get_client_ip(request) - voto_parlamentar.save() - - resultado = form.cleaned_data['resultado_votacao'] - - materia_votacao.resultado = resultado.nome + **{fase_sessao_field: fase_sessao_value}).delete() + + votacao = RegistroVotacao( + numero_votos_sim=votos_sim, + numero_votos_nao=votos_nao, + numero_abstencoes=abstencoes, + observacao=request.POST.get('observacao', None), + user=request.user, + ip=get_client_ip(request), + materia_id=materia_votacao.materia.id, + tipo_resultado_votacao=form.cleaned_data['resultado_votacao']) + setattr(votacao, fase_sessao_field, fase_sessao_value) + votacao.save() + + votos_atuais.update(votacao_id=votacao.id) + + materia_votacao.resultado = form.cleaned_data['resultado_votacao'].nome materia_votacao.votacao_aberta = False + materia_votacao.registro_aberto = False materia_votacao.save() - # Verifica se existe algum VotoParlamentar sem RegistroVotacao - # Por exemplo, se algum parlamentar votar e sua presença for - # removida da ordem do dia/expediente antes da conclusão da - # votação - if self.ordem: + # Verifica se existe algum VotoParlamentar sem RegistroVotacao + # Por exemplo, se algum parlamentar votar e sua presença for + # removida da ordem do dia/expediente antes da conclusão da + # votação VotoParlamentar.objects.filter( - ordem_id=ordem_id, - votacao__isnull=True).delete() - elif self.expediente: - VotoParlamentar.objects.filter( - expediente_id=expediente_id, - votacao__isnull=True).delete() + **{fase_sessao_field: fase_sessao_value}, votacao__isnull=True).delete() + + if skipped_parlamentares: + nomes = ', '.join(p.nome_parlamentar for p in skipped_parlamentares) + messages.add_message( + request, messages.WARNING, + _('O(s) voto(s) de %(nomes)s já haviam sido registrados ' + 'e não foram sobrescritos.') % {'nomes': nomes}) + return self.form_valid(form) - else: - return self.form_invalid(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) 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 d5dc8dc4c..9be5f19cf 100644 --- a/sapl/templates/painel/index.html +++ b/sapl/templates/painel/index.html @@ -506,6 +506,10 @@ function show_voto(parlamentar) { color = 'white' + + if (!parlamentar.voto) { + parlamentar.voto = "Não votou"; + } if (parlamentar.voto == "Sim"){ color = 'green' } @@ -529,4 +533,3 @@ - diff --git a/sapl/templates/painel/voto_individual.html b/sapl/templates/painel/voto_individual.html index 1dde0990d..2f33026e4 100644 --- a/sapl/templates/painel/voto_individual.html +++ b/sapl/templates/painel/voto_individual.html @@ -77,6 +77,11 @@

+ {% if status_message %} +

+ {{status_message}} +

+ {% endif %}

@@ -111,7 +116,8 @@ {% else %}
-

{{error_message}}

+

+

{{error_message}}




@@ -184,11 +190,47 @@ $("#date").text(n); startTime(); - - setTimeout(function() { - document.location.reload(true); - }, 30000) }); {% endif %} + + diff --git a/sapl/templates/sessao/votacao/nominal.html b/sapl/templates/sessao/votacao/nominal.html index 540e0f3da..30f617c8e 100644 --- a/sapl/templates/sessao/votacao/nominal.html +++ b/sapl/templates/sessao/votacao/nominal.html @@ -13,6 +13,15 @@ Ementa: {{materia.ementa|safe}}

+ {% if total == 0 %}