Browse Source

Inclui registros de leitura no lote da sessão plenária

pull/3866/head
Samuel Zwinglio 2 weeks ago
parent
commit
496dd56b54
  1. 49
      docs/api-sessao-lote.md
  2. 76
      sapl/api/sessao_lote.py
  3. 56
      sapl/sessao/tests/test_sessao_lote.py

49
docs/api-sessao-lote.md

@ -14,12 +14,28 @@ sistema de votação etc.) precisa registrar numa sessão plenária:
| `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}/` |
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.
## 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.
@ -43,7 +59,7 @@ Os endpoints individuais continuam funcionando sem alteração.
- **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.
votações, leituras, matérias da ordem do dia, matérias do expediente.
- Máximo de 500 itens por bloco.
## Payload
@ -75,6 +91,10 @@ Os endpoints individuais continuam funcionando sem alteração.
"numero_abstencoes": 0
}
],
"leituras": [
{"expediente": 5, "observacao": ""},
{"expediente": 6, "lida": false}
],
"ordens_dia": [
{"id": 10, "resultado": "Aprovado", "votacao_aberta": false}
],
@ -118,6 +138,21 @@ 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
@ -144,13 +179,17 @@ Confira `resumo.erro`.
{"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": 2, "atualizado": 2, "removido": 0,
"resumo": {"criado": 3, "atualizado": 2, "removido": 1,
"inalterado": 2, "erro": 1}
}
```

76
sapl/api/sessao_lote.py

@ -2,8 +2,9 @@
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.
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 é
@ -24,8 +25,9 @@ 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)
PresencaOrdemDia, RegistroLeitura,
RegistroVotacao, SessaoPlenariaPresenca,
VotoParlamentar)
from sapl.utils import get_client_ip
@ -49,6 +51,7 @@ BLOCOS = (
'presencas_sessao',
'presencas_ordem_dia',
'votacoes',
'leituras',
'ordens_dia',
'expedientes_materia',
)
@ -115,9 +118,9 @@ class SessaoLote:
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.
# 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(
@ -125,6 +128,7 @@ class SessaoLote:
('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)),
@ -213,9 +217,10 @@ class SessaoLote:
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)."""
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:
@ -269,7 +274,7 @@ class SessaoLote:
"votos" (simbólica/secreta), informe numero_votos_sim,
numero_votos_nao e numero_abstencoes.
"""
campo, alvo = self.alvo_votacao(item)
campo, alvo = self.alvo(item)
r = {campo: alvo.pk}
votos = self.votos_do_item(item) if 'votos' in item else None
@ -356,6 +361,55 @@ class SessaoLote:
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')

56
sapl/sessao/tests/test_sessao_lote.py

@ -6,7 +6,8 @@ from rest_framework.test import APIClient
from sapl.parlamentares.models import Parlamentar
from sapl.sessao.models import (ExpedienteMateria, OrdemDia,
PresencaOrdemDia, RegistroVotacao,
PresencaOrdemDia, RegistroLeitura,
RegistroVotacao,
SessaoPlenaria, SessaoPlenariaPresenca,
TipoResultadoVotacao, VotoParlamentar)
@ -235,6 +236,59 @@ def test_lote_votacao_alvo_invalido(client, sessao, ordem, resultado):
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):

Loading…
Cancel
Save