Browse Source

Feat(Legislativo): Primeira fase integração, envio dos dados [AB#849]

pull/3858/head
root 10 months ago
parent
commit
5b6c45d19e
  1. 0
      docs/LOCALHOST_SETUP.md
  2. 163
      sapl/api/views_sessao.py
  3. 1
      sapl/sessao/views.py
  4. 8
      sapl/settings.py
  5. 116
      sapl/templates/sessao/sessaoplenaria_detail.html

0
LOCALHOST_SETUP.md → docs/LOCALHOST_SETUP.md

163
sapl/api/views_sessao.py

@ -1,18 +1,24 @@
import logging
import requests
from django.apps.registry import apps 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.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from drfautoapi.drfautoapi import ApiViewSetConstrutor, \ from drfautoapi.drfautoapi import ApiViewSetConstrutor, \
customize, wrapper_queryset_response_for_drf_action customize, wrapper_queryset_response_for_drf_action
from sapl.api.serializers import ChoiceSerializer,\ from sapl.api.serializers import ChoiceSerializer,\
SessaoPlenariaECidadaniaSerializer 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 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( ApiViewSetConstrutor.build_class(
[ [
@ -49,16 +55,13 @@ class _SessaoPlenariaViewSet:
self.serializer_class = SessaoPlenariaECidadaniaSerializer self.serializer_class = SessaoPlenariaECidadaniaSerializer
return self.list(request, *args, **kwargs) return self.list(request, *args, **kwargs)
# New endpoint: materias da sessão (expediente e ordem do dia) def _build_materias_payload(self, request, sessao: SessaoPlenaria,
@action(detail=True, methods=['get'], url_path='materias') include_momento_meta=False,
def materias(self, request, *args, **kwargs): include_momentos_block=False):
sessao: SessaoPlenaria = self.get_object()
pk = sessao.pk pk = sessao.pk
# Data de referência da sessão para status de tramitação
data_ref = sessao.data_fim or sessao.data_inicio data_ref = sessao.data_fim or sessao.data_inicio
def autores_info(m: MateriaLegislativa): def autores_info(m: MateriaLegislativa):
# retorna lista de {id, nome}
return list(m.autores.values('id', 'nome')) return list(m.autores.values('id', 'nome'))
def tramitacao_info(m: MateriaLegislativa): def tramitacao_info(m: MateriaLegislativa):
@ -79,11 +82,11 @@ class _SessaoPlenariaViewSet:
fase = '' fase = ''
return status, 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 {} extra = extra or {}
pdf_url = None pdf_url = None
try: try:
# só expõe URL se houver ao menos 1 PDF acessório
if m.documentoacessorio_set.filter(arquivo__iendswith='pdf').exists(): if m.documentoacessorio_set.filter(arquivo__iendswith='pdf').exists():
pdf_url = request.build_absolute_uri( pdf_url = request.build_absolute_uri(
reverse('sapl.materia:merge_docacessorios', kwargs={'pk': m.pk}) reverse('sapl.materia:merge_docacessorios', kwargs={'pk': m.pk})
@ -94,15 +97,15 @@ class _SessaoPlenariaViewSet:
status_tram, fase_materia = tramitacao_info(m) status_tram, fase_materia = tramitacao_info(m)
return { return {
'id': m.pk, 'id_materia': m.pk,
'nome_documento': str(m), 'nome_documento': str(m),
'autoria': autores_info(m), # [{id, nome}] 'autoria': autores_info(m),
'tipo_materia': { 'tipo_materia': {
'id': m.tipo_id, 'id': m.tipo_id,
'nome': getattr(m.tipo, 'descricao', None) 'nome': getattr(m.tipo, 'descricao', None)
}, },
'ementa': m.ementa, 'ementa': m.ementa,
'status': status_sessao, # alias de status da sessão 'status': status_sessao,
'status_sessao': status_sessao, 'status_sessao': status_sessao,
'status_tramitacao': status_tram, 'status_tramitacao': status_tram,
'fase_materia': fase_materia, 'fase_materia': fase_materia,
@ -114,11 +117,10 @@ class _SessaoPlenariaViewSet:
**extra **extra
} }
# Expediente
expediente_items = [] expediente_items = []
momentos_items = []
for em in ExpedienteMateria.objects.select_related('materia', 'materia__tipo').filter(sessao_plenaria_id=pk): for em in ExpedienteMateria.objects.select_related('materia', 'materia__tipo').filter(sessao_plenaria_id=pk):
m = em.materia m = em.materia
# Determina status no contexto da sessão (votação/retirada/leitura)
rv = em.registrovotacao_set.filter(materia=m).first() rv = em.registrovotacao_set.filter(materia=m).first()
rp = em.retiradapauta_set.filter(materia=m).first() rp = em.retiradapauta_set.filter(materia=m).first()
rl = em.registroleitura_set.filter(materia=m).first() rl = em.registroleitura_set.filter(materia=m).first()
@ -130,15 +132,34 @@ class _SessaoPlenariaViewSet:
status_sessao = 'Leitura' status_sessao = 'Leitura'
else: else:
status_sessao = '' status_sessao = ''
expediente_items.append(material_item( exp_item = {
'materia': material_item(
m, m,
status_sessao, status_sessao,
{ extra={
'tipo_votacao': em.get_tipo_votacao_display(), '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 = [] ordem_items = []
for od in OrdemDia.objects.select_related('materia', 'materia__tipo').filter(sessao_plenaria_id=pk): for od in OrdemDia.objects.select_related('materia', 'materia__tipo').filter(sessao_plenaria_id=pk):
m = od.materia m = od.materia
@ -153,16 +174,37 @@ class _SessaoPlenariaViewSet:
status_sessao = 'Leitura' status_sessao = 'Leitura'
else: else:
status_sessao = '' status_sessao = ''
ordem_items.append(material_item( mat_item = material_item(
m, m,
status_sessao, status_sessao,
{ extra={
'numero_ordem': od.numero_ordem,
'tipo_votacao': od.get_tipo_votacao_display(), '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': pk,
'sessao_nome': str(sessao), 'sessao_nome': str(sessao),
'tipo_sessao': { 'tipo_sessao': {
@ -171,4 +213,69 @@ class _SessaoPlenariaViewSet:
}, },
'expediente': expediente_items, 'expediente': expediente_items,
'ordem_dia': ordem_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)

1
sapl/sessao/views.py

@ -1347,6 +1347,7 @@ class SessaoCrud(Crud):
return context return context
class DetailView(Crud.DetailView): class DetailView(Crud.DetailView):
template_name = 'sessao/sessaoplenaria_detail.html'
@property @property
def layout_key(self): def layout_key(self):

8
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_SECRET = config('ONLYOFFICE_JWT_SECRET', default='')
ONLYOFFICE_JWT_ENABLED = config('ONLYOFFICE_JWT_ENABLED', cast=bool, default=False) 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 # Feature Flag
WAFFLE_FLAG_DEFAULT = False WAFFLE_FLAG_DEFAULT = False
WAFFLE_SWITCH_DEFAULT = False WAFFLE_SWITCH_DEFAULT = False

116
sapl/templates/sessao/sessaoplenaria_detail.html

@ -0,0 +1,116 @@
{% extends "crud/detail.html" %}
{% load i18n %}
{% block extra_actions %}
<div class="actions btn-group btn-group-sm ml-2" role="group">
<button type="button" id="sync-painel-btn" class="btn btn-outline-primary">
{% trans "Sincronizar Painel" %}
</button>
</div>
<span id="sync-painel-feedback" class="ml-2 small text-muted"></span>
<script>
(function () {
const btn = document.getElementById('sync-painel-btn');
if (!btn) {
console.warn('Sincronizar Painel: botão não encontrado na página.');
return;
}
const feedback = document.getElementById('sync-painel-feedback');
const endpoint = window.location.origin + "/api/sessao/sessaoplenaria/{{ view.object.pk }}/materias/enviar/";
console.log('Sincronizar Painel: endpoint', endpoint);
function getCsrfToken() {
const name = 'csrftoken=';
const decodedCookie = decodeURIComponent(document.cookie || '');
const ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i].trim();
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
return '';
}
function setStatus(message, isError) {
feedback.textContent = message || '';
if (isError) {
feedback.classList.remove('text-muted');
feedback.classList.add('text-danger');
} else {
feedback.classList.remove('text-danger');
feedback.classList.add('text-muted');
}
}
btn.addEventListener('click', function () {
setStatus('{% trans "Enviando..." %}', false);
btn.disabled = true;
const body = {};
// Exibe no console o payload que será enviado
try {
console.log('Sincronizar Painel - payload:', JSON.stringify(body));
} catch (e) {
console.log('Sincronizar Painel - payload (obj):', body);
}
console.log('Sincronizar Painel: iniciando requisição');
const doRequest = () => {
return fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken(),
},
credentials: 'same-origin',
body: JSON.stringify(body)
});
};
const handleResponse = async (resp) => {
const statusCode = resp.status;
let msg = resp.ok ? '{% trans "Sincronizado com sucesso." %}' : '{% trans "Sincronizado (mensagem da API)." %}';
let detail = '';
try {
const data = await resp.json();
if (data && data.remote_status_code) {
// se 400 mas com mensagem de atualizado, trata como sucesso
if (data.remote_status_code === 400 && data.remote_response && typeof data.remote_response === 'string') {
const xmlMatch = data.remote_response.match(/<localizedMessage>(.*?)<\/localizedMessage>/i);
if (xmlMatch && xmlMatch[1]) {
detail = xmlMatch[1];
msg = '{% trans "Sincronizado" %}';
} else {
msg = '{% trans "Sincronizado" %}';
}
} else {
msg += ` (HTTP ${data.remote_status_code})`;
}
} else {
msg += ` (HTTP ${statusCode})`;
}
} catch (jsonErr) {
msg += ` (HTTP ${statusCode})`;
}
const finalMsg = detail ? `${msg} - ${detail}` : msg;
setStatus(finalMsg, false);
};
const handleError = (err) => {
setStatus('{% trans "Erro de rede ao sincronizar." %}', true);
console.error(err);
};
// usa fetch com credenciais; se falhar, loga o erro no console
doRequest()
.then(handleResponse)
.catch(handleError)
.finally(() => {
btn.disabled = false;
});
});
})();
</script>
{% endblock extra_actions %}
Loading…
Cancel
Save