diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..b3a1102eb --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,145 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +SAPL (Sistema de Apoio ao Processo Legislativo) is a Django-based legislative management system used by Brazilian municipal and state legislative houses. It manages bills, parliamentary sessions, committees, norms, protocols, and related legislative workflows. + +## Commands + +### Development + +```bash +# Run dev server +python manage.py runserver + +# Docker (dev, without bundled DB) +docker-compose -f docker/docker-compose-dev.yml up + +# Docker (dev, with PostgreSQL container) +docker-compose -f docker/docker-compose-dev-db.yml up +``` + +### Database Setup (local PostgreSQL) + +```bash +sudo -u postgres psql -c "CREATE ROLE sapl LOGIN ENCRYPTED PASSWORD 'sapl' NOSUPERUSER INHERIT CREATEDB NOCREATEROLE NOREPLICATION;" +sudo -u postgres psql -c "CREATE DATABASE sapl WITH OWNER=sapl ENCODING='UTF8' LC_COLLATE='pt_BR.UTF-8' LC_CTYPE='pt_BR.UTF-8' CONNECTION LIMIT=-1 TEMPLATE template0;" +python manage.py migrate +``` + +### Testing + +```bash +# All tests (reuses DB by default for speed) +pytest + +# Single test file or test function +pytest sapl/materia/tests/test_materia.py +pytest sapl/materia/tests/test_materia.py::test_function_name + +# Force DB recreation +pytest --create-db + +# With coverage +pytest --cov=sapl +``` + +Tests require `DJANGO_SETTINGS_MODULE=sapl.settings` (set in `pytest.ini`). All tests must be marked with `@pytest.mark.django_db`. The `conftest.py` root fixture provides an `app` fixture (WebTest `DjangoTestApp`). + +### Linting / Formatting + +```bash +flake8 . +isort . +autopep8 --in-place +``` + +### Restore Database from Backup + +```bash +./scripts/restore_db.sh -f /path/to/dump +./scripts/restore_db.sh -f /path/to/dump -p 5433 # Docker port +``` + +## Architecture + +### Django Apps + +Apps are under `sapl/` and follow domain boundaries: + +| App | Domain | +|-----|--------| +| `base` | `CasaLegislativa` (legislative house config), `AppConfig`, `Autor` (authorship) | +| `parliamentary` | `Parlamentar`, `Legislatura`, `SessaoLegislativa`, `Coligacao` | +| `materia` | Bills (`MateriaLegislativa`), types, tracking, annexes | +| `norma` | Laws/norms (`NormaJuridica`) and hierarchies | +| `sessao` | Plenary sessions, agenda, attendance, voting | +| `comissoes` | Committees (`Comissao`) and meetings (`Reuniao`) | +| `protocoloadm` | Administrative protocols and document intake | +| `compilacao` | Structured/articulated texts (LexML-like tree structure) | +| `lexml` | LexML XML standard integration | +| `audiencia` | Public hearings | +| `painel` | Real-time session display panel | +| `relatorios` | PDF report generation | +| `api` | REST API entry point (auto-generated ViewSets) | +| `crud` | Generic CRUD base views | +| `rules` | Business rules and permission definitions | + +### REST API + +The API uses a custom `drfautoapi` package (`drfautoapi/drfautoapi.py`) that auto-generates DRF ViewSets, Serializers, and FilterSets from Django models. Authentication is Token + Session. Permissions use a custom `SaplModelPermissions` class that maps HTTP methods to Django model permissions. + +OpenAPI 3.0 docs are generated by drf-spectacular. + +### Caching + +- **Default:** File-based (`/var/tmp/django_cache`) +- **Production:** Redis via django-redis; configured at startup by `configure_redis_cache()` in `sapl/settings.py` +- **Cache key prefix:** `cache:{POD_NAMESPACE}:` (namespace-isolated for multi-tenant k8s) +- **Rate limiter state** is shared via Redis keys + +### Feature Flags + +django-waffle is used for feature flags. Switches (global on/off) can be toggled via: + +```bash +python manage.py waffle_switch on|off +``` + +### Key Environment Variables + +| Variable | Purpose | +|----------|---------| +| `DATABASE_URL` | PostgreSQL connection string | +| `SECRET_KEY` | Django secret key | +| `DEBUG` | Debug mode | +| `REDIS_URL` | Redis host:port | +| `CACHE_BACKEND` | `file` or `redis` | +| `POD_NAMESPACE` | K8s namespace (used in cache key prefix) | +| `USE_SOLR` | Enable Haystack/Solr full-text search | +| `SOLR_URL` / `SOLR_COLLECTION` | Solr connection | + +### Docker Build + +The production build requires a MaxMind GeoLite2-ASN license key (for nginx ASN-based bot blocking): + +```bash +docker build --secret id=maxmind_key,src=.env -f docker/Dockerfile -t sapl:local . +``` + +Optional build args: `WITH_NGINX`, `WITH_GRAPHVIZ`, `WITH_POPPLER`, `WITH_PSQL_CLIENT`. + +### Key File Locations + +| File | Purpose | +|------|---------| +| `sapl/settings.py` | All Django settings, including cache/rate-limit setup | +| `pytest.ini` | Test configuration (DJANGO_SETTINGS_MODULE, addopts) | +| `conftest.py` | Root pytest fixtures | +| `drfautoapi/drfautoapi.py` | Auto-API generation logic | +| `docker/startup_scripts/start.sh` | Container entrypoint (migrations, waffle, gunicorn) | +| `requirements/requirements.txt` | Production deps | +| `requirements/test-requirements.txt` | Test deps | +| `requirements/dev-requirements.txt` | Dev/lint deps | diff --git a/sapl/materia/migrations/0088_fix_view_materiaemtramitacao.py b/sapl/materia/migrations/0088_fix_view_materiaemtramitacao.py new file mode 100644 index 000000000..1f3ca52d8 --- /dev/null +++ b/sapl/materia/migrations/0088_fix_view_materiaemtramitacao.py @@ -0,0 +1,62 @@ +from django.db import migrations, models + +_OLD_VIEW = """ + create or replace view materia_materiaemtramitacao as + select m.id as id, + m.id as materia_id, + t.id as tramitacao_id, + t.unidade_tramitacao_destino_id as unidade_tramitacao_atual_id + from materia_materialegislativa m + inner join materia_tramitacao t on (m.id = t.materia_id) + where t.id = (select max(id) from materia_tramitacao where materia_id = m.id) + order by m.id DESC +""" + +_NEW_VIEW = """ + create or replace view materia_materiaemtramitacao as + select distinct on (m.id) + m.id as id, + m.id as materia_id, + t.id as tramitacao_id, + t.unidade_tramitacao_destino_id as unidade_tramitacao_atual_id + from materia_materialegislativa m + inner join materia_tramitacao t on t.materia_id = m.id + order by m.id desc, t.id desc +""" + + +class Migration(migrations.Migration): + # CREATE INDEX CONCURRENTLY cannot run inside a transaction. + atomic = False + + dependencies = [ + ('materia', '0087_update_viewdb_materiaemtramitacao'), + ] + + operations = [ + migrations.RunSQL(sql=_NEW_VIEW, reverse_sql=_OLD_VIEW), + migrations.SeparateDatabaseAndState( + database_operations=[ + migrations.RunSQL( + sql=""" + CREATE INDEX CONCURRENTLY IF NOT EXISTS + tram_materia_id_desc + ON materia_tramitacao (materia_id, id DESC) + """, + reverse_sql=""" + DROP INDEX CONCURRENTLY IF EXISTS + tram_materia_id_desc + """, + ), + ], + state_operations=[ + migrations.AddIndex( + model_name='tramitacao', + index=models.Index( + fields=['materia', '-id'], + name='tram_materia_id_desc', + ), + ), + ], + ), + ] diff --git a/sapl/materia/models.py b/sapl/materia/models.py index bdeb402e3..fb0df6ee5 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -1350,6 +1350,9 @@ class Tramitacao(models.Model): verbose_name = _('Tramitação') verbose_name_plural = _('Tramitações') ordering = ('-data_tramitacao', '-id') + indexes = [ + models.Index(fields=['materia', '-id'], name='tram_materia_id_desc'), + ] def __str__(self): return _('%(materia)s | %(status)s | %(data)s') % { diff --git a/sapl/painel/urls.py b/sapl/painel/urls.py index 0795d0a35..b645c7bbe 100644 --- a/sapl/painel/urls.py +++ b/sapl/painel/urls.py @@ -1,8 +1,7 @@ from django.conf.urls import url from .apps import AppConfig -from .views import (cronometro_painel, get_dados_painel, painel_mensagem_view, - painel_parlamentar_view, painel_view, painel_votacao_view, +from .views import (cronometro_painel, get_dados_painel, painel_view, switch_painel, verifica_painel, votante_view) app_name = AppConfig.name @@ -11,12 +10,8 @@ urlpatterns = [ url(r'^painel-principal/(?P\d+)$', painel_view, name="painel_principal"), url(r'^painel/(?P\d+)/dados$', get_dados_painel, name='dados_painel'), - url(r'^painel/mensagem$', painel_mensagem_view, name="painel_mensagem"), - url(r'^painel/parlamentar$', painel_parlamentar_view, - name='painel_parlamentar'), url(r'^painel/switch-painel$', switch_painel, name="switch_painel"), - url(r'^painel/votacao$', painel_votacao_view, name='painel_votacao'), url(r'^painel/verifica-painel$', verifica_painel, name="verifica_painel"), url(r'^painel/cronometro$', cronometro_painel, name='cronometro_painel'), diff --git a/sapl/painel/views.py b/sapl/painel/views.py index bfe9df2c1..c0356edcb 100644 --- a/sapl/painel/views.py +++ b/sapl/painel/views.py @@ -327,21 +327,6 @@ def verifica_painel(request): return resposta -@user_passes_test(check_permission) -def painel_mensagem_view(request): - return render(request, 'painel/mensagem.html') - - -@user_passes_test(check_permission) -def painel_parlamentar_view(request): - return render(request, 'painel/parlamentares.html') - - -@user_passes_test(check_permission) -def painel_votacao_view(request): - return render(request, 'painel/votacao.html') - - @user_passes_test(check_permission) def cronometro_painel(request): request.session[request.GET['tipo']] = request.GET['action'] diff --git a/sapl/relatorios/forms.py b/sapl/relatorios/forms.py index f659c254c..726501524 100644 --- a/sapl/relatorios/forms.py +++ b/sapl/relatorios/forms.py @@ -543,9 +543,7 @@ class RelatorioMateriasTramitacaoFilterSet(django_filters.FilterSet): @property def qs(self): parent = super(RelatorioMateriasTramitacaoFilterSet, self).qs - return parent.distinct().order_by( - '-materia__ano', 'materia__tipo', '-materia__numero' - ) + return parent.order_by('-materia__ano', 'materia__tipo', '-materia__numero') class Meta: model = MateriaEmTramitacao diff --git a/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py b/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py index 5d3f259b0..78b571bde 100644 --- a/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py +++ b/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py @@ -198,6 +198,32 @@ def presenca(lst_presenca_sessao, lst_ausencia_sessao): return tmp +def correspondencias(lst_correspondencias): + tmp = '' + if lst_correspondencias: + tmp += '\t\tCorrespondências\n' + tmp += '\t\t\n' + tmp += '\t\t\t
\n' + tmp += '\t\t
\n' + tmp += '>\n' + tmp += 'TipoDocumentoInteressadoAssunto\n' + for c in lst_correspondencias: + tmp += '' + tmp += '' + str(c['tipo']) + '\n' + tmp += '' + str(c['epigrafe']) + \ + ' - ' + str(c['data']) + '\n' + tmp += '' + \ + str(c['interessado'] or '') + '\n' + tmp += '' + \ + str(c['assunto'] or '') + '\n' + tmp += '\n' + tmp += '\n' + tmp += '\t\t\n' + tmp += '\t\t\t
\n' + tmp += '\t\t
\n' + return tmp + + def expedientes(lst_expedientes): tmp = '' if lst_expedientes: @@ -415,7 +441,7 @@ def consideracoes(lst_consideracoes): return tmp -def principal(rodape_dic, imagem, inf_basicas_dic, cont_mult_dic, lst_mesa, lst_presenca_sessao, lst_ausencia_sessao, lst_expedientes, lst_expediente_materia, lst_expediente_materia_vot_nom, lst_oradores_expediente, lst_presenca_ordem_dia, lst_votacao, lst_votacao_vot_nom, lst_oradores_ordemdia, lst_oradores, lst_ocorrencias, lst_consideracoes): +def principal(rodape_dic, imagem, inf_basicas_dic, cont_mult_dic, lst_mesa, lst_presenca_sessao, lst_ausencia_sessao, lst_correspondencias, lst_expedientes, lst_expediente_materia, lst_expediente_materia_vot_nom, lst_oradores_expediente, lst_presenca_ordem_dia, lst_votacao, lst_votacao_vot_nom, lst_oradores_ordemdia, lst_oradores, lst_ocorrencias, lst_consideracoes): """ """ arquivoPdf = str(int(time.time() * 100)) + ".pdf" @@ -440,6 +466,7 @@ def principal(rodape_dic, imagem, inf_basicas_dic, cont_mult_dic, lst_mesa, lst_ ordenacao = ResumoOrdenacao.objects.first() dict_ord_template = { 'cont_mult': multimidia(cont_mult_dic), + 'correspondencia': correspondencias(lst_correspondencias), 'exp': expedientes(lst_expedientes), 'id_basica': inf_basicas(inf_basicas_dic), 'lista_p': presenca(lst_presenca_sessao, lst_ausencia_sessao), @@ -473,6 +500,7 @@ def principal(rodape_dic, imagem, inf_basicas_dic, cont_mult_dic, lst_mesa, lst_ tmp += dict_ord_template[ordenacao.decimo_terceiro] tmp += dict_ord_template[ordenacao.decimo_quarto] tmp += dict_ord_template[ordenacao.decimo_quinto] + tmp += dict_ord_template[ordenacao.decimo_sexto] except KeyError as e: logger.error("KeyError: " + str(e) + ". Erro ao tentar utilizar " "configuração de ordenação. Utilizando ordenação padrão.") @@ -480,6 +508,7 @@ def principal(rodape_dic, imagem, inf_basicas_dic, cont_mult_dic, lst_mesa, lst_ tmp += multimidia(cont_mult_dic) tmp += mesa(lst_mesa) tmp += presenca(lst_presenca_sessao, lst_ausencia_sessao) + tmp += correspondencias(lst_correspondencias) tmp += expedientes(lst_expedientes) tmp += expediente_materia(lst_expediente_materia) tmp += expediente_materia_vot_nom(lst_expediente_materia_vot_nom) @@ -497,6 +526,7 @@ def principal(rodape_dic, imagem, inf_basicas_dic, cont_mult_dic, lst_mesa, lst_ tmp += multimidia(cont_mult_dic) tmp += mesa(lst_mesa) tmp += presenca(lst_presenca_sessao, lst_ausencia_sessao) + tmp += correspondencias(lst_correspondencias) tmp += expedientes(lst_expedientes) tmp += expediente_materia(lst_expediente_materia) tmp += expediente_materia_vot_nom(lst_expediente_materia_vot_nom) diff --git a/sapl/relatorios/views.py b/sapl/relatorios/views.py index 8b89342fd..6a4e76795 100755 --- a/sapl/relatorios/views.py +++ b/sapl/relatorios/views.py @@ -8,6 +8,7 @@ import unidecode from django.core.exceptions import ObjectDoesNotExist from django.db.models import Count, Q, F from django.http import Http404, HttpResponse +from django.shortcuts import get_object_or_404 from django.template.loader import render_to_string from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -23,7 +24,7 @@ from sapl.materia.models import (Autoria, MateriaLegislativa, Numeracao, TipoMateriaLegislativa, MateriaEmTramitacao, DocumentoAcessorio, TipoDocumento, StatusTramitacao) from sapl.norma.models import TipoNormaJuridica, NormaJuridica -from sapl.parlamentares.models import Filiacao, Parlamentar, SessaoLegislativa, Legislatura +from sapl.parlamentares.models import Parlamentar, SessaoLegislativa, Legislatura from sapl.protocoloadm.models import (DocumentoAdministrativo, Protocolo, TramitacaoAdministrativo, StatusTramitacaoAdministrativo, TipoDocumentoAdministrativo) @@ -39,7 +40,8 @@ from sapl.sessao.models import (ExpedienteMateria, ExpedienteSessao, OrdemDia, PresencaOrdemDia, SessaoPlenaria, SessaoPlenariaPresenca, OcorrenciaSessao, RegistroVotacao, VotoParlamentar, OradorOrdemDia, - ConsideracoesFinais, ResumoOrdenacao, TipoSessaoPlenaria) + ConsideracoesFinais, ResumoOrdenacao, TipoSessaoPlenaria, + restringe_sessoes_visiveis) from sapl.sessao.views import (get_identificacao_basica, get_mesa_diretora, get_presenca_sessao, get_expedientes, get_materias_expediente, get_oradores_expediente, @@ -47,7 +49,8 @@ from sapl.sessao.views import (get_identificacao_basica, get_mesa_diretora, get_oradores_ordemdia, get_oradores_explicacoes_pessoais, get_consideracoes_finais, get_ocorrencias_da_sessao, get_assinaturas, - get_correspondencias) + get_correspondencias, get_conteudo_multimidia, + get_votos_nominais) from sapl.settings import MEDIA_URL, RATE_LIMITER_RATE from sapl.settings import STATIC_ROOT from sapl.utils import LISTA_DE_UFS, TrocaTag, filiacao_data, create_barcode, show_results_filter_set, \ @@ -92,7 +95,8 @@ def get_imagem(casa): def get_rodape(casa): if len(casa.cep) == 8: - cep = casa.cep[:4] + "-" + casa.cep[5:] + # CEP tem 8 dígitos e se formata como XXXXX-XXX. + cep = casa.cep[:5] + "-" + casa.cep[5:] else: cep = "" @@ -542,6 +546,8 @@ def get_sessao_plenaria(sessao, casa, user): if sessao.tipo.nome == 'Solene': inf_basicas_dic["tema_solene"] = sessao.tema_solene + data_sessao = sessao.data_fim if sessao.data_fim else sessao.data_inicio + # Conteudo multimidia cont_mult_dic = { "multimidia_audio": str(sessao.url_audio) if sessao.url_audio else "Indisponível", @@ -553,9 +559,11 @@ def get_sessao_plenaria(sessao, casa, user): for composicao in IntegranteMesa.objects.select_related('parlamentar', 'cargo') \ .filter(sessao_plenaria=sessao) \ .order_by('cargo_id'): - partido_sigla = Filiacao.objects.filter( - parlamentar=composicao.parlamentar).first() - sigla = '' if not partido_sigla else partido_sigla.partido.sigla + # Antes usava Filiacao...first() (filiação mais recente), enquanto a + # tela usava .last() (a mais antiga) — e as listas de presença deste + # mesmo PDF já usavam filiacao_data. Unifica no critério correto: o + # partido vigente na data da sessão. + sigla = filiacao_data(composicao.parlamentar, data_sessao) lst_mesa.append({ 'nom_parlamentar': composicao.parlamentar.nome_parlamentar, 'sgl_partido': sigla, @@ -652,7 +660,7 @@ def get_sessao_plenaria(sessao, casa, user): "num_ordem": expediente_materia.numero_ordem, "id_materia": f"{materia.tipo.sigla} {materia.tipo.descricao} {str(materia.numero)}/{str(materia.ano)}", "des_numeracao": ' ', - "des_turno": get_turno(materia)[0], + "des_turno": get_turno(materia, data_sessao)[0], "situacao": materia_em_tramitacao.tramitacao.status if materia_em_tramitacao else _("Não informada"), "txt_ementa": str(materia.ementa), "materia_observacao": materia.observacao, @@ -732,13 +740,11 @@ def get_sessao_plenaria(sessao, casa, user): for orador_expediente in OradorExpediente.objects.filter(sessao_plenaria=sessao).order_by('numero_ordem'): parlamentar = Parlamentar.objects.get( id=orador_expediente.parlamentar.id) - partido_sigla = Filiacao.objects.filter( - parlamentar=parlamentar).first() lst_oradores_expediente.append({ "num_ordem": orador_expediente.numero_ordem, "nom_parlamentar": parlamentar.nome_parlamentar, "observacao": orador_expediente.observacao, - "sgl_partido": "" if not partido_sigla else partido_sigla.partido.sigla + "sgl_partido": filiacao_data(parlamentar, data_sessao) }) # Lista presença na ordem do dia @@ -774,7 +780,7 @@ def get_sessao_plenaria(sessao, casa, user): materia_em_tramitacao = materia.materiaemtramitacao_set.first() dic_votacao.update({ - "des_turno": get_turno(materia)[0], + "des_turno": get_turno(materia, data_sessao)[0], # https://github.com/interlegis/sapl/issues/1009 "txt_ementa": html.unescape(materia.ementa), "materia_observacao": materia.observacao, @@ -849,27 +855,23 @@ def get_sessao_plenaria(sessao, casa, user): for orador_ordemdia in oradores_ordem_dia: parlamentar_orador = Parlamentar.objects.get( id=orador_ordemdia.parlamentar.id) - sigla_partido = Filiacao.objects.filter( - parlamentar=parlamentar_orador).first() lst_oradores_ordemdia.append({ "num_ordem": orador_ordemdia.numero_ordem, "nome_parlamentar": parlamentar_orador.nome_parlamentar, "observacao": orador_ordemdia.observacao, - "sigla": "" if not sigla_partido else sigla_partido.partido.sigla + "sigla": filiacao_data(parlamentar_orador, data_sessao) }) # Lista dos oradores nas Explicações Pessoais lst_oradores = [] for orador in Orador.objects.select_related('parlamentar').filter(sessao_plenaria=sessao).order_by('numero_ordem'): parlamentar = orador.parlamentar - partido_sigla = orador.parlamentar.filiacao_set.select_related( - 'partido', 'parlamentar').first() lst_oradores.append({ "num_ordem": orador.numero_ordem, "nom_parlamentar": parlamentar.nome_parlamentar, "observacao": orador.observacao, - "sgl_partido": "" if not partido_sigla else partido_sigla.partido.sigla + "sgl_partido": filiacao_data(parlamentar, data_sessao) }) # Ocorrências da Sessão @@ -930,11 +932,20 @@ def get_sessao_plenaria(sessao, casa, user): lst_consideracoes) -def get_turno(materia): +def get_turno(materia, data_sessao=None): + """Turno e situação de uma matéria. + + `data_sessao` corta as tramitações posteriores à sessão, como já fazem + `get_materias_expediente` / `get_materias_ordem_do_dia` em + sapl.sessao.views. Sem esse corte, o PDF do resumo exibia um turno + registrado depois da sessão que ele documenta. + """ descricao_turno = '' descricao_tramitacao = '' tramitacoes = materia.tramitacao_set.order_by( '-data_tramitacao', '-id').all() + if data_sessao: + tramitacoes = tramitacoes.filter(data_tramitacao__lte=data_sessao) tramitacoes_turno = tramitacoes.exclude(turno="") if tramitacoes: @@ -969,7 +980,8 @@ def relatorio_sessao_plenaria(request, pk): try: logger.debug("user=" + username + ". Tentando obter SessaoPlenaria com id={}.".format(pk)) - sessao = SessaoPlenaria.objects.get(id=pk) + sessao = restringe_sessoes_visiveis( + SessaoPlenaria.objects.all(), request.user).get(id=pk) except ObjectDoesNotExist as e: logger.error("user=" + username + ". Essa SessaoPlenaria não existe (pk={}). ".format(pk) + str(e)) @@ -1143,8 +1155,29 @@ def relatorio_etiqueta_protocolo(request, nro, ano): def get_etiqueta_protocolos(prots): + prot_list = list(prots) + if not prot_list: + return [] + + # Pre-fetch MateriaLegislativa for all protocols in one query. + materia_query = Q() + for p in prot_list: + materia_query |= Q(numero_protocolo=p.numero, ano=p.ano) + materias_map = { + (m.numero_protocolo, m.ano): m + for m in MateriaLegislativa.objects.filter( + materia_query).select_related('tipo') + } + + # Pre-fetch DocumentoAdministrativo for all protocols in one query. + documentos_map = { + doc.protocolo_id: doc + for doc in DocumentoAdministrativo.objects.filter( + protocolo__in=prot_list).select_related('tipo') + } + protocolos = [] - for p in prots: + for p in prot_list: dic = {} dic['titulo'] = str(p.numero) + '/' + str(p.ano) @@ -1161,11 +1194,11 @@ def get_etiqueta_protocolos(prots): dic['nom_autor'] = str(p.autor or ' ') - dic['num_materia'] = '' - for materia in MateriaLegislativa.objects.filter( - numero_protocolo=p.numero, ano=p.ano): - dic['num_materia'] = materia.tipo.sigla + ' ' + \ - str(materia.numero) + '/' + str(materia.ano) + materia = materias_map.get((p.numero, p.ano)) + dic['num_materia'] = ( + materia.tipo.sigla + ' ' + str(materia.numero) + '/' + str(materia.ano) + if materia else '' + ) dic['natureza'] = '' if p.tipo_processo == 0: @@ -1173,11 +1206,11 @@ def get_etiqueta_protocolos(prots): if p.tipo_processo == 1: dic['natureza'] = 'Legislativo' - dic['num_documento'] = '' - for documento in DocumentoAdministrativo.objects.filter( - protocolo=p): - dic['num_documento'] = documento.tipo.sigla + ' ' + \ - str(documento.numero) + '/' + str(documento.ano) + documento = documentos_map.get(p.pk) + dic['num_documento'] = ( + documento.tipo.sigla + ' ' + str(documento.numero) + '/' + str(documento.ano) + if documento else '' + ) dic['ident_processo'] = dic['num_materia'] or dic['num_documento'] @@ -1382,9 +1415,15 @@ def make_pdf(base_url, main_template, header_template, main_css='', header_css=' def resumo_ata_pdf(request, pk): base_url = request.build_absolute_uri() casa = CasaLegislativa.objects.first() + if not casa: + raise Http404 rodape = ' '.join(get_rodape(casa)) - sessao_plenaria = SessaoPlenaria.objects.get(pk=pk) + # A mesma restrição de visibilidade aplicada por ResumoAtaView: sem ela, + # uma sessão oculta na consulta pública era legível por aqui. + sessao_plenaria = get_object_or_404( + restringe_sessoes_visiveis(SessaoPlenaria.objects.all(), request.user), + pk=pk) dict_ord_template = { 'cont_mult': 'conteudo_multimidia.html', @@ -1408,6 +1447,9 @@ def resumo_ata_pdf(request, pk): context = {} context.update(get_identificacao_basica(sessao_plenaria)) + # Sem cont_mult / votos nominais, esses blocos saíam vazios no PDF quando + # a ResumoOrdenacao os incluía, divergindo da ata exibida em tela. + context.update(get_conteudo_multimidia(sessao_plenaria)) context.update(get_mesa_diretora(sessao_plenaria)) context.update(get_presenca_sessao(sessao_plenaria)) context.update(get_correspondencias(sessao_plenaria, request.user)) @@ -1420,7 +1462,14 @@ def resumo_ata_pdf(request, pk): context.update(get_ocorrencias_da_sessao(sessao_plenaria)) context.update(get_consideracoes_finais(sessao_plenaria)) context.update(get_oradores_explicacoes_pessoais(sessao_plenaria)) - context.update(get_assinaturas(sessao_plenaria)) + context.update(get_assinaturas( + sessao_plenaria, + mesa=context['mesa'], + presenca_ordem=context['presenca_ordem'])) + context.update({'votos_nominais_materia_expediente': get_votos_nominais( + sessao_plenaria.id, ExpedienteMateria, 'expediente')}) + context.update({'votos_nominais_materia_ordem_dia': get_votos_nominais( + sessao_plenaria.id, OrdemDia, 'ordem')}) context.update({'object': sessao_plenaria}) context.update({'data': dt.today().strftime('%d/%m/%Y')}) context.update({'rodape': rodape}) @@ -1594,7 +1643,8 @@ def relatorio_sessao_plenaria_pdf(request, pk): try: logger.debug("user=" + username + ". Tentando obter SessaoPlenaria com id={}.".format(pk)) - sessao = SessaoPlenaria.objects.get(id=pk) + sessao = restringe_sessoes_visiveis( + SessaoPlenaria.objects.all(), request.user).get(id=pk) except ObjectDoesNotExist as e: logger.error("user=" + username + ". Essa SessaoPlenaria não existe (pk={}). ".format(pk) + str(e)) diff --git a/sapl/sessao/views.py b/sapl/sessao/views.py index 64f7ce7c1..2eff70841 100755 --- a/sapl/sessao/views.py +++ b/sapl/sessao/views.py @@ -1,4 +1,4 @@ -from collections import OrderedDict +from collections import OrderedDict, defaultdict from datetime import datetime import json import logging @@ -9,7 +9,7 @@ from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.contrib.auth.mixins import PermissionRequiredMixin from django.core.exceptions import ObjectDoesNotExist -from django.db.models import Max, Q +from django.db.models import Max, Prefetch, Q from django.http import JsonResponse from django.http.response import Http404, HttpResponseRedirect from django.urls import reverse @@ -37,7 +37,7 @@ from sapl.crud.base import (RP_DETAIL, RP_LIST, Crud, CrudAux, PermissionRequiredForAppCrudMixin, make_pagination) from sapl.materia.forms import filtra_tramitacao_status from sapl.materia.models import (Autoria, TipoMateriaLegislativa, - Tramitacao, MateriaEmTramitacao, Numeracao) + Tramitacao, MateriaEmTramitacao) from sapl.materia.views import MateriaLegislativaPesquisaView from sapl.parlamentares.models import (Filiacao, Legislatura, Mandato, Parlamentar, SessaoLegislativa) @@ -49,7 +49,8 @@ from sapl.sessao.forms import ExpedienteMateriaForm, OrdemDiaForm, OrdemExpedien from sapl.sessao.models import Correspondencia from sapl.settings import TIME_ZONE, RATE_LIMITER_RATE from sapl.utils import show_results_filter_set, remover_acentos, get_client_ip, \ - MultiFormatOutputMixin, PautaMultiFormatOutputMixin, ratelimit_ip + MultiFormatOutputMixin, PautaMultiFormatOutputMixin, ratelimit_ip, \ + filiacao_data from .forms import (AdicionarVariasMateriasFilterSet, AdicionarVariasMateriasForm, BancadaForm, ExpedienteForm, JustificativaAusenciaForm, OcorrenciaSessaoForm, ListMateriaForm, @@ -224,38 +225,43 @@ def abrir_votacao(request, pk, spk): def customize_link_materia(context, pk, has_permission, is_expediente): + # sessao_plenaria is the same for every row — resolve once + object_list = context['object_list'] + if object_list: + sessao_plenaria = object_list[0].sessao_plenaria + else: + sessao_plenaria = SessaoPlenaria.objects.get(id=pk) + data_sessao = sessao_plenaria.data_fim or sessao_plenaria.data_inicio + for i, row in enumerate(context['rows']): - materia = context['object_list'][i].materia - obj = context['object_list'][i] + obj = object_list[i] + materia = obj.materia # already select_related + url_materia = reverse( 'sapl.materia:materialegislativa_detail', kwargs={'pk': materia.id}) - numeracao = materia.numeracao_set.first() if materia.numeracao_set.first() else "-" - todos_autoria = materia.autoria_set.all() - autoria = todos_autoria.filter(primeiro_autor=True) + + numeracao = materia._numeracao_prefetch[0] if materia._numeracao_prefetch else "-" + + todos_autoria = materia._autoria_prefetch + autoria = [a for a in todos_autoria if a.primeiro_autor] autor = ', '.join([str(a.autor) for a in autoria]) if autoria else "-" + todos_autores = ', '.join([str(a.autor) for a in todos_autoria]) if autoria else "-" - todos_autores = ', '.join([str(a.autor) - for a in todos_autoria]) if autoria else "-" + num_protocolo = materia.numero_protocolo or "-" - num_protocolo = materia.numero_protocolo if materia.numero_protocolo else "-" - sessao_plenaria = SessaoPlenaria.objects.get(id=pk) - data_sessao = sessao_plenaria.data_fim if sessao_plenaria.data_fim else sessao_plenaria.data_inicio - tramitacao = Tramitacao.objects \ - .select_related('materia', 'status', 'materia__tipo') \ - .filter(materia=materia, turno__isnull=False, data_tramitacao__lte=data_sessao) \ - .exclude(turno__exact='') \ - .order_by('-data_tramitacao', '-id') \ - .first() + tramitacao = next( + (t for t in materia._tramitacao_prefetch if t.data_tramitacao <= data_sessao), + None, + ) turno = '-' if tramitacao: for t in Tramitacao.TURNO_CHOICES: if t[0] == tramitacao.turno: turno = t[1] break - materia_em_tramitacao = MateriaEmTramitacao.objects \ - .select_related("materia", "tramitacao") \ - .filter(materia=materia) \ - .first() + + materia_em_tramitacao = materia._met_prefetch[0] if materia._met_prefetch else None + # idUnica para cada materia idAutor = "autor" + str(i) idAutores = "autores" + str(i) @@ -281,12 +287,9 @@ def customize_link_materia(context, pk, has_permission, is_expediente): # url em toda a string de title_materia context['rows'][i][1] = (title_materia, None) - exist_resultado = obj.registrovotacao_set.filter( - materia=obj.materia).exists() - exist_retirada = obj.retiradapauta_set.filter( - materia=obj.materia).exists() - exist_leitura = obj.registroleitura_set.filter( - materia=obj.materia).exists() + exist_resultado = bool(obj._votacao_prefetch) + exist_retirada = bool(obj._retirada_prefetch) + exist_leitura = bool(obj._leitura_prefetch) if (obj.tipo_votacao != LEITURA and not exist_resultado and not exist_retirada) or \ (obj.tipo_votacao == LEITURA and not exist_leitura): @@ -408,8 +411,7 @@ def customize_link_materia(context, pk, has_permission, is_expediente): resultado = '''Não há resultado''' elif exist_retirada: - retirada = obj.retiradapauta_set.filter( - materia_id=obj.materia_id).last() + retirada = obj._retirada_prefetch[-1] retirada_descricao = retirada.tipo_de_retirada.descricao retirada_observacao = retirada.observacao url = reverse('sapl.sessao:retiradapauta_detail', @@ -421,13 +423,11 @@ def customize_link_materia(context, pk, has_permission, is_expediente): else: if obj.tipo_votacao == LEITURA: - resultado = obj.registroleitura_set.filter( - materia_id=obj.materia_id).last() + resultado = obj._leitura_prefetch[-1] resultado_descricao = "Matéria lida" resultado_observacao = resultado.observacao else: - resultado = obj.registrovotacao_set.filter( - materia_id=obj.materia_id).last() + resultado = obj._votacao_prefetch[-1] resultado_descricao = resultado.tipo_resultado_votacao.nome resultado_observacao = resultado.observacao @@ -833,20 +833,58 @@ class MateriaOrdemDiaCrud(MasterDetailCrud): layout_key = 'OrdemDiaDetail' class ListView(MasterDetailCrud.ListView): - paginate_by = None + paginate_by = 100 ordering = ['numero_ordem', 'materia', 'resultado'] def get_context_data(self, **kwargs): - if self.get_queryset().count() > 500: - self.paginate_by = 50 - else: - self.paginate_by = None - context = super().get_context_data(**kwargs) - has_permition = self.request.user.has_module_perms(AppConfig.label) return customize_link_materia(context, self.kwargs['pk'], has_permition, False) + def get_queryset(self): + return super().get_queryset().select_related( + 'materia', 'materia__tipo', 'sessao_plenaria', + ).prefetch_related( + Prefetch( + 'materia__materiaemtramitacao_set', + to_attr='_met_prefetch', + ), + Prefetch( + 'materia__numeracao_set', + to_attr='_numeracao_prefetch', + ), + Prefetch( + 'materia__autoria_set', + queryset=Autoria.objects.select_related('autor'), + to_attr='_autoria_prefetch', + ), + Prefetch( + 'materia__tramitacao_set', + queryset=Tramitacao.objects.filter( + turno__isnull=False, + ).exclude(turno='').order_by('-data_tramitacao', '-id'), + to_attr='_tramitacao_prefetch', + ), + Prefetch( + 'registrovotacao_set', + queryset=RegistroVotacao.objects.select_related( + 'tipo_resultado_votacao', + ), + to_attr='_votacao_prefetch', + ), + Prefetch( + 'retiradapauta_set', + queryset=RetiradaPauta.objects.select_related( + 'tipo_de_retirada', + ), + to_attr='_retirada_prefetch', + ), + Prefetch( + 'registroleitura_set', + to_attr='_leitura_prefetch', + ), + ) + def recuperar_materia(request): tipo = TipoMateriaLegislativa.objects.get(pk=request.GET['tipo_materia']) @@ -905,24 +943,60 @@ class ExpedienteMateriaCrud(MasterDetailCrud): 'resultado'] class ListView(MasterDetailCrud.ListView): - paginate_by = None + paginate_by = 100 ordering = ['numero_ordem', 'materia', 'resultado'] def get_context_data(self, **kwargs): - - if self.get_queryset().count() > 500: - self.paginate_by = 50 - else: - self.paginate_by = None - context = super().get_context_data(**kwargs) - if self.request.GET.get('page'): context['page'] = self.request.GET.get('page') - has_permition = self.request.user.has_module_perms(AppConfig.label) return customize_link_materia(context, self.kwargs['pk'], has_permition, True) + def get_queryset(self): + return super().get_queryset().select_related( + 'materia', 'materia__tipo', 'sessao_plenaria', + ).prefetch_related( + Prefetch( + 'materia__materiaemtramitacao_set', + to_attr='_met_prefetch', + ), + Prefetch( + 'materia__numeracao_set', + to_attr='_numeracao_prefetch', + ), + Prefetch( + 'materia__autoria_set', + queryset=Autoria.objects.select_related('autor'), + to_attr='_autoria_prefetch', + ), + Prefetch( + 'materia__tramitacao_set', + queryset=Tramitacao.objects.filter( + turno__isnull=False, + ).exclude(turno='').order_by('-data_tramitacao', '-id'), + to_attr='_tramitacao_prefetch', + ), + Prefetch( + 'registrovotacao_set', + queryset=RegistroVotacao.objects.select_related( + 'tipo_resultado_votacao', + ), + to_attr='_votacao_prefetch', + ), + Prefetch( + 'retiradapauta_set', + queryset=RetiradaPauta.objects.select_related( + 'tipo_de_retirada', + ), + to_attr='_retirada_prefetch', + ), + Prefetch( + 'registroleitura_set', + to_attr='_leitura_prefetch', + ), + ) + class CreateView(MasterDetailCrud.CreateView): form_class = ExpedienteMateriaForm @@ -1982,9 +2056,24 @@ def get_conteudo_multimidia(sessao_plenaria): return context +def prefetch_filiacao(): + """Prefetch de `filiacao_set` para os Parlamentares exibidos no resumo. + + Permite que `filiacao_data` (usada pelo filtro `filiacao_data_filter` nos + blocos do resumo/ata) resolva a sigla do partido em memória, sem uma query + por parlamentar. A ordenação replica `Filiacao.Meta.ordering`. + """ + return Prefetch( + 'parlamentar__filiacao_set', + queryset=Filiacao.objects.select_related('partido').order_by( + '-data', '-data_desfiliacao')) + + def get_mesa_diretora(sessao_plenaria): mesa = IntegranteMesa.objects.filter( - sessao_plenaria=sessao_plenaria).order_by('cargo_id') + sessao_plenaria=sessao_plenaria).select_related( + 'parlamentar', 'cargo').prefetch_related( + prefetch_filiacao()).order_by('cargo_id') integrantes = [{'parlamentar': m.parlamentar, 'cargo': m.cargo} for m in mesa] return {'mesa': integrantes} @@ -1993,18 +2082,21 @@ def get_mesa_diretora(sessao_plenaria): def get_presenca_sessao(sessao_plenaria): parlamentares_sessao = [p.parlamentar for p in SessaoPlenariaPresenca.objects.filter( sessao_plenaria_id=sessao_plenaria.id - ).order_by('parlamentar__nome_parlamentar').distinct()] + ).select_related('parlamentar').prefetch_related( + prefetch_filiacao()).order_by('parlamentar__nome_parlamentar').distinct()] ausentes_sessao = JustificativaAusencia.objects.filter( sessao_plenaria_id=sessao_plenaria.id - ).distinct().order_by('parlamentar__nome_parlamentar') + ).select_related('parlamentar', 'tipo_ausencia').distinct().order_by( + 'parlamentar__nome_parlamentar') return ({'presenca_sessao': parlamentares_sessao, 'justificativa_ausencia': ausentes_sessao}) def get_correspondencias(sessao_plenaria, user): - qs = sessao_plenaria.correspondencia_set.all() + qs = sessao_plenaria.correspondencia_set.select_related( + 'documento', 'documento__tipo') is_anon = user.is_anonymous is_ostensivo = AppsAppConfig.attr( @@ -2036,30 +2128,92 @@ def get_correspondencias(sessao_plenaria, user): def get_expedientes(sessao_plenaria): expediente = ExpedienteSessao.objects.filter( - sessao_plenaria_id=sessao_plenaria.id).order_by('tipo__ordenacao', 'tipo__nome') + sessao_plenaria_id=sessao_plenaria.id).select_related( + 'tipo').order_by('tipo__ordenacao', 'tipo__nome') expedientes = [] for e in expediente: - tipo = TipoExpediente.objects.get(id=e.tipo_id) + tipo = e.tipo conteudo = e.conteudo ex = {'tipo': tipo, 'conteudo': conteudo} expedientes.append(ex) return ({'expedientes': expedientes}) +def prefetch_materias_sessao(qs): + """Prefetches compartilhados por `get_materias_expediente` e + `get_materias_ordem_do_dia`. + + Os querysets dos `Prefetch` não recebem `order_by` explícito justamente + para herdarem o `Meta.ordering` de cada model — é dele que dependem os + `.first()` / `.last()` que este código substitui por indexação de lista. + """ + return qs.select_related('materia', 'materia__tipo').prefetch_related( + Prefetch( + 'materia__tramitacao_set', + queryset=Tramitacao.objects.exclude(turno='').order_by( + '-data_tramitacao', '-id'), + to_attr='_tramitacao_prefetch'), + Prefetch( + 'materia__materiaemtramitacao_set', + queryset=MateriaEmTramitacao.objects.select_related( + 'tramitacao', 'tramitacao__status'), + to_attr='_met_prefetch'), + Prefetch('materia__numeracao_set', to_attr='_numeracao_prefetch'), + Prefetch( + 'materia__autoria_set', + queryset=Autoria.objects.select_related('autor'), + to_attr='_autoria_prefetch'), + Prefetch( + 'registrovotacao_set', + queryset=RegistroVotacao.objects.select_related( + 'tipo_resultado_votacao'), + to_attr='_votacao_prefetch'), + Prefetch( + 'retiradapauta_set', + queryset=RetiradaPauta.objects.select_related('tipo_de_retirada'), + to_attr='_retirada_prefetch'), + Prefetch('registroleitura_set', to_attr='_leitura_prefetch'), + ) + + +def agrupa_votos_por_materia(campo, ids): + """Busca todos os VotoParlamentar de uma seção em uma única query. + + `campo` é 'expediente' ou 'ordem' — as FKs desnormalizadas de + VotoParlamentar. O agrupamento preserva a ordem da query, que herda o + `Meta.ordering = ('id',)` do model. + """ + votos = defaultdict(list) + if not ids: + return votos + qs = VotoParlamentar.objects.filter( + **{'%s_id__in' % campo: ids}).select_related('parlamentar') + for voto in qs: + votos[getattr(voto, '%s_id' % campo)].append(voto) + return votos + + def get_materias_expediente(sessao_plenaria): materias_expediente = [] - for m in ExpedienteMateria.objects.select_related("materia").filter(sessao_plenaria_id=sessao_plenaria.id): - tramitacao = '' - data_sessao = sessao_plenaria.data_fim if sessao_plenaria.data_fim else sessao_plenaria.data_inicio - for aux_tramitacao in Tramitacao.objects.filter(materia=m.materia, data_tramitacao__lte=data_sessao).order_by( - '-data_tramitacao', '-id'): - if aux_tramitacao.turno: - tramitacao = aux_tramitacao - break - - rv = m.registrovotacao_set.filter(materia=m.materia).first() - rp = m.retiradapauta_set.filter(materia=m.materia).first() - rl = m.registroleitura_set.filter(materia=m.materia).first() + data_sessao = sessao_plenaria.data_fim if sessao_plenaria.data_fim else sessao_plenaria.data_inicio + + expedientes = list(prefetch_materias_sessao( + ExpedienteMateria.objects.filter( + sessao_plenaria_id=sessao_plenaria.id))) + votos_por_materia = agrupa_votos_por_materia( + 'expediente', [m.id for m in expedientes]) + + for m in expedientes: + tramitacao = next( + (t for t in m.materia._tramitacao_prefetch + if t.data_tramitacao <= data_sessao), '') + + rv = next((r for r in m._votacao_prefetch + if r.materia_id == m.materia_id), None) + rp = next((r for r in m._retirada_prefetch + if r.materia_id == m.materia_id), None) + rl = next((r for r in m._leitura_prefetch + if r.materia_id == m.materia_id), None) if rv: resultado = rv.tipo_resultado_votacao.nome resultado_observacao = rv.observacao @@ -2076,11 +2230,11 @@ def get_materias_expediente(sessao_plenaria): voto_nominal = [] if m.tipo_votacao == 2: - for voto in VotoParlamentar.objects.filter(expediente=m.id): + for voto in votos_por_materia[m.id]: voto_nominal.append( (voto.parlamentar.nome_completo, voto.voto)) - voto = RegistroVotacao.objects.filter(expediente=m.id).last() + voto = m._votacao_prefetch[-1] if m._votacao_prefetch else None if voto: voto_sim = voto.numero_votos_sim voto_nao = voto.numero_votos_nao @@ -2090,7 +2244,9 @@ def get_materias_expediente(sessao_plenaria): voto_nao = " Não Informado" voto_abstencoes = " Não Informado" - materia_em_tramitacao = m.materia.materiaemtramitacao_set.first() + met = m.materia._met_prefetch + materia_em_tramitacao = met[0] if met else None + numeracao = m.materia._numeracao_prefetch materias_expediente.append({ 'ementa': m.materia.ementa, 'titulo': m.materia, @@ -2099,9 +2255,9 @@ def get_materias_expediente(sessao_plenaria): 'situacao': materia_em_tramitacao.tramitacao.status if materia_em_tramitacao else _("Não informada"), 'resultado': resultado, 'resultado_observacao': resultado_observacao, - 'autor': [str(x.autor) for x in Autoria.objects.select_related("autor").filter(materia_id=m.materia_id)], + 'autor': [str(x.autor) for x in m.materia._autoria_prefetch], 'numero_protocolo': m.materia.numero_protocolo, - 'numero_processo': m.materia.numeracao_set.last(), + 'numero_processo': numeracao[-1] if numeracao else None, 'tipo_votacao': m.TIPO_VOTACAO_CHOICES[m.tipo_votacao], 'voto_sim': voto_sim, 'voto_nao': voto_nao, @@ -2117,12 +2273,13 @@ def get_materias_expediente(sessao_plenaria): def get_oradores_expediente(sessao_plenaria): oradores = [] for orador in OradorExpediente.objects.filter( - sessao_plenaria_id=sessao_plenaria.id).order_by('numero_ordem'): + sessao_plenaria_id=sessao_plenaria.id).select_related( + 'parlamentar').prefetch_related( + prefetch_filiacao()).order_by('numero_ordem'): numero_ordem = orador.numero_ordem url_discurso = orador.url_discurso observacao = orador.observacao - parlamentar = Parlamentar.objects.get( - id=orador.parlamentar_id) + parlamentar = orador.parlamentar ora = {'numero_ordem': numero_ordem, 'url_discurso': url_discurso, 'parlamentar': parlamentar, @@ -2135,21 +2292,32 @@ def get_oradores_expediente(sessao_plenaria): def get_presenca_ordem_do_dia(sessao_plenaria): parlamentares_ordem = [p.parlamentar for p in PresencaOrdemDia.objects.filter( sessao_plenaria_id=sessao_plenaria.id - ).distinct().order_by('parlamentar__nome_parlamentar')] + ).select_related('parlamentar').prefetch_related( + prefetch_filiacao()).distinct().order_by('parlamentar__nome_parlamentar')] return {'presenca_ordem': parlamentares_ordem} -def get_assinaturas(sessao_plenaria): - mesa_dia = get_mesa_diretora(sessao_plenaria)['mesa'] +def get_assinaturas(sessao_plenaria, mesa=None, presenca_ordem=None): + """`mesa` e `presenca_ordem` podem ser reaproveitados de + `get_mesa_diretora` / `get_presenca_ordem_do_dia` quando o chamador já os + calculou, evitando repetir as duas queries. O `.distinct()` que + `get_presenca_ordem_do_dia` aplica é um no-op (a projeção inclui a PK), + então as duas listas são equivalentes. + """ + mesa_dia = get_mesa_diretora( + sessao_plenaria)['mesa'] if mesa is None else mesa presidente_dia = [next(iter( [m['parlamentar'] for m in mesa_dia if m['cargo'].descricao == 'Presidente']), '')] - parlamentares_ordem = [p.parlamentar for p in PresencaOrdemDia.objects.filter( - sessao_plenaria_id=sessao_plenaria.id - ).order_by('parlamentar__nome_parlamentar')] + if presenca_ordem is None: + presenca_ordem = [p.parlamentar for p in PresencaOrdemDia.objects.filter( + sessao_plenaria_id=sessao_plenaria.id + ).select_related('parlamentar').prefetch_related( + prefetch_filiacao()).order_by('parlamentar__nome_parlamentar')] + parlamentares_ordem = presenca_ordem parlamentares_mesa = [m['parlamentar'] for m in mesa_dia] @@ -2195,19 +2363,25 @@ def get_assinaturas_presidente(sessao_plenaria): def get_materias_ordem_do_dia(sessao_plenaria): materias_ordem = [] - for o in OrdemDia.objects.filter(sessao_plenaria_id=sessao_plenaria.id): - tramitacao = '' - data_sessao = sessao_plenaria.data_fim if sessao_plenaria.data_fim else sessao_plenaria.data_inicio - for aux_tramitacao in Tramitacao.objects.filter(materia=o.materia, data_tramitacao__lte=data_sessao).order_by( - '-data_tramitacao', '-id'): - if aux_tramitacao.turno: - tramitacao = aux_tramitacao - break + data_sessao = sessao_plenaria.data_fim if sessao_plenaria.data_fim else sessao_plenaria.data_inicio + + ordens = list(prefetch_materias_sessao( + OrdemDia.objects.filter(sessao_plenaria_id=sessao_plenaria.id))) + votos_por_materia = agrupa_votos_por_materia( + 'ordem', [o.id for o in ordens]) + + for o in ordens: + tramitacao = next( + (t for t in o.materia._tramitacao_prefetch + if t.data_tramitacao <= data_sessao), '') # Verificar resultado - rv = o.registrovotacao_set.filter(materia=o.materia).first() - rp = o.retiradapauta_set.filter(materia=o.materia).first() - rl = o.registroleitura_set.filter(materia=o.materia).first() + rv = next((r for r in o._votacao_prefetch + if r.materia_id == o.materia_id), None) + rp = next((r for r in o._retirada_prefetch + if r.materia_id == o.materia_id), None) + rl = next((r for r in o._leitura_prefetch + if r.materia_id == o.materia_id), None) if rv: resultado = rv.tipo_resultado_votacao.nome resultado_observacao = rv.observacao @@ -2224,11 +2398,11 @@ def get_materias_ordem_do_dia(sessao_plenaria): voto_nominal = [] if o.tipo_votacao == 2: - for voto in VotoParlamentar.objects.filter(ordem=o.id): + for voto in votos_por_materia[o.id]: voto_nominal.append( (voto.parlamentar.nome_parlamentar, voto.voto)) - voto = RegistroVotacao.objects.filter(ordem=o.id).last() + voto = o._votacao_prefetch[-1] if o._votacao_prefetch else None if voto: voto_sim = voto.numero_votos_sim voto_nao = voto.numero_votos_nao @@ -2238,7 +2412,9 @@ def get_materias_ordem_do_dia(sessao_plenaria): voto_nao = " Não Informado" voto_abstencoes = " Não Informado" - materia_em_tramitacao = o.materia.materiaemtramitacao_set.first() + met = o.materia._met_prefetch + materia_em_tramitacao = met[0] if met else None + numeracao = o.materia._numeracao_prefetch materias_ordem.append({ 'ementa': o.materia.ementa, 'ementa_observacao': o.observacao, @@ -2248,9 +2424,9 @@ def get_materias_ordem_do_dia(sessao_plenaria): 'situacao': materia_em_tramitacao.tramitacao.status if materia_em_tramitacao else _("Não informada"), 'resultado': resultado, 'resultado_observacao': resultado_observacao, - 'autor': [str(x.autor) for x in Autoria.objects.select_related("autor").filter(materia_id=o.materia_id)], + 'autor': [str(x.autor) for x in o.materia._autoria_prefetch], 'numero_protocolo': o.materia.numero_protocolo, - 'numero_processo': o.materia.numeracao_set.last(), + 'numero_processo': numeracao[-1] if numeracao else None, 'tipo_votacao': o.TIPO_VOTACAO_CHOICES[o.tipo_votacao], 'voto_sim': voto_sim, 'voto_nao': voto_nao, @@ -2267,15 +2443,14 @@ def get_oradores_ordemdia(sessao_plenaria): oradores_ordem_dia = OradorOrdemDia.objects.filter( sessao_plenaria_id=sessao_plenaria.id - ).order_by('numero_ordem') + ).select_related('parlamentar').prefetch_related( + prefetch_filiacao()).order_by('numero_ordem') for orador in oradores_ordem_dia: numero_ordem = orador.numero_ordem url_discurso = orador.url_discurso observacao = orador.observacao - parlamentar = Parlamentar.objects.get( - id=orador.parlamentar_id - ) + parlamentar = orador.parlamentar o = { 'numero_ordem': numero_ordem, 'url_discurso': url_discurso, @@ -2291,26 +2466,26 @@ def get_oradores_ordemdia(sessao_plenaria): def get_oradores_explicacoes_pessoais(sessao_plenaria): oradores_explicacoes = [] for orador in Orador.objects.filter( - sessao_plenaria_id=sessao_plenaria.id).order_by('numero_ordem'): - for parlamentar in Parlamentar.objects.filter( - id=orador.parlamentar.id): - partido_sigla = Filiacao.objects.filter( - parlamentar=parlamentar).last() - if not partido_sigla: - sigla = '' - else: - sigla = partido_sigla.partido.sigla - observacao = orador.observacao - url_discurso = orador.url_discurso - - oradores = { - 'numero_ordem': orador.numero_ordem, - 'parlamentar': parlamentar, - 'sgl_partido': sigla, - 'observacao': observacao, - 'url_discurso': url_discurso - } - oradores_explicacoes.append(oradores) + sessao_plenaria_id=sessao_plenaria.id).select_related( + 'parlamentar').prefetch_related( + prefetch_filiacao()).order_by('numero_ordem'): + parlamentar = orador.parlamentar + # Antes era Filiacao...last() (a filiação mais antiga, dado o + # ordering decrescente por data), enquanto o PDF usava .first(). + # Unifica no critério já usado pelas listas de presença: o partido + # vigente na data da sessão. + sigla = filiacao_data(parlamentar, sessao_plenaria.data_inicio) + observacao = orador.observacao + url_discurso = orador.url_discurso + + oradores = { + 'numero_ordem': orador.numero_ordem, + 'parlamentar': parlamentar, + 'sgl_partido': sigla, + 'observacao': observacao, + 'url_discurso': url_discurso + } + oradores_explicacoes.append(oradores) context = {'oradores_explicacoes': oradores_explicacoes} return context @@ -2329,6 +2504,34 @@ def get_consideracoes_finais(sessao_plenaria): return context +def get_votos_nominais(sessao_plenaria_id, model, campo): + """Blocos de votação nominal de uma seção, em 3 queries em vez de 2 por matéria. + + Replica `VotoParlamentar.objects.filter(votacao__in=registro).order_by('parlamentar')`: + a query dos votos é ordenada globalmente por parlamentar e o agrupamento + preserva essa ordem dentro de cada matéria. + """ + materias = list(model.objects.filter( + sessao_plenaria_id=sessao_plenaria_id, tipo_votacao=2 + ).select_related('materia').order_by('-materia')) + + votos_por_materia = defaultdict(list) + if materias: + registro_para_materia = { + r.id: getattr(r, '%s_id' % campo) + for r in RegistroVotacao.objects.filter( + **{'%s_id__in' % campo: [m.id for m in materias]}) + } + if registro_para_materia: + for vp in VotoParlamentar.objects.filter( + votacao_id__in=registro_para_materia).select_related( + 'parlamentar').order_by('parlamentar'): + votos_por_materia[registro_para_materia[vp.votacao_id]].append(vp) + + return [{'titulo': m.materia, 'votos': votos_por_materia[m.id]} + for m in materias] + + class ResumoView(DetailView): template_name = 'sessao/resumo.html' model = SessaoPlenaria @@ -2343,22 +2546,8 @@ class ResumoView(DetailView): context = self.get_context_data(object=self.object) # Votos de Votação Nominal de Matérias Expediente - votacoes = [] - for mevn in ExpedienteMateria.objects.filter(sessao_plenaria_id=self.object.id, tipo_votacao=2) \ - .order_by('-materia'): - votos_materia = [] - titulo_materia = mevn.materia - registro = RegistroVotacao.objects.filter(expediente=mevn) - if registro: - for vp in VotoParlamentar.objects.filter(votacao__in=registro).order_by('parlamentar'): - votos_materia.append(vp) - - votacoes.append({ - 'titulo': titulo_materia, - 'votos': votos_materia - }) - - context.update({'votos_nominais_materia_expediente': votacoes}) + context.update({'votos_nominais_materia_expediente': get_votos_nominais( + self.object.id, ExpedienteMateria, 'expediente')}) # ===================================================================== # Identificação Básica @@ -2389,25 +2578,15 @@ class ResumoView(DetailView): context.update(get_presenca_ordem_do_dia(self.object)) # ===================================================================== # Assinaturas - context.update(get_assinaturas(self.object)) + context.update(get_assinaturas( + self.object, + mesa=context['mesa'], + presenca_ordem=context['presenca_ordem'])) # ===================================================================== # Matérias Ordem do Dia # Votos de Votação Nominal de Matérias Ordem do Dia - votacoes_od = [] - for modvn in OrdemDia.objects.filter(sessao_plenaria_id=self.object.id, tipo_votacao=2).order_by('-materia'): - votos_materia_od = [] - t_materia = modvn.materia - registro_od = RegistroVotacao.objects.filter(ordem=modvn) - if registro_od: - for vp_od in VotoParlamentar.objects.filter(votacao__in=registro_od).order_by('parlamentar'): - votos_materia_od.append(vp_od) - - votacoes_od.append({ - 'titulo': t_materia, - 'votos': votos_materia_od - }) - - context.update({'votos_nominais_materia_ordem_dia': votacoes_od}) + context.update({'votos_nominais_materia_ordem_dia': get_votos_nominais( + self.object.id, OrdemDia, 'ordem')}) context.update(get_materias_ordem_do_dia(self.object)) # ===================================================================== diff --git a/sapl/templates/painel/mensagem.html b/sapl/templates/painel/mensagem.html deleted file mode 100644 index da8e355d4..000000000 --- a/sapl/templates/painel/mensagem.html +++ /dev/null @@ -1,120 +0,0 @@ -{% load i18n %} -{% load common_tags %} -{% load render_bundle from webpack_loader %} -{% load webpack_static from webpack_loader %} - - - - - - - - - - - {% block head_title %}{% trans 'SAPL - Sistema de Apoio ao Processo Legislativo' %}{% endblock %} - - - {% render_chunk_vendors 'css' %} - {% render_bundle 'global' 'css' %} - {% render_bundle 'painel' 'css' %} - - - - - - -

