Browse Source

Adiciona endpoint de gravação em lote da sessão plenária

POST /api/sessao/sessaoplenaria/{id}/lote/ grava numa única requisição
presenças da sessão, presenças da ordem do dia, votações (registro e votos
nominais) e atualizações de matérias da ordem do dia e do expediente.

Cada item é gravado em sua própria transação e devolve um status
(criado, atualizado, removido, inalterado ou erro). O reenvio do mesmo
lote é idempotente. Validações e permissões são as mesmas dos endpoints
individuais, que seguem sem alteração.
pull/3866/head
Samuel Zwinglio 2 weeks ago
parent
commit
6531f70df3
  1. 168
      docs/api-sessao-lote.md
  2. 386
      sapl/api/sessao_lote.py
  3. 16
      sapl/api/views_sessao.py
  4. 296
      sapl/sessao/tests/test_sessao_lote.py

168
docs/api-sessao-lote.md

@ -0,0 +1,168 @@
# API: gravação em lote da Sessão Plenária
```
POST /api/sessao/sessaoplenaria/{id}/lote/
Authorization: Token <token>
Content-Type: application/json
```
Grava numa única requisição tudo o que uma integração (painel eletrônico,
sistema de votação etc.) precisa registrar numa sessão plenária:
| Bloco | Model | Equivale a |
|---|---|---|
| `presencas_sessao` | `SessaoPlenariaPresenca` | `POST`/`DELETE /api/sessao/sessaoplenariapresenca/` |
| `presencas_ordem_dia` | `PresencaOrdemDia` | `POST`/`DELETE /api/sessao/presencaordemdia/` |
| `votacoes` | `RegistroVotacao` + `VotoParlamentar` | `POST /api/sessao/registrovotacao/` + um `POST /api/sessao/votoparlamentar/` por voto |
| `ordens_dia` | `OrdemDia` | `PATCH /api/sessao/ordemdia/{id}/` |
| `expedientes_materia` | `ExpedienteMateria` | `PATCH /api/sessao/expedientemateria/{id}/` |
Numa Casa com 21 parlamentares, uma sessão com 20 matérias votadas
nominalmente gera mais de 460 requisições pelos endpoints individuais
(420 só de votos). Com este endpoint, a mesma sessão pode ser enviada em uma.
Os endpoints individuais continuam funcionando sem alteração.
## Regras
- **Qualquer combinação de blocos.** Blocos não enviados não são alterados.
Ao menos um bloco deve ser informado.
- **Itens independentes.** Cada item é gravado em sua própria transação: a
falha de um não impede a gravação dos demais. Uma votação (registro +
votos) é um único item: ou é gravada inteira, ou nada é gravado.
- **Relatório por item.** A resposta traz o status de cada item, na mesma
ordem em que foi enviado, para que apenas os que falharam sejam
reenviados.
- **Idempotente.** Reenviar o mesmo lote não duplica presenças, registros de
votação nem votos; os itens já gravados voltam como `inalterado`.
- **Mesmas validações e permissões.** Os dados são validados pelos mesmos
serializers dos endpoints individuais e cada item exige as mesmas
permissões (`sessao.add_*`, `sessao.change_*`, `sessao.delete_*`). Sem
permissão para um bloco, os itens daquele bloco voltam com erro e os
demais são gravados.
- **Escopo da sessão.** Matérias da ordem do dia e do expediente precisam
pertencer à sessão da URL.
- **Ordem de processamento:** presenças da sessão, presenças da ordem do dia,
votações, matérias da ordem do dia, matérias do expediente.
- Máximo de 500 itens por bloco.
## Payload
```json
{
"presencas_sessao": [
{"parlamentar": 1, "presente": true},
{"parlamentar": 2, "presente": false}
],
"presencas_ordem_dia": [
{"parlamentar": 1}
],
"votacoes": [
{
"ordem": 10,
"tipo_resultado_votacao": 1,
"observacao": "",
"votos": [
{"parlamentar": 1, "voto": "Sim"},
{"parlamentar": 3, "voto": "Não"}
]
},
{
"expediente": 4,
"tipo_resultado_votacao": 1,
"numero_votos_sim": 15,
"numero_votos_nao": 2,
"numero_abstencoes": 0
}
],
"ordens_dia": [
{"id": 10, "resultado": "Aprovado", "votacao_aberta": false}
],
"expedientes_materia": [
{"id": 4, "resultado": "Aprovado", "votacao_aberta": false}
]
}
```
### Presenças
`presente` é opcional (padrão `true`):
- `true`: registra a presença se ela ainda não existir (`criado`); se já
existir, `inalterado`.
- `false`: remove a presença se existir (`removido`); se não existir,
`inalterado`.
Na presença da sessão, `data_sessao` é preenchida com a data de início da
sessão.
### Votações
Cada votação informa **exatamente um** entre `ordem` (id de `OrdemDia`) e
`expediente` (id de `ExpedienteMateria`). A matéria é obtida do item da
pauta.
- **Nominal**: envie `votos`. Valores aceitos: `Sim`, `Não`, `Abstenção`,
`Não Votou`. Os totais do registro são calculados a partir dos votos; se
`numero_votos_sim`, `numero_votos_nao` ou `numero_abstencoes` também
forem enviados, precisam bater com o cálculo. A lista é o conjunto
completo de votos da matéria: votos de parlamentares fora da lista são
removidos, como acontece ao finalizar a votação nominal pela tela.
- **Simbólica / secreta**: omita `votos` e informe `numero_votos_sim`,
`numero_votos_nao` e `numero_abstencoes`.
Se a matéria ainda não tem registro de votação, ele é criado; se já tem, é
atualizado. Usuário e IP da requisição são gravados no registro e nos votos.
A votação não altera a matéria da pauta. Para gravar `resultado`, fechar a
votação etc., use os blocos `ordens_dia`/`expedientes_materia`, que são
processados depois das votações.
### Matérias da ordem do dia e do expediente
Cada item informa o `id` e os campos a alterar, com a mesma semântica de um
`PATCH` no endpoint individual. Não é permitido mover a matéria para outra
sessão.
## Resposta
`200 OK` sempre que o lote for processado, mesmo que algum item falhe.
Confira `resumo.erro`.
```json
{
"sessao_plenaria": 7,
"presencas_sessao": [
{"parlamentar": 1, "presente": true, "status": "criado", "id": 101},
{"parlamentar": 2, "presente": false, "status": "inalterado"}
],
"presencas_ordem_dia": [
{"parlamentar": 1, "presente": true, "status": "inalterado", "id": 55}
],
"votacoes": [
{"ordem": 10, "status": "criado", "id": 300},
{"expediente": 4, "status": "erro",
"erros": {"tipo_resultado_votacao": ["Este campo não pode ser nulo."]}}
],
"ordens_dia": [
{"id": 10, "status": "atualizado"}
],
"expedientes_materia": [
{"id": 4, "status": "atualizado"}
],
"resumo": {"criado": 2, "atualizado": 2, "removido": 0,
"inalterado": 2, "erro": 1}
}
```
Status por item: `criado`, `atualizado`, `removido`, `inalterado`, `erro`.
Itens com `erro` trazem `erros`, no mesmo formato dos erros de validação dos
endpoints individuais.
Erros que recusam o lote inteiro:
| Código | Quando |
|---|---|
| `400` | corpo não é um objeto, bloco desconhecido, nenhum bloco, bloco que não é lista, item que não é objeto ou mais de 500 itens num bloco |
| `401`/`403` | requisição sem autenticação |
| `404` | sessão plenária inexistente |

