mirror of https://github.com/interlegis/sapl.git
Browse Source
GET /api/sessao/sessaoplenaria/{id}/painel/ devolve numa única requisição
sessão, mesa, parlamentares, presenças, expediente, ordem do dia, matérias,
autorias, votações, votos, leituras e oradores, no mesmo formato dos
endpoints de listagem.
Segue as permissões de leitura de cada model e a visibilidade pública das
sessões. O número de consultas ao banco não cresce com o tamanho da sessão.
pull/3866/head
5 changed files with 427 additions and 2 deletions
@ -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,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,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