diff --git a/LOCALHOST_SETUP.md b/docs/LOCALHOST_SETUP.md similarity index 100% rename from LOCALHOST_SETUP.md rename to docs/LOCALHOST_SETUP.md diff --git a/sapl/api/views_sessao.py b/sapl/api/views_sessao.py index 2cb9c1e72..f24fc6b6d 100644 --- a/sapl/api/views_sessao.py +++ b/sapl/api/views_sessao.py @@ -1,19 +1,25 @@ +import logging + +import requests from django.apps.registry import apps +from django.conf import settings +from django.urls import reverse from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from drfautoapi.drfautoapi import ApiViewSetConstrutor, \ customize, wrapper_queryset_response_for_drf_action from sapl.api.serializers import ChoiceSerializer,\ SessaoPlenariaECidadaniaSerializer -from sapl.sessao.models import SessaoPlenaria, ExpedienteSessao -from sapl.utils import choice_anos_com_sessaoplenaria -# Add imports for building materias payload -from sapl.sessao.models import ExpedienteMateria, OrdemDia from sapl.materia.models import MateriaLegislativa, Tramitacao -from django.urls import reverse +from sapl.sessao.models import (ExpedienteMateria, ExpedienteSessao, OrdemDia, + SessaoPlenaria) +from sapl.utils import choice_anos_com_sessaoplenaria +logger = logging.getLogger(__name__) + ApiViewSetConstrutor.build_class( [ apps.get_app_config('sessao') @@ -49,16 +55,13 @@ class _SessaoPlenariaViewSet: self.serializer_class = SessaoPlenariaECidadaniaSerializer return self.list(request, *args, **kwargs) - # New endpoint: materias da sessão (expediente e ordem do dia) - @action(detail=True, methods=['get'], url_path='materias') - def materias(self, request, *args, **kwargs): - sessao: SessaoPlenaria = self.get_object() + def _build_materias_payload(self, request, sessao: SessaoPlenaria, + include_momento_meta=False, + include_momentos_block=False): pk = sessao.pk - # Data de referência da sessão para status de tramitação data_ref = sessao.data_fim or sessao.data_inicio def autores_info(m: MateriaLegislativa): - # retorna lista de {id, nome} return list(m.autores.values('id', 'nome')) def tramitacao_info(m: MateriaLegislativa): @@ -79,11 +82,11 @@ class _SessaoPlenariaViewSet: fase = '' return status, fase - def material_item(m: MateriaLegislativa, status_sessao: str, extra: dict = None): + def material_item(m: MateriaLegislativa, status_sessao: str, + extra: dict = None): extra = extra or {} pdf_url = None try: - # só expõe URL se houver ao menos 1 PDF acessório if m.documentoacessorio_set.filter(arquivo__iendswith='pdf').exists(): pdf_url = request.build_absolute_uri( reverse('sapl.materia:merge_docacessorios', kwargs={'pk': m.pk}) @@ -94,15 +97,15 @@ class _SessaoPlenariaViewSet: status_tram, fase_materia = tramitacao_info(m) return { - 'id': m.pk, + 'id_materia': m.pk, 'nome_documento': str(m), - 'autoria': autores_info(m), # [{id, nome}] + 'autoria': autores_info(m), 'tipo_materia': { 'id': m.tipo_id, 'nome': getattr(m.tipo, 'descricao', None) }, 'ementa': m.ementa, - 'status': status_sessao, # alias de status da sessão + 'status': status_sessao, 'status_sessao': status_sessao, 'status_tramitacao': status_tram, 'fase_materia': fase_materia, @@ -114,11 +117,10 @@ class _SessaoPlenariaViewSet: **extra } - # Expediente expediente_items = [] + momentos_items = [] for em in ExpedienteMateria.objects.select_related('materia', 'materia__tipo').filter(sessao_plenaria_id=pk): m = em.materia - # Determina status no contexto da sessão (votação/retirada/leitura) rv = em.registrovotacao_set.filter(materia=m).first() rp = em.retiradapauta_set.filter(materia=m).first() rl = em.registroleitura_set.filter(materia=m).first() @@ -130,15 +132,34 @@ class _SessaoPlenariaViewSet: status_sessao = 'Leitura' else: status_sessao = '' - expediente_items.append(material_item( - m, - status_sessao, - { - 'tipo_votacao': em.get_tipo_votacao_display(), - } - )) + exp_item = { + 'materia': material_item( + m, + status_sessao, + extra={ + 'tipo_votacao': em.get_tipo_votacao_display(), + } + ) + } + if include_momento_meta: + exp_item.update({ + 'momento_id': em.pk, + 'numero_ordem': em.numero_ordem, + }) + expediente_items.append(exp_item) + + if include_momentos_block: + momentos_items.append({ + 'momento_sessao_id': em.pk, + 'momento_sessao': 'expediente', + 'materias': [{ + 'momento_id': em.pk, + 'numero_ordem': em.numero_ordem, + 'tipo_votacao': em.get_tipo_votacao_display(), + 'materia': exp_item['materia'], + }] + }) - # Ordem do Dia ordem_items = [] for od in OrdemDia.objects.select_related('materia', 'materia__tipo').filter(sessao_plenaria_id=pk): m = od.materia @@ -153,16 +174,37 @@ class _SessaoPlenariaViewSet: status_sessao = 'Leitura' else: status_sessao = '' - ordem_items.append(material_item( + mat_item = material_item( m, status_sessao, - { - 'numero_ordem': od.numero_ordem, + extra={ 'tipo_votacao': od.get_tipo_votacao_display(), } - )) + ) - return Response({ + ord_item = { + 'numero_ordem': od.numero_ordem, + 'materia': mat_item + } + if include_momento_meta: + ord_item.update({ + 'momento_id': od.pk, + }) + ordem_items.append(ord_item) + + if include_momentos_block: + momentos_items.append({ + 'momento_sessao_id': od.pk, + 'momento_sessao': 'ordem_dia', + 'materias': [{ + 'momento_id': od.pk, + 'numero_ordem': od.numero_ordem, + 'tipo_votacao': od.get_tipo_votacao_display(), + 'materia': mat_item, + }] + }) + + payload = { 'sessao': pk, 'sessao_nome': str(sessao), 'tipo_sessao': { @@ -171,4 +213,69 @@ class _SessaoPlenariaViewSet: }, 'expediente': expediente_items, 'ordem_dia': ordem_items - }) + } + + if include_momentos_block: + payload['momentos'] = momentos_items + + return payload + + @action(detail=True, methods=['get'], url_path='materias') + def materias(self, request, *args, **kwargs): + sessao: SessaoPlenaria = self.get_object() + payload = self._build_materias_payload( + request, + sessao, + include_momento_meta=False, + include_momentos_block=False + ) + return Response(payload) + + @action(detail=True, methods=['post'], url_path='materias/enviar', + permission_classes=[IsAuthenticated]) + def materias_enviar(self, request, *args, **kwargs): + sessao: SessaoPlenaria = self.get_object() + payload = self._build_materias_payload( + request, + sessao, + include_momento_meta=False, + include_momentos_block=False + ) + + base_url = request.data.get('url') or getattr(settings, 'SESSAO_MATERIAS_API_URL', '') + api_key = request.data.get('api_key') or getattr(settings, 'SESSAO_MATERIAS_API_KEY', '') + + if not base_url: + return Response({ + 'detail': 'Configurar SESSAO_MATERIAS_API_URL ou enviar "url" no body da requisição.' + }, status=400) + + target_url = f"{base_url.rstrip('/')}/api/integracaoLegisSessao" + + headers = {'Content-Type': 'application/json'} + if api_key: + headers['x-api-key'] = api_key + + body = payload + + try: + resp = requests.post(target_url, json=body, headers=headers, timeout=15) + except requests.RequestException as exc: + logger.exception('Erro ao enviar materias da sessão %s para API externa', sessao.pk) + return Response({ + 'detail': 'Erro ao enviar para API externa', + 'error': str(exc) + }, status=502) + + try: + resp_body = resp.json() + except ValueError: + resp_body = resp.text + + return Response({ + 'sessao': sessao.pk, + 'target_url': target_url, + 'remote_status_code': resp.status_code, + 'remote_response': resp_body, + 'sent_payload': body + }, status=200 if resp.ok else 502) diff --git a/sapl/sessao/views.py b/sapl/sessao/views.py index 7c76d3196..438ae9ce7 100755 --- a/sapl/sessao/views.py +++ b/sapl/sessao/views.py @@ -1347,6 +1347,7 @@ class SessaoCrud(Crud): return context class DetailView(Crud.DetailView): + template_name = 'sessao/sessaoplenaria_detail.html' @property def layout_key(self): diff --git a/sapl/settings.py b/sapl/settings.py index d5a539959..ee6a5f06b 100644 --- a/sapl/settings.py +++ b/sapl/settings.py @@ -308,6 +308,14 @@ ONLYOFFICE_URL = config('ONLYOFFICE_URL', default='http://localhost:8002') ONLYOFFICE_JWT_SECRET = config('ONLYOFFICE_JWT_SECRET', default='') ONLYOFFICE_JWT_ENABLED = config('ONLYOFFICE_JWT_ENABLED', cast=bool, default=False) +# Integração externa de matérias da sessão +SESSAO_MATERIAS_API_URL = config('SESSAO_MATERIAS_API_URL', default='') +SESSAO_MATERIAS_API_KEY = config('SESSAO_MATERIAS_API_KEY', default='') +SESSAO_MATERIAS_API_USERNAME = config('SESSAO_MATERIAS_API_USERNAME', default='') +SESSAO_MATERIAS_API_PASSWORD = config('SESSAO_MATERIAS_API_PASSWORD', default='') +# lista separada por vírgula, ex: "ROLE_USER,ROLE_ADMIN" +SESSAO_MATERIAS_API_AUTHORITIES = config('SESSAO_MATERIAS_API_AUTHORITIES', default='') + # Feature Flag WAFFLE_FLAG_DEFAULT = False WAFFLE_SWITCH_DEFAULT = False diff --git a/sapl/templates/sessao/sessaoplenaria_detail.html b/sapl/templates/sessao/sessaoplenaria_detail.html new file mode 100644 index 000000000..c8fa36537 --- /dev/null +++ b/sapl/templates/sessao/sessaoplenaria_detail.html @@ -0,0 +1,116 @@ +{% extends "crud/detail.html" %} +{% load i18n %} + +{% block extra_actions %} +