fix(backend): corrigir exibicao de idiomas e selos multilingue

- Adicionar idiomas e formacoes ao _source das queries ES (client.py)
- Corrigir type mismatch int/str no endpoint paginado (routes.py)
- Adicionar campo evento nas inscricoes para nome do premio
- Implementar extracao de idiomas do ES no repository
- Ajustar frontend para exibir selo multilingue corretamente
This commit is contained in:
Frederico Castro
2025-12-24 18:12:22 -03:00
parent 0d355e705e
commit 919d95d1e8
14 changed files with 266 additions and 12 deletions

View File

@@ -57,6 +57,7 @@ class InscricaoDTO:
premio: str
ano: int
situacao: str
evento: str = ""
@dataclass
@@ -112,6 +113,15 @@ class MembroBancaDTO:
ano: Optional[int]
@dataclass
class IdiomaDTO:
idioma: str
nivel_leitura: str = ""
nivel_escrita: str = ""
nivel_fala: str = ""
nivel_compreensao: str = ""
@dataclass
class PontuacaoAtuacaoDTO:
codigo: str
@@ -168,6 +178,7 @@ class ConsultorDetalhadoDTO:
participacoes: List[ParticipacaoDTO]
orientacoes: List[OrientacaoDTO]
membros_banca: List[MembroBancaDTO]
idiomas: List[IdiomaDTO]
pontuacao: PontuacaoCompletaDTO
rank: Optional[int] = None

View File

@@ -143,7 +143,8 @@ class ProcessarRankingJob:
"tipo": i.tipo,
"premio": i.premio,
"ano": i.ano,
"situacao": i.situacao
"situacao": i.situacao,
"evento": i.evento
}
for i in consultor.inscricoes
],
@@ -223,6 +224,17 @@ class ProcessarRankingJob:
}
for d in consultor.docencias
],
"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 consultor.idiomas
],
"titulacao": consultor.titulacao,
"pontuacao": pontuacao,
}

View File

@@ -17,6 +17,7 @@ from ..dtos.consultor_dto import (
ParticipacaoDTO,
OrientacaoDTO,
MembroBancaDTO,
IdiomaDTO,
PontuacaoAtuacaoDTO,
PontuacaoBlocoDTO,
PontuacaoCompletaDTO,
@@ -118,6 +119,7 @@ class ObterRankingUseCase:
premio=i.premio,
ano=i.ano,
situacao=i.situacao,
evento=i.evento,
)
for i in consultor.inscricoes
],
@@ -180,6 +182,16 @@ class ObterRankingUseCase:
)
for m in consultor.membros_banca
],
idiomas=[
IdiomaDTO(
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 consultor.idiomas
],
pontuacao=PontuacaoCompletaDTO(
bloco_a=PontuacaoBlocoDTO(
bloco="A",

View File

@@ -50,6 +50,7 @@ class Inscricao:
premio: str
ano: int
situacao: str = ""
evento: str = ""
@dataclass
@@ -119,6 +120,15 @@ class DocenciaPPG:
linhas_pesquisa: List[str] = field(default_factory=list)
@dataclass
class Idioma:
idioma: str
nivel_leitura: str = ""
nivel_escrita: str = ""
nivel_fala: str = ""
nivel_compreensao: str = ""
@dataclass
class Consultor:
id_pessoa: int
@@ -135,6 +145,8 @@ class Consultor:
orientacoes: List[Orientacao] = field(default_factory=list)
membros_banca: List[MembroBanca] = field(default_factory=list)
docencias: List[DocenciaPPG] = field(default_factory=list)
idiomas: List[Idioma] = field(default_factory=list)
titulacao: Optional[str] = None
pontuacao: Optional[PontuacaoCompleta] = None
@property

View File

@@ -52,7 +52,7 @@ class ElasticsearchClient:
try:
query = {
"query": {"term": {"id": id_pessoa}},
"_source": ["id", "dadosPessoais", "atuacoes"],
"_source": ["id", "dadosPessoais", "atuacoes", "idiomas", "formacoes"],
"size": 1,
}
@@ -68,6 +68,28 @@ class ElasticsearchClient:
except Exception as e:
raise RuntimeError(f"Erro ao buscar consultor {id_pessoa}: {e}")
async def buscar_por_ids(self, ids: list, source_fields: Optional[list] = None) -> list:
if not ids:
return []
try:
query = {
"query": {"terms": {"id": ids}},
"size": len(ids),
}
if source_fields:
query["_source"] = source_fields
response = await self.client.post(
f"{self.url}/{self.index}/_search",
json=query
)
response.raise_for_status()
data = response.json()
return [hit["_source"] for hit in data.get("hits", {}).get("hits", [])]
except Exception as e:
raise RuntimeError(f"Erro ao buscar consultores por ids: {e}")
async def buscar_documento_completo(self, id_pessoa: int) -> Optional[dict]:
try:
query = {
@@ -104,7 +126,7 @@ class ElasticsearchClient:
"query": {"exists": {"field": "atuacoes.tipo"}}
}
},
"_source": ["id", "dadosPessoais", "atuacoes"],
"_source": ["id", "dadosPessoais", "atuacoes", "idiomas", "formacoes"],
"size": size,
"from": from_,
"sort": [{"id": "asc"}],
@@ -196,7 +218,7 @@ class ElasticsearchClient:
"minimum_should_match": 1
}
},
"_source": ["id", "dadosPessoais", "atuacoes"],
"_source": ["id", "dadosPessoais", "atuacoes", "idiomas", "formacoes"],
"sort": [{"_score": "desc"}]
}
@@ -379,7 +401,7 @@ class ElasticsearchClient:
"boost_mode": "replace"
}
},
"_source": ["id", "dadosPessoais", "atuacoes"],
"_source": ["id", "dadosPessoais", "atuacoes", "idiomas", "formacoes"],
"sort": [{"_score": "desc"}]
}
@@ -412,7 +434,7 @@ class ElasticsearchClient:
"query": {"exists": {"field": "atuacoes.tipo"}}
}
},
"_source": ["id", "dadosPessoais", "atuacoes"],
"_source": ["id", "dadosPessoais", "atuacoes", "idiomas", "formacoes"],
"size": size,
"sort": [{"id": "asc"}]
}
@@ -638,7 +660,7 @@ class ElasticsearchClient:
"minimum_should_match": 1
}
},
"_source": ["id", "dadosPessoais", "atuacoes"],
"_source": ["id", "dadosPessoais", "atuacoes", "idiomas", "formacoes"],
"sort": [{"_score": "desc"}]
}

