- Aprimorar extracao de dados Lattes no backend - Melhorar estilos CSS da secao Lattes - Ajustes no componente ConsultorCard para exibicao do Lattes
845 lines
33 KiB
Python
845 lines
33 KiB
Python
import asyncio
|
|
import html
|
|
import re
|
|
import unicodedata
|
|
from io import BytesIO
|
|
from datetime import datetime
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from fastapi.responses import StreamingResponse
|
|
from pydantic import BaseModel
|
|
from typing import Optional, List
|
|
|
|
|
|
LATIN1_BYTE_MAP = {
|
|
192: 'À', 193: 'Á', 194: 'Â', 195: 'Ã', 196: 'Ä',
|
|
199: 'Ç',
|
|
200: 'È', 201: 'É', 202: 'Ê', 203: 'Ë',
|
|
204: 'Ì', 205: 'Í', 206: 'Î', 207: 'Ï',
|
|
209: 'Ñ',
|
|
210: 'Ò', 211: 'Ó', 212: 'Ô', 213: 'Õ', 214: 'Ö',
|
|
217: 'Ù', 218: 'Ú', 219: 'Û', 220: 'Ü',
|
|
224: 'à', 225: 'á', 226: 'â', 227: 'ã', 228: 'ä',
|
|
231: 'ç',
|
|
232: 'è', 233: 'é', 234: 'ê', 235: 'ë',
|
|
236: 'ì', 237: 'í', 238: 'î', 239: 'ï',
|
|
241: 'ñ',
|
|
242: 'ò', 243: 'ó', 244: 'ô', 245: 'õ', 246: 'ö',
|
|
249: 'ù', 250: 'ú', 251: 'û', 252: 'ü',
|
|
}
|
|
|
|
|
|
def corrigir_encoding(texto: str) -> str:
|
|
if not texto:
|
|
return texto
|
|
|
|
def substituir_byte(match):
|
|
byte_val = int(match.group(1))
|
|
return LATIN1_BYTE_MAP.get(byte_val, match.group(0))
|
|
|
|
pattern = r'(?<=[\w\u00C0-\u00FF])(\d{3})(?=[\w\u00C0-\u00FF])'
|
|
resultado = texto
|
|
for _ in range(5):
|
|
novo = re.sub(pattern, substituir_byte, resultado)
|
|
if novo == resultado:
|
|
break
|
|
resultado = novo
|
|
return resultado
|
|
|
|
|
|
def normalizar_texto(texto: str) -> str:
|
|
if not texto:
|
|
return ""
|
|
texto = html.unescape(texto)
|
|
texto = unicodedata.normalize('NFD', texto)
|
|
texto = ''.join(c for c in texto if unicodedata.category(c) != 'Mn')
|
|
return texto.lower()
|
|
|
|
from ...application.use_cases.obter_ranking import ObterRankingUseCase
|
|
from ...application.use_cases.obter_consultor import ObterConsultorUseCase
|
|
from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
|
|
from ...application.mappers.ranking_mapper import RankingMapper
|
|
from ..schemas.consultor_schema import (
|
|
RankingResponseSchema,
|
|
RankingDetalhadoResponseSchema,
|
|
ConsultorDetalhadoSchema,
|
|
ConsultorResumoSchema,
|
|
)
|
|
from ..schemas.ranking_schema import (
|
|
RankingPaginadoResponseSchema,
|
|
ConsultorRankingResumoSchema,
|
|
EstatisticasRankingSchema,
|
|
JobStatusSchema,
|
|
ProcessarRankingRequestSchema,
|
|
ProcessarRankingResponseSchema,
|
|
ConsultaNomeSchema,
|
|
PosicaoRankingSchema,
|
|
SugestaoConsultorSchema,
|
|
SugerirConsultoresResponseSchema,
|
|
AreaAvaliacaoSchema,
|
|
)
|
|
from .dependencies import get_repository, get_ranking_store, get_processar_job, get_es_client, get_ranking_oracle_repo
|
|
from ...infrastructure.elasticsearch.client import ElasticsearchClient
|
|
from ...application.jobs.job_status import job_status
|
|
|
|
router = APIRouter(prefix="/api/v1", tags=["ranking"])
|
|
|
|
|
|
@router.get("/ranking", response_model=RankingResponseSchema)
|
|
async def obter_ranking(
|
|
limite: int = Query(default=100, ge=1, le=1000, description="Limite de consultores"),
|
|
offset: int = Query(default=0, ge=0, description="Offset para paginação"),
|
|
componente: Optional[str] = Query(
|
|
default=None, description="Filtrar por bloco (a, c, d)"
|
|
),
|
|
repository: ConsultorRepositoryImpl = Depends(get_repository),
|
|
store = Depends(get_ranking_store),
|
|
):
|
|
if store.is_ready():
|
|
total, entries = store.get_slice(offset=offset, limit=limite)
|
|
consultores_schema = [
|
|
ConsultorResumoSchema(
|
|
id_pessoa=e.id_pessoa,
|
|
nome=e.nome,
|
|
anos_atuacao=e.anos_atuacao,
|
|
ativo=e.ativo,
|
|
veterano=e.anos_atuacao >= 10,
|
|
pontuacao_total=e.pontuacao_total,
|
|
bloco_a=e.bloco_a,
|
|
bloco_c=e.bloco_c,
|
|
bloco_d=e.bloco_d,
|
|
rank=e.posicao,
|
|
)
|
|
for e in entries
|
|
]
|
|
return RankingResponseSchema(
|
|
total=total, limite=limite, offset=offset, consultores=consultores_schema
|
|
)
|
|
|
|
use_case = ObterRankingUseCase(repository=repository)
|
|
consultores_dto = await use_case.executar(limite=limite, componente=componente)
|
|
total = await repository.contar_total()
|
|
consultores_schema = [ConsultorResumoSchema(**vars(dto)) for dto in consultores_dto]
|
|
return RankingResponseSchema(total=total, limite=limite, offset=offset, consultores=consultores_schema)
|
|
|
|
|
|
@router.get("/ranking/detalhado", response_model=RankingDetalhadoResponseSchema)
|
|
async def obter_ranking_detalhado(
|
|
limite: int = Query(default=100, ge=1, le=1000, description="Limite de consultores"),
|
|
componente: Optional[str] = Query(
|
|
default=None, description="Filtrar por bloco (a, c, d)"
|
|
),
|
|
repository: ConsultorRepositoryImpl = Depends(get_repository),
|
|
):
|
|
use_case = ObterRankingUseCase(repository=repository)
|
|
consultores_dto = await use_case.executar_detalhado(limite=limite, componente=componente)
|
|
|
|
total = await repository.contar_total()
|
|
|
|
consultores_schema = [
|
|
ConsultorDetalhadoSchema(**dto.to_dict()) for dto in consultores_dto
|
|
]
|
|
|
|
return RankingDetalhadoResponseSchema(total=total, limite=limite, consultores=consultores_schema)
|
|
|
|
|
|
@router.get("/consultor/{id_pessoa}", response_model=ConsultorDetalhadoSchema)
|
|
async def obter_consultor(
|
|
id_pessoa: int,
|
|
repository: ConsultorRepositoryImpl = Depends(get_repository),
|
|
store = Depends(get_ranking_store),
|
|
):
|
|
use_case = ObterConsultorUseCase(repository=repository)
|
|
rank = None
|
|
if store.is_ready():
|
|
found = store.get_by_id(id_pessoa)
|
|
rank = found.posicao if found else None
|
|
|
|
consultor = await use_case.executar(id_pessoa=id_pessoa, rank=rank)
|
|
|
|
if not consultor:
|
|
raise HTTPException(status_code=404, detail=f"Consultor {id_pessoa} não encontrado")
|
|
|
|
return consultor
|
|
|
|
|
|
@router.get("/health")
|
|
async def health_check():
|
|
return {"status": "ok", "message": "API Ranking CAPES funcionando"}
|
|
|
|
|
|
@router.get("/ranking/selos")
|
|
async def listar_selos():
|
|
from ...infrastructure.ranking_store import SELOS_DISPONIVEIS
|
|
selos_info = {
|
|
"PRESID_CAMARA": {"label": "Presidente Câmara", "icone": "👑", "grupo": "funcoes"},
|
|
"COORD_PPG": {"label": "Coord. PPG", "icone": "🎓", "grupo": "funcoes"},
|
|
"BPQ": {"label": "Bolsista PQ", "icone": "🏅", "grupo": "funcoes"},
|
|
"AUTOR_GP": {"label": "Autor Grande Prêmio", "icone": "🏆", "grupo": "premiacoes"},
|
|
"AUTOR_PREMIO": {"label": "Autor Prêmio", "icone": "🥇", "grupo": "premiacoes"},
|
|
"AUTOR_MENCAO": {"label": "Autor Menção", "icone": "🥈", "grupo": "premiacoes"},
|
|
"ORIENT_GP": {"label": "Orientador GP", "icone": "🏆", "grupo": "premiacoes"},
|
|
"ORIENT_PREMIO": {"label": "Orientador Prêmio", "icone": "🎖️", "grupo": "premiacoes"},
|
|
"ORIENT_MENCAO": {"label": "Orientador Menção", "icone": "📜", "grupo": "premiacoes"},
|
|
"COORIENT_GP": {"label": "Coorientador GP", "icone": "🏆", "grupo": "premiacoes"},
|
|
"COORIENT_PREMIO": {"label": "Coorientador Prêmio", "icone": "🎖️", "grupo": "premiacoes"},
|
|
"COORIENT_MENCAO": {"label": "Coorientador Menção", "icone": "📜", "grupo": "premiacoes"},
|
|
"ORIENT_POS_DOC": {"label": "Orient. Pós-Doc", "icone": "🔬", "grupo": "orientacoes"},
|
|
"ORIENT_TESE": {"label": "Orient. Tese", "icone": "📚", "grupo": "orientacoes"},
|
|
"ORIENT_DISS": {"label": "Orient. Dissertação", "icone": "📄", "grupo": "orientacoes"},
|
|
"CO_ORIENT_POS_DOC": {"label": "Coorient. Pós-Doc", "icone": "🔬", "grupo": "coorientacoes"},
|
|
"CO_ORIENT_TESE": {"label": "Coorient. Tese", "icone": "📚", "grupo": "coorientacoes"},
|
|
"CO_ORIENT_DISS": {"label": "Coorient. Dissertação", "icone": "📄", "grupo": "coorientacoes"},
|
|
}
|
|
return {
|
|
"selos": [
|
|
{"codigo": s, **selos_info.get(s, {"label": s, "icone": "🏷️", "grupo": "outros"})}
|
|
for s in SELOS_DISPONIVEIS
|
|
]
|
|
}
|
|
|
|
|
|
@router.get("/ranking/paginado", response_model=RankingPaginadoResponseSchema)
|
|
async def ranking_paginado(
|
|
page: int = Query(default=1, ge=1, description="Número da página"),
|
|
size: int = Query(default=50, ge=1, le=1000, description="Tamanho da página (máx 1000)"),
|
|
ativo: Optional[bool] = Query(default=None, description="Filtrar por status ativo"),
|
|
selos: Optional[str] = Query(default=None, description="Filtrar por selos (separados por vírgula)"),
|
|
oracle_repo = Depends(get_ranking_oracle_repo),
|
|
es_client: ElasticsearchClient = Depends(get_es_client),
|
|
repository: ConsultorRepositoryImpl = Depends(get_repository),
|
|
):
|
|
import json as json_lib
|
|
|
|
if not oracle_repo:
|
|
raise HTTPException(status_code=503, detail="Oracle não configurado")
|
|
|
|
total = oracle_repo.contar_total(filtro_ativo=ativo)
|
|
if total == 0:
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="Ranking ainda não foi processado. Execute POST /api/v1/ranking/processar.",
|
|
)
|
|
|
|
consultores = oracle_repo.buscar_paginado(page=page, size=size, filtro_ativo=ativo)
|
|
total_pages = (total + size - 1) // size
|
|
|
|
consultores_schema = []
|
|
consultores_dados = []
|
|
faltando_idiomas = []
|
|
for c in consultores:
|
|
try:
|
|
d = json_lib.loads(c.json_detalhes) if isinstance(c.json_detalhes, str) else c.json_detalhes or {}
|
|
except (json_lib.JSONDecodeError, TypeError):
|
|
d = {}
|
|
consultores_dados.append((c, d))
|
|
if not d.get("idiomas"):
|
|
faltando_idiomas.append((c.id_pessoa, d))
|
|
|
|
faltando_lattes = [(c.id_pessoa, d) for c, d in consultores_dados if not d.get("lattes")]
|
|
ids_buscar = list(set([item[0] for item in faltando_idiomas] + [item[0] for item in faltando_lattes]))
|
|
|
|
if ids_buscar:
|
|
docs = await es_client.buscar_por_ids(
|
|
ids_buscar,
|
|
source_fields=["id", "dadosPessoais", "idiomas", "atuacoes", "formacoes", "identificadorLattes", "titulacoes"],
|
|
)
|
|
docs_map = {int(doc.get("id")): doc for doc in docs if doc.get("id")}
|
|
|
|
for id_pessoa, detalhes in faltando_idiomas:
|
|
doc = docs_map.get(int(id_pessoa))
|
|
if not doc:
|
|
continue
|
|
idiomas = repository._extrair_idiomas(doc)
|
|
if idiomas:
|
|
detalhes["idiomas"] = [
|
|
{
|
|
"idioma": i.idioma,
|
|
"nivel_leitura": i.nivel_leitura,
|
|
"nivel_escrita": i.nivel_escrita,
|
|
"nivel_fala": i.nivel_fala,
|
|
"nivel_compreensao": i.nivel_compreensao,
|
|
}
|
|
for i in idiomas
|
|
]
|
|
if not detalhes.get("titulacao"):
|
|
titulacao = repository._extrair_titulacao(doc)
|
|
if titulacao:
|
|
detalhes["titulacao"] = titulacao
|
|
|
|
for id_pessoa, detalhes in faltando_lattes:
|
|
doc = docs_map.get(int(id_pessoa))
|
|
if not doc:
|
|
continue
|
|
id_lattes_obj = doc.get("identificadorLattes")
|
|
titulacoes_raw = doc.get("titulacoes", [])
|
|
if id_lattes_obj and id_lattes_obj.get("descricao"):
|
|
id_lattes = id_lattes_obj.get("descricao")
|
|
titulacoes_formatadas = []
|
|
for t in titulacoes_raw:
|
|
grau_obj = t.get("grauAcademico", {})
|
|
ies_obj = t.get("ies", {})
|
|
titulacoes_formatadas.append({
|
|
"grau": grau_obj.get("nome", ""),
|
|
"ano": t.get("ano"),
|
|
"ies_nome": ies_obj.get("nome"),
|
|
"ies_sigla": ies_obj.get("sigla"),
|
|
"area": t.get("areaConhecimento", {}).get("nome"),
|
|
"pais": "Brasil",
|
|
})
|
|
detalhes["lattes"] = {
|
|
"id_lattes": id_lattes,
|
|
"url": f"http://lattes.cnpq.br/{id_lattes}",
|
|
"titulacoes": titulacoes_formatadas,
|
|
}
|
|
|
|
for c, d in consultores_dados:
|
|
tipos_atuacao = RankingMapper._extrair_tipos_atuacao(d)
|
|
consultores_schema.append(
|
|
ConsultorRankingResumoSchema(
|
|
id_pessoa=c.id_pessoa,
|
|
nome=c.nome,
|
|
posicao=c.posicao,
|
|
pontuacao_total=float(c.pontuacao_total),
|
|
bloco_a=float(c.componente_a),
|
|
bloco_b=float(c.componente_b),
|
|
bloco_c=float(c.componente_c),
|
|
bloco_d=float(c.componente_d),
|
|
bloco_e=float(c.componente_e),
|
|
ativo=c.ativo,
|
|
anos_atuacao=float(c.anos_atuacao),
|
|
tipos_atuacao=tipos_atuacao,
|
|
coordenador_ppg=bool(d.get("coordenador_ppg", False)),
|
|
consultoria=d.get("consultoria"),
|
|
coordenacoes_capes=d.get("coordenacoes_capes"),
|
|
inscricoes=d.get("inscricoes"),
|
|
avaliacoes_comissao=d.get("avaliacoes_comissao"),
|
|
premiacoes=d.get("premiacoes"),
|
|
bolsas_cnpq=d.get("bolsas_cnpq"),
|
|
participacoes=d.get("participacoes"),
|
|
orientacoes=d.get("orientacoes"),
|
|
membros_banca=d.get("membros_banca"),
|
|
docencias=d.get("docencias"),
|
|
idiomas=d.get("idiomas"),
|
|
titulacao=d.get("titulacao"),
|
|
pontuacao=d.get("pontuacao"),
|
|
lattes=d.get("lattes"),
|
|
)
|
|
)
|
|
|
|
return RankingPaginadoResponseSchema(
|
|
total=total,
|
|
page=page,
|
|
size=size,
|
|
total_pages=total_pages,
|
|
consultores=consultores_schema
|
|
)
|
|
|
|
|
|
@router.get("/ranking/busca", response_model=List[ConsultaNomeSchema])
|
|
async def buscar_por_nome(
|
|
nome: str = Query(..., min_length=3, description="Nome (ou parte) para buscar"),
|
|
limit: int = Query(default=5, ge=1, le=20, description="Limite de resultados"),
|
|
oracle_repo = Depends(get_ranking_oracle_repo),
|
|
):
|
|
if not oracle_repo:
|
|
raise HTTPException(status_code=503, detail="Oracle não configurado")
|
|
|
|
resultados = oracle_repo.buscar_por_nome(nome=nome, limit=limit)
|
|
return [
|
|
ConsultaNomeSchema(
|
|
id_pessoa=r["ID_PESSOA"],
|
|
nome=r["NOME"],
|
|
posicao=r["POSICAO"],
|
|
pontuacao_total=float(r["PONTUACAO_TOTAL"]),
|
|
)
|
|
for r in resultados
|
|
]
|
|
|
|
|
|
@router.get("/ranking/estatisticas", response_model=EstatisticasRankingSchema)
|
|
async def ranking_estatisticas(
|
|
oracle_repo = Depends(get_ranking_oracle_repo),
|
|
):
|
|
if not oracle_repo:
|
|
raise HTTPException(status_code=503, detail="Oracle não configurado")
|
|
|
|
total = oracle_repo.contar_total()
|
|
if total == 0:
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="Ranking ainda não foi processado. Execute POST /api/v1/ranking/processar.",
|
|
)
|
|
|
|
estatisticas = oracle_repo.obter_estatisticas()
|
|
distribuicao = oracle_repo.obter_distribuicao()
|
|
|
|
return EstatisticasRankingSchema(
|
|
total_consultores=estatisticas.get("total_consultores", 0),
|
|
total_ativos=estatisticas.get("total_ativos", 0),
|
|
total_inativos=estatisticas.get("total_inativos", 0),
|
|
ultima_atualizacao=estatisticas.get("ultima_atualizacao"),
|
|
pontuacao_media=estatisticas.get("pontuacao_media", 0),
|
|
pontuacao_maxima=estatisticas.get("pontuacao_maxima", 0),
|
|
pontuacao_minima=estatisticas.get("pontuacao_minima", 0),
|
|
media_blocos=estatisticas.get("media_componentes", {}),
|
|
distribuicao=distribuicao
|
|
)
|
|
|
|
|
|
@router.get("/ranking/status", response_model=JobStatusSchema)
|
|
async def status_processamento():
|
|
return JobStatusSchema(**job_status.to_dict())
|
|
|
|
|
|
@router.post("/ranking/processar", response_model=ProcessarRankingResponseSchema)
|
|
async def processar_ranking(
|
|
request: ProcessarRankingRequestSchema = ProcessarRankingRequestSchema(),
|
|
job = Depends(get_processar_job),
|
|
):
|
|
if job_status.is_running:
|
|
raise HTTPException(status_code=409, detail="Job já está em execução")
|
|
|
|
asyncio.create_task(job.executar(limpar_antes=request.limpar_antes))
|
|
|
|
return ProcessarRankingResponseSchema(
|
|
sucesso=True,
|
|
mensagem="Processamento do ranking iniciado em background",
|
|
job_id="ranking_job"
|
|
)
|
|
|
|
|
|
@router.get("/ranking/posicao/{id_pessoa}", response_model=PosicaoRankingSchema)
|
|
async def obter_posicao_ranking(
|
|
id_pessoa: int,
|
|
oracle_repo = Depends(get_ranking_oracle_repo),
|
|
):
|
|
if not oracle_repo:
|
|
raise HTTPException(status_code=503, detail="Oracle não configurado")
|
|
|
|
total = oracle_repo.contar_total()
|
|
entry = oracle_repo.buscar_por_id(id_pessoa)
|
|
|
|
if not entry:
|
|
return PosicaoRankingSchema(
|
|
id_pessoa=id_pessoa,
|
|
nome="",
|
|
posicao=None,
|
|
total_consultores=total,
|
|
pontuacao_total=0,
|
|
bloco_a=0,
|
|
bloco_b=0,
|
|
bloco_c=0,
|
|
bloco_d=0,
|
|
bloco_e=0,
|
|
ativo=False,
|
|
encontrado=False,
|
|
)
|
|
|
|
return PosicaoRankingSchema(
|
|
id_pessoa=entry.id_pessoa,
|
|
nome=entry.nome,
|
|
posicao=entry.posicao,
|
|
total_consultores=total,
|
|
pontuacao_total=float(entry.pontuacao_total),
|
|
bloco_a=float(entry.componente_a),
|
|
bloco_b=float(entry.componente_b),
|
|
bloco_c=float(entry.componente_c),
|
|
bloco_d=float(entry.componente_d),
|
|
bloco_e=float(entry.componente_e),
|
|
ativo=entry.ativo,
|
|
encontrado=True,
|
|
)
|
|
|
|
|
|
@router.get("/consultor/{id_pessoa}/raw")
|
|
async def obter_consultor_raw(
|
|
id_pessoa: int,
|
|
es_client: ElasticsearchClient = Depends(get_es_client),
|
|
):
|
|
try:
|
|
documento = await es_client.buscar_documento_completo(id_pessoa)
|
|
if not documento:
|
|
raise HTTPException(status_code=404, detail=f"Consultor {id_pessoa} não encontrado no Elasticsearch")
|
|
return documento
|
|
except RuntimeError as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.get("/consultor/{id_pessoa}/lattes")
|
|
async def obter_lattes(
|
|
id_pessoa: int,
|
|
es_client: ElasticsearchClient = Depends(get_es_client),
|
|
):
|
|
docs = await es_client.buscar_por_ids(
|
|
[id_pessoa],
|
|
source_fields=[
|
|
"id", "dadosPessoais", "identificadorLattes", "titulacoes",
|
|
"idiomas", "areasConhecimento", "enderecos", "atuacoes"
|
|
],
|
|
)
|
|
if not docs:
|
|
return {"encontrado": False, "motivo": "Consultor não encontrado"}
|
|
|
|
doc = docs[0]
|
|
id_lattes_obj = doc.get("identificadorLattes")
|
|
|
|
if not id_lattes_obj or not id_lattes_obj.get("descricao"):
|
|
return {"encontrado": False, "motivo": "Currículo Lattes não cadastrado"}
|
|
|
|
id_lattes = id_lattes_obj.get("descricao")
|
|
dados_pessoais = doc.get("dadosPessoais", {})
|
|
titulacoes_raw = doc.get("titulacoes", [])
|
|
idiomas_raw = doc.get("idiomas", [])
|
|
areas_raw = doc.get("areasConhecimento", [])
|
|
enderecos_raw = doc.get("enderecos", [])
|
|
atuacoes_raw = doc.get("atuacoes", [])
|
|
|
|
titulacoes = []
|
|
for t in titulacoes_raw:
|
|
grau_obj = t.get("grauAcademico", {})
|
|
ies_obj = t.get("ies", {})
|
|
area_obj = t.get("areaConhecimento", {})
|
|
programa_obj = t.get("programa", {})
|
|
titulacoes.append({
|
|
"grau": grau_obj.get("nome", ""),
|
|
"hierarquia": grau_obj.get("hierarquia"),
|
|
"ano": t.get("ano"),
|
|
"inicio": t.get("inicio"),
|
|
"fim": t.get("fim"),
|
|
"ies_nome": ies_obj.get("nome"),
|
|
"ies_sigla": ies_obj.get("sigla"),
|
|
"ies_status": ies_obj.get("statusJuridico"),
|
|
"area": area_obj.get("nome"),
|
|
"area_avaliacao": area_obj.get("areaAvaliacao", {}).get("nome") if area_obj.get("areaAvaliacao") else None,
|
|
"programa": programa_obj.get("nome") if programa_obj else None,
|
|
"codigo_programa": programa_obj.get("codigo") if programa_obj else None,
|
|
})
|
|
titulacoes.sort(key=lambda x: (x.get("hierarquia") or 99, -(x.get("ano") or 0)))
|
|
|
|
idiomas = []
|
|
for i in idiomas_raw:
|
|
idiomas.append({
|
|
"idioma": i.get("idioma"),
|
|
"proficiencia_leitura": i.get("proficienciaLeitura"),
|
|
"proficiencia_escrita": i.get("proficienciaEscrita"),
|
|
"proficiencia_fala": i.get("proficienciaFala"),
|
|
"proficiencia_compreensao": i.get("proficienciaCompreensao"),
|
|
})
|
|
|
|
areas_conhecimento = []
|
|
for a in areas_raw:
|
|
areas_conhecimento.append({
|
|
"nome": a.get("nome"),
|
|
"area_avaliacao": a.get("areaAvaliacao", {}).get("nome") if a.get("areaAvaliacao") else None,
|
|
})
|
|
|
|
endereco_profissional = None
|
|
for e in enderecos_raw:
|
|
if e.get("tipo") == "Profissional" or e.get("principalFinalidade") == "Sim":
|
|
endereco_profissional = {
|
|
"logradouro": e.get("endereco"),
|
|
"numero": e.get("numero"),
|
|
"complemento": e.get("complemento"),
|
|
"bairro": e.get("bairro"),
|
|
"cep": e.get("cep"),
|
|
"cidade": e.get("cidadeExterior") or e.get("cidade"),
|
|
"pais": e.get("pais"),
|
|
}
|
|
break
|
|
|
|
orientacoes_concluidas = []
|
|
for a in atuacoes_raw:
|
|
tipo = a.get("tipo", "")
|
|
if "Orientação" in tipo and "Concluída" in tipo:
|
|
dados = a.get("dadosOrientacao", {})
|
|
orientacoes_concluidas.append({
|
|
"tipo": tipo,
|
|
"titulo": dados.get("titulo"),
|
|
"ano": dados.get("ano"),
|
|
"orientando": dados.get("orientando", {}).get("nome") if dados.get("orientando") else None,
|
|
"programa": dados.get("programa", {}).get("nome") if dados.get("programa") else None,
|
|
"ies": dados.get("ies", {}).get("sigla") if dados.get("ies") else None,
|
|
})
|
|
|
|
return {
|
|
"encontrado": True,
|
|
"id_lattes": id_lattes,
|
|
"url": f"http://lattes.cnpq.br/{id_lattes}",
|
|
"nome": dados_pessoais.get("nome"),
|
|
"data_nascimento": dados_pessoais.get("nascimento"),
|
|
"nacionalidade": dados_pessoais.get("nacionalidade"),
|
|
"titulacoes": titulacoes,
|
|
"idiomas": idiomas,
|
|
"areas_conhecimento": areas_conhecimento,
|
|
"endereco_profissional": endereco_profissional,
|
|
"orientacoes_concluidas": orientacoes_concluidas[:20],
|
|
"total_orientacoes": len(orientacoes_concluidas),
|
|
"data_atualizacao_lattes": None,
|
|
}
|
|
|
|
|
|
@router.get("/consultor/{id_pessoa}/pdf")
|
|
async def exportar_ficha_pdf(
|
|
id_pessoa: int,
|
|
repository: ConsultorRepositoryImpl = Depends(get_repository),
|
|
es_client: ElasticsearchClient = Depends(get_es_client),
|
|
store = Depends(get_ranking_store),
|
|
):
|
|
from ...application.services.pdf_service import PDFService
|
|
|
|
use_case = ObterConsultorUseCase(repository=repository)
|
|
rank = None
|
|
if store.is_ready():
|
|
found = store.get_by_id(id_pessoa)
|
|
rank = found.posicao if found else None
|
|
|
|
consultor = await use_case.executar(id_pessoa=id_pessoa, rank=rank)
|
|
|
|
if not consultor:
|
|
raise HTTPException(status_code=404, detail=f"Consultor {id_pessoa} não encontrado")
|
|
|
|
try:
|
|
pdf_service = PDFService()
|
|
raw_documento = await es_client.buscar_documento_completo(id_pessoa)
|
|
raw_source = raw_documento.get("_source") if raw_documento else {}
|
|
pdf_bytes = pdf_service.gerar_ficha_consultor(consultor, raw_source)
|
|
except Exception as e:
|
|
import traceback
|
|
traceback.print_exc()
|
|
raise HTTPException(status_code=500, detail=f"Erro ao gerar PDF: {str(e)}")
|
|
|
|
nome_sanitizado = "".join(c if c.isalnum() or c in " -_" else "_" for c in consultor.nome)
|
|
nome_arquivo = f"ficha_consultor_{id_pessoa}_{nome_sanitizado[:30]}_{datetime.now().strftime('%Y%m%d')}.pdf"
|
|
|
|
return StreamingResponse(
|
|
BytesIO(pdf_bytes),
|
|
media_type="application/pdf",
|
|
headers={
|
|
"Content-Disposition": f'attachment; filename="{nome_arquivo}"'
|
|
}
|
|
)
|
|
|
|
|
|
class ConsultorEquipeSchema(BaseModel):
|
|
id_pessoa: int
|
|
nome: str
|
|
ies: Optional[str] = None
|
|
areas_avaliacao: List[str] = []
|
|
areas_conhecimento: List[str] = []
|
|
linhas_pesquisa: List[str] = []
|
|
situacao: str = ""
|
|
foi_coordenador: bool = False
|
|
foi_premiado: bool = False
|
|
posicao_ranking: Optional[int] = None
|
|
pontuacao_ranking: float = 0
|
|
motivos_match: List[str] = []
|
|
|
|
|
|
class GerarEquipePdfRequest(BaseModel):
|
|
tema: str
|
|
area_avaliacao: Optional[str] = None
|
|
consultores: List[ConsultorEquipeSchema]
|
|
|
|
|
|
@router.post("/equipe/pdf")
|
|
async def gerar_pdf_equipe(
|
|
request: GerarEquipePdfRequest,
|
|
):
|
|
from ...application.services.pdf_service import PDFService
|
|
|
|
if not request.consultores:
|
|
raise HTTPException(status_code=400, detail="Nenhum consultor selecionado")
|
|
|
|
try:
|
|
pdf_service = PDFService()
|
|
consultores_dict = [c.model_dump() for c in request.consultores]
|
|
pdf_bytes = pdf_service.gerar_pdf_equipe(
|
|
tema=request.tema,
|
|
area_avaliacao=request.area_avaliacao,
|
|
consultores=consultores_dict
|
|
)
|
|
except Exception as e:
|
|
import traceback
|
|
traceback.print_exc()
|
|
raise HTTPException(status_code=500, detail=f"Erro ao gerar PDF: {str(e)}")
|
|
|
|
tema_sanitizado = "".join(c if c.isalnum() or c in " -_" else "_" for c in request.tema)
|
|
nome_arquivo = f"equipe_{tema_sanitizado[:30]}_{datetime.now().strftime('%Y%m%d')}.pdf"
|
|
|
|
return StreamingResponse(
|
|
BytesIO(pdf_bytes),
|
|
media_type="application/pdf",
|
|
headers={
|
|
"Content-Disposition": f'attachment; filename="{nome_arquivo}"'
|
|
}
|
|
)
|
|
|
|
|
|
@router.get("/consultores/sugerir", response_model=SugerirConsultoresResponseSchema)
|
|
async def sugerir_consultores(
|
|
tema: str = Query(..., min_length=2, description="Tema ou assunto para buscar consultores"),
|
|
area_avaliacao: Optional[str] = Query(None, description="Filtrar por area de avaliacao especifica"),
|
|
apenas_ativos: bool = Query(True, description="Filtrar apenas consultores ativos"),
|
|
quantidade: int = Query(20, ge=1, le=100, description="Quantidade maxima de sugestoes"),
|
|
es_client: ElasticsearchClient = Depends(get_es_client),
|
|
store = Depends(get_ranking_store),
|
|
):
|
|
try:
|
|
resultados = await es_client.sugerir_consultores(
|
|
tema=tema,
|
|
area_avaliacao=area_avaliacao,
|
|
apenas_ativos=apenas_ativos,
|
|
size=quantidade * 3
|
|
)
|
|
|
|
consultores_raw = []
|
|
for doc in resultados:
|
|
id_pessoa = doc.get("id")
|
|
nome = doc.get("dadosPessoais", {}).get("nome", "")
|
|
score_match = doc.get("_score_match", 0)
|
|
|
|
areas_avaliacao = set()
|
|
areas_conhecimento = set()
|
|
linhas_pesquisa = set()
|
|
situacao = ""
|
|
ies = None
|
|
foi_coordenador = False
|
|
foi_premiado = False
|
|
motivos_match = set()
|
|
|
|
tema_norm = normalizar_texto(tema)
|
|
tema_palavras = tema_norm.split()
|
|
|
|
for atuacao in doc.get("atuacoes", []):
|
|
tipo = atuacao.get("tipo", "")
|
|
|
|
if tipo == "Consultor":
|
|
dados = atuacao.get("dadosConsultoria", {})
|
|
situacao = dados.get("situacaoConsultoria", "")
|
|
if dados.get("ies"):
|
|
ies = dados["ies"].get("sigla") or dados["ies"].get("nome")
|
|
|
|
for area in dados.get("areaConhecimentoPos", []):
|
|
if area.get("nome"):
|
|
area_nome = corrigir_encoding(html.unescape(area["nome"]))
|
|
areas_conhecimento.add(area_nome)
|
|
area_norm = normalizar_texto(area["nome"])
|
|
if tema_norm in area_norm or any(p in area_norm for p in tema_palavras if len(p) > 3):
|
|
motivos_match.add(f"Area: {area_nome}")
|
|
area_aval = area.get("areaAvaliacao", {})
|
|
if area_aval and area_aval.get("nome"):
|
|
aval_nome = corrigir_encoding(html.unescape(area_aval["nome"]))
|
|
areas_avaliacao.add(aval_nome)
|
|
aval_norm = normalizar_texto(area_aval["nome"])
|
|
if tema_norm in aval_norm or any(p in aval_norm for p in tema_palavras if len(p) > 3):
|
|
motivos_match.add(f"Area Avaliacao: {aval_nome}")
|
|
|
|
for pesq in dados.get("areaPesquisa", []):
|
|
if pesq.get("descricao"):
|
|
pesq_desc = corrigir_encoding(html.unescape(pesq["descricao"]))
|
|
linhas_pesquisa.add(pesq_desc)
|
|
pesq_norm = normalizar_texto(pesq["descricao"])
|
|
if tema_norm in pesq_norm or any(p in pesq_norm for p in tema_palavras if len(p) > 3):
|
|
motivos_match.add(f"Pesquisa: {pesq_desc[:50]}...")
|
|
|
|
elif "Coordenação" in tipo:
|
|
foi_coordenador = True
|
|
|
|
elif "Premiação" in tipo:
|
|
foi_premiado = True
|
|
|
|
posicao_ranking = None
|
|
pontuacao_ranking = 0
|
|
if store.is_ready():
|
|
entry = store.get_by_id(id_pessoa)
|
|
if entry:
|
|
posicao_ranking = entry.posicao
|
|
pontuacao_ranking = entry.pontuacao_total
|
|
|
|
score_final = score_match
|
|
if posicao_ranking:
|
|
bonus_ranking = max(0, (10000 - posicao_ranking) / 100)
|
|
score_final += bonus_ranking
|
|
if foi_coordenador:
|
|
score_final += 20
|
|
motivos_match.add("Foi Coordenador de Area")
|
|
if foi_premiado:
|
|
score_final += 10
|
|
motivos_match.add("Foi Premiado")
|
|
|
|
consultores_raw.append({
|
|
"id_pessoa": id_pessoa,
|
|
"nome": nome,
|
|
"score_match": score_match,
|
|
"score_final": score_final,
|
|
"posicao_ranking": posicao_ranking,
|
|
"pontuacao_ranking": pontuacao_ranking,
|
|
"areas_avaliacao": list(areas_avaliacao),
|
|
"areas_conhecimento": list(areas_conhecimento),
|
|
"linhas_pesquisa": list(linhas_pesquisa),
|
|
"situacao": situacao,
|
|
"ies": ies,
|
|
"foi_coordenador": foi_coordenador,
|
|
"foi_premiado": foi_premiado,
|
|
"motivos_match": list(motivos_match)[:5],
|
|
})
|
|
|
|
consultores_raw.sort(key=lambda x: x["score_final"], reverse=True)
|
|
|
|
ies_selecionadas = set()
|
|
consultores_finais = []
|
|
for c in consultores_raw:
|
|
if len(consultores_finais) >= quantidade:
|
|
break
|
|
if c["ies"] and c["ies"] in ies_selecionadas and len(consultores_finais) < quantidade // 2:
|
|
continue
|
|
consultores_finais.append(c)
|
|
if c["ies"]:
|
|
ies_selecionadas.add(c["ies"])
|
|
|
|
while len(consultores_finais) < quantidade and len(consultores_finais) < len(consultores_raw):
|
|
for c in consultores_raw:
|
|
if c not in consultores_finais:
|
|
consultores_finais.append(c)
|
|
if len(consultores_finais) >= quantidade:
|
|
break
|
|
|
|
consultores = [
|
|
SugestaoConsultorSchema(
|
|
id_pessoa=c["id_pessoa"],
|
|
nome=c["nome"],
|
|
score_match=c["score_final"],
|
|
areas_avaliacao=c["areas_avaliacao"],
|
|
areas_conhecimento=c["areas_conhecimento"],
|
|
linhas_pesquisa=c["linhas_pesquisa"],
|
|
situacao=c["situacao"],
|
|
ies=c["ies"],
|
|
foi_coordenador=c["foi_coordenador"],
|
|
foi_premiado=c["foi_premiado"],
|
|
posicao_ranking=c["posicao_ranking"],
|
|
pontuacao_ranking=c["pontuacao_ranking"],
|
|
motivos_match=c["motivos_match"],
|
|
)
|
|
for c in consultores_finais
|
|
]
|
|
|
|
return SugerirConsultoresResponseSchema(
|
|
tema_buscado=tema,
|
|
total_encontrados=len(consultores),
|
|
consultores=consultores
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Erro ao sugerir consultores: {str(e)}")
|
|
|
|
|
|
@router.get("/consultores/areas-avaliacao", response_model=List[AreaAvaliacaoSchema])
|
|
async def listar_areas_avaliacao(
|
|
es_client: ElasticsearchClient = Depends(get_es_client),
|
|
):
|
|
try:
|
|
areas = await es_client.listar_areas_avaliacao()
|
|
return [AreaAvaliacaoSchema(**a) for a in areas]
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Erro ao listar areas de avaliacao: {str(e)}")
|