{{ context.title }}

- -

Ajax refresh counter:

-

-

-

-

-




-
- - - - - {% render_chunk_vendors 'js' %} - {% render_bundle 'global' 'js' %} - {% render_bundle 'painel' 'js' %} - - - diff --git a/sapl/templates/painel/parlamentares.html b/sapl/templates/painel/parlamentares.html deleted file mode 100644 index 3dbe6e740..000000000 --- a/sapl/templates/painel/parlamentares.html +++ /dev/null @@ -1,128 +0,0 @@ -{% load i18n %} -{% load common_tags %} - -{% load render_bundle from webpack_loader %} -{% load webpack_static from webpack_loader %} - - - - - - - - - - - {% block head_title %}{% trans 'SAPL - Sistema de Apoio ao Processo Legislativo' %}{% endblock %} - - - {% render_chunk_vendors 'css' %} - {% render_bundle 'global' 'css' %} - {% render_bundle 'painel' 'css' %} - - - - - - - -

{{ context.title }}

- -

-

-

-

-

- - - - -
-
    -
-
- - - - {% render_chunk_vendors 'js' %} - {% render_bundle 'global' 'js' %} - {% render_bundle 'painel' 'js' %} - - - diff --git a/sapl/templates/painel/votacao.html b/sapl/templates/painel/votacao.html deleted file mode 100644 index e73551160..000000000 --- a/sapl/templates/painel/votacao.html +++ /dev/null @@ -1,123 +0,0 @@ -{% load i18n %} -{% load render_bundle from webpack_loader %} -{% load webpack_static from webpack_loader %} - - - - - - - - - - - {% block head_title %}{% trans 'SAPL - Sistema de Apoio ao Processo Legislativo' %}{% endblock %} - - - {% render_chunk_vendors 'css' %} - {% render_bundle 'global' 'css' %} - {% render_bundle 'painel' 'css' %} - - - - -