View File

@@ -18,6 +18,7 @@ from ...domain.entities.consultor import (
Orientacao,
MembroBanca,
DocenciaPPG,
Idioma,
)
from ...domain.repositories.consultor_repository import ConsultorRepository
from ...domain.services.calculador_pontuacao import CalculadorPontuacao
@@ -277,6 +278,7 @@ class ConsultorRepositoryImpl(ConsultorRepository):
premio=nome_premio,
ano=ano,
situacao=dados.get("situacao", ""),
evento=dados.get("evento", ""),
))
return inscricoes
@@ -604,6 +606,77 @@ class ConsultorRepositoryImpl(ConsultorRepository):
docencias.sort(key=lambda d: (d.periodo.fim is not None, d.periodo.inicio or datetime.min), reverse=True)
return docencias
def _extrair_idiomas(self, doc: Dict[str, Any]) -> List[Idioma]:
idiomas_data = doc.get("idiomas") or []
if not idiomas_data:
dados_pessoais = doc.get("dadosPessoais", {}) or {}
idiomas_data = (
dados_pessoais.get("idiomas")
or dados_pessoais.get("idiomasEstrangeiros")
or []
)
idiomas = []
for item in idiomas_data:
if isinstance(item, str):
idioma_nome = item
item_dict = {}
else:
item_dict = item or {}
idioma_nome = item_dict.get("idioma") or item_dict.get("nome") or item_dict.get("descricao")
if isinstance(idioma_nome, dict):
idioma_nome = (
idioma_nome.get("nome")
or idioma_nome.get("descricao")
or idioma_nome.get("idioma")
)
if not idioma_nome:
continue
nivel_padrao = (
item_dict.get("nivel")
or item_dict.get("nivelConhecimento")
or item_dict.get("nivel_leitura")
or item_dict.get("nivelLeitura")
or ""
)
idiomas.append(Idioma(
idioma=idioma_nome,
nivel_leitura=item_dict.get("nivelLeitura", "") or item_dict.get("leitura", "") or nivel_padrao,
nivel_escrita=item_dict.get("nivelEscrita", "") or item_dict.get("escrita", "") or nivel_padrao,
nivel_fala=item_dict.get("nivelFala", "") or item_dict.get("fala", "") or nivel_padrao,
nivel_compreensao=item_dict.get("nivelCompreensao", "") or item_dict.get("compreensao", "") or nivel_padrao,
))
return idiomas
def _extrair_titulacao(self, doc: Dict[str, Any]) -> Optional[str]:
dados_pessoais = doc.get("dadosPessoais", {}) or {}
titulacao = dados_pessoais.get("titulacao") or dados_pessoais.get("grauFormacao")
if titulacao:
return titulacao
formacoes = doc.get("formacoes", []) or []
for f in formacoes:
nivel = f.get("nivel", "").lower()
if "pós-doutorado" in nivel or "pos-doutorado" in nivel or "posdoc" in nivel:
return "Pós-Doutorado"
elif "doutorado" in nivel:
return "Doutorado"
elif "mestrado" in nivel:
return "Mestrado"
atuacoes = doc.get("atuacoes", []) or []
for a in atuacoes:
if a.get("tipo") == "Docência":
dados = a.get("dadosDocencia", {}) or {}
categoria = (dados.get("categoria", "") or "").lower()
if "doutor" in categoria:
return "Doutorado"
elif "mestre" in categoria:
return "Mestrado"
return None
async def _construir_consultor(self, doc: Dict[str, Any]) -> Consultor:
id_pessoa = doc["id"]
dados_pessoais = doc.get("dadosPessoais", {})
@@ -621,6 +694,8 @@ class ConsultorRepositoryImpl(ConsultorRepository):
membros_banca = self._extrair_membros_banca(atuacoes)
docencias = self._extrair_docencias(atuacoes)
coordenador_ppg = self._tem_coordenacao_ppg(atuacoes)
idiomas = self._extrair_idiomas(doc)
titulacao = self._extrair_titulacao(doc)
consultor = Consultor(
id_pessoa=id_pessoa,
@@ -637,6 +712,8 @@ class ConsultorRepositoryImpl(ConsultorRepository):
orientacoes=orientacoes + coorientacoes,
membros_banca=membros_banca,
docencias=docencias,
idiomas=idiomas,
titulacao=titulacao,
)
consultor.pontuacao = self.calculador.calcular_pontuacao_completa(consultor)