386
sapl/api/sessao_lote.py

@ -0,0 +1,386 @@
"""
Gravação em lote dos dados de uma Sessão Plenária.
Recebe, numa única requisição, presenças da sessão, presenças da ordem do
dia, votações (registro + votos nominais) e atualizações de matérias da
ordem do dia/expediente. Ver docs/api-sessao-lote.md.
Regras:
- qualquer combinação de blocos é aceita; o que não for enviado não é
alterado;
- cada item é gravado em sua própria transação: a falha de um não
impede a gravação dos demais;
- cada item devolve um status (criado, atualizado, removido, inalterado
ou erro) para que o cliente reenvie apenas o que falhou;
- reenviar o mesmo lote não duplica presenças nem votos;
- validações vêm dos mesmos serializers e as permissões são as mesmas
exigidas pelos endpoints individuais de cada model.
"""
import logging
from django.db import transaction
from rest_framework import serializers
from drfautoapi.drfautoapi import ApiViewSetConstrutor
from sapl.parlamentares.models import Parlamentar
from sapl.sessao.models import (ExpedienteMateria, OrdemDia,
PresencaOrdemDia, RegistroVotacao,
SessaoPlenariaPresenca, VotoParlamentar)
from sapl.utils import get_client_ip
logger = logging.getLogger(__name__)
MAX_ITENS_POR_BLOCO = 500
CRIADO = 'criado'
ATUALIZADO = 'atualizado'
REMOVIDO = 'removido'
INALTERADO = 'inalterado'
ERRO = 'erro'
VOTO_SIM = 'Sim'
VOTO_NAO = 'Não'
VOTO_ABSTENCAO = 'Abstenção'
VOTO_NAO_VOTOU = 'Não Votou'
VOTOS_VALIDOS = (VOTO_SIM, VOTO_NAO, VOTO_ABSTENCAO, VOTO_NAO_VOTOU)
BLOCOS = (
'presencas_sessao',
'presencas_ordem_dia',
'votacoes',
'ordens_dia',
'expedientes_materia',
)
class ItemInvalido(Exception):
def __init__(self, erros):
super().__init__(erros)
self.erros = erros
def perm(model, acao):
return '{}.{}_{}'.format(
model._meta.app_label, acao, model._meta.model_name)
def serializer_class_for(model):
return ApiViewSetConstrutor.get_viewset_for_model(model).serializer_class
class SessaoLote:
def __init__(self, sessao, request):
self.sessao = sessao
self.request = request
self.user = request.user
self.ip = get_client_ip(request)
@classmethod
def validar_payload(cls, data):
"""Valida apenas a estrutura do lote. Erros aqui recusam o lote
inteiro (HTTP 400); erros de item são reportados por item."""
if not isinstance(data, dict):
raise serializers.ValidationError(
'O corpo da requisição deve ser um objeto JSON.')
desconhecidos = sorted(set(data) - set(BLOCOS))
if desconhecidos:
raise serializers.ValidationError({
k: 'Bloco desconhecido. Blocos aceitos: {}.'.format(
', '.join(BLOCOS)) for k in desconhecidos})
if not any(b in data for b in BLOCOS):
raise serializers.ValidationError(
'Informe ao menos um dos blocos: {}.'.format(
', '.join(BLOCOS)))
erros = {}
for bloco in BLOCOS:
if bloco not in data:
continue
itens = data[bloco]
if not isinstance(itens, list):
erros[bloco] = 'Deve ser uma lista.'
elif len(itens) > MAX_ITENS_POR_BLOCO:
erros[bloco] = 'Máximo de {} itens por bloco.'.format(
MAX_ITENS_POR_BLOCO)
elif not all(isinstance(i, dict) for i in itens):
erros[bloco] = 'Todos os itens devem ser objetos JSON.'
if erros:
raise serializers.ValidationError(erros)
def processar(self, data):
self.validar_payload(data)
# Presenças antes das votações e votações antes das atualizações de
# matéria, na mesma ordem em que as integrações fariam com os
# endpoints individuais.
relatorio = {'sessao_plenaria': self.sessao.pk}
processadores = (
('presencas_sessao', lambda i: self.presenca(
SessaoPlenariaPresenca, i)),
('presencas_ordem_dia', lambda i: self.presenca(
PresencaOrdemDia, i)),
('votacoes', self.votacao),
('ordens_dia', lambda i: self.materia(OrdemDia, i)),
('expedientes_materia', lambda i: self.materia(
ExpedienteMateria, i)),
)
for bloco, processador in processadores:
if bloco in data:
relatorio[bloco] = [
self.processar_item(processador, item)
for item in data[bloco]]
resumo = dict.fromkeys(
(CRIADO, ATUALIZADO, REMOVIDO, INALTERADO, ERRO), 0)
for bloco in BLOCOS:
for item in relatorio.get(bloco, []):
resumo[item['status']] += 1
relatorio['resumo'] = resumo
logger.info('user={}. Lote da sessão plenária (pk={}) processado: '
'{}'.format(self.user.username, self.sessao.pk, resumo))
return relatorio
def processar_item(self, processador, item):
try:
with transaction.atomic():
return processador(item)
except ItemInvalido as e:
return self.item_erro(item, e.erros)
except serializers.ValidationError as e:
return self.item_erro(item, e.detail)
except Exception as e:
logger.error('user={}. Erro ao processar item do lote da sessão '
'plenária (pk={}): {}. {}'.format(
self.user.username, self.sessao.pk, item, e))
return self.item_erro(item, str(e))
def item_erro(self, item, erros):
chaves = ('parlamentar', 'ordem', 'expediente', 'id')
r = {k: item[k] for k in chaves if k in item}
r.update(status=ERRO, erros=erros)
return r
def exigir_perms(self, *perms):
faltando = [p for p in perms if not self.user.has_perm(p)]
if faltando:
raise ItemInvalido(
'Usuário sem permissão: {}.'.format(', '.join(faltando)))
def parlamentar_id(self, valor, campo='parlamentar'):
if isinstance(valor, bool) or not isinstance(valor, int):
raise ItemInvalido({campo: 'Informe o id do parlamentar.'})
if not Parlamentar.objects.filter(pk=valor).exists():
raise ItemInvalido(
{campo: 'Parlamentar (pk={}) não existe.'.format(valor)})
return valor
def presenca(self, model, item):
"""{"parlamentar": <id>, "presente": true|false}"""
parlamentar = self.parlamentar_id(item.get('parlamentar'))
presente = item.get('presente', True)
if not isinstance(presente, bool):
raise ItemInvalido({'presente': 'Deve ser true ou false.'})
r = {'parlamentar': parlamentar, 'presente': presente}
existentes = model.objects.filter(
sessao_plenaria=self.sessao, parlamentar_id=parlamentar)
if not presente:
if not existentes.exists():
return dict(r, status=INALTERADO)
self.exigir_perms(perm(model, 'delete'))
existentes.delete()
return dict(r, status=REMOVIDO)
atual = existentes.first()
if atual:
return dict(r, status=INALTERADO, id=atual.pk)
self.exigir_perms(perm(model, 'add'))
dados = {'sessao_plenaria': self.sessao.pk,
'parlamentar': parlamentar}
if model is SessaoPlenariaPresenca:
dados['data_sessao'] = self.sessao.data_inicio
serializer = serializer_class_for(model)(
data=dados, context={'request': self.request})
serializer.is_valid(raise_exception=True)
obj = serializer.save()
return dict(r, status=CRIADO, id=obj.pk)
def alvo_votacao(self, item):
"""Votação é de uma matéria da ordem do dia ou do expediente,
nunca das duas (mesma regra de RegistroVotacao.clean)."""
tem_ordem = item.get('ordem') is not None
tem_expediente = item.get('expediente') is not None
if tem_ordem == tem_expediente:
raise ItemInvalido('Informe exatamente um dos campos '
'"ordem" ou "expediente".')
campo, model = (('ordem', OrdemDia) if tem_ordem
else ('expediente', ExpedienteMateria))
alvo = model.objects.filter(
pk=item[campo], sessao_plenaria=self.sessao).first()
if not alvo:
raise ItemInvalido({campo: '{} (pk={}) não existe nesta sessão '
'plenária.'.format(
model._meta.verbose_name, item[campo])})
return campo, alvo
def votos_do_item(self, item):
votos = item['votos']
if not isinstance(votos, list):
raise ItemInvalido({'votos': 'Deve ser uma lista.'})
erros = {}
resultado = {}
for i, v in enumerate(votos):
if not isinstance(v, dict):
erros[i] = 'Deve ser um objeto JSON.'
continue
try:
p = self.parlamentar_id(v.get('parlamentar'))
except ItemInvalido as e:
erros[i] = e.erros
continue
if p in resultado:
erros[i] = {'parlamentar': 'Parlamentar repetido.'}
elif v.get('voto') not in VOTOS_VALIDOS:
erros[i] = {'voto': 'Valores aceitos: {}.'.format(
', '.join(VOTOS_VALIDOS))}
else:
resultado[p] = v['voto']
if erros:
raise ItemInvalido({'votos': erros})
return resultado
def votacao(self, item):
"""
{"ordem"|"expediente": <id>, "tipo_resultado_votacao": <id>,
"observacao": "...",
"votos": [{"parlamentar": <id>, "voto": "Sim"}, ...]}
Com "votos" (votação nominal), os totais são calculados a partir
deles e a lista substitui os votos já gravados para a matéria. Sem
"votos" (simbólica/secreta), informe numero_votos_sim,
numero_votos_nao e numero_abstencoes.
"""
campo, alvo = self.alvo_votacao(item)
r = {campo: alvo.pk}
votos = self.votos_do_item(item) if 'votos' in item else None
dados = {
campo: alvo.pk,
'materia': alvo.materia_id,
'tipo_resultado_votacao': item.get('tipo_resultado_votacao'),
'observacao': item.get('observacao', ''),
}
totais = ('numero_votos_sim', 'numero_votos_nao', 'numero_abstencoes')
if votos is not None:
contagem = list(votos.values())
calculados = dict(zip(totais, (
contagem.count(VOTO_SIM),
contagem.count(VOTO_NAO),
contagem.count(VOTO_ABSTENCAO))))
divergentes = {k: 'Diverge do total calculado a partir dos '
'votos ({}).'.format(calculados[k])
for k in totais
if k in item and item[k] != calculados[k]}
if divergentes:
raise ItemInvalido(divergentes)
dados.update(calculados)
else:
dados.update({k: item.get(k) for k in totais})
registro = RegistroVotacao.objects.filter(**{campo: alvo}).first()
if registro:
self.exigir_perms(perm(RegistroVotacao, 'change'))
else:
self.exigir_perms(perm(RegistroVotacao, 'add'))
if votos is not None:
self.exigir_perms(*(perm(VotoParlamentar, a)
for a in ('add', 'change', 'delete')))
serializer = serializer_class_for(RegistroVotacao)(
registro, data=dados, context={'request': self.request})
serializer.is_valid(raise_exception=True)
criado = registro is None
alterado = criado or any(
getattr(registro, f) != v
for f, v in serializer.validated_data.items())
if alterado:
registro = serializer.save(user=self.user, ip=self.ip)
if votos is not None and self.gravar_votos(campo, alvo, registro,
votos):
alterado = True
if criado:
status = CRIADO
else:
status = ATUALIZADO if alterado else INALTERADO
return dict(r, status=status, id=registro.pk)
def gravar_votos(self, campo, alvo, registro, votos):
"""Upsert por (matéria, parlamentar). Remove votos de parlamentares
fora da lista e votos sem registro, como a votação nominal da tela
faz ao finalizar."""
alterado = False
existentes = {}
for v in VotoParlamentar.objects.filter(**{campo: alvo}):
if v.parlamentar_id in existentes or \
v.parlamentar_id not in votos:
v.delete()
alterado = True
else:
existentes[v.parlamentar_id] = v
for parlamentar, voto in votos.items():
v = existentes.get(parlamentar)
if v and v.voto == voto and v.votacao_id == registro.pk:
continue
if not v:
v = VotoParlamentar(parlamentar_id=parlamentar,
**{campo: alvo})
v.voto = voto
v.votacao = registro
v.user = self.user
v.ip = self.ip
v.save()
alterado = True
return alterado
def materia(self, model, item):
"""{"id": <id>, <campos do PATCH do model>}"""
pk = item.get('id')
obj = model.objects.filter(
pk=pk, sessao_plenaria=self.sessao).first() \
if isinstance(pk, int) and not isinstance(pk, bool) else None
if not obj:
raise ItemInvalido({'id': '{} (pk={}) não existe nesta sessão '
'plenária.'.format(
model._meta.verbose_name, pk)})
dados = {k: v for k, v in item.items() if k != 'id'}
if 'sessao_plenaria' in dados and \
dados['sessao_plenaria'] != self.sessao.pk:
raise ItemInvalido({'sessao_plenaria': 'Não é possível mover a '
'matéria para outra sessão pelo lote.'})
self.exigir_perms(perm(model, 'change'))
serializer = serializer_class_for(model)(
obj, data=dados, partial=True, context={'request': self.request})
serializer.is_valid(raise_exception=True)
r = {'id': obj.pk}
if not any(getattr(obj, f) != v
for f, v in serializer.validated_data.items()):
return dict(r, status=INALTERADO)
serializer.save()
return dict(r, status=ATUALIZADO)