{{ context.title }}

- -

-

-

-

-

- - - - -
-
    -
-
-
- - - - {% render_chunk_vendors 'js' %} - {% render_bundle 'global' 'js' %} - {% render_bundle 'painel' 'js' %} - - - diff --git a/sapl/utils.py b/sapl/utils.py index 9bd6231dc..ea58d1eb2 100644 --- a/sapl/utils.py +++ b/sapl/utils.py @@ -935,23 +935,59 @@ def qs_override_django_filter(self): def filiacao_data(parlamentar, data_inicio, data_fim=None): from sapl.parlamentares.models import Filiacao - filiacoes_parlamentar = Filiacao.objects.filter( - parlamentar=parlamentar) + filiacoes = _filiacoes_prefetchadas(parlamentar, data_inicio, data_fim) - filiacoes = filiacoes_parlamentar.filter(Q( - data__lte=data_inicio, - data_desfiliacao__isnull=True) | Q( - data__lte=data_inicio, - data_desfiliacao__gte=data_inicio)) + if filiacoes is None: + filiacoes_parlamentar = Filiacao.objects.filter( + parlamentar=parlamentar) - if data_fim: - filiacoes = filiacoes | filiacoes_parlamentar.filter( - data__gte=data_inicio, - data__lte=data_fim) + filiacoes = filiacoes_parlamentar.filter(Q( + data__lte=data_inicio, + data_desfiliacao__isnull=True) | Q( + data__lte=data_inicio, + data_desfiliacao__gte=data_inicio)) + + if data_fim: + filiacoes = filiacoes | filiacoes_parlamentar.filter( + data__gte=data_inicio, + data__lte=data_fim) return ' | '.join([f.partido.sigla for f in filiacoes]) +def _filiacoes_prefetchadas(parlamentar, data_inicio, data_fim=None): + """Aplica em memória o mesmo predicado de `filiacao_data`, quando + `filiacao_set` já veio prefetchado. + + O resumo/ata da sessão chama `filiacao_data_filter` duas vezes por + parlamentar exibido; sem isso cada chamada custa 1+N queries. Retorna + `None` quando não há prefetch, para que o chamador use o caminho SQL. + + O queryset do prefetch deve replicar `Filiacao.Meta.ordering` para que a + sequência de siglas saia idêntica à da consulta ao banco. + """ + if not data_inicio: + return None + + cache = getattr(parlamentar, '_prefetched_objects_cache', None) + if not cache: + return None + + cache_name = parlamentar.filiacao_set.field.remote_field.get_cache_name() + if cache_name not in cache: + return None + + def selecionada(f): + if f.data <= data_inicio and (f.data_desfiliacao is None + or f.data_desfiliacao >= data_inicio): + return True + if data_fim: + return data_inicio <= f.data <= data_fim + return False + + return [f for f in cache[cache_name] if selecionada(f)] + + def parlamentares_ativos(data_inicio, data_fim=None): from sapl.parlamentares.models import Mandato, Parlamentar '''