@ -2,12 +2,11 @@ import html
import json
import json
import logging
import logging
from django . contrib import messages
from django . contrib . auth . decorators import ( login_required , permission_required ,
from django . contrib . auth . decorators import ( login_required , permission_required ,
user_passes_test )
user_passes_test )
from django . core . cache import cache
from django . core . cache import cache
from django . core . exceptions import PermissionDenied
from django . core . exceptions import PermissionDenied
from django . db import IntegrityError , transaction
from django . db import transaction
from django . urls import reverse
from django . urls import reverse
from django . db . models import Q
from django . db . models import Q
from django . http import HttpResponse , JsonResponse
from django . http import HttpResponse , JsonResponse
@ -49,6 +48,10 @@ def votacao_aberta(request):
Função que verifica se há somente 1 uma matéria aberta ou
Função que verifica se há somente 1 uma matéria aberta ou
nenhuma . É utilizada como uma função auxiliar para a view
nenhuma . É utilizada como uma função auxiliar para a view
votante_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__ )
logger = logging . getLogger ( __name__ )
username = request . user . username
username = request . user . username
@ -58,19 +61,13 @@ def votacao_aberta(request):
Q ( expedientemateria__votacao_aberta = True ) ) . distinct ( )
Q ( expedientemateria__votacao_aberta = True ) ) . distinct ( )
if len ( votacoes_abertas ) > 1 :
if len ( votacoes_abertas ) > 1 :
msg_abertas = [ ]
sessoes = ' , ' . join ( str ( v ) for v in votacoes_abertas )
for v in votacoes_abertas :
msg_abertas . append ( ''' <li><a href= " %s " > %s </a></li> ''' % (
reverse ( ' sapl.sessao:sessaoplenaria_detail ' ,
kwargs = { ' pk ' : v . id } ) ,
v . __str__ ( ) ) )
logger . info ( ' user= ' + username + ' . Existe mais de uma votações aberta. Elas se encontram '
logger . info ( ' user= ' + username + ' . Existe mais de uma votações aberta. Elas se encontram '
' nas seguintes Sessões: ' + ' , ' . join ( msg_abertas ) + ' . '
' nas seguintes Sessões: ' + sessoes + ' . '
' Para votar, peça para que o Operador feche-as. ' )
' Para votar, peça para que o Operador feche-as. ' )
msg = _ ( ' Existe mais de uma votações aberta. Elas se encontram '
msg = _ ( ' Existe mais de uma votações aberta. Elas se encontram '
' nas seguintes Sessões: ' + ' , ' . join ( msg_abertas ) + ' . '
' nas seguintes Sessões: ' + sessoes + ' . '
' 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
return None , msg
elif len ( votacoes_abertas ) == 1 :
elif len ( votacoes_abertas ) == 1 :
@ -83,18 +80,11 @@ def votacao_aberta(request):
numero_materias_abertas = len ( ordens ) + len ( expedientes )
numero_materias_abertas = len ( ordens ) + len ( expedientes )
if numero_materias_abertas > 1 :
if numero_materias_abertas > 1 :
sessao = str ( votacoes_abertas . first ( ) )
logger . info ( ' user= ' + username + ' . Existe mais de uma votação aberta na Sessão: ' +
logger . info ( ' user= ' + username + ' . Existe mais de uma votação aberta na Sessão: ' +
( ''' <li><a href= " %s " > %s </a></li> ''' % (
sessao )
reverse ( ' sapl.sessao:sessaoplenaria_detail ' ,
msg = _ ( ' Existe mais de uma votação aberta na Sessão: ' + sessao +
kwargs = { ' pk ' : votacoes_abertas . first ( ) . id } ) ,
' . Para votar, peça para que o Operador as feche. ' )
votacoes_abertas . first ( ) . __str__ ( ) ) ) )
msg = _ ( ' Existe mais de uma votação aberta na Sessão: ' +
( ''' <li><a href= " %s " > %s </a></li> ''' % (
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 )
return None , msg
return None , msg
return votacoes_abertas . first ( ) , None
return votacoes_abertas . first ( ) , None
@ -250,6 +240,37 @@ def votante_status(request):
} )
} )
def _trava_materia_para_voto ( materia , parlamentar ) :
"""
Trava a linha da matéria ( select_for_update ) e confere , sob o lock , que
ela ainda aceita voto deste parlamentar : presente , votação aberta , não
bloqueada pela Mesa , nominal e ainda sem RegistroVotacao . Deve ser
chamada dentro de transaction . atomic ( ) . Devolve o filtro da matéria
para VotoParlamentar ( { ' ordem ' : . . . } ou { ' expediente ' : . . . } ) .
"""
if isinstance ( materia , OrdemDia ) :
materia = OrdemDia . objects . select_for_update ( ) . get ( pk = materia . pk )
fase_sessao = { ' ordem ' : materia }
presenca_model = PresencaOrdemDia
else :
materia = ExpedienteMateria . objects . select_for_update ( ) . get ( pk = materia . pk )
fase_sessao = { ' expediente ' : materia }
presenca_model = SessaoPlenariaPresenca
esta_presente = presenca_model . objects . filter (
sessao_plenaria_id = materia . sessao_plenaria_id ,
parlamentar = parlamentar ) . exists ( )
if not esta_presente :
raise VoteError (
" O parlamentar não está presente na sessão. " , status = 409 )
if ( not materia . votacao_aberta or materia . registro_aberto or
materia . tipo_votacao != VOTACAO_NOMINAL or
RegistroVotacao . objects . filter ( * * fase_sessao ) . exists ( ) ) :
raise VoteError (
" A votação não está mais disponível para novos votos. " , status = 409 )
return fase_sessao
def _save_voto_individual ( request , context_vars , voto_valor = None , ip = None ) :
def _save_voto_individual ( request , context_vars , voto_valor = None , ip = None ) :
"""
"""
Salva o voto do Votante autenticado — compartilhado por votante_view
Salva o voto do Votante autenticado — compartilhado por votante_view
@ -259,48 +280,37 @@ def _save_voto_individual(request, context_vars, voto_valor=None, ip=None):
voto / IP explicitamente em vez de ler de request . POST / request . META ,
voto / IP explicitamente em vez de ler de request . POST / request . META ,
que uma conexão WebSocket não tem ; o caminho HTTP não passa nada e
que uma conexão WebSocket não tem ; o caminho HTTP não passa nada e
mantém o comportamento de sempre .
mantém o comportamento de sempre .
O voto do próprio parlamentar sempre prevalece sobre um valor lançado
pela Mesa . Levanta VoteError quando a votação não aceita mais votos .
"""
"""
logger = logging . getLogger ( __name__ )
logger = logging . getLogger ( __name__ )
username = request . user . username
username = request . user . username
if context_vars [ ' ordem_dia ' ] :
materia = context_vars . get ( ' ordem_dia ' ) or context_vars . get ( ' expediente ' )
fase_sessao = { ' ordem ' : context_vars [ ' ordem_dia ' ] }
parlamentar = context_vars . get ( ' parlamentar ' )
elif context_vars [ ' expediente ' ] :
if not materia or not parlamentar :
fase_sessao = { ' expediente ' : context_vars [ ' expediente ' ] }
raise VoteError ( " A votação não está disponível para novos votos. " , status = 409 )
else :
fase_sessao = None
if fase_sessao is None :
return
voto_valor = voto_valor if voto_valor is not None else request . POST [ ' voto ' ]
voto_valor = voto_valor if voto_valor is not None else request . POST . get ( ' voto ' )
if voto_valor not in VALID_VOTE_VALUE S :
if voto_valor not in VOTOS_REAIS :
raise VoteError (
raise VoteError (
f " Invalid vote value: { voto_valor } . Must be one of: { VALID_VOTE_VALUES } " )
f " Invalid vote value: { voto_valor } . Must be one of: { list ( VOTOS_REAIS ) } " )
# select_for_update+atomic evita corrida com uma escrita concorrente
with transaction . atomic ( ) :
# na mesma linha (ex.: o operador registrando este mesmo parlamentar
fase_sessao = _trava_materia_para_voto ( materia , parlamentar )
# em lote na tela "Registrar Votação" ao mesmo tempo). Diferente do
voto , _created = VotoParlamentar . objects . select_for_update ( ) . get_or_create (
# formulário em lote do operador, aqui é sempre seguro aplicar o
parlamentar = parlamentar , * * fase_sessao )
# valor enviado: é o próprio parlamentar atualizando o próprio voto.
voto . voto = voto_valor
try :
voto . ip = ip if ip is not None else get_client_ip ( request )
with transaction . atomic ( ) :
voto . user = request . user
voto , created = VotoParlamentar . objects . select_for_update ( ) . get_or_create (
# Único caminho onde é o próprio parlamentar votando — vote_controller/
parlamentar = context_vars [ ' parlamentar ' ] , * * fase_sessao )
# _cast_vote (operador) sempre grava False.
except IntegrityError :
voto . votado_pelo_parlamentar = True
voto = VotoParlamentar . objects . select_for_update ( ) . get (
voto . save ( )
parlamentar = context_vars [ ' parlamentar ' ] , * * fase_sessao )
logger . info ( " user= " + username + " . VotoParlamentar para parlamentar= {} salvo com sucesso. "
logger . info ( " user= " + username + " . VotoParlamentar para parlamentar= {} obtido com sucesso. "
. format ( parlamentar ) )
. format ( context_vars [ ' parlamentar ' ] ) )
voto . voto = voto_valor
voto . ip = ip if ip is not None else get_client_ip ( request )
voto . user = request . user
# Único caminho onde é o próprio parlamentar votando — vote_controller/
# _cast_vote (operador) e o formulário em lote sempre gravam False.
voto . votado_pelo_parlamentar = True
voto . save ( )
broadcast_dados_painel ( request , context_vars [ ' sessao ' ] . id )
broadcast_dados_painel ( request , context_vars [ ' sessao ' ] . id )
return voto_valor
return voto_valor
@ -311,7 +321,13 @@ def votante_view(request):
context , context_vars = _resolve_votante_context ( request )
context , context_vars = _resolve_votante_context ( request )
if request . method == ' POST ' :
if request . method == ' POST ' :
_save_voto_individual ( request , context_vars )
try :
_save_voto_individual ( request , context_vars )
except VoteError as e :
# Recarregar a tela já mostra o estado atual (bloqueada,
# encerrada, ausente) em error_message.
logging . getLogger ( __name__ ) . info (
" user= %s . Voto recusado: %s " , request . user . username , e . message )
return HttpResponseRedirect (
return HttpResponseRedirect (
reverse ( ' sapl.painel:voto_individual ' ) )
reverse ( ' sapl.painel:voto_individual ' ) )
@ -732,6 +748,8 @@ def painel_view(request, sessao_id):
VALID_VOTE_VALUES = [ " Sim " , " Não " , " Abstenção " , " Não Votou " ]
VALID_VOTE_VALUES = [ " Sim " , " Não " , " Abstenção " , " Não Votou " ]
# Valores que contam como voto; "Não Votou" só é gravado no encerramento.
VOTOS_REAIS = ( " Sim " , " Não " , " Abstenção " )
class VoteError ( Exception ) :
class VoteError ( Exception ) :
@ -782,17 +800,33 @@ def _cast_vote(user, controller_id, parlamentar_id, voto, ip=None):
except Parlamentar . DoesNotExist :
except Parlamentar . DoesNotExist :
raise VoteError ( f " Parlamentar { parlamentar_id } not found " , status = 404 )
raise VoteError ( f " Parlamentar { parlamentar_id } not found " , status = 404 )
# votado_pelo_parlamentar=False explícito (não só o default do campo):
with transaction . atomic ( ) :
# se este parlamentar já tinha um voto marcado como do próprio (tablet)
fase_sessao = _trava_materia_para_voto ( materia_aberta , parlamentar )
# e o operador está sobrescrevendo agora, a flag precisa acompanhar —
voto_obj = VotoParlamentar . objects . select_for_update ( ) . filter (
# o valor atual deixou de ser o que o parlamentar escolheu.
parlamentar = parlamentar , * * fase_sessao ) . first ( )
defaults = { ' voto ' : voto , ' user ' : user , ' ip ' : ip , ' votado_pelo_parlamentar ' : False }
if ordem_dia :
# O voto registrado pelo próprio parlamentar (tablet) prevalece: a
voto_obj , created = VotoParlamentar . objects . update_or_create (
# Mesa só lança ou corrige votos que ela mesma registrou.
parlamentar = parlamentar , ordem = ordem_dia , defaults = defaults )
if voto_obj and voto_obj . votado_pelo_parlamentar :
else :
raise VoteError (
voto_obj , created = VotoParlamentar . objects . update_or_create (
f " O voto de { parlamentar . nome_parlamentar } já foi registrado "
parlamentar = parlamentar , expediente = expediente , defaults = defaults )
f " pelo próprio parlamentar e não pode ser alterado pela Mesa. " ,
status = 409 )
created = voto_obj is None
if voto == ' Não Votou ' :
# Desfaz o lançamento da Mesa; a linha 'Não Votou' só é
# gravada no encerramento, para os presentes sem voto.
if voto_obj :
voto_obj . delete ( )
else :
if created :
voto_obj = VotoParlamentar ( parlamentar = parlamentar , * * fase_sessao )
voto_obj . voto = voto
voto_obj . user = user
voto_obj . ip = ip
voto_obj . votado_pelo_parlamentar = False
voto_obj . save ( )
logger . info (
logger . info (
f " Vote { ' created ' if created else ' updated ' } : "
f " Vote { ' created ' if created else ' updated ' } : "
@ -859,8 +893,10 @@ def _toggle_registro(user, controller_id, aberto):
if materia_aberta . tipo_votacao != VOTACAO_NOMINAL :
if materia_aberta . tipo_votacao != VOTACAO_NOMINAL :
raise VoteError ( " Materia is not nominal voting type " )
raise VoteError ( " Materia is not nominal voting type " )
materia_aberta . registro_aberto = aberto
# update() só toca registro_aberto: um save() completo regravaria
materia_aberta . save ( )
# resultado/votacao_aberta lidos antes de um encerramento concorrente.
type ( materia_aberta ) . objects . filter ( pk = materia_aberta . pk ) . update (
registro_aberto = aberto )
logging . getLogger ( __name__ ) . info (
logging . getLogger ( __name__ ) . info (
f " registro_aberto= { aberto } para materia id= { materia_aberta . pk } "
f " registro_aberto= { aberto } para materia id= { materia_aberta . pk } "
f " (sessao= { controller_id } , user= { user . username } ) " )
f " (sessao= { controller_id } , user= { user . username } ) " )
@ -985,57 +1021,79 @@ def close_voting(request, controller_id):
if not materia_aberta :
if not materia_aberta :
return JsonResponse ( { " type " : " error " , " message " : " No open materia for voting " } , status = 400 )
return JsonResponse ( { " type " : " error " , " message " : " No open materia for voting " } , status = 400 )
# Count votes from VotoParlamentar
with transaction . atomic ( ) :
if ordem_dia :
# Trava a matéria: votos do tablet/da Mesa esperam o encerramento
votos = VotoParlamentar . objects . filter ( ordem = ordem_dia )
# terminar, e um segundo "Encerrar" (duplo clique) vê a votação já
else :
# registrada.
votos = VotoParlamentar . objects . filter ( expediente = expediente )
materia = type ( materia_aberta ) . objects . select_for_update ( ) . get (
pk = materia_aberta . pk )
votos_sim = votos . filter ( voto = ' Sim ' ) . count ( )
if ordem_dia :
votos_nao = votos . filter ( voto = ' Não ' ) . count ( )
fase_sessao = { ' ordem ' : materia }
abstencoes = votos . filter ( voto = ' Abstenção ' ) . count ( )
presenca_model = PresencaOrdemDia
else :
# All votes must not be "Não Votou"
fase_sessao = { ' expediente ' : materia }
total_votados = votos_sim + votos_nao + abstencoes
presenca_model = SessaoPlenariaPresenca
if total_votados == 0 :
return JsonResponse ( {
if ( not materia . votacao_aberta or
" type " : " error " ,
RegistroVotacao . objects . filter ( * * fase_sessao ) . exists ( ) ) :
" message " : " Não é possível finalizar a votação sem nenhum voto "
return JsonResponse ( {
} , status = 400 )
" type " : " error " ,
" message " : " A votação já foi encerrada por outra operação. "
# Remove old RegistroVotacao if exists
} , status = 409 )
if ordem_dia :
RegistroVotacao . objects . filter ( ordem = ordem_dia ) . delete ( )
# Só contam os presentes: quem votou e teve a presença removida
else :
# antes do encerramento fica de fora (e o voto é apagado abaixo).
RegistroVotacao . objects . filter ( expediente = expediente ) . delete ( )
presentes = presenca_model . objects . filter (
sessao_plenaria_id = materia . sessao_plenaria_id )
# Create RegistroVotacao
votos = VotoParlamentar . objects . filter (
registro = RegistroVotacao ( )
* * fase_sessao ,
registro . numero_votos_sim = votos_sim
parlamentar_id__in = presentes . values ( ' parlamentar_id ' ) )
registro . numero_votos_nao = votos_nao
registro . numero_abstencoes = abstencoes
votos_sim = votos . filter ( voto = ' Sim ' ) . count ( )
registro . observacao = observacoes
votos_nao = votos . filter ( voto = ' Não ' ) . count ( )
registro . user = request . user
abstencoes = votos . filter ( voto = ' Abstenção ' ) . count ( )
registro . ip = get_client_ip ( request )
registro . materia = materia_aberta . materia
if votos_sim + votos_nao + abstencoes == 0 :
registro . tipo_resultado_votacao = tipo_resultado
return JsonResponse ( {
" type " : " error " ,
if ordem_dia :
" message " : " Não é possível finalizar a votação sem nenhum voto "
registro . ordem = ordem_dia
} , status = 400 )
else :
registro . expediente = expediente
registro = RegistroVotacao (
numero_votos_sim = votos_sim ,
registro . save ( )
numero_votos_nao = votos_nao ,
numero_abstencoes = abstencoes ,
# Link VotoParlamentar records to RegistroVotacao and update user/ip
observacao = observacoes ,
for voto_obj in votos :
user = request . user ,
voto_obj . votacao = registro
ip = get_client_ip ( request ) ,
voto_obj . user = request . user
materia = materia . materia ,
voto_obj . ip = get_client_ip ( request )
tipo_resultado_votacao = tipo_resultado ,
voto_obj . save ( )
* * fase_sessao )
registro . save ( )
# Build redirect URL before closing
materia_id = materia_aberta . materia_id
# user/ip de cada voto continuam sendo de quem votou.
votos . update ( votacao = registro )
VotoParlamentar . objects . bulk_create ( [
VotoParlamentar (
parlamentar_id = presenca . parlamentar_id ,
voto = ' Não Votou ' ,
votacao = registro ,
user = request . user ,
ip = get_client_ip ( request ) ,
* * fase_sessao )
for presenca in presentes . exclude (
parlamentar_id__in = votos . values ( ' parlamentar_id ' ) ) ] )
materia . resultado = tipo_resultado . nome
materia . votacao_aberta = False
materia . registro_aberto = False
materia . save ( )
VotoParlamentar . objects . filter (
* * fase_sessao , votacao__isnull = True ) . delete ( )
# Build redirect URL
materia_id = materia . materia_id
if ordem_dia :
if ordem_dia :
redirect_url = reverse ( ' sapl.sessao:ordemdia_list ' ,
redirect_url = reverse ( ' sapl.sessao:ordemdia_list ' ,
kwargs = { ' pk ' : controller_id } ) + f ' #id { materia_id } '
kwargs = { ' pk ' : controller_id } ) + f ' #id { materia_id } '
@ -1043,17 +1101,6 @@ def close_voting(request, controller_id):
redirect_url = reverse ( ' sapl.sessao:expedientemateria_list ' ,
redirect_url = reverse ( ' sapl.sessao:expedientemateria_list ' ,
kwargs = { ' pk ' : controller_id } ) + f ' #id { materia_id } '
kwargs = { ' pk ' : controller_id } ) + f ' #id { materia_id } '
# Close materia with resultado
materia_aberta . resultado = tipo_resultado . nome
materia_aberta . votacao_aberta = False
materia_aberta . save ( )
# Clean up orphan VotoParlamentar (without votacao)
if ordem_dia :
VotoParlamentar . objects . filter ( ordem = ordem_dia , votacao__isnull = True ) . delete ( )
else :
VotoParlamentar . objects . filter ( expediente = expediente , votacao__isnull = True ) . delete ( )
logger . info (
logger . info (
f " Voting closed for sessao= { controller_id } : "
f " Voting closed for sessao= { controller_id } : "
f " sim= { votos_sim } , nao= { votos_nao } , abstencoes= { abstencoes } , "
f " sim= { votos_sim } , nao= { votos_nao } , abstencoes= { abstencoes } , "