16
sapl/api/views_sessao.py

@ -1,12 +1,15 @@
from django.apps.registry import apps
from django.shortcuts import get_object_or_404
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from drfautoapi.drfautoapi import ApiViewSetConstrutor, \
customize, wrapper_queryset_response_for_drf_action
from sapl.api.serializers import ChoiceSerializer,\
SessaoPlenariaECidadaniaSerializer
from sapl.api.sessao_lote import SessaoLote
from sapl.sessao.models import SessaoPlenaria, ExpedienteSessao,\
restringe_sessoes_visiveis
from sapl.utils import choice_anos_com_sessaoplenaria
@ -55,6 +58,19 @@ class _SessaoPlenariaViewSet:
self.serializer_class = SessaoPlenariaECidadaniaSerializer
return self.list(request, *args, **kwargs)
# As permissões são verificadas por item em SessaoLote, com as mesmas
# regras dos endpoints individuais de cada model.
@action(detail=True, methods=['post'],
permission_classes=[IsAuthenticated])
def lote(self, request, *args, **kwargs):
"""
Grava em uma única requisição presenças da sessão, presenças da
ordem do dia, votações e atualizações de matérias da sessão.
Detalhes em docs/api-sessao-lote.md.
"""
sessao = get_object_or_404(SessaoPlenaria, pk=kwargs['pk'])
return Response(SessaoLote(sessao, request).processar(request.data))
class _RestringeSessoesVisiveisMixin:

