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_sessao` | `SessaoPlenariaPresenca` | `POST`/`DELETE /api/sessao/sessaoplenariapresenca/` |
| `presencas_ordem_dia` | `PresencaOrdemDia` | `POST`/`DELETE /api/sessao/presencaordemdia/` | | `presencas_ordem_dia` | `PresencaOrdemDia` | `POST`/`DELETE /api/sessao/presencaordemdia/` |
| `votacoes` | `RegistroVotacao` + `VotoParlamentar` | `POST /api/sessao/registrovotacao/` + um `POST /api/sessao/votoparlamentar/` por voto | | `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}/` | | `ordens_dia` | `OrdemDia` | `PATCH /api/sessao/ordemdia/{id}/` |
| `expedientes_materia` | `ExpedienteMateria` | `PATCH /api/sessao/expedientemateria/{id}/` | | `expedientes_materia` | `ExpedienteMateria` | `PATCH /api/sessao/expedientemateria/{id}/` |
Numa Casa com 21 parlamentares, uma sessão com 20 matérias votadas ## Requisições por sessão
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. 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. 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 - **Escopo da sessão.** Matérias da ordem do dia e do expediente precisam
pertencer à sessão da URL. pertencer à sessão da URL.
- **Ordem de processamento:** presenças da sessão, presenças da ordem do dia, - **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. - Máximo de 500 itens por bloco.
## Payload ## Payload
@ -75,6 +91,10 @@ Os endpoints individuais continuam funcionando sem alteração.
"numero_abstencoes": 0 "numero_abstencoes": 0
} }
], ],
"leituras": [
{"expediente": 5, "observacao": ""},
{"expediente": 6, "lida": false}
],
"ordens_dia": [ "ordens_dia": [
{"id": 10, "resultado": "Aprovado", "votacao_aberta": false} {"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 votação etc., use os blocos `ordens_dia`/`expedientes_materia`, que são
processados depois das votações. 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 ### 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 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", {"expediente": 4, "status": "erro",
"erros": {"tipo_resultado_votacao": ["Este campo não pode ser nulo."]}} "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": [ "ordens_dia": [
{"id": 10, "status": "atualizado"} {"id": 10, "status": "atualizado"}
], ],
"expedientes_materia": [ "expedientes_materia": [
{"id": 4, "status": "atualizado"} {"id": 4, "status": "atualizado"}
], ],
"resumo": {"criado": 2, "atualizado": 2, "removido": 0, "resumo": {"criado": 3, "atualizado": 2, "removido": 1,
"inalterado": 2, "erro": 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. 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 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 dia, votações (registro + votos nominais), registros de leitura e
ordem do dia/expediente. Ver docs/api-sessao-lote.md. atualizações de matérias da ordem do dia/expediente.
Ver docs/api-sessao-lote.md.
Regras: Regras:
- qualquer combinação de blocos é aceita; o que não for enviado não é - 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 drfautoapi.drfautoapi import ApiViewSetConstrutor
from sapl.parlamentares.models import Parlamentar from sapl.parlamentares.models import Parlamentar
from sapl.sessao.models import (ExpedienteMateria, OrdemDia, from sapl.sessao.models import (ExpedienteMateria, OrdemDia,
PresencaOrdemDia, RegistroVotacao, PresencaOrdemDia, RegistroLeitura,
SessaoPlenariaPresenca, VotoParlamentar) RegistroVotacao, SessaoPlenariaPresenca,
VotoParlamentar)
from sapl.utils import get_client_ip from sapl.utils import get_client_ip
@ -49,6 +51,7 @@ BLOCOS = (
'presencas_sessao', 'presencas_sessao',
'presencas_ordem_dia', 'presencas_ordem_dia',
'votacoes', 'votacoes',
'leituras',
'ordens_dia', 'ordens_dia',
'expedientes_materia', 'expedientes_materia',
) )
@ -115,9 +118,9 @@ class SessaoLote:
def processar(self, data): def processar(self, data):
self.validar_payload(data) self.validar_payload(data)
# Presenças antes das votações e votações antes das atualizações de # Presenças antes das votações e leituras, e estas antes das
# matéria, na mesma ordem em que as integrações fariam com os # atualizações de matéria, na mesma ordem em que as integrações
# endpoints individuais. # fariam com os endpoints individuais.
relatorio = {'sessao_plenaria': self.sessao.pk} relatorio = {'sessao_plenaria': self.sessao.pk}
processadores = ( processadores = (
('presencas_sessao', lambda i: self.presenca( ('presencas_sessao', lambda i: self.presenca(
@ -125,6 +128,7 @@ class SessaoLote:
('presencas_ordem_dia', lambda i: self.presenca( ('presencas_ordem_dia', lambda i: self.presenca(
PresencaOrdemDia, i)), PresencaOrdemDia, i)),
('votacoes', self.votacao), ('votacoes', self.votacao),
('leituras', self.leitura),
('ordens_dia', lambda i: self.materia(OrdemDia, i)), ('ordens_dia', lambda i: self.materia(OrdemDia, i)),
('expedientes_materia', lambda i: self.materia( ('expedientes_materia', lambda i: self.materia(
ExpedienteMateria, i)), ExpedienteMateria, i)),
@ -213,9 +217,10 @@ class SessaoLote:
obj = serializer.save() obj = serializer.save()
return dict(r, status=CRIADO, id=obj.pk) return dict(r, status=CRIADO, id=obj.pk)
def alvo_votacao(self, item): def alvo(self, item):
"""Votação é de uma matéria da ordem do dia ou do expediente, """Votação e leitura são de uma matéria da ordem do dia ou do
nunca das duas (mesma regra de RegistroVotacao.clean).""" expediente, nunca das duas (mesma regra de RegistroVotacao.clean e
RegistroLeitura.clean)."""
tem_ordem = item.get('ordem') is not None tem_ordem = item.get('ordem') is not None
tem_expediente = item.get('expediente') is not None tem_expediente = item.get('expediente') is not None
if tem_ordem == tem_expediente: if tem_ordem == tem_expediente:
@ -269,7 +274,7 @@ class SessaoLote:
"votos" (simbólica/secreta), informe numero_votos_sim, "votos" (simbólica/secreta), informe numero_votos_sim,
numero_votos_nao e numero_abstencoes. numero_votos_nao e numero_abstencoes.
""" """
campo, alvo = self.alvo_votacao(item) campo, alvo = self.alvo(item)
r = {campo: alvo.pk} r = {campo: alvo.pk}
votos = self.votos_do_item(item) if 'votos' in item else None votos = self.votos_do_item(item) if 'votos' in item else None
@ -356,6 +361,55 @@ class SessaoLote:
alterado = True alterado = True
return alterado 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): def materia(self, model, item):
"""{"id": <id>, <campos do PATCH do model>}""" """{"id": <id>, <campos do PATCH do model>}"""
pk = item.get('id') 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.parlamentares.models import Parlamentar
from sapl.sessao.models import (ExpedienteMateria, OrdemDia, from sapl.sessao.models import (ExpedienteMateria, OrdemDia,
PresencaOrdemDia, RegistroVotacao, PresencaOrdemDia, RegistroLeitura,
RegistroVotacao,
SessaoPlenaria, SessaoPlenariaPresenca, SessaoPlenaria, SessaoPlenariaPresenca,
TipoResultadoVotacao, VotoParlamentar) TipoResultadoVotacao, VotoParlamentar)
@ -235,6 +236,59 @@ def test_lote_votacao_alvo_invalido(client, sessao, ordem, resultado):
assert not RegistroVotacao.objects.exists() 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) @pytest.mark.django_db(transaction=False)
def test_lote_atualiza_ordem_dia(client, sessao, ordem, resultado, def test_lote_atualiza_ordem_dia(client, sessao, ordem, resultado,
parlamentares): parlamentares):

Loading…
Cancel
Save