mirror of https://github.com/interlegis/sapl.git
committed by
GitHub
8 changed files with 1439 additions and 1 deletions
@ -0,0 +1,207 @@ |
|||||
|
# 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 | |
||||
|
| `leituras` | `RegistroLeitura` | `POST`/`PATCH`/`DELETE /api/sessao/registroleitura/` | |
||||
|
| `ordens_dia` | `OrdemDia` | `PATCH /api/sessao/ordemdia/{id}/` | |
||||
|
| `expedientes_materia` | `ExpedienteMateria` | `PATCH /api/sessao/expedientemateria/{id}/` | |
||||
|
|
||||
|
## Requisições por sessão |
||||
|
|
||||
|
Considerando uma Casa com 21 parlamentares, uma sessão com 20 matérias |
||||
|
votadas nominalmente e 10 matérias lidas no expediente: |
||||
|
|
||||
|
| O que é gravado | Endpoints individuais | Lote | |
||||
|
|---|---|---| |
||||
|
| Presenças da sessão | 21 | | |
||||
|
| Presenças da ordem do dia | 21 | | |
||||
|
| Votações: 1 registro + 21 votos + 1 `PATCH` na ordem do dia, por matéria | 20 × 23 = 460 | | |
||||
|
| Leituras: 1 registro + 1 `PATCH` no expediente, por matéria | 10 × 2 = 20 | | |
||||
|
| **Total** | **522** | **1** | |
||||
|
|
||||
|
De modo geral, pelos endpoints individuais são `2 × P + M × (P + 2) + 2 × L` |
||||
|
requisições (P parlamentares, M matérias votadas nominalmente, L matérias |
||||
|
lidas). Com este endpoint, a integração pode registrar a sessão localmente e |
||||
|
fazer **uma única requisição ao final da sessão, apenas para |
||||
|
sincronização** com o SAPL. |
||||
|
|
||||
|
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, leituras, 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 |
||||
|
} |
||||
|
], |
||||
|
"leituras": [ |
||||
|
{"expediente": 5, "observacao": ""}, |
||||
|
{"expediente": 6, "lida": false} |
||||
|
], |
||||
|
"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. |
||||
|
|
||||
|
### Leituras |
||||
|
|
||||
|
Cada leitura informa **exatamente um** entre `ordem` e `expediente`; a |
||||
|
matéria é obtida do item da pauta. Há no máximo uma leitura por item da |
||||
|
pauta. `lida` é opcional (padrão `true`): |
||||
|
|
||||
|
- `true`: cria a leitura (`criado`) ou atualiza a `observacao` da existente |
||||
|
(`atualizado`/`inalterado`). Leituras duplicadas do mesmo item são |
||||
|
removidas. |
||||
|
- `false`: remove a leitura, como "retirar leitura" na tela (`removido`); se |
||||
|
não existir, `inalterado`. |
||||
|
|
||||
|
Assim como a votação, a leitura não altera a matéria da pauta: para gravar |
||||
|
`resultado: "Matéria lida"`, use `ordens_dia`/`expedientes_materia`. |
||||
|
|
||||
|
### 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."]}} |
||||
|
], |
||||
|
"leituras": [ |
||||
|
{"expediente": 5, "lida": true, "status": "criado", "id": 80}, |
||||
|
{"expediente": 6, "lida": false, "status": "removido"} |
||||
|
], |
||||
|
"ordens_dia": [ |
||||
|
{"id": 10, "status": "atualizado"} |
||||
|
], |
||||
|
"expedientes_materia": [ |
||||
|
{"id": 4, "status": "atualizado"} |
||||
|
], |
||||
|
"resumo": {"criado": 3, "atualizado": 2, "removido": 1, |
||||
|
"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 | |
||||
@ -0,0 +1,78 @@ |
|||||
|
# API: dados da Sessão Plenária para painel |
||||
|
|
||||
|
``` |
||||
|
GET /api/sessao/sessaoplenaria/{id}/painel/ |
||||
|
``` |
||||
|
|
||||
|
Devolve numa única requisição tudo o que um painel eletrônico ou integração |
||||
|
precisa para montar uma sessão plenária. É o par de leitura do |
||||
|
[lote de gravação](api-sessao-lote.md): a integração baixa a sessão no início |
||||
|
com este endpoint, trabalha localmente e envia tudo no fim com o lote. |
||||
|
|
||||
|
## Requisições por sessão |
||||
|
|
||||
|
Para montar o painel pelos endpoints de listagem, uma integração precisa |
||||
|
buscar sessão, tipo, casa, parlamentares, mandatos, presenças, pauta, |
||||
|
expediente, matérias, autorias, autores, registros de votação, votos, |
||||
|
leituras e cadastros auxiliares, paginando de 100 em 100. Numa Casa com 21 |
||||
|
parlamentares e 20 matérias votadas nominalmente, só os votos (420) ocupam 5 |
||||
|
páginas. |
||||
|
|
||||
|
| | Endpoints de listagem | Este endpoint | |
||||
|
|---|---|---| |
||||
|
| Montar o painel (início da sessão) | ~20, usando `expand` e filtros `__in`; bem mais sem eles | **1** | |
||||
|
| Sincronizar a sessão (fim) | 522 (ver [lote](api-sessao-lote.md)) | **1** com o lote | |
||||
|
|
||||
|
## Permissões |
||||
|
|
||||
|
Mesmas regras dos endpoints de listagem: todos os models devolvidos são de |
||||
|
leitura pública no SAPL, então o endpoint dispensa autenticação. Se alguma |
||||
|
dessas regras deixar de ser pública, o endpoint passa a exigir a permissão de |
||||
|
listagem de cada model e responde `403` indicando quais faltam. |
||||
|
|
||||
|
A visibilidade da sessão segue a consulta pública: sessões futuras |
||||
|
cadastradas com antecedência (não iniciadas e sem pauta publicada) respondem |
||||
|
`404` para usuários não autenticados. |
||||
|
|
||||
|
## Resposta |
||||
|
|
||||
|
Cada bloco usa o mesmo serializer da listagem do model correspondente, então |
||||
|
cada registro tem exatamente o formato de `/api/<app>/<model>/`. |
||||
|
|
||||
|
| Chave | Model | Conteúdo | |
||||
|
|---|---|---| |
||||
|
| `sessao_plenaria` | `SessaoPlenaria` | objeto da sessão | |
||||
|
| `tipo_sessao` | `TipoSessaoPlenaria` | objeto do tipo da sessão | |
||||
|
| `casa_legislativa` | `CasaLegislativa` | lista | |
||||
|
| `parlamentares` | `Parlamentar` | com mandato na legislatura da sessão ou citados nela (presença, mesa, voto, orador) | |
||||
|
| `mandatos` | `Mandato` | da legislatura da sessão | |
||||
|
| `filiacoes` | `Filiacao` | dos parlamentares acima | |
||||
|
| `partidos` | `Partido` | das filiações acima | |
||||
|
| `cargos_mesa` | `CargoMesa` | todos | |
||||
|
| `integrantes_mesa` | `IntegranteMesa` | mesa da sessão | |
||||
|
| `presencas_sessao` | `SessaoPlenariaPresenca` | da sessão | |
||||
|
| `presencas_ordem_dia` | `PresencaOrdemDia` | da sessão | |
||||
|
| `tipos_expediente` | `TipoExpediente` | todos | |
||||
|
| `expedientes` | `ExpedienteSessao` | da sessão | |
||||
|
| `expedientes_materia` | `ExpedienteMateria` | da sessão | |
||||
|
| `ordens_dia` | `OrdemDia` | da sessão | |
||||
|
| `materias` | `MateriaLegislativa` | da ordem do dia e do expediente | |
||||
|
| `autorias` | `Autoria` | dessas matérias | |
||||
|
| `autores` | `Autor` | dessas autorias | |
||||
|
| `tipos_autor` | `TipoAutor` | todos | |
||||
|
| `tipos_resultado_votacao` | `TipoResultadoVotacao` | todos | |
||||
|
| `registros_votacao` | `RegistroVotacao` | da ordem do dia e do expediente | |
||||
|
| `votos` | `VotoParlamentar` | da ordem do dia e do expediente | |
||||
|
| `registros_leitura` | `RegistroLeitura` | da ordem do dia e do expediente | |
||||
|
| `oradores` | `Orador` | explicações pessoais | |
||||
|
| `oradores_expediente` | `OradorExpediente` | da sessão | |
||||
|
| `oradores_ordem_dia` | `OradorOrdemDia` | da sessão | |
||||
|
|
||||
|
Os blocos não são paginados. |
||||
|
|
||||
|
## Desempenho |
||||
|
|
||||
|
O número de consultas ao banco não cresce com o tamanho da sessão: |
||||
|
relacionamentos usados no `__str__`, campos many-to-many e `metadata` são |
||||
|
carregados de uma vez por bloco. Numa sessão com cerca de 600 registros, a |
||||
|
resposta faz menos de 100 consultas. |
||||
@ -0,0 +1,440 @@ |
|||||
|
""" |
||||
|
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), registros de leitura 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, RegistroLeitura, |
||||
|
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', |
||||
|
'leituras', |
||||
|
'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 leituras, e estas 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), |
||||
|
('leituras', self.leitura), |
||||
|
('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(self, item): |
||||
|
"""Votação e leitura são de uma matéria da ordem do dia ou do |
||||
|
expediente, nunca das duas (mesma regra de RegistroVotacao.clean e |
||||
|
RegistroLeitura.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(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 leitura(self, item): |
||||
|
""" |
||||
|
{"ordem"|"expediente": <id>, "observacao": "...", |
||||
|
"lida": true|false} |
||||
|
|
||||
|
Uma leitura por matéria da pauta: "lida" (padrão true) cria ou |
||||
|
atualiza o registro; false remove, como "retirar leitura" na tela. |
||||
|
""" |
||||
|
campo, alvo = self.alvo(item) |
||||
|
lida = item.get('lida', True) |
||||
|
if not isinstance(lida, bool): |
||||
|
raise ItemInvalido({'lida': 'Deve ser true ou false.'}) |
||||
|
|
||||
|
r = {campo: alvo.pk, 'lida': lida} |
||||
|
existentes = RegistroLeitura.objects.filter(**{campo: alvo}) |
||||
|
|
||||
|
if not lida: |
||||
|
if not existentes.exists(): |
||||
|
return dict(r, status=INALTERADO) |
||||
|
self.exigir_perms(perm(RegistroLeitura, 'delete')) |
||||
|
existentes.delete() |
||||
|
return dict(r, status=REMOVIDO) |
||||
|
|
||||
|
registro = existentes.first() |
||||
|
self.exigir_perms(perm(RegistroLeitura, |
||||
|
'change' if registro else 'add')) |
||||
|
duplicados = existentes.exclude(pk=registro.pk) if registro \ |
||||
|
else existentes.none() |
||||
|
limpou = duplicados.exists() |
||||
|
if limpou: |
||||
|
self.exigir_perms(perm(RegistroLeitura, 'delete')) |
||||
|
duplicados.delete() |
||||
|
|
||||
|
serializer = serializer_class_for(RegistroLeitura)( |
||||
|
registro, data={campo: alvo.pk, |
||||
|
'materia': alvo.materia_id, |
||||
|
'observacao': item.get('observacao', '')}, |
||||
|
context={'request': self.request}) |
||||
|
serializer.is_valid(raise_exception=True) |
||||
|
|
||||
|
if registro and not any( |
||||
|
getattr(registro, f) != v |
||||
|
for f, v in serializer.validated_data.items()): |
||||
|
status = ATUALIZADO if limpou else INALTERADO |
||||
|
return dict(r, status=status, id=registro.pk) |
||||
|
status = ATUALIZADO if registro else CRIADO |
||||
|
registro = serializer.save(user=self.user, ip=self.ip) |
||||
|
return dict(r, status=status, id=registro.pk) |
||||
|
|
||||
|
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) |
||||
@ -0,0 +1,193 @@ |
|||||
|
""" |
||||
|
Leitura, em uma única requisição, de tudo o que um painel eletrônico ou |
||||
|
integração precisa para montar uma Sessão Plenária: sessão, mesa, |
||||
|
parlamentares, presenças, expediente, ordem do dia, matérias, autorias, |
||||
|
votações, votos, leituras e oradores. Ver docs/api-sessao-painel.md. |
||||
|
|
||||
|
Cada bloco usa o mesmo serializer do endpoint de listagem do model, então |
||||
|
os registros têm o mesmo formato de /api/<app>/<model>/. |
||||
|
""" |
||||
|
from django.contrib.contenttypes.models import ContentType |
||||
|
from django.db.models import Q, prefetch_related_objects |
||||
|
|
||||
|
from drfautoapi.drfautoapi import ApiViewSetConstrutor |
||||
|
from sapl.api.permissions import SaplModelPermissions |
||||
|
from sapl.base.models import Autor, CasaLegislativa, Metadata, TipoAutor |
||||
|
from sapl.materia.models import Autoria, MateriaLegislativa |
||||
|
from sapl.parlamentares.models import (CargoMesa, Filiacao, Mandato, |
||||
|
Parlamentar, Partido) |
||||
|
from sapl.sessao.models import (ExpedienteMateria, ExpedienteSessao, |
||||
|
IntegranteMesa, Orador, OradorExpediente, |
||||
|
OradorOrdemDia, OrdemDia, PresencaOrdemDia, |
||||
|
RegistroLeitura, RegistroVotacao, |
||||
|
SessaoPlenariaPresenca, TipoExpediente, |
||||
|
TipoResultadoVotacao, TipoSessaoPlenaria, |
||||
|
VotoParlamentar) |
||||
|
|
||||
|
|
||||
|
def serializar(request, model, objs, many=True): |
||||
|
"""Serializa com o serializer da listagem do model. Metadata e campos |
||||
|
many-to-many de todos os registros são lidos de uma vez, em vez de uma |
||||
|
consulta por registro.""" |
||||
|
serializer_class = ApiViewSetConstrutor.get_viewset_for_model( |
||||
|
model).serializer_class |
||||
|
lista = list(objs) if many else [objs] |
||||
|
prefetch_related_objects( |
||||
|
lista, *(f.name for f in model._meta.many_to_many)) |
||||
|
content_type = ContentType.objects.get_for_model(model) |
||||
|
metadata = dict.fromkeys(((model._meta.label, o.pk) for o in lista), {}) |
||||
|
metadata.update({ |
||||
|
(model._meta.label, m.object_id): m.metadata |
||||
|
for m in Metadata.objects.filter( |
||||
|
content_type=content_type, |
||||
|
object_id__in=[o.pk for o in lista])}) |
||||
|
return serializer_class( |
||||
|
lista if many else objs, many=many, |
||||
|
context={'request': request, 'metadata': metadata}).data |
||||
|
|
||||
|
|
||||
|
def por_id(objs): |
||||
|
return {o.pk: o for o in objs} |
||||
|
|
||||
|
|
||||
|
class SessaoPainel: |
||||
|
|
||||
|
# Models lidos, para conferir a permissão de leitura de cada um com as |
||||
|
# mesmas regras dos endpoints de listagem. |
||||
|
MODELS = ( |
||||
|
TipoSessaoPlenaria, CasaLegislativa, Parlamentar, Mandato, Filiacao, |
||||
|
Partido, CargoMesa, IntegranteMesa, SessaoPlenariaPresenca, |
||||
|
PresencaOrdemDia, TipoExpediente, ExpedienteSessao, |
||||
|
ExpedienteMateria, OrdemDia, MateriaLegislativa, Autoria, Autor, |
||||
|
TipoAutor, TipoResultadoVotacao, RegistroVotacao, VotoParlamentar, |
||||
|
RegistroLeitura, Orador, OradorExpediente, OradorOrdemDia, |
||||
|
) |
||||
|
|
||||
|
def __init__(self, sessao, request): |
||||
|
self.sessao = sessao |
||||
|
self.request = request |
||||
|
|
||||
|
def models_sem_permissao(self): |
||||
|
permissao = SaplModelPermissions() |
||||
|
sem = [] |
||||
|
for model in self.MODELS: |
||||
|
# SaplModelPermissions confere a permissão pelo queryset da view. |
||||
|
view = type('View', (), {'queryset': model.objects.none()})() |
||||
|
if not permissao.has_permission(self.request, view): |
||||
|
sem.append('{}.{}'.format(model._meta.app_label, |
||||
|
model._meta.model_name)) |
||||
|
return sem |
||||
|
|
||||
|
def dados(self): |
||||
|
""" |
||||
|
Os registros da sessão recebem a própria sessão já carregada, e |
||||
|
votações, votos e leituras recebem os itens da pauta já carregados: |
||||
|
o __str__ de cada um sobe por essas relações e, sem isso, faria |
||||
|
consultas por registro. |
||||
|
""" |
||||
|
s = self.sessao |
||||
|
r = self.request |
||||
|
|
||||
|
def da_sessao(model, *relacionados): |
||||
|
objs = list(model.objects.filter( |
||||
|
sessao_plenaria=s).select_related(*relacionados)) |
||||
|
for o in objs: |
||||
|
o.sessao_plenaria = s |
||||
|
return objs |
||||
|
|
||||
|
ordens = da_sessao(OrdemDia, 'materia__tipo') |
||||
|
expedientes_materia = da_sessao(ExpedienteMateria, 'materia__tipo') |
||||
|
ordens_por_id = por_id(ordens) |
||||
|
expedientes_por_id = por_id(expedientes_materia) |
||||
|
da_pauta = Q(ordem_id__in=ordens_por_id) | \ |
||||
|
Q(expediente_id__in=expedientes_por_id) |
||||
|
|
||||
|
def na_pauta(objs): |
||||
|
for o in objs: |
||||
|
if o.ordem_id: |
||||
|
o.ordem = ordens_por_id[o.ordem_id] |
||||
|
if o.expediente_id: |
||||
|
o.expediente = expedientes_por_id[o.expediente_id] |
||||
|
return objs |
||||
|
|
||||
|
registros_votacao = na_pauta(list(RegistroVotacao.objects.filter( |
||||
|
da_pauta).select_related('tipo_resultado_votacao'))) |
||||
|
registros_por_id = por_id(registros_votacao) |
||||
|
votos = na_pauta(list(VotoParlamentar.objects.filter( |
||||
|
da_pauta).select_related('parlamentar'))) |
||||
|
for v in votos: |
||||
|
if v.votacao_id in registros_por_id: |
||||
|
v.votacao = registros_por_id[v.votacao_id] |
||||
|
registros_leitura = na_pauta(list(RegistroLeitura.objects.filter( |
||||
|
da_pauta).select_related('materia__tipo'))) |
||||
|
|
||||
|
materias_ids = {o.materia_id for o in ordens + expedientes_materia} |
||||
|
materias = list(MateriaLegislativa.objects.filter( |
||||
|
id__in=materias_ids).select_related('tipo')) |
||||
|
autorias = list(Autoria.objects.filter( |
||||
|
materia_id__in=materias_ids).select_related( |
||||
|
'autor__tipo', 'materia__tipo').prefetch_related( |
||||
|
'autor__autor_related')) |
||||
|
autores = {a.autor_id: a.autor for a in autorias} |
||||
|
|
||||
|
presencas = da_sessao(SessaoPlenariaPresenca, 'parlamentar') |
||||
|
presencas_ordem = da_sessao(PresencaOrdemDia, 'parlamentar') |
||||
|
mesa = da_sessao(IntegranteMesa, 'parlamentar', 'cargo') |
||||
|
expedientes = da_sessao(ExpedienteSessao, 'tipo') |
||||
|
oradores = {model: da_sessao(model, 'parlamentar') |
||||
|
for model in (Orador, OradorExpediente, OradorOrdemDia)} |
||||
|
|
||||
|
# Parlamentares com mandato na legislatura da sessão e qualquer |
||||
|
# outro citado nela (presença, mesa, voto ou orador). |
||||
|
mandatos = list(Mandato.objects.filter( |
||||
|
legislatura_id=s.legislatura_id).select_related( |
||||
|
'parlamentar', 'legislatura')) |
||||
|
parlamentares_ids = {o.parlamentar_id for o in ( |
||||
|
mandatos + presencas + presencas_ordem + mesa + votos + |
||||
|
[o for lista in oradores.values() for o in lista])} |
||||
|
filiacoes = list(Filiacao.objects.filter( |
||||
|
parlamentar_id__in=parlamentares_ids).select_related( |
||||
|
'parlamentar', 'partido')) |
||||
|
|
||||
|
return { |
||||
|
'sessao_plenaria': serializar(r, type(s), s, many=False), |
||||
|
'tipo_sessao': serializar( |
||||
|
r, TipoSessaoPlenaria, s.tipo, many=False), |
||||
|
'casa_legislativa': serializar( |
||||
|
r, CasaLegislativa, CasaLegislativa.objects.all()), |
||||
|
'parlamentares': serializar( |
||||
|
r, Parlamentar, Parlamentar.objects.filter( |
||||
|
id__in=parlamentares_ids)), |
||||
|
'mandatos': serializar(r, Mandato, mandatos), |
||||
|
'filiacoes': serializar(r, Filiacao, filiacoes), |
||||
|
'partidos': serializar(r, Partido, Partido.objects.filter( |
||||
|
id__in={f.partido_id for f in filiacoes})), |
||||
|
'cargos_mesa': serializar(r, CargoMesa, CargoMesa.objects.all()), |
||||
|
'integrantes_mesa': serializar(r, IntegranteMesa, mesa), |
||||
|
'presencas_sessao': serializar( |
||||
|
r, SessaoPlenariaPresenca, presencas), |
||||
|
'presencas_ordem_dia': serializar( |
||||
|
r, PresencaOrdemDia, presencas_ordem), |
||||
|
'tipos_expediente': serializar( |
||||
|
r, TipoExpediente, TipoExpediente.objects.all()), |
||||
|
'expedientes': serializar(r, ExpedienteSessao, expedientes), |
||||
|
'expedientes_materia': serializar( |
||||
|
r, ExpedienteMateria, expedientes_materia), |
||||
|
'ordens_dia': serializar(r, OrdemDia, ordens), |
||||
|
'materias': serializar(r, MateriaLegislativa, materias), |
||||
|
'autorias': serializar(r, Autoria, autorias), |
||||
|
'autores': serializar(r, Autor, autores.values()), |
||||
|
'tipos_autor': serializar(r, TipoAutor, TipoAutor.objects.all()), |
||||
|
'tipos_resultado_votacao': serializar( |
||||
|
r, TipoResultadoVotacao, TipoResultadoVotacao.objects.all()), |
||||
|
'registros_votacao': serializar( |
||||
|
r, RegistroVotacao, registros_votacao), |
||||
|
'votos': serializar(r, VotoParlamentar, votos), |
||||
|
'registros_leitura': serializar( |
||||
|
r, RegistroLeitura, registros_leitura), |
||||
|
'oradores': serializar(r, Orador, oradores[Orador]), |
||||
|
'oradores_expediente': serializar( |
||||
|
r, OradorExpediente, oradores[OradorExpediente]), |
||||
|
'oradores_ordem_dia': serializar( |
||||
|
r, OradorOrdemDia, oradores[OradorOrdemDia]), |
||||
|
} |
||||
@ -0,0 +1,350 @@ |
|||||
|
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, RegistroLeitura, |
||||
|
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_leitura_expediente(client, sessao): |
||||
|
expediente = baker.make(ExpedienteMateria, sessao_plenaria=sessao) |
||||
|
data = { |
||||
|
'leituras': [{'expediente': expediente.pk, 'observacao': 'Lida'}], |
||||
|
'expedientes_materia': [{'id': expediente.pk, |
||||
|
'resultado': 'Matéria lida', |
||||
|
'votacao_aberta': False}], |
||||
|
} |
||||
|
|
||||
|
r = post(client, sessao, data).json() |
||||
|
assert r['leituras'][0]['status'] == 'criado' |
||||
|
assert r['expedientes_materia'][0]['status'] == 'atualizado' |
||||
|
leitura = RegistroLeitura.objects.get(expediente=expediente) |
||||
|
assert r['leituras'][0]['id'] == leitura.pk |
||||
|
assert leitura.materia_id == expediente.materia_id |
||||
|
assert leitura.ordem is None |
||||
|
assert leitura.observacao == 'Lida' |
||||
|
|
||||
|
# Reenvio não duplica. |
||||
|
r = post(client, sessao, data).json() |
||||
|
assert r['leituras'][0]['status'] == 'inalterado' |
||||
|
assert r['resumo']['erro'] == 0 |
||||
|
assert RegistroLeitura.objects.filter(expediente=expediente).count() == 1 |
||||
|
|
||||
|
data['leituras'][0]['observacao'] = 'Lida em plenário' |
||||
|
r = post(client, sessao, {'leituras': data['leituras']}).json() |
||||
|
assert r['leituras'][0]['status'] == 'atualizado' |
||||
|
leitura.refresh_from_db() |
||||
|
assert leitura.observacao == 'Lida em plenário' |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_lote_retira_leitura_e_remove_duplicadas(client, sessao, ordem): |
||||
|
baker.make(RegistroLeitura, ordem=ordem, materia=ordem.materia, |
||||
|
_quantity=2) |
||||
|
|
||||
|
r = post(client, sessao, {'leituras': [{'ordem': ordem.pk}]}).json() |
||||
|
assert r['leituras'][0]['status'] == 'atualizado' |
||||
|
assert RegistroLeitura.objects.filter(ordem=ordem).count() == 1 |
||||
|
|
||||
|
r = post(client, sessao, {'leituras': [ |
||||
|
{'ordem': ordem.pk, 'lida': False}]}).json() |
||||
|
assert r['leituras'][0]['status'] == 'removido' |
||||
|
assert not RegistroLeitura.objects.exists() |
||||
|
|
||||
|
r = post(client, sessao, {'leituras': [ |
||||
|
{'ordem': ordem.pk, 'lida': False}, |
||||
|
{'ordem': baker.make(OrdemDia).pk}, |
||||
|
]}).json() |
||||
|
assert [i['status'] for i in r['leituras']] == ['inalterado', 'erro'] |
||||
|
|
||||
|
|
||||
|
@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() |
||||
@ -0,0 +1,127 @@ |
|||||
|
from datetime import timedelta |
||||
|
|
||||
|
import pytest |
||||
|
from django.db import connection |
||||
|
from django.test.utils import CaptureQueriesContext |
||||
|
from django.utils import timezone |
||||
|
from model_bakery import baker |
||||
|
from rest_framework.test import APIClient |
||||
|
|
||||
|
from sapl.base.models import Autor |
||||
|
from sapl.materia.models import Autoria |
||||
|
from sapl.parlamentares.models import Mandato, Parlamentar |
||||
|
from sapl.sessao.models import (ExpedienteMateria, IntegranteMesa, OrdemDia, |
||||
|
OradorExpediente, PresencaOrdemDia, |
||||
|
RegistroLeitura, RegistroVotacao, |
||||
|
SessaoPlenaria, SessaoPlenariaPresenca, |
||||
|
VotoParlamentar) |
||||
|
|
||||
|
|
||||
|
def url(sessao): |
||||
|
return '/api/sessao/sessaoplenaria/{}/painel/'.format(sessao.pk) |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def sessao(): |
||||
|
return baker.make(SessaoPlenaria, data_inicio=timezone.localdate(), |
||||
|
iniciada=True) |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def sessao_completa(sessao): |
||||
|
"""Casa com 21 parlamentares e 20 matérias votadas nominalmente.""" |
||||
|
parlamentares = baker.make(Parlamentar, ativo=True, _quantity=21) |
||||
|
for p in parlamentares: |
||||
|
baker.make(Mandato, parlamentar=p, legislatura=sessao.legislatura) |
||||
|
baker.make(SessaoPlenariaPresenca, sessao_plenaria=sessao, |
||||
|
parlamentar=p) |
||||
|
baker.make(PresencaOrdemDia, sessao_plenaria=sessao, parlamentar=p) |
||||
|
baker.make(IntegranteMesa, sessao_plenaria=sessao, |
||||
|
parlamentar=parlamentares[0]) |
||||
|
for _ in range(20): |
||||
|
ordem = baker.make(OrdemDia, sessao_plenaria=sessao) |
||||
|
baker.make(Autoria, materia=ordem.materia, autor=baker.make(Autor)) |
||||
|
registro = baker.make(RegistroVotacao, ordem=ordem, |
||||
|
materia=ordem.materia) |
||||
|
for p in parlamentares: |
||||
|
baker.make(VotoParlamentar, ordem=ordem, votacao=registro, |
||||
|
parlamentar=p, voto='Sim') |
||||
|
for _ in range(5): |
||||
|
expediente = baker.make(ExpedienteMateria, sessao_plenaria=sessao) |
||||
|
baker.make(RegistroLeitura, expediente=expediente, |
||||
|
materia=expediente.materia) |
||||
|
baker.make(OradorExpediente, sessao_plenaria=sessao, |
||||
|
parlamentar=parlamentares[1]) |
||||
|
return sessao |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_painel_publico_traz_toda_a_sessao(sessao_completa): |
||||
|
response = APIClient().get(url(sessao_completa)) |
||||
|
assert response.status_code == 200 |
||||
|
r = response.json() |
||||
|
|
||||
|
assert r['sessao_plenaria']['id'] == sessao_completa.pk |
||||
|
assert r['tipo_sessao']['id'] == sessao_completa.tipo_id |
||||
|
assert len(r['parlamentares']) == 21 |
||||
|
assert len(r['mandatos']) == 21 |
||||
|
assert len(r['presencas_sessao']) == 21 |
||||
|
assert len(r['presencas_ordem_dia']) == 21 |
||||
|
assert len(r['integrantes_mesa']) == 1 |
||||
|
assert len(r['ordens_dia']) == 20 |
||||
|
assert len(r['expedientes_materia']) == 5 |
||||
|
assert len(r['materias']) == 25 |
||||
|
assert len(r['autorias']) == 20 |
||||
|
assert len(r['autores']) == 20 |
||||
|
assert len(r['registros_votacao']) == 20 |
||||
|
assert len(r['votos']) == 420 |
||||
|
assert len(r['registros_leitura']) == 5 |
||||
|
assert len(r['oradores_expediente']) == 1 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_painel_mesmo_formato_da_listagem(sessao_completa): |
||||
|
c = APIClient() |
||||
|
painel = c.get(url(sessao_completa)).json() |
||||
|
lista = c.get('/api/sessao/ordemdia/', { |
||||
|
'sessao_plenaria': sessao_completa.pk, 'page_size': 100}).json() |
||||
|
assert sorted(painel['ordens_dia'], key=lambda o: o['id']) == \ |
||||
|
sorted(lista['results'], key=lambda o: o['id']) |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_painel_nao_mistura_outras_sessoes(sessao_completa): |
||||
|
outra = baker.make(SessaoPlenaria, data_inicio=timezone.localdate()) |
||||
|
baker.make(OrdemDia, sessao_plenaria=outra) |
||||
|
baker.make(SessaoPlenariaPresenca, sessao_plenaria=outra) |
||||
|
|
||||
|
r = APIClient().get(url(outra)).json() |
||||
|
assert len(r['ordens_dia']) == 1 |
||||
|
assert len(r['presencas_sessao']) == 1 |
||||
|
assert r['votos'] == [] |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_painel_numero_de_consultas_limitado(sessao_completa): |
||||
|
with CaptureQueriesContext(connection) as consultas: |
||||
|
assert APIClient().get(url(sessao_completa)).status_code == 200 |
||||
|
# ~620 registros serializados: o número de consultas não pode crescer |
||||
|
# com o volume (sem N+1 em __str__, metadata e many-to-many). |
||||
|
assert len(consultas) < 100 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_painel_sessao_futura_oculta_ao_publico(admin_user): |
||||
|
futura = baker.make(SessaoPlenaria, iniciada=False, publicar_pauta=False, |
||||
|
data_inicio=timezone.localdate() + timedelta(days=3)) |
||||
|
assert APIClient().get(url(futura)).status_code == 404 |
||||
|
|
||||
|
c = APIClient() |
||||
|
c.force_authenticate(admin_user) |
||||
|
assert c.get(url(futura)).status_code == 200 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_painel_sessao_inexistente(): |
||||
|
response = APIClient().get('/api/sessao/sessaoplenaria/999999/painel/') |
||||
|
assert response.status_code == 404 |
||||
Loading…
Reference in new issue