296
sapl/sessao/tests/test_sessao_lote.py

@ -0,0 +1,296 @@
import pytest
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from model_bakery import baker
from rest_framework.test import APIClient
from sapl.parlamentares.models import Parlamentar
from sapl.sessao.models import (ExpedienteMateria, OrdemDia,
PresencaOrdemDia, RegistroVotacao,
SessaoPlenaria, SessaoPlenariaPresenca,
TipoResultadoVotacao, VotoParlamentar)
def url(sessao):
return '/api/sessao/sessaoplenaria/{}/lote/'.format(sessao.pk)
@pytest.fixture
def client(admin_user):
c = APIClient()
c.force_authenticate(admin_user)
return c
@pytest.fixture
def sessao():
return baker.make(SessaoPlenaria)
@pytest.fixture
def parlamentares():
return baker.make(Parlamentar, ativo=True, _quantity=3)
@pytest.fixture
def ordem(sessao):
return baker.make(OrdemDia, sessao_plenaria=sessao, votacao_aberta=True)
@pytest.fixture
def resultado():
return baker.make(TipoResultadoVotacao, nome='Aprovado', natureza='A')
def post(client, sessao, data):
return client.post(url(sessao), data, format='json')
def votacao_nominal(ordem, resultado, parlamentares, votos):
return {
'ordem': ordem.pk,
'tipo_resultado_votacao': resultado.pk,
'votos': [{'parlamentar': p.pk, 'voto': v}
for p, v in zip(parlamentares, votos)],
}
@pytest.mark.django_db(transaction=False)
def test_lote_exige_autenticacao(sessao):
response = APIClient().post(url(sessao), {}, format='json')
assert response.status_code in (401, 403)
@pytest.mark.django_db(transaction=False)
def test_lote_sessao_inexistente(client):
response = client.post('/api/sessao/sessaoplenaria/999999/lote/',
{'presencas_sessao': []}, format='json')
assert response.status_code == 404
@pytest.mark.django_db(transaction=False)
def test_lote_estrutura_invalida(client, sessao):
assert post(client, sessao, {}).status_code == 400
assert post(client, sessao, {'votos': []}).status_code == 400
assert post(client, sessao, {'presencas_sessao': {}}).status_code == 400
assert post(client, sessao, {'presencas_sessao': [1]}).status_code == 400
@pytest.mark.django_db(transaction=False)
def test_lote_presencas_idempotente(client, sessao, parlamentares):
data = {
'presencas_sessao': [{'parlamentar': p.pk} for p in parlamentares],
'presencas_ordem_dia': [{'parlamentar': parlamentares[0].pk,
'presente': True}],
}
response = post(client, sessao, data)
assert response.status_code == 200
r = response.json()
assert [i['status'] for i in r['presencas_sessao']] == ['criado'] * 3
assert r['presencas_ordem_dia'][0]['status'] == 'criado'
assert r['resumo']['criado'] == 4
assert SessaoPlenariaPresenca.objects.filter(
sessao_plenaria=sessao).count() == 3
assert SessaoPlenariaPresenca.objects.filter(
sessao_plenaria=sessao, data_sessao=sessao.data_inicio).count() == 3
r = post(client, sessao, data).json()
assert r['resumo']['inalterado'] == 4
assert r['resumo']['criado'] == 0
assert SessaoPlenariaPresenca.objects.filter(
sessao_plenaria=sessao).count() == 3
assert PresencaOrdemDia.objects.filter(
sessao_plenaria=sessao).count() == 1
@pytest.mark.django_db(transaction=False)
def test_lote_remove_presenca(client, sessao, parlamentares):
baker.make(SessaoPlenariaPresenca, sessao_plenaria=sessao,
parlamentar=parlamentares[0])
r = post(client, sessao, {'presencas_sessao': [
{'parlamentar': parlamentares[0].pk, 'presente': False},
{'parlamentar': parlamentares[1].pk, 'presente': False},
]}).json()
assert [i['status'] for i in r['presencas_sessao']] == [
'removido', 'inalterado']
assert not SessaoPlenariaPresenca.objects.filter(
sessao_plenaria=sessao).exists()
@pytest.mark.django_db(transaction=False)
def test_lote_falha_de_item_nao_impede_os_demais(client, sessao,
parlamentares):
r = post(client, sessao, {'presencas_sessao': [
{'parlamentar': parlamentares[0].pk},
{'parlamentar': 999999},
{'parlamentar': 'abc'},
{'parlamentar': parlamentares[1].pk},
]}).json()
status = [i['status'] for i in r['presencas_sessao']]
assert status == ['criado', 'erro', 'erro', 'criado']
assert 'parlamentar' in r['presencas_sessao'][1]['erros']
assert r['resumo']['erro'] == 2
assert SessaoPlenariaPresenca.objects.filter(
sessao_plenaria=sessao).count() == 2
@pytest.mark.django_db(transaction=False)
def test_lote_votacao_nominal(client, sessao, ordem, resultado,
parlamentares):
data = {'votacoes': [votacao_nominal(
ordem, resultado, parlamentares, ['Sim', 'Sim', 'Não'])]}
r = post(client, sessao, data).json()
assert r['votacoes'][0]['status'] == 'criado'
registro = RegistroVotacao.objects.get(ordem=ordem)
assert r['votacoes'][0]['id'] == registro.pk
assert registro.materia_id == ordem.materia_id
assert (registro.numero_votos_sim, registro.numero_votos_nao,
registro.numero_abstencoes) == (2, 1, 0)
assert VotoParlamentar.objects.filter(
ordem=ordem, votacao=registro).count() == 3
# Reenvio do mesmo lote não duplica registro nem votos.
r = post(client, sessao, data).json()
assert r['votacoes'][0]['status'] == 'inalterado'
assert RegistroVotacao.objects.filter(ordem=ordem).count() == 1
assert VotoParlamentar.objects.filter(ordem=ordem).count() == 3
@pytest.mark.django_db(transaction=False)
def test_lote_votacao_nominal_atualiza_votos(client, sessao, ordem,
resultado, parlamentares):
post(client, sessao, {'votacoes': [votacao_nominal(
ordem, resultado, parlamentares, ['Sim', 'Sim', 'Não'])]})
# Último parlamentar deixa a lista; segundo muda o voto.
r = post(client, sessao, {'votacoes': [votacao_nominal(
ordem, resultado, parlamentares[:2], ['Sim', 'Abstenção'])]}).json()
assert r['votacoes'][0]['status'] == 'atualizado'
registro = RegistroVotacao.objects.get(ordem=ordem)
assert (registro.numero_votos_sim, registro.numero_votos_nao,
registro.numero_abstencoes) == (1, 0, 1)
votos = dict(VotoParlamentar.objects.filter(
ordem=ordem).values_list('parlamentar_id', 'voto'))
assert votos == {parlamentares[0].pk: 'Sim',
parlamentares[1].pk: 'Abstenção'}
@pytest.mark.django_db(transaction=False)
def test_lote_votacao_invalida_nao_grava_nada(client, sessao, ordem,
resultado, parlamentares):
item = votacao_nominal(ordem, resultado, parlamentares,
['Sim', 'Talvez', 'Não'])
r = post(client, sessao, {'votacoes': [item]}).json()
assert r['votacoes'][0]['status'] == 'erro'
assert '1' in r['votacoes'][0]['erros']['votos']
assert not RegistroVotacao.objects.exists()
assert not VotoParlamentar.objects.exists()
item = votacao_nominal(ordem, resultado, parlamentares,
['Sim', 'Sim', 'Não'])
item['numero_votos_sim'] = 3
r = post(client, sessao, {'votacoes': [item]}).json()
assert 'numero_votos_sim' in r['votacoes'][0]['erros']
assert not RegistroVotacao.objects.exists()
@pytest.mark.django_db(transaction=False)
def test_lote_votacao_simbolica_expediente(client, sessao, resultado):
expediente = baker.make(ExpedienteMateria, sessao_plenaria=sessao)
item = {'expediente': expediente.pk,
'tipo_resultado_votacao': resultado.pk,
'numero_votos_sim': 10, 'numero_votos_nao': 1,
'numero_abstencoes': 0}
r = post(client, sessao, {'votacoes': [item]}).json()
assert r['votacoes'][0]['status'] == 'criado'
registro = RegistroVotacao.objects.get(expediente=expediente)
assert registro.ordem is None
assert registro.numero_votos_sim == 10
del item['numero_votos_nao']
r = post(client, sessao, {'votacoes': [item]}).json()
assert 'numero_votos_nao' in r['votacoes'][0]['erros']
@pytest.mark.django_db(transaction=False)
def test_lote_votacao_alvo_invalido(client, sessao, ordem, resultado):
outra = baker.make(OrdemDia)
expediente = baker.make(ExpedienteMateria, sessao_plenaria=sessao)
r = post(client, sessao, {'votacoes': [
{'ordem': outra.pk, 'tipo_resultado_votacao': resultado.pk,
'votos': []},
{'ordem': ordem.pk, 'expediente': expediente.pk,
'tipo_resultado_votacao': resultado.pk, 'votos': []},
{'tipo_resultado_votacao': resultado.pk, 'votos': []},
]}).json()
assert [i['status'] for i in r['votacoes']] == ['erro'] * 3
assert not RegistroVotacao.objects.exists()
@pytest.mark.django_db(transaction=False)
def test_lote_atualiza_ordem_dia(client, sessao, ordem, resultado,
parlamentares):
data = {
'votacoes': [votacao_nominal(
ordem, resultado, parlamentares, ['Sim', 'Sim', 'Sim'])],
'ordens_dia': [{'id': ordem.pk, 'resultado': resultado.nome,
'votacao_aberta': False}],
}
r = post(client, sessao, data).json()
assert r['ordens_dia'][0]['status'] == 'atualizado'
ordem.refresh_from_db()
assert ordem.resultado == 'Aprovado'
assert ordem.votacao_aberta is False
r = post(client, sessao, data).json()
assert r['ordens_dia'][0]['status'] == 'inalterado'
assert r['resumo']['erro'] == 0
@pytest.mark.django_db(transaction=False)
def test_lote_ordem_dia_de_outra_sessao(client, sessao):
outra = baker.make(OrdemDia)
ordem = baker.make(OrdemDia, sessao_plenaria=sessao)
r = post(client, sessao, {'ordens_dia': [
{'id': outra.pk, 'resultado': 'x'},
{'id': ordem.pk, 'sessao_plenaria': outra.sessao_plenaria_id},
]}).json()
assert [i['status'] for i in r['ordens_dia']] == ['erro', 'erro']
outra.refresh_from_db()
ordem.refresh_from_db()
assert outra.resultado != 'x'
assert ordem.sessao_plenaria == sessao
@pytest.mark.django_db(transaction=False)
def test_lote_respeita_permissoes_por_bloco(sessao, ordem, resultado,
parlamentares):
user = get_user_model().objects.create_user(
username='operador', password='operador')
user.user_permissions.add(Permission.objects.get(
codename='add_sessaoplenariapresenca'))
c = APIClient()
c.force_authenticate(user)
r = post(c, sessao, {
'presencas_sessao': [{'parlamentar': parlamentares[0].pk}],
'presencas_ordem_dia': [{'parlamentar': parlamentares[0].pk}],
'votacoes': [votacao_nominal(
ordem, resultado, parlamentares, ['Sim', 'Sim', 'Sim'])],
}).json()
assert r['presencas_sessao'][0]['status'] == 'criado'
assert r['presencas_ordem_dia'][0]['status'] == 'erro'
assert 'sessao.add_presencaordemdia' in r['presencas_ordem_dia'][0][
'erros']
assert r['votacoes'][0]['status'] == 'erro'
assert not PresencaOrdemDia.objects.exists()
assert not RegistroVotacao.objects.exists()
Loading…
Cancel
Save