@ -13,7 +13,6 @@ from django.db import IntegrityError, transaction
from django . db . models import Max , Q
from django . db . models import Max , Q
from django . http import JsonResponse
from django . http import JsonResponse
from django . http . response import Http404 , HttpResponseRedirect
from django . http . response import Http404 , HttpResponseRedirect
from django . middleware . csrf import get_token
from django . urls import reverse
from django . urls import reverse
from django . urls . base import reverse_lazy
from django . urls . base import reverse_lazy
from django . utils import timezone
from django . utils import timezone
@ -130,7 +129,12 @@ def verifica_presenca(request, model, spk, is_leitura=False):
return True
return True
def verifica_votacoes_abertas ( request ) :
def verifica_votacoes_abertas ( request , mensagens ) :
"""
Fecha as votações abertas . A mensagem para o usuário vai para
` mensagens ` , e o chamador só a registra depois do commit : se a transação
for desfeita , as votações não foram fechadas .
"""
votacoes_abertas = SessaoPlenaria . objects . filter (
votacoes_abertas = SessaoPlenaria . objects . filter (
Q ( ordemdia__votacao_aberta = True ) |
Q ( ordemdia__votacao_aberta = True ) |
Q ( expedientemateria__votacao_aberta = True ) ) . distinct ( )
Q ( expedientemateria__votacao_aberta = True ) ) . distinct ( )
@ -149,18 +153,15 @@ def verifica_votacoes_abertas(request):
' , ' . join ( msg_abertas ) + ' . Estas votações ou leituras foram fechadas. ' )
' , ' . join ( msg_abertas ) + ' . Estas votações ou leituras foram fechadas. ' )
msg = _ ( ' Já existem votações ou leituras abertas nas seguintes Sessões: ' +
msg = _ ( ' Já existem votações ou leituras abertas nas seguintes Sessões: ' +
' , ' . join ( msg_abertas ) + ' . Estas votações ou leituras foram fechadas. ' )
' , ' . join ( msg_abertas ) + ' . Estas votações ou leituras foram fechadas. ' )
messages . add_message ( request , messages . INFO , msg )
mensagens . append ( msg )
for sessao in votacoes_abertas :
# update() em vez de save(): só toca estes dois campos e reavalia o
ordens = sessao . ordemdia_set . filter ( votacao_aberta = True )
# WHERE sob o lock da linha, sem regravar um resultado que um
expediente = sessao . expedientemateria_set . filter (
# "Encerrar Votação" concorrente tenha acabado de salvar.
votacao_aberta = True )
OrdemDia . objects . filter ( votacao_aberta = True ) . update (
for o in ordens :
votacao_aberta = False , registro_aberto = False )
o . votacao_aberta = False
ExpedienteMateria . objects . filter ( votacao_aberta = True ) . update (
o . save ( )
votacao_aberta = False , registro_aberto = False )
for e in expediente :
e . votacao_aberta = False
e . save ( )
return True
return True
@ -205,7 +206,9 @@ def abrir_votacao(request, pk, spk):
is_leitura = materia_votacao . tipo_votacao == 4
is_leitura = materia_votacao . tipo_votacao == 4
is_expediente = model is ExpedienteMateria
is_expediente = model is ExpedienteMateria
opened = False
opened = False
mensagens = [ ]
try :
with transaction . atomic ( ) :
with transaction . atomic ( ) :
# select_for_update trava a linha da SessaoPlenaria durante toda a
# select_for_update trava a linha da SessaoPlenaria durante toda a
# checagem+fechamento+abertura, para que dois "abrir votação"
# checagem+fechamento+abertura, para que dois "abrir votação"
@ -227,14 +230,25 @@ def abrir_votacao(request, pk, spk):
# votações abertas... foram fechadas") não faz sentido quando a
# votações abertas... foram fechadas") não faz sentido quando a
# única "conflitante" é ela mesma.
# única "conflitante" é ela mesma.
if ( verifica_presenca ( request , presenca_model , spk , is_leitura ) and
if ( verifica_presenca ( request , presenca_model , spk , is_leitura ) and
( ja_aberta or verifica_votacoes_abertas ( request ) ) and
( ja_aberta or verifica_votacoes_abertas ( request , mensagens ) ) and
verifica_sessao_iniciada ( request , spk , is_leitura ) ) :
verifica_sessao_iniciada ( request , spk , is_leitura ) ) :
materia_votacao . votacao_aberta = True
materia_votacao . votacao_aberta = True
if not ja_aberta :
materia_votacao . registro_aberto = False
sessao = SessaoPlenaria . objects . get ( id = spk )
sessao = SessaoPlenaria . objects . get ( id = spk )
sessao . painel_aberto = True
sessao . painel_aberto = True
sessao . save ( )
sessao . save ( )
materia_votacao . save ( )
materia_votacao . save ( )
opened = True
opened = True
for msg in mensagens :
messages . add_message ( request , messages . INFO , msg )
except IntegrityError :
# O lock acima é por sessão, mas a unicidade de votacao_aberta é
# global: uma abertura simultânea em outra sessão cai no índice
# parcial.
opened = False
messages . add_message ( request , messages . ERROR , _ (
' Outra votação foi aberta simultaneamente. Tente novamente. ' ) )
if opened :
if opened :
# Leva direto para a tela de registro do tipo de votação recém
# Leva direto para a tela de registro do tipo de votação recém
@ -269,7 +283,7 @@ def abrir_votacao(request, pk, spk):
return HttpResponseRedirect ( success_url )
return HttpResponseRedirect ( success_url )
def customize_link_materia ( context , pk , has_permission , is_expediente , request = None ) :
def customize_link_materia ( context , pk , has_permission , is_expediente ) :
for i , row in enumerate ( context [ ' rows ' ] ) :
for i , row in enumerate ( context [ ' rows ' ] ) :
materia = context [ ' object_list ' ] [ i ] . materia
materia = context [ ' object_list ' ] [ i ] . materia
obj = context [ ' object_list ' ] [ i ]
obj = context [ ' object_list ' ] [ i ]
@ -397,26 +411,13 @@ def customize_link_materia(context, pk, has_permission, is_expediente, request=N
if has_permission :
if has_permission :
if obj . tipo_votacao != LEITURA :
if obj . tipo_votacao != LEITURA :
# Votação Nominal é a única que envolve votos individuais
# pelos tablets (VOTACAO_NOMINAL em sapl/painel/views.py), e
# é a única cuja tela de registro (VotacaoNominalAbstract)
# sabe tratar POST sem efeito colateral — por isso só ela é
# convertida para POST aqui; Simbólica/Secreta continuam GET.
metodo = ' '
csrf_input = ' '
if obj . tipo_votacao == NOMINAL :
metodo = ' method= " post " '
csrf_input = (
' <input type= " hidden " name= " csrfmiddlewaretoken " value= " %s " /> '
% get_token ( request ) )
btn_registrar = '''
btn_registrar = '''
< form action = " %s " % s >
< form action = " %s " >
% s
< input type = " submit " class = " btn btn-primary "
< input type = " submit " class = " btn btn-primary "
value = " Registrar Votação " / >
value = " Registrar Votação " / >
% s
% s
< / form > ''' % (
< / form > ''' % (
url , metodo , csrf_input , page_number )
url , page_number )
else :
else :
btn_registrar = '''
btn_registrar = '''
< form action = " %s " >
< form action = " %s " >
@ -904,8 +905,7 @@ class MateriaOrdemDiaCrud(MasterDetailCrud):
context = super ( ) . get_context_data ( * * kwargs )
context = super ( ) . get_context_data ( * * kwargs )
has_permition = self . request . user . has_module_perms ( AppConfig . label )
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 ) :
def recuperar_materia ( request ) :
@ -981,8 +981,7 @@ class ExpedienteMateriaCrud(MasterDetailCrud):
context [ ' page ' ] = self . request . GET . get ( ' page ' )
context [ ' page ' ] = self . request . GET . get ( ' page ' )
has_permition = self . request . user . has_module_perms ( AppConfig . label )
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 ) :
class CreateView ( MasterDetailCrud . CreateView ) :
form_class = ExpedienteMateriaForm
form_class = ExpedienteMateriaForm
@ -3075,22 +3074,6 @@ class VotacaoNominalAbstract(SessaoPermissionMixin):
' votos ' : votos ,
' 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 ) :
def _lock_materia_votacao ( self , materia_votacao ) :
model = OrdemDia if self . ordem else ExpedienteMateria
model = OrdemDia if self . ordem else ExpedienteMateria
return model . objects . select_for_update ( ) . get ( pk = materia_votacao . pk )
return model . objects . select_for_update ( ) . get ( pk = materia_votacao . pk )
@ -3168,20 +3151,20 @@ class VotacaoNominalAbstract(SessaoPermissionMixin):
return self . _redirect_lista ( kwargs , page )
return self . _redirect_lista ( kwargs , page )
for voto_submetido , parlamentar_id in votos_submetidos :
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 ' :
if voto_submetido == ' Não Votou ' :
# "Não Votou" é só o valor padrão do <select>
# "Não Votou" é só o valor padrão do <select> para
# para quem o operador não escolheu nada — não é
# quem o operador não escolheu nada — não é um voto
# um voto de fato. Persisti-lo aqui travaria a
# de fato. Gravá-lo agora travaria a linha desse
# linha desse parlamentar (nominal.html desabilita
# parlamentar (nominal.html desabilita o <select>
# o <select> sempre que existe um VotoParlamentar)
# sempre que existe um VotoParlamentar) mesmo quando
# mesmo quando o fechamento falha por falta de
# o fechamento falha por falta de votos reais. Os
# votos reais, impedindo o operador de corrigir e
# presentes que continuarem sem voto recebem
# tentar de novo.
# 'Não Votou' só depois que o RegistroVotacao existe.
voto_parlamentar . delete ( )
continue
continue
voto_parlamentar , created = VotoParlamentar . objects . select_for_update ( ) . get_or_create (
parlamentar_id = parlamentar_id ,
* * { fase_sessao_field : fase_sessao_value } )
if created :
voto_parlamentar . voto = voto_submetido
voto_parlamentar . voto = voto_submetido
voto_parlamentar . user = request . user
voto_parlamentar . user = request . user
voto_parlamentar . ip = get_client_ip ( request )
voto_parlamentar . ip = get_client_ip ( request )
@ -3195,7 +3178,8 @@ class VotacaoNominalAbstract(SessaoPermissionMixin):
skipped_parlamentares . append ( voto_parlamentar . parlamentar )
skipped_parlamentares . append ( voto_parlamentar . parlamentar )
votos_atuais = VotoParlamentar . objects . filter (
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_sim = votos_atuais . filter ( voto = ' Sim ' ) . count ( )
votos_nao = votos_atuais . filter ( voto = ' Não ' ) . count ( )
votos_nao = votos_atuais . filter ( voto = ' Não ' ) . count ( )
abstencoes = votos_atuais . filter ( voto = ' Abstenção ' ) . count ( )
abstencoes = votos_atuais . filter ( voto = ' Abstenção ' ) . count ( )
@ -3207,10 +3191,6 @@ class VotacaoNominalAbstract(SessaoPermissionMixin):
' nenhum voto ' ) )
' nenhum voto ' ) )
return self . form_invalid ( form )
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 (
votacao = RegistroVotacao (
numero_votos_sim = votos_sim ,
numero_votos_sim = votos_sim ,
numero_votos_nao = votos_nao ,
numero_votos_nao = votos_nao ,
@ -3224,6 +3204,16 @@ class VotacaoNominalAbstract(SessaoPermissionMixin):
votacao . save ( )
votacao . save ( )
votos_atuais . update ( votacao_id = votacao . id )
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 . resultado = form . cleaned_data [ ' resultado_votacao ' ] . nome
materia_votacao . votacao_aberta = False
materia_votacao . votacao_aberta = False
@ -3246,10 +3236,8 @@ class VotacaoNominalAbstract(SessaoPermissionMixin):
return self . form_valid ( form )
return self . form_valid ( form )
# Nenhuma chave de ação reconhecida: navegação simples para a tela
# Nenhuma ação reconhecida: volta para a tela de registro (GET).
# de registro (botão "Registrar Votação"), sem efeito colateral.
return self . _redirect_same_registro ( kwargs , page )
context = self . _build_registro_context ( materia_votacao , presentes , total )
return self . render_to_response ( context )
def form_invalid ( self , form ) :
def form_invalid ( self , form ) :
errors_tuple = [ ( form [ e ] . label , form . errors [ e ] )
errors_tuple = [ ( form [ e ] . label , form . errors [ e ] )