View File

@@ -206,6 +206,8 @@ async def ranking_paginado(
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
@@ -223,12 +225,46 @@ async def ranking_paginado(
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))
if faltando_idiomas:
ids = [item[0] for item in faltando_idiomas]
docs = await es_client.buscar_por_ids(
ids,
source_fields=["id", "dadosPessoais", "idiomas", "atuacoes", "formacoes"],
)
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 c, d in consultores_dados:
tipos_atuacao = RankingMapper._extrair_tipos_atuacao(d)
consultores_schema.append(
ConsultorRankingResumoSchema(
@@ -255,6 +291,8 @@ async def ranking_paginado(
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"),
)
)

View File

@@ -47,6 +47,7 @@ class InscricaoSchema(BaseModel):
premio: str
ano: int
situacao: str
evento: str = ""
class AvaliacaoComissaoSchema(BaseModel):
@@ -96,6 +97,14 @@ class MembroBancaSchema(BaseModel):
ano: Optional[int] = None
class IdiomaSchema(BaseModel):
idioma: str
nivel_leitura: str = ""
nivel_escrita: str = ""
nivel_fala: str = ""
nivel_compreensao: str = ""
class PontuacaoAtuacaoSchema(BaseModel):
codigo: str
base: int
@@ -147,6 +156,7 @@ class ConsultorDetalhadoSchema(BaseModel):
participacoes: List[ParticipacaoSchema]
orientacoes: List[OrientacaoSchema]
membros_banca: List[MembroBancaSchema]
idiomas: List[IdiomaSchema] = []
pontuacao: PontuacaoCompletaSchema
rank: Optional[int] = None

View File

@@ -27,6 +27,8 @@ class ConsultorRankingResumoSchema(BaseModel):
orientacoes: Optional[list] = None
membros_banca: Optional[list] = None
docencias: Optional[list] = None
idiomas: Optional[list] = None
titulacao: Optional[str] = None
pontuacao: Optional[dict] = None