Compare commits
10 Commits
6b07cecf4e
...
c273349663
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c273349663 | ||
|
|
0a0a47ecc4 | ||
|
|
3558a4b6ca | ||
|
|
7d02289605 | ||
|
|
d48fff2236 | ||
|
|
143ec401f5 | ||
|
|
e0692ee49c | ||
|
|
78670c40de | ||
|
|
840934a187 | ||
|
|
015c8f5741 |
@@ -9,3 +9,4 @@ rich==13.7.0
|
|||||||
oracledb==2.5.1
|
oracledb==2.5.1
|
||||||
weasyprint>=62.3
|
weasyprint>=62.3
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
|
xlsxwriter==3.2.0
|
||||||
|
|||||||
@@ -14,17 +14,20 @@ CREATE TABLE TB_RANKING_CONSULTOR (
|
|||||||
COMPONENTE_D NUMBER(10,2) DEFAULT 0,
|
COMPONENTE_D NUMBER(10,2) DEFAULT 0,
|
||||||
COMPONENTE_E NUMBER(10,2) DEFAULT 0,
|
COMPONENTE_E NUMBER(10,2) DEFAULT 0,
|
||||||
ATIVO CHAR(1) DEFAULT 'N',
|
ATIVO CHAR(1) DEFAULT 'N',
|
||||||
|
E_CONSULTOR CHAR(1) DEFAULT 'N',
|
||||||
ANOS_ATUACAO NUMBER(5,1) DEFAULT 0,
|
ANOS_ATUACAO NUMBER(5,1) DEFAULT 0,
|
||||||
DT_CALCULO TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
DT_CALCULO TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
JSON_DETALHES CLOB,
|
JSON_DETALHES CLOB,
|
||||||
CONSTRAINT PK_RANKING_CONSULTOR PRIMARY KEY (ID_PESSOA),
|
CONSTRAINT PK_RANKING_CONSULTOR PRIMARY KEY (ID_PESSOA),
|
||||||
CONSTRAINT CHK_ATIVO CHECK (ATIVO IN ('S', 'N'))
|
CONSTRAINT CHK_ATIVO CHECK (ATIVO IN ('S', 'N')),
|
||||||
|
CONSTRAINT CHK_E_CONSULTOR CHECK (E_CONSULTOR IN ('S', 'N'))
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Índices para performance
|
-- Índices para performance
|
||||||
CREATE INDEX IDX_RANKING_POSICAO ON TB_RANKING_CONSULTOR(POSICAO);
|
CREATE INDEX IDX_RANKING_POSICAO ON TB_RANKING_CONSULTOR(POSICAO);
|
||||||
CREATE INDEX IDX_RANKING_PONTUACAO ON TB_RANKING_CONSULTOR(PONTUACAO_TOTAL DESC);
|
CREATE INDEX IDX_RANKING_PONTUACAO ON TB_RANKING_CONSULTOR(PONTUACAO_TOTAL DESC);
|
||||||
CREATE INDEX IDX_RANKING_ATIVO ON TB_RANKING_CONSULTOR(ATIVO);
|
CREATE INDEX IDX_RANKING_ATIVO ON TB_RANKING_CONSULTOR(ATIVO);
|
||||||
|
CREATE INDEX IDX_RANKING_E_CONSULTOR ON TB_RANKING_CONSULTOR(E_CONSULTOR);
|
||||||
CREATE INDEX IDX_RANKING_DT_CALCULO ON TB_RANKING_CONSULTOR(DT_CALCULO DESC);
|
CREATE INDEX IDX_RANKING_DT_CALCULO ON TB_RANKING_CONSULTOR(DT_CALCULO DESC);
|
||||||
|
|
||||||
-- Procedure para atualizar posições após processamento
|
-- Procedure para atualizar posições após processamento
|
||||||
|
|||||||
@@ -260,6 +260,7 @@ class ProcessarRankingJob:
|
|||||||
bloco_d=int(c.get("bloco_d", 0)),
|
bloco_d=int(c.get("bloco_d", 0)),
|
||||||
bloco_e=int(c.get("bloco_e", 0)),
|
bloco_e=int(c.get("bloco_e", 0)),
|
||||||
ativo=bool(c.get("ativo", False)),
|
ativo=bool(c.get("ativo", False)),
|
||||||
|
e_consultor=c.get("consultoria") is not None,
|
||||||
anos_atuacao=float(c.get("anos_atuacao", 0) or 0),
|
anos_atuacao=float(c.get("anos_atuacao", 0) or 0),
|
||||||
detalhes=c,
|
detalhes=c,
|
||||||
)
|
)
|
||||||
|
|||||||
216
backend/src/application/services/excel_service.py
Normal file
216
backend/src/application/services/excel_service.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
from io import BytesIO
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
import json
|
||||||
|
|
||||||
|
import xlsxwriter
|
||||||
|
|
||||||
|
|
||||||
|
class ExcelService:
|
||||||
|
def gerar_ranking_excel(
|
||||||
|
self,
|
||||||
|
consultores: List[Dict[str, Any]],
|
||||||
|
filtros_aplicados: Optional[Dict[str, Any]] = None
|
||||||
|
) -> bytes:
|
||||||
|
output = BytesIO()
|
||||||
|
wb = xlsxwriter.Workbook(output, {'in_memory': True, 'constant_memory': True})
|
||||||
|
ws = wb.add_worksheet('Ranking Consultores')
|
||||||
|
|
||||||
|
title_fmt = wb.add_format({
|
||||||
|
'font_name': 'Calibri', 'font_size': 16, 'bold': True,
|
||||||
|
'font_color': '#1F4E79', 'align': 'center', 'valign': 'vcenter'
|
||||||
|
})
|
||||||
|
subtitle_fmt = wb.add_format({
|
||||||
|
'font_name': 'Calibri', 'font_size': 11,
|
||||||
|
'font_color': '#666666', 'align': 'center', 'valign': 'vcenter'
|
||||||
|
})
|
||||||
|
header_fmt = wb.add_format({
|
||||||
|
'font_name': 'Calibri', 'font_size': 11, 'bold': True,
|
||||||
|
'font_color': '#FFFFFF', 'bg_color': '#1F4E79',
|
||||||
|
'align': 'center', 'valign': 'vcenter', 'text_wrap': True,
|
||||||
|
'border': 1, 'border_color': '#D9D9D9'
|
||||||
|
})
|
||||||
|
data_fmt = wb.add_format({
|
||||||
|
'font_name': 'Calibri', 'font_size': 10,
|
||||||
|
'border': 1, 'border_color': '#D9D9D9'
|
||||||
|
})
|
||||||
|
data_center_fmt = wb.add_format({
|
||||||
|
'font_name': 'Calibri', 'font_size': 10,
|
||||||
|
'align': 'center', 'valign': 'vcenter',
|
||||||
|
'border': 1, 'border_color': '#D9D9D9'
|
||||||
|
})
|
||||||
|
data_number_fmt = wb.add_format({
|
||||||
|
'font_name': 'Calibri', 'font_size': 10,
|
||||||
|
'align': 'center', 'valign': 'vcenter',
|
||||||
|
'border': 1, 'border_color': '#D9D9D9',
|
||||||
|
'num_format': '#,##0'
|
||||||
|
})
|
||||||
|
data_decimal_fmt = wb.add_format({
|
||||||
|
'font_name': 'Calibri', 'font_size': 10,
|
||||||
|
'align': 'center', 'valign': 'vcenter',
|
||||||
|
'border': 1, 'border_color': '#D9D9D9',
|
||||||
|
'num_format': '#,##0.0'
|
||||||
|
})
|
||||||
|
ativo_fmt = wb.add_format({
|
||||||
|
'font_name': 'Calibri', 'font_size': 10, 'bold': True,
|
||||||
|
'align': 'center', 'valign': 'vcenter',
|
||||||
|
'bg_color': '#C6EFCE', 'font_color': '#006100',
|
||||||
|
'border': 1, 'border_color': '#D9D9D9'
|
||||||
|
})
|
||||||
|
inativo_fmt = wb.add_format({
|
||||||
|
'font_name': 'Calibri', 'font_size': 10, 'bold': True,
|
||||||
|
'align': 'center', 'valign': 'vcenter',
|
||||||
|
'bg_color': '#FFC7CE', 'font_color': '#9C0006',
|
||||||
|
'border': 1, 'border_color': '#D9D9D9'
|
||||||
|
})
|
||||||
|
|
||||||
|
ws.merge_range('A1:O1', 'RANKING DE CONSULTORES CAPES', title_fmt)
|
||||||
|
ws.set_row(0, 30)
|
||||||
|
|
||||||
|
data_geracao = datetime.now().strftime('%d/%m/%Y às %H:%M')
|
||||||
|
total = len(consultores)
|
||||||
|
filtros_texto = self._formatar_filtros(filtros_aplicados) if filtros_aplicados else ""
|
||||||
|
subtitulo = f"Gerado em {data_geracao} | Total: {total:,} consultores com pontuação"
|
||||||
|
if filtros_texto:
|
||||||
|
subtitulo += f" | Filtros: {filtros_texto}"
|
||||||
|
ws.merge_range('A2:O2', subtitulo, subtitle_fmt)
|
||||||
|
ws.set_row(1, 20)
|
||||||
|
|
||||||
|
headers = [
|
||||||
|
("Posição", 10),
|
||||||
|
("ID", 12),
|
||||||
|
("Nome", 40),
|
||||||
|
("Pontuação Total", 15),
|
||||||
|
("Bloco A\n(Coord. CAPES)", 14),
|
||||||
|
("Bloco C\n(Consultoria)", 14),
|
||||||
|
("Bloco D\n(Prêmios/Aval.)", 14),
|
||||||
|
("Bloco E\n(Coord. PPG)", 14),
|
||||||
|
("Status", 10),
|
||||||
|
("Anos Atuação", 12),
|
||||||
|
("Selos", 30),
|
||||||
|
("Coord. CAPES", 25),
|
||||||
|
("Situação Consultoria", 18),
|
||||||
|
("Prêmios", 25),
|
||||||
|
("Titulação", 35),
|
||||||
|
]
|
||||||
|
|
||||||
|
header_row = 3
|
||||||
|
for col_idx, (header_text, width) in enumerate(headers):
|
||||||
|
ws.write(header_row, col_idx, header_text, header_fmt)
|
||||||
|
ws.set_column(col_idx, col_idx, width)
|
||||||
|
ws.set_row(header_row, 35)
|
||||||
|
|
||||||
|
for row_idx, consultor in enumerate(consultores):
|
||||||
|
excel_row = header_row + 1 + row_idx
|
||||||
|
|
||||||
|
detalhes = self._parse_json_detalhes(consultor.get("JSON_DETALHES"))
|
||||||
|
selos = consultor.get("SELOS") or ""
|
||||||
|
is_ativo = consultor.get("ATIVO") == "S"
|
||||||
|
|
||||||
|
coord_capes = self._extrair_coordenacoes_resumo(detalhes)
|
||||||
|
situacao_cons = self._extrair_situacao_consultoria(detalhes)
|
||||||
|
premios = self._extrair_premios_resumo(detalhes)
|
||||||
|
titulacao = self._extrair_titulacao(detalhes)
|
||||||
|
|
||||||
|
ws.write_number(excel_row, 0, consultor.get("POSICAO") or 0, data_center_fmt)
|
||||||
|
ws.write_number(excel_row, 1, consultor.get("ID_PESSOA") or 0, data_center_fmt)
|
||||||
|
ws.write_string(excel_row, 2, consultor.get("NOME") or "", data_fmt)
|
||||||
|
ws.write_number(excel_row, 3, float(consultor.get("PONTUACAO_TOTAL") or 0), data_number_fmt)
|
||||||
|
ws.write_number(excel_row, 4, float(consultor.get("COMPONENTE_A") or 0), data_number_fmt)
|
||||||
|
ws.write_number(excel_row, 5, float(consultor.get("COMPONENTE_C") or 0), data_number_fmt)
|
||||||
|
ws.write_number(excel_row, 6, float(consultor.get("COMPONENTE_D") or 0), data_number_fmt)
|
||||||
|
ws.write_number(excel_row, 7, float(consultor.get("COMPONENTE_E") or 0), data_number_fmt)
|
||||||
|
ws.write_string(excel_row, 8, "Ativo" if is_ativo else "Inativo", ativo_fmt if is_ativo else inativo_fmt)
|
||||||
|
ws.write_number(excel_row, 9, float(consultor.get("ANOS_ATUACAO") or 0), data_decimal_fmt)
|
||||||
|
ws.write_string(excel_row, 10, selos.replace(",", ", "), data_fmt)
|
||||||
|
ws.write_string(excel_row, 11, coord_capes, data_fmt)
|
||||||
|
ws.write_string(excel_row, 12, situacao_cons, data_fmt)
|
||||||
|
ws.write_string(excel_row, 13, premios, data_fmt)
|
||||||
|
ws.write_string(excel_row, 14, titulacao, data_fmt)
|
||||||
|
|
||||||
|
last_row = header_row + len(consultores)
|
||||||
|
ws.autofilter(header_row, 0, last_row, 14)
|
||||||
|
ws.freeze_panes(header_row + 1, 0)
|
||||||
|
|
||||||
|
wb.close()
|
||||||
|
output.seek(0)
|
||||||
|
return output.getvalue()
|
||||||
|
|
||||||
|
def _parse_json_detalhes(self, json_str) -> Dict[str, Any]:
|
||||||
|
if not json_str:
|
||||||
|
return {}
|
||||||
|
if hasattr(json_str, "read"):
|
||||||
|
json_str = json_str.read()
|
||||||
|
try:
|
||||||
|
return json.loads(json_str) if isinstance(json_str, str) else json_str
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _formatar_filtros(self, filtros: Dict[str, Any]) -> str:
|
||||||
|
partes = []
|
||||||
|
if filtros.get("ativo") is not None:
|
||||||
|
partes.append("Ativos" if filtros["ativo"] else "Inativos")
|
||||||
|
if filtros.get("selos"):
|
||||||
|
partes.append(f"Selos: {', '.join(filtros['selos'])}")
|
||||||
|
return " | ".join(partes)
|
||||||
|
|
||||||
|
def _extrair_coordenacoes_resumo(self, detalhes: Dict[str, Any]) -> str:
|
||||||
|
coords = detalhes.get("coordenacoes_capes", [])
|
||||||
|
if not coords:
|
||||||
|
return ""
|
||||||
|
resumos = []
|
||||||
|
for c in coords[:3]:
|
||||||
|
tipo = c.get("tipo", "")
|
||||||
|
area = c.get("area", "")
|
||||||
|
if tipo and area:
|
||||||
|
resumos.append(f"{tipo}: {area}")
|
||||||
|
elif tipo:
|
||||||
|
resumos.append(tipo)
|
||||||
|
return "; ".join(resumos)
|
||||||
|
|
||||||
|
def _extrair_situacao_consultoria(self, detalhes: Dict[str, Any]) -> str:
|
||||||
|
cons = detalhes.get("consultoria")
|
||||||
|
if not cons:
|
||||||
|
return ""
|
||||||
|
return cons.get("situacao", "")
|
||||||
|
|
||||||
|
def _extrair_premios_resumo(self, detalhes: Dict[str, Any]) -> str:
|
||||||
|
premios = detalhes.get("premiacoes", [])
|
||||||
|
if not premios:
|
||||||
|
return ""
|
||||||
|
resumos = []
|
||||||
|
for p in premios[:3]:
|
||||||
|
premio = p.get("premio", "")
|
||||||
|
tipo = p.get("tipo", "")
|
||||||
|
ano = p.get("ano", "")
|
||||||
|
if premio:
|
||||||
|
texto = f"{premio}"
|
||||||
|
if tipo:
|
||||||
|
texto += f" ({tipo})"
|
||||||
|
if ano:
|
||||||
|
texto += f" - {ano}"
|
||||||
|
resumos.append(texto)
|
||||||
|
if len(premios) > 3:
|
||||||
|
resumos.append(f"+{len(premios) - 3} outros")
|
||||||
|
return "; ".join(resumos)
|
||||||
|
|
||||||
|
def _extrair_titulacao(self, detalhes: Dict[str, Any]) -> str:
|
||||||
|
titulacao = detalhes.get("titulacao")
|
||||||
|
if titulacao and isinstance(titulacao, str):
|
||||||
|
return titulacao
|
||||||
|
|
||||||
|
lattes = detalhes.get("lattes", {})
|
||||||
|
titulacoes = lattes.get("titulacoes", []) if lattes else []
|
||||||
|
if titulacoes:
|
||||||
|
primeira = titulacoes[0]
|
||||||
|
grau = primeira.get("grau", "")
|
||||||
|
ies = primeira.get("ies_sigla", "")
|
||||||
|
ano = primeira.get("ano", "")
|
||||||
|
if grau:
|
||||||
|
texto = grau
|
||||||
|
if ies:
|
||||||
|
texto += f" - {ies}"
|
||||||
|
if ano:
|
||||||
|
texto += f" ({ano})"
|
||||||
|
return texto
|
||||||
|
return ""
|
||||||
@@ -153,7 +153,8 @@ class Consultor:
|
|||||||
def anos_atuacao(self) -> float:
|
def anos_atuacao(self) -> float:
|
||||||
if not self.consultoria or not self.consultoria.periodo.inicio:
|
if not self.consultoria or not self.consultoria.periodo.inicio:
|
||||||
return 0.0
|
return 0.0
|
||||||
dias = (datetime.now() - self.consultoria.periodo.inicio).days
|
fim = self.consultoria.periodo.fim or datetime.now()
|
||||||
|
dias = (fim - self.consultoria.periodo.inicio).days
|
||||||
return round(dias / 365.25, 1)
|
return round(dias / 365.25, 1)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ class RankingOracleRepository:
|
|||||||
INSERT INTO TB_RANKING_CONSULTOR (
|
INSERT INTO TB_RANKING_CONSULTOR (
|
||||||
ID_PESSOA, NOME, PONTUACAO_TOTAL,
|
ID_PESSOA, NOME, PONTUACAO_TOTAL,
|
||||||
COMPONENTE_A, COMPONENTE_B, COMPONENTE_C, COMPONENTE_D, COMPONENTE_E,
|
COMPONENTE_A, COMPONENTE_B, COMPONENTE_C, COMPONENTE_D, COMPONENTE_E,
|
||||||
ATIVO, ANOS_ATUACAO, JSON_DETALHES, SELOS, DT_CALCULO
|
ATIVO, E_CONSULTOR, ANOS_ATUACAO, JSON_DETALHES, SELOS, DT_CALCULO
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:id_pessoa, :nome, :pontuacao_total,
|
:id_pessoa, :nome, :pontuacao_total,
|
||||||
:componente_a, :componente_b, :componente_c, :componente_d, :componente_e,
|
:componente_a, :componente_b, :componente_c, :componente_d, :componente_e,
|
||||||
:ativo, :anos_atuacao, :json_detalhes, :selos, CURRENT_TIMESTAMP
|
:ativo, :e_consultor, :anos_atuacao, :json_detalhes, :selos, CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@ class RankingOracleRepository:
|
|||||||
json_str = json.dumps(consultor, ensure_ascii=False)
|
json_str = json.dumps(consultor, ensure_ascii=False)
|
||||||
selos_set = extrair_selos_entry(consultor)
|
selos_set = extrair_selos_entry(consultor)
|
||||||
selos_str = ",".join(sorted(selos_set)) if selos_set else None
|
selos_str = ",".join(sorted(selos_set)) if selos_set else None
|
||||||
|
e_consultor = consultor.get("consultoria") is not None
|
||||||
batch_data.append({
|
batch_data.append({
|
||||||
"id_pessoa": int(consultor["id_pessoa"]),
|
"id_pessoa": int(consultor["id_pessoa"]),
|
||||||
"nome": str(consultor.get("nome", ""))[:500],
|
"nome": str(consultor.get("nome", ""))[:500],
|
||||||
@@ -48,6 +49,7 @@ class RankingOracleRepository:
|
|||||||
"componente_d": int(consultor.get("bloco_d") or consultor.get("componente_d") or 0),
|
"componente_d": int(consultor.get("bloco_d") or consultor.get("componente_d") or 0),
|
||||||
"componente_e": int(consultor.get("bloco_e") or consultor.get("componente_e") or 0),
|
"componente_e": int(consultor.get("bloco_e") or consultor.get("componente_e") or 0),
|
||||||
"ativo": "S" if consultor.get("ativo") else "N",
|
"ativo": "S" if consultor.get("ativo") else "N",
|
||||||
|
"e_consultor": "S" if e_consultor else "N",
|
||||||
"anos_atuacao": float(consultor.get("anos_atuacao") or 0),
|
"anos_atuacao": float(consultor.get("anos_atuacao") or 0),
|
||||||
"json_detalhes": json_str,
|
"json_detalhes": json_str,
|
||||||
"selos": selos_str
|
"selos": selos_str
|
||||||
@@ -91,14 +93,15 @@ class RankingOracleRepository:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if filtro_ativo is not None:
|
if filtro_ativo is not None:
|
||||||
|
where_clauses.append("E_CONSULTOR = 'S'")
|
||||||
where_clauses.append("ATIVO = :ativo")
|
where_clauses.append("ATIVO = :ativo")
|
||||||
params["ativo"] = "S" if filtro_ativo else "N"
|
params["ativo"] = "S" if filtro_ativo else "N"
|
||||||
|
|
||||||
if filtro_selos:
|
if filtro_selos:
|
||||||
for i, selo in enumerate(filtro_selos):
|
for i, selo in enumerate(filtro_selos):
|
||||||
param_name = f"selo_{i}"
|
param_name = f"selo_{i}"
|
||||||
where_clauses.append(f"((',' || SELOS || ',') LIKE '%,' || :{param_name} || ',%')")
|
where_clauses.append(f"((',' || UPPER(SELOS) || ',') LIKE '%,' || :{param_name} || ',%')")
|
||||||
params[param_name] = selo
|
params[param_name] = str(selo).upper()
|
||||||
|
|
||||||
where_clause = ""
|
where_clause = ""
|
||||||
if where_clauses:
|
if where_clauses:
|
||||||
@@ -169,14 +172,15 @@ class RankingOracleRepository:
|
|||||||
params = {}
|
params = {}
|
||||||
|
|
||||||
if filtro_ativo is not None:
|
if filtro_ativo is not None:
|
||||||
|
where_clauses.append("E_CONSULTOR = 'S'")
|
||||||
where_clauses.append("ATIVO = :ativo")
|
where_clauses.append("ATIVO = :ativo")
|
||||||
params["ativo"] = "S" if filtro_ativo else "N"
|
params["ativo"] = "S" if filtro_ativo else "N"
|
||||||
|
|
||||||
if filtro_selos:
|
if filtro_selos:
|
||||||
for i, selo in enumerate(filtro_selos):
|
for i, selo in enumerate(filtro_selos):
|
||||||
param_name = f"selo_{i}"
|
param_name = f"selo_{i}"
|
||||||
where_clauses.append(f"((',' || SELOS || ',') LIKE '%,' || :{param_name} || ',%')")
|
where_clauses.append(f"((',' || UPPER(SELOS) || ',') LIKE '%,' || :{param_name} || ',%')")
|
||||||
params[param_name] = selo
|
params[param_name] = str(selo).upper()
|
||||||
|
|
||||||
where_clause = ""
|
where_clause = ""
|
||||||
if where_clauses:
|
if where_clauses:
|
||||||
@@ -394,3 +398,101 @@ class RankingOracleRepository:
|
|||||||
raise RuntimeError(f"Erro ao limpar tabela: {e2}")
|
raise RuntimeError(f"Erro ao limpar tabela: {e2}")
|
||||||
finally:
|
finally:
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
||||||
|
def buscar_para_exportacao(
|
||||||
|
self,
|
||||||
|
filtro_ativo: Optional[bool] = None,
|
||||||
|
filtro_selos: Optional[List[str]] = None,
|
||||||
|
batch_size: int = 5000
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Busca todos os consultores com pontuação > 0 para exportação.
|
||||||
|
Otimizado para alto volume usando fetch em batches.
|
||||||
|
Retorna dicts crus do Oracle para processamento direto.
|
||||||
|
"""
|
||||||
|
where_clauses = ["PONTUACAO_TOTAL > 0"]
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
if filtro_ativo is not None:
|
||||||
|
where_clauses.append("E_CONSULTOR = 'S'")
|
||||||
|
where_clauses.append("ATIVO = :ativo")
|
||||||
|
params["ativo"] = "S" if filtro_ativo else "N"
|
||||||
|
|
||||||
|
if filtro_selos:
|
||||||
|
for i, selo in enumerate(filtro_selos):
|
||||||
|
param_name = f"selo_{i}"
|
||||||
|
where_clauses.append(f"((',' || UPPER(SELOS) || ',') LIKE '%,' || :{param_name} || ',%')")
|
||||||
|
params[param_name] = str(selo).upper()
|
||||||
|
|
||||||
|
where_clause = " AND ".join(where_clauses)
|
||||||
|
|
||||||
|
query = f"""
|
||||||
|
SELECT
|
||||||
|
ID_PESSOA,
|
||||||
|
NOME,
|
||||||
|
POSICAO,
|
||||||
|
PONTUACAO_TOTAL,
|
||||||
|
COMPONENTE_A,
|
||||||
|
COMPONENTE_B,
|
||||||
|
COMPONENTE_C,
|
||||||
|
COMPONENTE_D,
|
||||||
|
COMPONENTE_E,
|
||||||
|
ATIVO,
|
||||||
|
ANOS_ATUACAO,
|
||||||
|
SELOS,
|
||||||
|
JSON_DETALHES
|
||||||
|
FROM TB_RANKING_CONSULTOR
|
||||||
|
WHERE {where_clause}
|
||||||
|
ORDER BY POSICAO NULLS LAST, PONTUACAO_TOTAL DESC
|
||||||
|
"""
|
||||||
|
|
||||||
|
resultados = []
|
||||||
|
with self.client.get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
try:
|
||||||
|
cursor.arraysize = batch_size
|
||||||
|
cursor.execute(query, params)
|
||||||
|
|
||||||
|
colunas = [col[0] for col in cursor.description]
|
||||||
|
|
||||||
|
while True:
|
||||||
|
rows = cursor.fetchmany(batch_size)
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
for row in rows:
|
||||||
|
registro = dict(zip(colunas, row))
|
||||||
|
json_det = registro.get("JSON_DETALHES")
|
||||||
|
if hasattr(json_det, "read"):
|
||||||
|
registro["JSON_DETALHES"] = json_det.read()
|
||||||
|
resultados.append(registro)
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
return resultados
|
||||||
|
|
||||||
|
def contar_para_exportacao(
|
||||||
|
self,
|
||||||
|
filtro_ativo: Optional[bool] = None,
|
||||||
|
filtro_selos: Optional[List[str]] = None
|
||||||
|
) -> int:
|
||||||
|
"""
|
||||||
|
Conta consultores com pontuação > 0 que seriam exportados.
|
||||||
|
"""
|
||||||
|
where_clauses = ["PONTUACAO_TOTAL > 0"]
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
if filtro_ativo is not None:
|
||||||
|
where_clauses.append("E_CONSULTOR = 'S'")
|
||||||
|
where_clauses.append("ATIVO = :ativo")
|
||||||
|
params["ativo"] = "S" if filtro_ativo else "N"
|
||||||
|
|
||||||
|
if filtro_selos:
|
||||||
|
for i, selo in enumerate(filtro_selos):
|
||||||
|
param_name = f"selo_{i}"
|
||||||
|
where_clauses.append(f"((',' || UPPER(SELOS) || ',') LIKE '%,' || :{param_name} || ',%')")
|
||||||
|
params[param_name] = str(selo).upper()
|
||||||
|
|
||||||
|
where_clause = " AND ".join(where_clauses)
|
||||||
|
query = f"SELECT COUNT(*) AS TOTAL FROM TB_RANKING_CONSULTOR WHERE {where_clause}"
|
||||||
|
results = self.client.executar_query(query, params)
|
||||||
|
return results[0]["TOTAL"] if results else 0
|
||||||
|
|||||||
@@ -24,11 +24,33 @@ SELOS_DISPONIVEIS = [
|
|||||||
"COORIENT_GP",
|
"COORIENT_GP",
|
||||||
"COORIENT_PREMIO",
|
"COORIENT_PREMIO",
|
||||||
"COORIENT_MENCAO",
|
"COORIENT_MENCAO",
|
||||||
|
"ORIENT_POS_DOC",
|
||||||
|
"ORIENT_POS_DOC_PREM",
|
||||||
"ORIENT_TESE",
|
"ORIENT_TESE",
|
||||||
|
"ORIENT_TESE_PREM",
|
||||||
"ORIENT_DISS",
|
"ORIENT_DISS",
|
||||||
|
"ORIENT_DISS_PREM",
|
||||||
|
"CO_ORIENT_POS_DOC",
|
||||||
|
"CO_ORIENT_POS_DOC_PREM",
|
||||||
|
"CO_ORIENT_TESE",
|
||||||
|
"CO_ORIENT_TESE_PREM",
|
||||||
|
"CO_ORIENT_DISS",
|
||||||
|
"CO_ORIENT_DISS_PREM",
|
||||||
|
"MB_BANCA_POS_DOC",
|
||||||
|
"MB_BANCA_POS_DOC_PREM",
|
||||||
|
"MB_BANCA_TESE",
|
||||||
|
"MB_BANCA_TESE_PREM",
|
||||||
|
"MB_BANCA_DISS",
|
||||||
|
"MB_BANCA_DISS_PREM",
|
||||||
|
"IDIOMA_BILINGUE",
|
||||||
|
"IDIOMA_MULTILINGUE",
|
||||||
|
"TITULACAO_MESTRE",
|
||||||
|
"TITULACAO_DOUTOR",
|
||||||
|
"TITULACAO_POS_DOUTOR",
|
||||||
|
"BOL_BPQ_NIVEL",
|
||||||
|
"PPG_COORD",
|
||||||
"EVENTO",
|
"EVENTO",
|
||||||
"PROJ",
|
"PROJ",
|
||||||
"IDIOMA_MULTILINGUE",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -139,6 +161,7 @@ class RankingEntry:
|
|||||||
bloco_d: int
|
bloco_d: int
|
||||||
bloco_e: int
|
bloco_e: int
|
||||||
ativo: bool
|
ativo: bool
|
||||||
|
e_consultor: bool
|
||||||
anos_atuacao: float
|
anos_atuacao: float
|
||||||
detalhes: Dict[str, Any]
|
detalhes: Dict[str, Any]
|
||||||
|
|
||||||
@@ -170,7 +193,7 @@ class RankingStore:
|
|||||||
def total(self, filtro_ativo: Optional[bool] = None) -> int:
|
def total(self, filtro_ativo: Optional[bool] = None) -> int:
|
||||||
if filtro_ativo is None:
|
if filtro_ativo is None:
|
||||||
return len(self._entries)
|
return len(self._entries)
|
||||||
return sum(1 for e in self._entries if e.ativo == filtro_ativo)
|
return sum(1 for e in self._entries if e.e_consultor and e.ativo == filtro_ativo)
|
||||||
|
|
||||||
def get_page(
|
def get_page(
|
||||||
self,
|
self,
|
||||||
@@ -187,7 +210,7 @@ class RankingStore:
|
|||||||
entries = self._entries
|
entries = self._entries
|
||||||
|
|
||||||
if filtro_ativo is not None:
|
if filtro_ativo is not None:
|
||||||
entries = [e for e in entries if e.ativo == filtro_ativo]
|
entries = [e for e in entries if e.e_consultor and e.ativo == filtro_ativo]
|
||||||
|
|
||||||
if filtro_selos:
|
if filtro_selos:
|
||||||
selos_set = set(filtro_selos)
|
selos_set = set(filtro_selos)
|
||||||
@@ -212,7 +235,7 @@ class RankingStore:
|
|||||||
if filtro_ativo is None:
|
if filtro_ativo is None:
|
||||||
entries = self._entries
|
entries = self._entries
|
||||||
else:
|
else:
|
||||||
entries = [e for e in self._entries if e.ativo == filtro_ativo]
|
entries = [e for e in self._entries if e.e_consultor and e.ativo == filtro_ativo]
|
||||||
|
|
||||||
total = len(entries)
|
total = len(entries)
|
||||||
return total, entries[offset : offset + limit]
|
return total, entries[offset : offset + limit]
|
||||||
|
|||||||
@@ -222,13 +222,24 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
|||||||
|
|
||||||
mesclados = mesclar_periodos(periodos)
|
mesclados = mesclar_periodos(periodos)
|
||||||
periodo_ativo = next((p for p in mesclados if p.ativo), None)
|
periodo_ativo = next((p for p in mesclados if p.ativo), None)
|
||||||
anos_consecutivos = periodo_ativo.anos_completos(datetime.now()) if periodo_ativo else 0
|
if periodo_ativo:
|
||||||
|
anos_consecutivos = periodo_ativo.anos_completos(datetime.now())
|
||||||
|
elif mesclados:
|
||||||
|
anos_list = [p.anos_completos(p.fim or datetime.now()) for p in mesclados]
|
||||||
|
anos_consecutivos = max(anos_list) if anos_list else 0
|
||||||
|
else:
|
||||||
|
anos_consecutivos = 0
|
||||||
retornos = max(0, len(mesclados) - 1)
|
retornos = max(0, len(mesclados) - 1)
|
||||||
ativo = any(p.ativo for p in periodos)
|
ativo = any(p.ativo for p in periodos)
|
||||||
|
|
||||||
situacao_final = situacoes[-1] if situacoes else "N/A"
|
situacao_final = situacoes[-1] if situacoes else "N/A"
|
||||||
is_ativo = ativo or "atividade" in situacao_final.lower() or "ativo" in situacao_final.lower()
|
situacao_lower = situacao_final.lower()
|
||||||
is_falecido = "falecido" in situacao_final.lower()
|
is_ativo = ativo or (
|
||||||
|
("atividade" in situacao_lower or "ativo" in situacao_lower)
|
||||||
|
and "inatividade" not in situacao_lower
|
||||||
|
and "inativo" not in situacao_lower
|
||||||
|
)
|
||||||
|
is_falecido = "falecido" in situacao_lower
|
||||||
|
|
||||||
if is_falecido:
|
if is_falecido:
|
||||||
codigo = "CONS_FALECIDO"
|
codigo = "CONS_FALECIDO"
|
||||||
|
|||||||
@@ -214,11 +214,15 @@ async def ranking_paginado(
|
|||||||
if not oracle_repo:
|
if not oracle_repo:
|
||||||
raise HTTPException(status_code=503, detail="Oracle não configurado")
|
raise HTTPException(status_code=503, detail="Oracle não configurado")
|
||||||
|
|
||||||
selos_lista = [s.strip() for s in selos.split(",") if s.strip()] if selos else None
|
selos_lista = (
|
||||||
|
[s.strip().upper() for s in selos.split(",") if s.strip()]
|
||||||
|
if selos
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
total = oracle_repo.contar_total(filtro_ativo=ativo, filtro_selos=selos_lista)
|
total = oracle_repo.contar_total(filtro_ativo=ativo, filtro_selos=selos_lista)
|
||||||
if total == 0:
|
if total == 0:
|
||||||
if selos_lista:
|
if selos_lista or ativo is not None:
|
||||||
return RankingPaginadoResponseSchema(
|
return RankingPaginadoResponseSchema(
|
||||||
total=0, page=page, size=size, total_pages=0, consultores=[]
|
total=0, page=page, size=size, total_pages=0, consultores=[]
|
||||||
)
|
)
|
||||||
@@ -988,3 +992,78 @@ async def listar_areas_avaliacao(
|
|||||||
return [AreaAvaliacaoSchema(**a) for a in areas]
|
return [AreaAvaliacaoSchema(**a) for a in areas]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Erro ao listar areas de avaliacao: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Erro ao listar areas de avaliacao: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ranking/exportar/excel")
|
||||||
|
async def exportar_ranking_excel(
|
||||||
|
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),
|
||||||
|
):
|
||||||
|
from ...application.services.excel_service import ExcelService
|
||||||
|
|
||||||
|
if not oracle_repo:
|
||||||
|
raise HTTPException(status_code=503, detail="Oracle não configurado")
|
||||||
|
|
||||||
|
selos_lista = (
|
||||||
|
[s.strip().upper() for s in selos.split(",") if s.strip()]
|
||||||
|
if selos
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
total = oracle_repo.contar_para_exportacao(filtro_ativo=ativo, filtro_selos=selos_lista)
|
||||||
|
if total == 0:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Nenhum consultor com pontuação encontrado para os filtros aplicados."
|
||||||
|
)
|
||||||
|
|
||||||
|
consultores = oracle_repo.buscar_para_exportacao(
|
||||||
|
filtro_ativo=ativo,
|
||||||
|
filtro_selos=selos_lista
|
||||||
|
)
|
||||||
|
|
||||||
|
filtros = {"ativo": ativo, "selos": selos_lista}
|
||||||
|
excel_service = ExcelService()
|
||||||
|
excel_bytes = excel_service.gerar_ranking_excel(consultores, filtros)
|
||||||
|
|
||||||
|
data_atual = datetime.now().strftime('%Y%m%d_%H%M')
|
||||||
|
nome_arquivo = f"ranking_consultores_capes_{data_atual}.xlsx"
|
||||||
|
|
||||||
|
from fastapi.responses import Response
|
||||||
|
return Response(
|
||||||
|
content=excel_bytes,
|
||||||
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": f'attachment; filename="{nome_arquivo}"',
|
||||||
|
"Content-Length": str(len(excel_bytes)),
|
||||||
|
"Cache-Control": "no-cache"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ranking/exportar/info")
|
||||||
|
async def info_exportacao(
|
||||||
|
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),
|
||||||
|
):
|
||||||
|
if not oracle_repo:
|
||||||
|
raise HTTPException(status_code=503, detail="Oracle não configurado")
|
||||||
|
|
||||||
|
selos_lista = (
|
||||||
|
[s.strip().upper() for s in selos.split(",") if s.strip()]
|
||||||
|
if selos
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
total = oracle_repo.contar_para_exportacao(filtro_ativo=ativo, filtro_selos=selos_lista)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_consultores": total,
|
||||||
|
"filtros": {
|
||||||
|
"ativo": ativo,
|
||||||
|
"selos": selos_lista
|
||||||
|
},
|
||||||
|
"estimativa_tamanho_mb": round(total * 0.003, 2)
|
||||||
|
}
|
||||||
|
|||||||
0
backend/tests/__init__.py
Normal file
0
backend/tests/__init__.py
Normal file
0
backend/tests/application/__init__.py
Normal file
0
backend/tests/application/__init__.py
Normal file
0
backend/tests/application/jobs/__init__.py
Normal file
0
backend/tests/application/jobs/__init__.py
Normal file
278
backend/tests/application/jobs/test_processar_ranking.py
Normal file
278
backend/tests/application/jobs/test_processar_ranking.py
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from src.application.jobs.processar_ranking import ProcessarRankingJob
|
||||||
|
from src.infrastructure.ranking_store import RankingEntry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_es_client():
|
||||||
|
client = AsyncMock()
|
||||||
|
client.contar_com_atuacoes.return_value = 100
|
||||||
|
client.buscar_todos_consultores.return_value = {"processados": 100, "batches": 10}
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_ranking_store():
|
||||||
|
store = AsyncMock()
|
||||||
|
store.set_entries = AsyncMock()
|
||||||
|
return store
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_oracle_repo():
|
||||||
|
repo = MagicMock()
|
||||||
|
repo.limpar_tabela = MagicMock()
|
||||||
|
repo.inserir_batch = MagicMock()
|
||||||
|
repo.atualizar_posicoes = MagicMock()
|
||||||
|
return repo
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def job(mock_es_client, mock_ranking_store, mock_oracle_repo):
|
||||||
|
return ProcessarRankingJob(
|
||||||
|
es_client=mock_es_client,
|
||||||
|
ranking_store=mock_ranking_store,
|
||||||
|
ranking_oracle_repo=mock_oracle_repo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGerarEntriesOrdenadas:
|
||||||
|
|
||||||
|
def test_lista_vazia(self):
|
||||||
|
entries = ProcessarRankingJob._gerar_entries_ordenadas([])
|
||||||
|
assert len(entries) == 0
|
||||||
|
|
||||||
|
def test_ordenacao_por_pontuacao(self):
|
||||||
|
consultores = [
|
||||||
|
{"id_pessoa": 1, "nome": "A", "pontuacao_total": 100, "bloco_a": 50, "bloco_b": 20, "bloco_c": 20, "bloco_d": 5, "bloco_e": 5, "ativo": True, "anos_atuacao": 5},
|
||||||
|
{"id_pessoa": 2, "nome": "B", "pontuacao_total": 300, "bloco_a": 150, "bloco_b": 80, "bloco_c": 50, "bloco_d": 10, "bloco_e": 10, "ativo": True, "anos_atuacao": 10},
|
||||||
|
{"id_pessoa": 3, "nome": "C", "pontuacao_total": 200, "bloco_a": 100, "bloco_b": 50, "bloco_c": 30, "bloco_d": 10, "bloco_e": 10, "ativo": False, "anos_atuacao": 3},
|
||||||
|
]
|
||||||
|
entries = ProcessarRankingJob._gerar_entries_ordenadas(consultores)
|
||||||
|
assert len(entries) == 3
|
||||||
|
assert entries[0].id_pessoa == 2
|
||||||
|
assert entries[0].posicao == 1
|
||||||
|
assert entries[0].pontuacao_total == 300
|
||||||
|
assert entries[1].id_pessoa == 3
|
||||||
|
assert entries[1].posicao == 2
|
||||||
|
assert entries[2].id_pessoa == 1
|
||||||
|
assert entries[2].posicao == 3
|
||||||
|
|
||||||
|
def test_posicoes_atribuidas_sequencialmente(self):
|
||||||
|
consultores = [
|
||||||
|
{"id_pessoa": i, "nome": f"C{i}", "pontuacao_total": 100 - i, "bloco_a": 50, "bloco_b": 20, "bloco_c": 20, "bloco_d": 5, "bloco_e": 5, "ativo": True, "anos_atuacao": 5}
|
||||||
|
for i in range(1, 11)
|
||||||
|
]
|
||||||
|
entries = ProcessarRankingJob._gerar_entries_ordenadas(consultores)
|
||||||
|
for i, entry in enumerate(entries, start=1):
|
||||||
|
assert entry.posicao == i
|
||||||
|
|
||||||
|
def test_empate_desempata_por_id(self):
|
||||||
|
consultores = [
|
||||||
|
{"id_pessoa": 10, "nome": "A", "pontuacao_total": 100, "bloco_a": 50, "bloco_b": 20, "bloco_c": 20, "bloco_d": 5, "bloco_e": 5, "ativo": True, "anos_atuacao": 5},
|
||||||
|
{"id_pessoa": 5, "nome": "B", "pontuacao_total": 100, "bloco_a": 50, "bloco_b": 20, "bloco_c": 20, "bloco_d": 5, "bloco_e": 5, "ativo": True, "anos_atuacao": 5},
|
||||||
|
]
|
||||||
|
entries = ProcessarRankingJob._gerar_entries_ordenadas(consultores)
|
||||||
|
assert entries[0].id_pessoa == 5
|
||||||
|
assert entries[1].id_pessoa == 10
|
||||||
|
|
||||||
|
|
||||||
|
class TestObterEstatisticas:
|
||||||
|
|
||||||
|
def test_lista_vazia(self):
|
||||||
|
stats = ProcessarRankingJob._obter_estatisticas([])
|
||||||
|
assert stats["total_consultores"] == 0
|
||||||
|
assert stats["pontuacao_media"] == 0
|
||||||
|
assert stats["pontuacao_maxima"] == 0
|
||||||
|
|
||||||
|
def test_estatisticas_calculadas(self):
|
||||||
|
entries = [
|
||||||
|
RankingEntry(id_pessoa=1, nome="A", posicao=1, pontuacao_total=300, bloco_a=100, bloco_b=80, bloco_c=60, bloco_d=40, bloco_e=20, ativo=True, e_consultor=True, anos_atuacao=5, detalhes={}),
|
||||||
|
RankingEntry(id_pessoa=2, nome="B", posicao=2, pontuacao_total=200, bloco_a=80, bloco_b=50, bloco_c=40, bloco_d=20, bloco_e=10, ativo=True, e_consultor=True, anos_atuacao=3, detalhes={}),
|
||||||
|
RankingEntry(id_pessoa=3, nome="C", posicao=3, pontuacao_total=100, bloco_a=40, bloco_b=30, bloco_c=20, bloco_d=5, bloco_e=5, ativo=False, e_consultor=True, anos_atuacao=2, detalhes={}),
|
||||||
|
]
|
||||||
|
stats = ProcessarRankingJob._obter_estatisticas(entries)
|
||||||
|
assert stats["total_consultores"] == 3
|
||||||
|
assert stats["total_ativos"] == 2
|
||||||
|
assert stats["total_inativos"] == 1
|
||||||
|
assert stats["pontuacao_maxima"] == 300
|
||||||
|
assert stats["pontuacao_minima"] == 100
|
||||||
|
assert stats["pontuacao_media"] == 200.0
|
||||||
|
|
||||||
|
def test_media_componentes(self):
|
||||||
|
entries = [
|
||||||
|
RankingEntry(id_pessoa=1, nome="A", posicao=1, pontuacao_total=300, bloco_a=100, bloco_b=50, bloco_c=80, bloco_d=40, bloco_e=30, ativo=True, e_consultor=True, anos_atuacao=5, detalhes={}),
|
||||||
|
RankingEntry(id_pessoa=2, nome="B", posicao=2, pontuacao_total=200, bloco_a=80, bloco_b=30, bloco_c=60, bloco_d=20, bloco_e=10, ativo=True, e_consultor=True, anos_atuacao=3, detalhes={}),
|
||||||
|
]
|
||||||
|
stats = ProcessarRankingJob._obter_estatisticas(entries)
|
||||||
|
assert stats["media_componentes"]["a"] == 90.0
|
||||||
|
assert stats["media_componentes"]["b"] == 40.0
|
||||||
|
|
||||||
|
|
||||||
|
class TestGerarJsonDetalhes:
|
||||||
|
|
||||||
|
def test_gerar_json_consultor_completo(self, job):
|
||||||
|
from src.domain.entities.consultor import Consultor, CoordenacaoCapes, Consultoria
|
||||||
|
from src.domain.value_objects.periodo import Periodo
|
||||||
|
from src.domain.value_objects.pontuacao import PontuacaoBloco, PontuacaoCompleta
|
||||||
|
|
||||||
|
periodo = Periodo(inicio=datetime(2020, 1, 1), fim=None)
|
||||||
|
consultor = Consultor(
|
||||||
|
id_pessoa=123,
|
||||||
|
nome="João Silva",
|
||||||
|
coordenacoes_capes=[
|
||||||
|
CoordenacaoCapes(
|
||||||
|
codigo="CA",
|
||||||
|
tipo="Coordenador de Área",
|
||||||
|
area_avaliacao="CIÊNCIAS",
|
||||||
|
periodo=periodo,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
consultoria=Consultoria(
|
||||||
|
codigo="CONS_ATIVO",
|
||||||
|
situacao="Atividade Contínua",
|
||||||
|
periodo=periodo,
|
||||||
|
anos_consecutivos=5,
|
||||||
|
retornos=0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
consultor.pontuacao = PontuacaoCompleta(
|
||||||
|
bloco_a=PontuacaoBloco(bloco="A", atuacoes=[]),
|
||||||
|
bloco_b=PontuacaoBloco(bloco="B", atuacoes=[]),
|
||||||
|
bloco_c=PontuacaoBloco(bloco="C", atuacoes=[]),
|
||||||
|
bloco_d=PontuacaoBloco(bloco="D", atuacoes=[]),
|
||||||
|
bloco_e=PontuacaoBloco(bloco="E", atuacoes=[]),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = job._gerar_json_detalhes(consultor)
|
||||||
|
|
||||||
|
assert result["id_pessoa"] == 123
|
||||||
|
assert result["nome"] == "João Silva"
|
||||||
|
assert len(result["coordenacoes_capes"]) == 1
|
||||||
|
assert result["consultoria"] is not None
|
||||||
|
assert result["consultoria"]["codigo"] == "CONS_ATIVO"
|
||||||
|
|
||||||
|
def test_gerar_json_consultor_sem_consultoria(self, job):
|
||||||
|
from src.domain.entities.consultor import Consultor
|
||||||
|
from src.domain.value_objects.pontuacao import PontuacaoBloco, PontuacaoCompleta
|
||||||
|
|
||||||
|
consultor = Consultor(
|
||||||
|
id_pessoa=456,
|
||||||
|
nome="Maria Santos",
|
||||||
|
)
|
||||||
|
consultor.pontuacao = PontuacaoCompleta(
|
||||||
|
bloco_a=PontuacaoBloco(bloco="A", atuacoes=[]),
|
||||||
|
bloco_b=PontuacaoBloco(bloco="B", atuacoes=[]),
|
||||||
|
bloco_c=PontuacaoBloco(bloco="C", atuacoes=[]),
|
||||||
|
bloco_d=PontuacaoBloco(bloco="D", atuacoes=[]),
|
||||||
|
bloco_e=PontuacaoBloco(bloco="E", atuacoes=[]),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = job._gerar_json_detalhes(consultor)
|
||||||
|
|
||||||
|
assert result["id_pessoa"] == 456
|
||||||
|
assert result["consultoria"] is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestProcessarBatch:
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_processar_batch_atualiza_progresso(self, job):
|
||||||
|
with patch.object(job.consultor_repo, "_construir_consultor") as mock_construir:
|
||||||
|
from src.domain.entities.consultor import Consultor
|
||||||
|
from src.domain.value_objects.pontuacao import PontuacaoBloco, PontuacaoCompleta
|
||||||
|
|
||||||
|
consultor = Consultor(id_pessoa=1, nome="Test")
|
||||||
|
consultor.pontuacao = PontuacaoCompleta(
|
||||||
|
bloco_a=PontuacaoBloco(bloco="A", atuacoes=[]),
|
||||||
|
bloco_b=PontuacaoBloco(bloco="B", atuacoes=[]),
|
||||||
|
bloco_c=PontuacaoBloco(bloco="C", atuacoes=[]),
|
||||||
|
bloco_d=PontuacaoBloco(bloco="D", atuacoes=[]),
|
||||||
|
bloco_e=PontuacaoBloco(bloco="E", atuacoes=[]),
|
||||||
|
)
|
||||||
|
mock_construir.return_value = consultor
|
||||||
|
|
||||||
|
docs = [{"id": 1, "dadosPessoais": {"nome": "Test"}, "atuacoes": []}]
|
||||||
|
progress = {"processados": 1, "batch_atual": 1, "percentual": 10}
|
||||||
|
|
||||||
|
with patch("src.application.jobs.processar_ranking.job_status"):
|
||||||
|
await job._processar_batch(docs, progress)
|
||||||
|
|
||||||
|
assert len(job._consultores) == 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestExecutar:
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_executar_job_ja_rodando(self, job):
|
||||||
|
with patch("src.application.jobs.processar_ranking.job_status") as mock_status:
|
||||||
|
mock_status.is_running = True
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="Job já está em execução"):
|
||||||
|
await job.executar()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_executar_sucesso(self, job, mock_es_client, mock_ranking_store):
|
||||||
|
with patch("src.application.jobs.processar_ranking.job_status") as mock_status:
|
||||||
|
mock_status.is_running = False
|
||||||
|
mock_status.iniciar = MagicMock()
|
||||||
|
mock_status.atualizar_progresso = MagicMock()
|
||||||
|
mock_status.finalizar = MagicMock()
|
||||||
|
mock_status.mensagem = ""
|
||||||
|
mock_status.tempo_decorrido = 10.5
|
||||||
|
|
||||||
|
job._consultores = [
|
||||||
|
{"id_pessoa": 1, "nome": "Test", "pontuacao_total": 100, "bloco_a": 50, "bloco_b": 20, "bloco_c": 20, "bloco_d": 5, "bloco_e": 5, "ativo": True, "anos_atuacao": 5}
|
||||||
|
]
|
||||||
|
|
||||||
|
resultado = await job.executar()
|
||||||
|
|
||||||
|
assert resultado["sucesso"] is True
|
||||||
|
mock_status.iniciar.assert_called_once()
|
||||||
|
mock_status.finalizar.assert_called_once_with(sucesso=True)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_executar_erro(self, job, mock_es_client):
|
||||||
|
with patch("src.application.jobs.processar_ranking.job_status") as mock_status:
|
||||||
|
mock_status.is_running = False
|
||||||
|
mock_status.iniciar = MagicMock()
|
||||||
|
mock_status.finalizar = MagicMock()
|
||||||
|
|
||||||
|
mock_es_client.contar_com_atuacoes.side_effect = Exception("ES Error")
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
await job.executar()
|
||||||
|
|
||||||
|
mock_status.finalizar.assert_called_once()
|
||||||
|
args = mock_status.finalizar.call_args
|
||||||
|
assert args[1]["sucesso"] is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestPersistirOracle:
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_persistir_oracle_limpa_antes(self, job, mock_oracle_repo):
|
||||||
|
consultores = [
|
||||||
|
{"id_pessoa": 1, "nome": "Test", "pontuacao_total": 100, "posicao": 1}
|
||||||
|
]
|
||||||
|
await job._persistir_oracle(consultores, limpar_antes=True)
|
||||||
|
mock_oracle_repo.limpar_tabela.assert_called_once()
|
||||||
|
mock_oracle_repo.inserir_batch.assert_called()
|
||||||
|
mock_oracle_repo.atualizar_posicoes.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_persistir_oracle_sem_limpar(self, job, mock_oracle_repo):
|
||||||
|
consultores = [
|
||||||
|
{"id_pessoa": 1, "nome": "Test", "pontuacao_total": 100, "posicao": 1}
|
||||||
|
]
|
||||||
|
await job._persistir_oracle(consultores, limpar_antes=False)
|
||||||
|
mock_oracle_repo.limpar_tabela.assert_not_called()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_persistir_oracle_em_batches(self, job, mock_oracle_repo):
|
||||||
|
consultores = [{"id_pessoa": i, "nome": f"Test{i}", "pontuacao_total": 100} for i in range(5000)]
|
||||||
|
await job._persistir_oracle(consultores, limpar_antes=True)
|
||||||
|
assert mock_oracle_repo.inserir_batch.call_count == 3
|
||||||
0
backend/tests/application/services/__init__.py
Normal file
0
backend/tests/application/services/__init__.py
Normal file
384
backend/tests/application/services/test_excel_service.py
Normal file
384
backend/tests/application/services/test_excel_service.py
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
import pytest
|
||||||
|
from io import BytesIO
|
||||||
|
import json
|
||||||
|
|
||||||
|
from openpyxl import load_workbook
|
||||||
|
|
||||||
|
from src.application.services.excel_service import ExcelService
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def excel_service():
|
||||||
|
return ExcelService()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def consultor_basico():
|
||||||
|
return {
|
||||||
|
"POSICAO": 1,
|
||||||
|
"ID_PESSOA": 12345,
|
||||||
|
"NOME": "MARIA SILVA SANTOS",
|
||||||
|
"PONTUACAO_TOTAL": 450.0,
|
||||||
|
"COMPONENTE_A": 200.0,
|
||||||
|
"COMPONENTE_C": 150.0,
|
||||||
|
"COMPONENTE_D": 50.0,
|
||||||
|
"COMPONENTE_E": 50.0,
|
||||||
|
"ATIVO": "S",
|
||||||
|
"ANOS_ATUACAO": 10.5,
|
||||||
|
"SELOS": "CA,CONS_ATIVO,AUTOR_GP",
|
||||||
|
"JSON_DETALHES": json.dumps({
|
||||||
|
"coordenacoes_capes": [
|
||||||
|
{"tipo": "CA", "area": "CIÊNCIAS DA COMPUTAÇÃO"}
|
||||||
|
],
|
||||||
|
"consultoria": {"situacao": "Atividade Contínua"},
|
||||||
|
"premiacoes": [
|
||||||
|
{"premio": "PCT", "tipo": "Grande Prêmio", "ano": 2023}
|
||||||
|
],
|
||||||
|
"titulacao": "Doutorado - USP (2010)"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def consultor_inativo():
|
||||||
|
return {
|
||||||
|
"POSICAO": 100,
|
||||||
|
"ID_PESSOA": 99999,
|
||||||
|
"NOME": "JOÃO PEREIRA",
|
||||||
|
"PONTUACAO_TOTAL": 100.0,
|
||||||
|
"COMPONENTE_A": 0.0,
|
||||||
|
"COMPONENTE_C": 100.0,
|
||||||
|
"COMPONENTE_D": 0.0,
|
||||||
|
"COMPONENTE_E": 0.0,
|
||||||
|
"ATIVO": "N",
|
||||||
|
"ANOS_ATUACAO": 3.0,
|
||||||
|
"SELOS": "",
|
||||||
|
"JSON_DETALHES": None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestGerarRankingExcel:
|
||||||
|
|
||||||
|
def test_gerar_excel_lista_vazia(self, excel_service):
|
||||||
|
resultado = excel_service.gerar_ranking_excel([])
|
||||||
|
|
||||||
|
assert isinstance(resultado, bytes)
|
||||||
|
assert len(resultado) > 0
|
||||||
|
|
||||||
|
wb = load_workbook(BytesIO(resultado))
|
||||||
|
ws = wb.active
|
||||||
|
assert ws.title == "Ranking Consultores"
|
||||||
|
|
||||||
|
def test_gerar_excel_um_consultor(self, excel_service, consultor_basico):
|
||||||
|
resultado = excel_service.gerar_ranking_excel([consultor_basico])
|
||||||
|
|
||||||
|
wb = load_workbook(BytesIO(resultado))
|
||||||
|
ws = wb.active
|
||||||
|
|
||||||
|
assert ws["A1"].value == "RANKING DE CONSULTORES CAPES"
|
||||||
|
assert ws["A5"].value == 1
|
||||||
|
assert ws["B5"].value == 12345
|
||||||
|
assert ws["C5"].value == "MARIA SILVA SANTOS"
|
||||||
|
assert ws["D5"].value == 450.0
|
||||||
|
|
||||||
|
def test_gerar_excel_multiplos_consultores(self, excel_service, consultor_basico, consultor_inativo):
|
||||||
|
consultores = [consultor_basico, consultor_inativo]
|
||||||
|
resultado = excel_service.gerar_ranking_excel(consultores)
|
||||||
|
|
||||||
|
wb = load_workbook(BytesIO(resultado))
|
||||||
|
ws = wb.active
|
||||||
|
|
||||||
|
assert ws["C5"].value == "MARIA SILVA SANTOS"
|
||||||
|
assert ws["C6"].value == "JOÃO PEREIRA"
|
||||||
|
|
||||||
|
def test_gerar_excel_com_filtros(self, excel_service, consultor_basico):
|
||||||
|
filtros = {"ativo": True, "selos": ["CA", "CONS_ATIVO"]}
|
||||||
|
resultado = excel_service.gerar_ranking_excel([consultor_basico], filtros_aplicados=filtros)
|
||||||
|
|
||||||
|
wb = load_workbook(BytesIO(resultado))
|
||||||
|
ws = wb.active
|
||||||
|
|
||||||
|
subtitulo = ws["A2"].value
|
||||||
|
assert "Ativos" in subtitulo
|
||||||
|
assert "CA" in subtitulo
|
||||||
|
|
||||||
|
def test_gerar_excel_consultor_ativo(self, excel_service, consultor_basico):
|
||||||
|
resultado = excel_service.gerar_ranking_excel([consultor_basico])
|
||||||
|
|
||||||
|
wb = load_workbook(BytesIO(resultado))
|
||||||
|
ws = wb.active
|
||||||
|
|
||||||
|
assert ws["I5"].value == "Ativo"
|
||||||
|
|
||||||
|
def test_gerar_excel_consultor_inativo(self, excel_service, consultor_inativo):
|
||||||
|
resultado = excel_service.gerar_ranking_excel([consultor_inativo])
|
||||||
|
|
||||||
|
wb = load_workbook(BytesIO(resultado))
|
||||||
|
ws = wb.active
|
||||||
|
|
||||||
|
assert ws["I5"].value == "Inativo"
|
||||||
|
|
||||||
|
def test_gerar_excel_extrai_coordenacoes(self, excel_service, consultor_basico):
|
||||||
|
resultado = excel_service.gerar_ranking_excel([consultor_basico])
|
||||||
|
|
||||||
|
wb = load_workbook(BytesIO(resultado))
|
||||||
|
ws = wb.active
|
||||||
|
|
||||||
|
coord_cell = ws["L5"].value
|
||||||
|
assert "CA" in coord_cell
|
||||||
|
assert "CIÊNCIAS DA COMPUTAÇÃO" in coord_cell
|
||||||
|
|
||||||
|
def test_gerar_excel_extrai_consultoria(self, excel_service, consultor_basico):
|
||||||
|
resultado = excel_service.gerar_ranking_excel([consultor_basico])
|
||||||
|
|
||||||
|
wb = load_workbook(BytesIO(resultado))
|
||||||
|
ws = wb.active
|
||||||
|
|
||||||
|
assert ws["M5"].value == "Atividade Contínua"
|
||||||
|
|
||||||
|
def test_gerar_excel_extrai_premios(self, excel_service, consultor_basico):
|
||||||
|
resultado = excel_service.gerar_ranking_excel([consultor_basico])
|
||||||
|
|
||||||
|
wb = load_workbook(BytesIO(resultado))
|
||||||
|
ws = wb.active
|
||||||
|
|
||||||
|
premios_cell = ws["N5"].value
|
||||||
|
assert "PCT" in premios_cell
|
||||||
|
assert "2023" in premios_cell
|
||||||
|
|
||||||
|
def test_gerar_excel_extrai_titulacao(self, excel_service, consultor_basico):
|
||||||
|
resultado = excel_service.gerar_ranking_excel([consultor_basico])
|
||||||
|
|
||||||
|
wb = load_workbook(BytesIO(resultado))
|
||||||
|
ws = wb.active
|
||||||
|
|
||||||
|
assert "Doutorado" in ws["O5"].value
|
||||||
|
|
||||||
|
def test_gerar_excel_selos_formatados(self, excel_service, consultor_basico):
|
||||||
|
resultado = excel_service.gerar_ranking_excel([consultor_basico])
|
||||||
|
|
||||||
|
wb = load_workbook(BytesIO(resultado))
|
||||||
|
ws = wb.active
|
||||||
|
|
||||||
|
selos_cell = ws["K5"].value
|
||||||
|
assert "CA" in selos_cell
|
||||||
|
assert ", " in selos_cell
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseJsonDetalhes:
|
||||||
|
|
||||||
|
def test_parse_json_string_valido(self, excel_service):
|
||||||
|
json_str = '{"coordenacoes_capes": [{"tipo": "CA"}]}'
|
||||||
|
resultado = excel_service._parse_json_detalhes(json_str)
|
||||||
|
|
||||||
|
assert resultado == {"coordenacoes_capes": [{"tipo": "CA"}]}
|
||||||
|
|
||||||
|
def test_parse_json_none(self, excel_service):
|
||||||
|
resultado = excel_service._parse_json_detalhes(None)
|
||||||
|
assert resultado == {}
|
||||||
|
|
||||||
|
def test_parse_json_string_vazia(self, excel_service):
|
||||||
|
resultado = excel_service._parse_json_detalhes("")
|
||||||
|
assert resultado == {}
|
||||||
|
|
||||||
|
def test_parse_json_invalido(self, excel_service):
|
||||||
|
resultado = excel_service._parse_json_detalhes("{invalid json")
|
||||||
|
assert resultado == {}
|
||||||
|
|
||||||
|
def test_parse_json_dict_direto(self, excel_service):
|
||||||
|
dados = {"coordenacoes_capes": []}
|
||||||
|
resultado = excel_service._parse_json_detalhes(dados)
|
||||||
|
assert resultado == dados
|
||||||
|
|
||||||
|
def test_parse_json_file_like_object(self, excel_service):
|
||||||
|
class MockLob:
|
||||||
|
def read(self):
|
||||||
|
return '{"test": "value"}'
|
||||||
|
|
||||||
|
resultado = excel_service._parse_json_detalhes(MockLob())
|
||||||
|
assert resultado == {"test": "value"}
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormatarFiltros:
|
||||||
|
|
||||||
|
def test_formatar_filtros_ativo_true(self, excel_service):
|
||||||
|
filtros = {"ativo": True}
|
||||||
|
resultado = excel_service._formatar_filtros(filtros)
|
||||||
|
assert resultado == "Ativos"
|
||||||
|
|
||||||
|
def test_formatar_filtros_ativo_false(self, excel_service):
|
||||||
|
filtros = {"ativo": False}
|
||||||
|
resultado = excel_service._formatar_filtros(filtros)
|
||||||
|
assert resultado == "Inativos"
|
||||||
|
|
||||||
|
def test_formatar_filtros_com_selos(self, excel_service):
|
||||||
|
filtros = {"selos": ["CA", "CAJ"]}
|
||||||
|
resultado = excel_service._formatar_filtros(filtros)
|
||||||
|
assert "Selos: CA, CAJ" in resultado
|
||||||
|
|
||||||
|
def test_formatar_filtros_combinados(self, excel_service):
|
||||||
|
filtros = {"ativo": True, "selos": ["CA"]}
|
||||||
|
resultado = excel_service._formatar_filtros(filtros)
|
||||||
|
assert "Ativos" in resultado
|
||||||
|
assert "Selos: CA" in resultado
|
||||||
|
|
||||||
|
def test_formatar_filtros_vazio(self, excel_service):
|
||||||
|
filtros = {}
|
||||||
|
resultado = excel_service._formatar_filtros(filtros)
|
||||||
|
assert resultado == ""
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtrairCoordenacoesResumo:
|
||||||
|
|
||||||
|
def test_extrair_coordenacoes_vazio(self, excel_service):
|
||||||
|
resultado = excel_service._extrair_coordenacoes_resumo({})
|
||||||
|
assert resultado == ""
|
||||||
|
|
||||||
|
def test_extrair_coordenacoes_lista_vazia(self, excel_service):
|
||||||
|
resultado = excel_service._extrair_coordenacoes_resumo({"coordenacoes_capes": []})
|
||||||
|
assert resultado == ""
|
||||||
|
|
||||||
|
def test_extrair_coordenacoes_com_tipo_e_area(self, excel_service):
|
||||||
|
detalhes = {
|
||||||
|
"coordenacoes_capes": [
|
||||||
|
{"tipo": "CA", "area": "MATEMÁTICA"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
resultado = excel_service._extrair_coordenacoes_resumo(detalhes)
|
||||||
|
assert resultado == "CA: MATEMÁTICA"
|
||||||
|
|
||||||
|
def test_extrair_coordenacoes_apenas_tipo(self, excel_service):
|
||||||
|
detalhes = {
|
||||||
|
"coordenacoes_capes": [
|
||||||
|
{"tipo": "CAJ"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
resultado = excel_service._extrair_coordenacoes_resumo(detalhes)
|
||||||
|
assert resultado == "CAJ"
|
||||||
|
|
||||||
|
def test_extrair_coordenacoes_multiplas(self, excel_service):
|
||||||
|
detalhes = {
|
||||||
|
"coordenacoes_capes": [
|
||||||
|
{"tipo": "CA", "area": "FÍSICA"},
|
||||||
|
{"tipo": "CAJ", "area": "QUÍMICA"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
resultado = excel_service._extrair_coordenacoes_resumo(detalhes)
|
||||||
|
assert "CA: FÍSICA" in resultado
|
||||||
|
assert "CAJ: QUÍMICA" in resultado
|
||||||
|
assert ";" in resultado
|
||||||
|
|
||||||
|
def test_extrair_coordenacoes_maximo_tres(self, excel_service):
|
||||||
|
detalhes = {
|
||||||
|
"coordenacoes_capes": [
|
||||||
|
{"tipo": "CA", "area": "A"},
|
||||||
|
{"tipo": "CAJ", "area": "B"},
|
||||||
|
{"tipo": "CAM", "area": "C"},
|
||||||
|
{"tipo": "CAJ_MP", "area": "D"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
resultado = excel_service._extrair_coordenacoes_resumo(detalhes)
|
||||||
|
assert "CA: A" in resultado
|
||||||
|
assert "CAJ: B" in resultado
|
||||||
|
assert "CAM: C" in resultado
|
||||||
|
assert "CAJ_MP" not in resultado
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtrairSituacaoConsultoria:
|
||||||
|
|
||||||
|
def test_extrair_situacao_vazio(self, excel_service):
|
||||||
|
resultado = excel_service._extrair_situacao_consultoria({})
|
||||||
|
assert resultado == ""
|
||||||
|
|
||||||
|
def test_extrair_situacao_sem_consultoria(self, excel_service):
|
||||||
|
resultado = excel_service._extrair_situacao_consultoria({"consultoria": None})
|
||||||
|
assert resultado == ""
|
||||||
|
|
||||||
|
def test_extrair_situacao_ativa(self, excel_service):
|
||||||
|
detalhes = {"consultoria": {"situacao": "Atividade Contínua"}}
|
||||||
|
resultado = excel_service._extrair_situacao_consultoria(detalhes)
|
||||||
|
assert resultado == "Atividade Contínua"
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtrairPremiosResumo:
|
||||||
|
|
||||||
|
def test_extrair_premios_vazio(self, excel_service):
|
||||||
|
resultado = excel_service._extrair_premios_resumo({})
|
||||||
|
assert resultado == ""
|
||||||
|
|
||||||
|
def test_extrair_premios_lista_vazia(self, excel_service):
|
||||||
|
resultado = excel_service._extrair_premios_resumo({"premiacoes": []})
|
||||||
|
assert resultado == ""
|
||||||
|
|
||||||
|
def test_extrair_premio_completo(self, excel_service):
|
||||||
|
detalhes = {
|
||||||
|
"premiacoes": [
|
||||||
|
{"premio": "PCT", "tipo": "Grande Prêmio", "ano": 2023}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
resultado = excel_service._extrair_premios_resumo(detalhes)
|
||||||
|
assert "PCT" in resultado
|
||||||
|
assert "Grande Prêmio" in resultado
|
||||||
|
assert "2023" in resultado
|
||||||
|
|
||||||
|
def test_extrair_premio_apenas_nome(self, excel_service):
|
||||||
|
detalhes = {
|
||||||
|
"premiacoes": [{"premio": "PCT"}]
|
||||||
|
}
|
||||||
|
resultado = excel_service._extrair_premios_resumo(detalhes)
|
||||||
|
assert resultado == "PCT"
|
||||||
|
|
||||||
|
def test_extrair_premios_mais_de_tres(self, excel_service):
|
||||||
|
detalhes = {
|
||||||
|
"premiacoes": [
|
||||||
|
{"premio": "A"},
|
||||||
|
{"premio": "B"},
|
||||||
|
{"premio": "C"},
|
||||||
|
{"premio": "D"},
|
||||||
|
{"premio": "E"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
resultado = excel_service._extrair_premios_resumo(detalhes)
|
||||||
|
assert "A" in resultado
|
||||||
|
assert "B" in resultado
|
||||||
|
assert "C" in resultado
|
||||||
|
assert "+2 outros" in resultado
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtrairTitulacao:
|
||||||
|
|
||||||
|
def test_extrair_titulacao_vazio(self, excel_service):
|
||||||
|
resultado = excel_service._extrair_titulacao({})
|
||||||
|
assert resultado == ""
|
||||||
|
|
||||||
|
def test_extrair_titulacao_string_direta(self, excel_service):
|
||||||
|
detalhes = {"titulacao": "Doutorado - UNICAMP (2015)"}
|
||||||
|
resultado = excel_service._extrair_titulacao(detalhes)
|
||||||
|
assert resultado == "Doutorado - UNICAMP (2015)"
|
||||||
|
|
||||||
|
def test_extrair_titulacao_do_lattes(self, excel_service):
|
||||||
|
detalhes = {
|
||||||
|
"lattes": {
|
||||||
|
"titulacoes": [
|
||||||
|
{"grau": "Doutorado", "ies_sigla": "USP", "ano": 2010}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultado = excel_service._extrair_titulacao(detalhes)
|
||||||
|
assert "Doutorado" in resultado
|
||||||
|
assert "USP" in resultado
|
||||||
|
assert "2010" in resultado
|
||||||
|
|
||||||
|
def test_extrair_titulacao_lattes_apenas_grau(self, excel_service):
|
||||||
|
detalhes = {
|
||||||
|
"lattes": {
|
||||||
|
"titulacoes": [{"grau": "Mestrado"}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultado = excel_service._extrair_titulacao(detalhes)
|
||||||
|
assert resultado == "Mestrado"
|
||||||
|
|
||||||
|
def test_extrair_titulacao_lattes_vazio(self, excel_service):
|
||||||
|
detalhes = {"lattes": {"titulacoes": []}}
|
||||||
|
resultado = excel_service._extrair_titulacao(detalhes)
|
||||||
|
assert resultado == ""
|
||||||
383
backend/tests/application/services/test_pdf_service.py
Normal file
383
backend/tests/application/services/test_pdf_service.py
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
from datetime import datetime
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from src.application.services.pdf_service import PDFService, ConsultorWrapper, DictWrapper
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormatDate:
|
||||||
|
|
||||||
|
def test_format_date_vazio(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
resultado = service._format_date("")
|
||||||
|
assert resultado == "-"
|
||||||
|
|
||||||
|
def test_format_date_none(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
resultado = service._format_date(None)
|
||||||
|
assert resultado == "-"
|
||||||
|
|
||||||
|
def test_format_date_formato_brasileiro(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
resultado = service._format_date("25/12/2023")
|
||||||
|
assert resultado == "25/12/2023"
|
||||||
|
|
||||||
|
def test_format_date_iso(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
resultado = service._format_date("2023-12-25T10:30:00")
|
||||||
|
assert resultado == "25/12/2023"
|
||||||
|
|
||||||
|
def test_format_date_iso_com_z(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
resultado = service._format_date("2023-12-25T10:30:00Z")
|
||||||
|
assert resultado == "25/12/2023"
|
||||||
|
|
||||||
|
def test_format_date_invalido(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
resultado = service._format_date("data-invalida")
|
||||||
|
assert resultado == "data-invalida"
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormatDateShort:
|
||||||
|
|
||||||
|
def test_format_date_short_vazio(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
resultado = service._format_date_short("")
|
||||||
|
assert resultado == "-"
|
||||||
|
|
||||||
|
def test_format_date_short_none(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
resultado = service._format_date_short(None)
|
||||||
|
assert resultado == "-"
|
||||||
|
|
||||||
|
def test_format_date_short_brasileiro_3_partes(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
resultado = service._format_date_short("25/12/2023")
|
||||||
|
assert resultado == "Dez/2023"
|
||||||
|
|
||||||
|
def test_format_date_short_brasileiro_2_partes(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
resultado = service._format_date_short("12/2023")
|
||||||
|
assert resultado == "Dez/2023"
|
||||||
|
|
||||||
|
def test_format_date_short_iso(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
resultado = service._format_date_short("2023-06-15T10:30:00")
|
||||||
|
assert resultado == "Jun/2023"
|
||||||
|
|
||||||
|
def test_format_date_short_janeiro(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
resultado = service._format_date_short("15/01/2024")
|
||||||
|
assert resultado == "Jan/2024"
|
||||||
|
|
||||||
|
|
||||||
|
class TestSortByDate:
|
||||||
|
|
||||||
|
def test_sort_by_date_lista_vazia(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
resultado = service._sort_by_date([], "data")
|
||||||
|
assert resultado == []
|
||||||
|
|
||||||
|
def test_sort_by_date_sem_campo(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
items = [{"nome": "A"}, {"nome": "B"}]
|
||||||
|
resultado = service._sort_by_date(items)
|
||||||
|
assert resultado == items
|
||||||
|
|
||||||
|
def test_sort_by_date_ordena_desc(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
items = [
|
||||||
|
{"nome": "A", "data": "2020-01-01"},
|
||||||
|
{"nome": "B", "data": "2023-01-01"},
|
||||||
|
{"nome": "C", "data": "2021-01-01"},
|
||||||
|
]
|
||||||
|
resultado = service._sort_by_date(items, "data")
|
||||||
|
assert resultado[0]["nome"] == "B"
|
||||||
|
assert resultado[1]["nome"] == "C"
|
||||||
|
assert resultado[2]["nome"] == "A"
|
||||||
|
|
||||||
|
def test_sort_by_date_formato_brasileiro(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
items = [
|
||||||
|
{"nome": "A", "data": "01/01/2020"},
|
||||||
|
{"nome": "B", "data": "01/01/2023"},
|
||||||
|
]
|
||||||
|
resultado = service._sort_by_date(items, "data")
|
||||||
|
assert resultado[0]["nome"] == "B"
|
||||||
|
|
||||||
|
def test_sort_by_date_com_ano_numerico(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
items = [
|
||||||
|
{"nome": "A", "ano": 2020},
|
||||||
|
{"nome": "B", "ano": 2023},
|
||||||
|
]
|
||||||
|
resultado = service._sort_by_date(items, "ano")
|
||||||
|
assert resultado[0]["nome"] == "B"
|
||||||
|
|
||||||
|
def test_sort_by_date_com_ano_string(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
items = [
|
||||||
|
{"nome": "A", "ano": "2020"},
|
||||||
|
{"nome": "B", "ano": "2023"},
|
||||||
|
]
|
||||||
|
resultado = service._sort_by_date(items, "ano")
|
||||||
|
assert resultado[0]["nome"] == "B"
|
||||||
|
|
||||||
|
def test_sort_by_date_valores_nulos_no_final(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
items = [
|
||||||
|
{"nome": "A", "data": None},
|
||||||
|
{"nome": "B", "data": "2023-01-01"},
|
||||||
|
{"nome": "C", "data": ""},
|
||||||
|
]
|
||||||
|
resultado = service._sort_by_date(items, "data")
|
||||||
|
assert resultado[0]["nome"] == "B"
|
||||||
|
|
||||||
|
def test_sort_by_date_fallback_campos(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
items = [
|
||||||
|
{"nome": "A", "inicio": "2020-01-01"},
|
||||||
|
{"nome": "B", "data": "2023-01-01"},
|
||||||
|
]
|
||||||
|
resultado = service._sort_by_date(items, "data", "inicio")
|
||||||
|
assert resultado[0]["nome"] == "B"
|
||||||
|
assert resultado[1]["nome"] == "A"
|
||||||
|
|
||||||
|
|
||||||
|
class TestConsultorToDict:
|
||||||
|
|
||||||
|
def test_consultor_to_dict_dict(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
dados = {"nome": "Teste", "id": 123}
|
||||||
|
resultado = service._consultor_to_dict(dados)
|
||||||
|
assert resultado == dados
|
||||||
|
|
||||||
|
def test_consultor_to_dict_dataclass(self):
|
||||||
|
@dataclass
|
||||||
|
class ConsultorTest:
|
||||||
|
nome: str
|
||||||
|
id: int
|
||||||
|
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
consultor = ConsultorTest(nome="Teste", id=123)
|
||||||
|
resultado = service._consultor_to_dict(consultor)
|
||||||
|
assert resultado == {"nome": "Teste", "id": 123}
|
||||||
|
|
||||||
|
def test_consultor_to_dict_pydantic_model_dump(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
mock_obj = MagicMock()
|
||||||
|
mock_obj.model_dump.return_value = {"nome": "Teste"}
|
||||||
|
del mock_obj.dict
|
||||||
|
resultado = service._consultor_to_dict(mock_obj)
|
||||||
|
assert resultado == {"nome": "Teste"}
|
||||||
|
|
||||||
|
def test_consultor_to_dict_pydantic_dict(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
mock_obj = MagicMock(spec=['dict'])
|
||||||
|
mock_obj.dict.return_value = {"nome": "Teste"}
|
||||||
|
resultado = service._consultor_to_dict(mock_obj)
|
||||||
|
assert resultado == {"nome": "Teste"}
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtrairPontuacaoCoord:
|
||||||
|
|
||||||
|
def test_extrair_pontuacao_coord_vazio(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
resultado = service._extrair_pontuacao_coord({})
|
||||||
|
assert resultado == {}
|
||||||
|
|
||||||
|
def test_extrair_pontuacao_coord_sem_pontuacao(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
resultado = service._extrair_pontuacao_coord({"nome": "Teste"})
|
||||||
|
assert resultado == {}
|
||||||
|
|
||||||
|
def test_extrair_pontuacao_coord_com_atuacoes(self):
|
||||||
|
service = PDFService.__new__(PDFService)
|
||||||
|
dados = {
|
||||||
|
"pontuacao": {
|
||||||
|
"bloco_a": {
|
||||||
|
"atuacoes": [
|
||||||
|
{"codigo": "CA", "base": 200, "tempo": 50, "bonus": 30, "total": 280},
|
||||||
|
{"codigo": "CAJ", "base": 150, "tempo": 40, "bonus": 20, "total": 210},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultado = service._extrair_pontuacao_coord(dados)
|
||||||
|
assert "CA" in resultado
|
||||||
|
assert resultado["CA"]["base"] == 200
|
||||||
|
assert resultado["CA"]["tempo"] == 50
|
||||||
|
assert resultado["CA"]["total"] == 280
|
||||||
|
assert "CAJ" in resultado
|
||||||
|
|
||||||
|
|
||||||
|
class TestConsultorWrapper:
|
||||||
|
|
||||||
|
def test_wrapper_acesso_atributo_simples(self):
|
||||||
|
wrapper = ConsultorWrapper({"nome": "Teste", "id": 123})
|
||||||
|
assert wrapper.nome == "Teste"
|
||||||
|
assert wrapper.id == 123
|
||||||
|
|
||||||
|
def test_wrapper_atributo_inexistente(self):
|
||||||
|
wrapper = ConsultorWrapper({"nome": "Teste"})
|
||||||
|
assert wrapper.id is None
|
||||||
|
|
||||||
|
def test_wrapper_atributo_dict_retorna_dictwrapper(self):
|
||||||
|
wrapper = ConsultorWrapper({
|
||||||
|
"dados": {"campo": "valor"}
|
||||||
|
})
|
||||||
|
assert isinstance(wrapper.dados, DictWrapper)
|
||||||
|
assert wrapper.dados.campo == "valor"
|
||||||
|
|
||||||
|
def test_wrapper_atributo_lista_de_dicts(self):
|
||||||
|
wrapper = ConsultorWrapper({
|
||||||
|
"itens": [{"nome": "A"}, {"nome": "B"}]
|
||||||
|
})
|
||||||
|
assert len(wrapper.itens) == 2
|
||||||
|
assert isinstance(wrapper.itens[0], DictWrapper)
|
||||||
|
assert wrapper.itens[0].nome == "A"
|
||||||
|
|
||||||
|
def test_wrapper_bool_true(self):
|
||||||
|
wrapper = ConsultorWrapper({"nome": "Teste"})
|
||||||
|
assert bool(wrapper) is True
|
||||||
|
|
||||||
|
def test_wrapper_bool_false(self):
|
||||||
|
wrapper = ConsultorWrapper({})
|
||||||
|
assert bool(wrapper) is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestDictWrapper:
|
||||||
|
|
||||||
|
def test_dictwrapper_acesso_atributo(self):
|
||||||
|
wrapper = DictWrapper({"campo": "valor"})
|
||||||
|
assert wrapper.campo == "valor"
|
||||||
|
|
||||||
|
def test_dictwrapper_atributo_inexistente(self):
|
||||||
|
wrapper = DictWrapper({"campo": "valor"})
|
||||||
|
assert wrapper.outro is None
|
||||||
|
|
||||||
|
def test_dictwrapper_aninhado(self):
|
||||||
|
wrapper = DictWrapper({
|
||||||
|
"nivel1": {"nivel2": {"nivel3": "valor"}}
|
||||||
|
})
|
||||||
|
assert wrapper.nivel1.nivel2.nivel3 == "valor"
|
||||||
|
|
||||||
|
def test_dictwrapper_lista(self):
|
||||||
|
wrapper = DictWrapper({
|
||||||
|
"itens": [{"a": 1}, {"b": 2}]
|
||||||
|
})
|
||||||
|
assert len(wrapper.itens) == 2
|
||||||
|
assert wrapper.itens[0].a == 1
|
||||||
|
|
||||||
|
def test_dictwrapper_get(self):
|
||||||
|
wrapper = DictWrapper({"campo": "valor"})
|
||||||
|
assert wrapper.get("campo") == "valor"
|
||||||
|
assert wrapper.get("inexistente", "default") == "default"
|
||||||
|
|
||||||
|
def test_dictwrapper_str(self):
|
||||||
|
wrapper = DictWrapper({"campo": "valor"})
|
||||||
|
assert "campo" in str(wrapper)
|
||||||
|
|
||||||
|
def test_dictwrapper_bool_true(self):
|
||||||
|
wrapper = DictWrapper({"campo": "valor"})
|
||||||
|
assert bool(wrapper) is True
|
||||||
|
|
||||||
|
def test_dictwrapper_bool_false(self):
|
||||||
|
wrapper = DictWrapper({})
|
||||||
|
assert bool(wrapper) is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestGerarFichaConsultor:
|
||||||
|
|
||||||
|
def test_gerar_ficha_consultor_mock(self):
|
||||||
|
import sys
|
||||||
|
mock_weasyprint = MagicMock()
|
||||||
|
mock_weasyprint.HTML.return_value.write_pdf.return_value = b"PDF_CONTENT"
|
||||||
|
mock_weasyprint.CSS.return_value = MagicMock()
|
||||||
|
sys.modules['weasyprint'] = mock_weasyprint
|
||||||
|
|
||||||
|
try:
|
||||||
|
with patch.object(PDFService, '__init__', lambda self: None):
|
||||||
|
service = PDFService()
|
||||||
|
service.template = MagicMock()
|
||||||
|
service.template.render.return_value = "<html></html>"
|
||||||
|
from pathlib import Path
|
||||||
|
service.template_dir = Path("/tmp")
|
||||||
|
|
||||||
|
consultor = {"nome": "Teste", "id": 123}
|
||||||
|
resultado = service.gerar_ficha_consultor(consultor)
|
||||||
|
|
||||||
|
assert resultado == b"PDF_CONTENT"
|
||||||
|
service.template.render.assert_called_once()
|
||||||
|
finally:
|
||||||
|
del sys.modules['weasyprint']
|
||||||
|
|
||||||
|
|
||||||
|
class TestGerarPdfEquipe:
|
||||||
|
|
||||||
|
def test_gerar_pdf_equipe_mock(self):
|
||||||
|
import sys
|
||||||
|
mock_weasyprint = MagicMock()
|
||||||
|
mock_weasyprint.HTML.return_value.write_pdf.return_value = b"PDF_EQUIPE"
|
||||||
|
mock_weasyprint.CSS.return_value = MagicMock()
|
||||||
|
sys.modules['weasyprint'] = mock_weasyprint
|
||||||
|
|
||||||
|
try:
|
||||||
|
with patch.object(PDFService, '__init__', lambda self: None):
|
||||||
|
service = PDFService()
|
||||||
|
service.env = MagicMock()
|
||||||
|
mock_template = MagicMock()
|
||||||
|
mock_template.render.return_value = "<html></html>"
|
||||||
|
service.env.get_template.return_value = mock_template
|
||||||
|
from pathlib import Path
|
||||||
|
service.template_dir = Path("/tmp")
|
||||||
|
|
||||||
|
consultores = [
|
||||||
|
{"nome": "A", "ies": "USP", "areas_avaliacao": ["FÍSICA"], "situacao": "Atividade Contínua", "foi_coordenador": True, "foi_premiado": False},
|
||||||
|
{"nome": "B", "ies": "UNICAMP", "areas_avaliacao": ["QUÍMICA"], "situacao": "Inativo", "foi_coordenador": False, "foi_premiado": True},
|
||||||
|
]
|
||||||
|
|
||||||
|
resultado = service.gerar_pdf_equipe("Tema X", "FÍSICA", consultores)
|
||||||
|
|
||||||
|
assert resultado == b"PDF_EQUIPE"
|
||||||
|
mock_template.render.assert_called_once()
|
||||||
|
call_kwargs = mock_template.render.call_args[1]
|
||||||
|
assert call_kwargs["tema"] == "Tema X"
|
||||||
|
assert call_kwargs["area_avaliacao"] == "FÍSICA"
|
||||||
|
assert call_kwargs["estatisticas"]["total"] == 2
|
||||||
|
assert call_kwargs["estatisticas"]["coordenadores"] == 1
|
||||||
|
assert call_kwargs["estatisticas"]["premiados"] == 1
|
||||||
|
assert call_kwargs["estatisticas"]["ies_distintas"] == 2
|
||||||
|
finally:
|
||||||
|
del sys.modules['weasyprint']
|
||||||
|
|
||||||
|
def test_estatisticas_equipe_ativos(self):
|
||||||
|
import sys
|
||||||
|
mock_weasyprint = MagicMock()
|
||||||
|
mock_weasyprint.HTML.return_value.write_pdf.return_value = b"PDF"
|
||||||
|
mock_weasyprint.CSS.return_value = MagicMock()
|
||||||
|
sys.modules['weasyprint'] = mock_weasyprint
|
||||||
|
|
||||||
|
try:
|
||||||
|
with patch.object(PDFService, '__init__', lambda self: None):
|
||||||
|
service = PDFService()
|
||||||
|
service.env = MagicMock()
|
||||||
|
mock_template = MagicMock()
|
||||||
|
mock_template.render.return_value = "<html></html>"
|
||||||
|
service.env.get_template.return_value = mock_template
|
||||||
|
from pathlib import Path
|
||||||
|
service.template_dir = Path("/tmp")
|
||||||
|
|
||||||
|
consultores = [
|
||||||
|
{"nome": "A", "situacao": "Atividade Contínua"},
|
||||||
|
{"nome": "B", "situacao": "Atividade Contínua"},
|
||||||
|
{"nome": "C", "situacao": "Inativo"},
|
||||||
|
]
|
||||||
|
|
||||||
|
service.gerar_pdf_equipe("Tema", "ÁREA", consultores)
|
||||||
|
|
||||||
|
call_kwargs = mock_template.render.call_args[1]
|
||||||
|
assert call_kwargs["estatisticas"]["ativos"] == 2
|
||||||
|
finally:
|
||||||
|
del sys.modules['weasyprint']
|
||||||
294
backend/tests/conftest.py
Normal file
294
backend/tests/conftest.py
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
import pytest
|
||||||
|
from datetime import datetime
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
from src.domain.entities.consultor import (
|
||||||
|
Consultor,
|
||||||
|
CoordenacaoCapes,
|
||||||
|
Consultoria,
|
||||||
|
Inscricao,
|
||||||
|
AvaliacaoComissao,
|
||||||
|
Premiacao,
|
||||||
|
BolsaCNPQ,
|
||||||
|
Participacao,
|
||||||
|
Orientacao,
|
||||||
|
MembroBanca,
|
||||||
|
)
|
||||||
|
from src.domain.value_objects.periodo import Periodo
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def data_referencia():
|
||||||
|
return datetime(2025, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def hoje():
|
||||||
|
return datetime.now()
|
||||||
|
|
||||||
|
|
||||||
|
def criar_periodo(anos_atras: int, ativo: bool = False, duracao_anos: int = 0) -> Periodo:
|
||||||
|
inicio = datetime.now() - relativedelta(years=anos_atras)
|
||||||
|
if ativo:
|
||||||
|
fim = None
|
||||||
|
else:
|
||||||
|
fim = inicio + relativedelta(years=duracao_anos) if duracao_anos > 0 else datetime.now()
|
||||||
|
return Periodo(inicio=inicio, fim=fim)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def periodo_ativo_5_anos():
|
||||||
|
return criar_periodo(anos_atras=5, ativo=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def periodo_historico_3_anos():
|
||||||
|
return criar_periodo(anos_atras=5, duracao_anos=3)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def periodo_historico_10_anos():
|
||||||
|
return criar_periodo(anos_atras=12, duracao_anos=10)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def coordenacao_ca_ativa(periodo_ativo_5_anos):
|
||||||
|
return CoordenacaoCapes(
|
||||||
|
codigo="CA",
|
||||||
|
tipo="Coordenador de Área",
|
||||||
|
area_avaliacao="CIÊNCIAS AMBIENTAIS",
|
||||||
|
periodo=periodo_ativo_5_anos,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def coordenacao_ca_historica(periodo_historico_3_anos):
|
||||||
|
return CoordenacaoCapes(
|
||||||
|
codigo="CA",
|
||||||
|
tipo="Coordenador de Área",
|
||||||
|
area_avaliacao="MEDICINA I",
|
||||||
|
periodo=periodo_historico_3_anos,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def coordenacao_caj_ativa():
|
||||||
|
return CoordenacaoCapes(
|
||||||
|
codigo="CAJ",
|
||||||
|
tipo="Coordenador Adjunto",
|
||||||
|
area_avaliacao="ENGENHARIA I",
|
||||||
|
periodo=criar_periodo(anos_atras=3, ativo=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def coordenacao_caj_mp_historica():
|
||||||
|
return CoordenacaoCapes(
|
||||||
|
codigo="CAJ_MP",
|
||||||
|
tipo="Coordenador Adjunto de Mestrado Profissionalizante",
|
||||||
|
area_avaliacao="ADMINISTRAÇÃO",
|
||||||
|
periodo=criar_periodo(anos_atras=6, duracao_anos=4),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def coordenacao_cam_ativa():
|
||||||
|
return CoordenacaoCapes(
|
||||||
|
codigo="CAM",
|
||||||
|
tipo="Câmara Temática",
|
||||||
|
area_avaliacao="INTERDISCIPLINAR",
|
||||||
|
periodo=criar_periodo(anos_atras=2, ativo=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def consultoria_ativa_8_anos():
|
||||||
|
return Consultoria(
|
||||||
|
codigo="CONS_ATIVO",
|
||||||
|
situacao="Atividade Contínua",
|
||||||
|
periodo=criar_periodo(anos_atras=8, ativo=True),
|
||||||
|
periodos=[criar_periodo(anos_atras=8, ativo=True)],
|
||||||
|
anos_consecutivos=8,
|
||||||
|
retornos=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def consultoria_historica_5_anos():
|
||||||
|
return Consultoria(
|
||||||
|
codigo="CONS_HIST",
|
||||||
|
situacao="Inativo",
|
||||||
|
periodo=criar_periodo(anos_atras=7, duracao_anos=5),
|
||||||
|
periodos=[criar_periodo(anos_atras=7, duracao_anos=5)],
|
||||||
|
anos_consecutivos=5,
|
||||||
|
retornos=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def consultoria_com_retorno():
|
||||||
|
periodo1 = criar_periodo(anos_atras=10, duracao_anos=3)
|
||||||
|
periodo2 = criar_periodo(anos_atras=4, ativo=True)
|
||||||
|
return Consultoria(
|
||||||
|
codigo="CONS_ATIVO",
|
||||||
|
situacao="Atividade Contínua",
|
||||||
|
periodo=periodo2,
|
||||||
|
periodos=[periodo1, periodo2],
|
||||||
|
anos_consecutivos=4,
|
||||||
|
retornos=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def inscricao_autor():
|
||||||
|
return Inscricao(
|
||||||
|
codigo="INSC_AUTOR",
|
||||||
|
tipo="Autor",
|
||||||
|
premio="PCT",
|
||||||
|
ano=2024,
|
||||||
|
situacao="Aprovada",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def inscricao_institucional():
|
||||||
|
return Inscricao(
|
||||||
|
codigo="INSC_INST_AUTOR",
|
||||||
|
tipo="Institucional",
|
||||||
|
premio="PCT",
|
||||||
|
ano=2024,
|
||||||
|
situacao="Aprovada",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def avaliacao_comissao_premio():
|
||||||
|
return AvaliacaoComissao(
|
||||||
|
codigo="AVAL_COMIS_PREMIO",
|
||||||
|
tipo="Membro de Comissão",
|
||||||
|
premio="PCT",
|
||||||
|
ano=2024,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def avaliacao_comissao_gp():
|
||||||
|
return AvaliacaoComissao(
|
||||||
|
codigo="AVAL_COMIS_GP",
|
||||||
|
tipo="Membro de Comissão",
|
||||||
|
premio="Grande Prêmio",
|
||||||
|
ano=2024,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def coord_comissao_premio():
|
||||||
|
return AvaliacaoComissao(
|
||||||
|
codigo="COORD_COMIS_PREMIO",
|
||||||
|
tipo="Coordenador/Presidente",
|
||||||
|
premio="PCT",
|
||||||
|
ano=2024,
|
||||||
|
comissao_tipo="Coordenador",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def premiacao_gp_autor():
|
||||||
|
return Premiacao(
|
||||||
|
codigo="PREMIACAO_GP_AUTOR",
|
||||||
|
tipo="Grande Prêmio",
|
||||||
|
nome_premio="Grande Prêmio CAPES de Tese",
|
||||||
|
ano=2024,
|
||||||
|
papel="Autor",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def premiacao_autor():
|
||||||
|
return Premiacao(
|
||||||
|
codigo="PREMIACAO_AUTOR",
|
||||||
|
tipo="Prêmio",
|
||||||
|
nome_premio="Prêmio CAPES de Tese",
|
||||||
|
ano=2024,
|
||||||
|
papel="Autor",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mencao_autor():
|
||||||
|
return Premiacao(
|
||||||
|
codigo="MENCAO_AUTOR",
|
||||||
|
tipo="Menção Honrosa",
|
||||||
|
nome_premio="Prêmio CAPES de Tese",
|
||||||
|
ano=2024,
|
||||||
|
papel="Autor",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def bolsa_cnpq():
|
||||||
|
return BolsaCNPQ(
|
||||||
|
codigo="BOL_BPQ_NIVEL",
|
||||||
|
nivel="1A",
|
||||||
|
area="Ciências Exatas",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def participacao_evento():
|
||||||
|
return Participacao(
|
||||||
|
codigo="EVENTO",
|
||||||
|
tipo="Evento",
|
||||||
|
descricao="Seminário CAPES 2024",
|
||||||
|
ano=2024,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def participacao_projeto():
|
||||||
|
return Participacao(
|
||||||
|
codigo="PROJ",
|
||||||
|
tipo="Projeto",
|
||||||
|
descricao="Projeto de Pesquisa",
|
||||||
|
ano=2024,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def consultor_vazio():
|
||||||
|
return Consultor(
|
||||||
|
id_pessoa=1,
|
||||||
|
nome="Consultor Vazio",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def consultor_coordenador_area(coordenacao_ca_ativa):
|
||||||
|
return Consultor(
|
||||||
|
id_pessoa=2,
|
||||||
|
nome="Coordenador de Área Ativo",
|
||||||
|
coordenacoes_capes=[coordenacao_ca_ativa],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def consultor_completo(
|
||||||
|
coordenacao_ca_ativa,
|
||||||
|
consultoria_ativa_8_anos,
|
||||||
|
inscricao_autor,
|
||||||
|
avaliacao_comissao_premio,
|
||||||
|
premiacao_autor,
|
||||||
|
bolsa_cnpq,
|
||||||
|
participacao_evento,
|
||||||
|
):
|
||||||
|
return Consultor(
|
||||||
|
id_pessoa=3,
|
||||||
|
nome="Consultor Completo",
|
||||||
|
coordenacoes_capes=[coordenacao_ca_ativa],
|
||||||
|
consultoria=consultoria_ativa_8_anos,
|
||||||
|
inscricoes=[inscricao_autor],
|
||||||
|
avaliacoes_comissao=[avaliacao_comissao_premio],
|
||||||
|
premiacoes=[premiacao_autor],
|
||||||
|
bolsas_cnpq=[bolsa_cnpq],
|
||||||
|
participacoes=[participacao_evento],
|
||||||
|
)
|
||||||
0
backend/tests/domain/__init__.py
Normal file
0
backend/tests/domain/__init__.py
Normal file
0
backend/tests/domain/services/__init__.py
Normal file
0
backend/tests/domain/services/__init__.py
Normal file
599
backend/tests/domain/services/test_calculador_pontuacao.py
Normal file
599
backend/tests/domain/services/test_calculador_pontuacao.py
Normal file
@@ -0,0 +1,599 @@
|
|||||||
|
import pytest
|
||||||
|
from datetime import datetime
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
from src.domain.services.calculador_pontuacao import CalculadorPontuacao
|
||||||
|
from src.domain.entities.consultor import (
|
||||||
|
Consultor,
|
||||||
|
CoordenacaoCapes,
|
||||||
|
Consultoria,
|
||||||
|
Inscricao,
|
||||||
|
AvaliacaoComissao,
|
||||||
|
Premiacao,
|
||||||
|
BolsaCNPQ,
|
||||||
|
Participacao,
|
||||||
|
)
|
||||||
|
from src.domain.value_objects.periodo import Periodo
|
||||||
|
from src.domain.value_objects.criterios_pontuacao import CRITERIOS
|
||||||
|
|
||||||
|
|
||||||
|
def criar_periodo(anos_atras: int, ativo: bool = False, duracao_anos: int = 0) -> Periodo:
|
||||||
|
inicio = datetime.now() - relativedelta(years=anos_atras)
|
||||||
|
if ativo:
|
||||||
|
fim = None
|
||||||
|
else:
|
||||||
|
fim = inicio + relativedelta(years=duracao_anos) if duracao_anos > 0 else datetime.now()
|
||||||
|
return Periodo(inicio=inicio, fim=fim)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBlocoACoordenacaoCapes:
|
||||||
|
|
||||||
|
def test_coordenacao_vazia_retorna_bloco_vazio(self):
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_a([])
|
||||||
|
assert resultado.bloco == "A"
|
||||||
|
assert resultado.total == 0
|
||||||
|
assert len(resultado.atuacoes) == 0
|
||||||
|
|
||||||
|
def test_coordenador_area_base_200_pontos(self):
|
||||||
|
coord = CoordenacaoCapes(
|
||||||
|
codigo="CA",
|
||||||
|
tipo="Coordenador de Área",
|
||||||
|
area_avaliacao="CIÊNCIAS AMBIENTAIS",
|
||||||
|
periodo=criar_periodo(anos_atras=0, duracao_anos=0),
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
|
||||||
|
assert resultado.total >= 200
|
||||||
|
|
||||||
|
def test_coordenador_area_ativo_recebe_bonus_atualidade(self):
|
||||||
|
coord = CoordenacaoCapes(
|
||||||
|
codigo="CA",
|
||||||
|
tipo="Coordenador de Área",
|
||||||
|
area_avaliacao="CIÊNCIAS AMBIENTAIS",
|
||||||
|
periodo=criar_periodo(anos_atras=1, ativo=True),
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
|
||||||
|
atuacao = resultado.atuacoes[0]
|
||||||
|
assert atuacao.bonus >= 30
|
||||||
|
|
||||||
|
def test_coordenador_area_historico_sem_bonus_atualidade(self):
|
||||||
|
coord = CoordenacaoCapes(
|
||||||
|
codigo="CA",
|
||||||
|
tipo="Coordenador de Área",
|
||||||
|
area_avaliacao="MEDICINA I",
|
||||||
|
periodo=criar_periodo(anos_atras=5, duracao_anos=3),
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
|
||||||
|
atuacao = resultado.atuacoes[0]
|
||||||
|
assert atuacao.codigo == "CA"
|
||||||
|
assert 30 not in [atuacao.bonus] or atuacao.bonus < 30
|
||||||
|
|
||||||
|
def test_coordenador_area_5_anos_pontos_tempo(self):
|
||||||
|
coord = CoordenacaoCapes(
|
||||||
|
codigo="CA",
|
||||||
|
tipo="Coordenador de Área",
|
||||||
|
area_avaliacao="CIÊNCIAS AMBIENTAIS",
|
||||||
|
periodo=criar_periodo(anos_atras=5, duracao_anos=5),
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
|
||||||
|
atuacao = resultado.atuacoes[0]
|
||||||
|
assert atuacao.tempo == 50
|
||||||
|
|
||||||
|
def test_coordenador_area_teto_tempo_100_pontos(self):
|
||||||
|
coord = CoordenacaoCapes(
|
||||||
|
codigo="CA",
|
||||||
|
tipo="Coordenador de Área",
|
||||||
|
area_avaliacao="CIÊNCIAS AMBIENTAIS",
|
||||||
|
periodo=criar_periodo(anos_atras=15, duracao_anos=15),
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
|
||||||
|
atuacao = resultado.atuacoes[0]
|
||||||
|
assert atuacao.tempo == 100
|
||||||
|
|
||||||
|
def test_coordenador_area_teto_maximo_450(self):
|
||||||
|
coord = CoordenacaoCapes(
|
||||||
|
codigo="CA",
|
||||||
|
tipo="Coordenador de Área",
|
||||||
|
area_avaliacao="CIÊNCIAS AMBIENTAIS",
|
||||||
|
periodo=criar_periodo(anos_atras=20, ativo=True),
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
|
||||||
|
atuacao = resultado.atuacoes[0]
|
||||||
|
assert atuacao.total <= 450
|
||||||
|
|
||||||
|
def test_coordenador_adjunto_base_150_pontos(self):
|
||||||
|
coord = CoordenacaoCapes(
|
||||||
|
codigo="CAJ",
|
||||||
|
tipo="Coordenador Adjunto",
|
||||||
|
area_avaliacao="ENGENHARIA I",
|
||||||
|
periodo=criar_periodo(anos_atras=0, duracao_anos=0),
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
|
||||||
|
assert resultado.total >= 150
|
||||||
|
|
||||||
|
def test_coordenador_adjunto_mp_base_120_pontos(self):
|
||||||
|
coord = CoordenacaoCapes(
|
||||||
|
codigo="CAJ_MP",
|
||||||
|
tipo="Coordenador Adjunto de Mestrado Profissionalizante",
|
||||||
|
area_avaliacao="ADMINISTRAÇÃO",
|
||||||
|
periodo=criar_periodo(anos_atras=0, duracao_anos=0),
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
|
||||||
|
assert resultado.total >= 120
|
||||||
|
|
||||||
|
def test_camara_tematica_base_100_pontos(self):
|
||||||
|
coord = CoordenacaoCapes(
|
||||||
|
codigo="CAM",
|
||||||
|
tipo="Câmara Temática",
|
||||||
|
area_avaliacao="INTERDISCIPLINAR",
|
||||||
|
periodo=criar_periodo(anos_atras=0, duracao_anos=0),
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
|
||||||
|
assert resultado.total >= 100
|
||||||
|
|
||||||
|
def test_multiplas_coordenacoes_mesmo_tipo_soma_tempo(self):
|
||||||
|
periodo1 = criar_periodo(anos_atras=10, duracao_anos=3)
|
||||||
|
periodo2 = criar_periodo(anos_atras=5, duracao_anos=3)
|
||||||
|
coords = [
|
||||||
|
CoordenacaoCapes(
|
||||||
|
codigo="CA", tipo="Coordenador de Área",
|
||||||
|
area_avaliacao="ÁREA 1", periodo=periodo1
|
||||||
|
),
|
||||||
|
CoordenacaoCapes(
|
||||||
|
codigo="CA", tipo="Coordenador de Área",
|
||||||
|
area_avaliacao="ÁREA 2", periodo=periodo2
|
||||||
|
),
|
||||||
|
]
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_a(coords)
|
||||||
|
assert len(resultado.atuacoes) == 1
|
||||||
|
atuacao = resultado.atuacoes[0]
|
||||||
|
assert atuacao.quantidade == 2
|
||||||
|
assert atuacao.tempo == 60
|
||||||
|
|
||||||
|
def test_retorno_coordenacao_gera_bonus(self):
|
||||||
|
periodo1 = criar_periodo(anos_atras=10, duracao_anos=3)
|
||||||
|
periodo2 = criar_periodo(anos_atras=3, ativo=True)
|
||||||
|
coords = [
|
||||||
|
CoordenacaoCapes(
|
||||||
|
codigo="CA", tipo="Coordenador de Área",
|
||||||
|
area_avaliacao="ÁREA 1", periodo=periodo1
|
||||||
|
),
|
||||||
|
CoordenacaoCapes(
|
||||||
|
codigo="CA", tipo="Coordenador de Área",
|
||||||
|
area_avaliacao="ÁREA 1", periodo=periodo2
|
||||||
|
),
|
||||||
|
]
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_a(coords)
|
||||||
|
atuacao = resultado.atuacoes[0]
|
||||||
|
assert atuacao.bonus >= 20
|
||||||
|
|
||||||
|
def test_tipos_diferentes_geram_atuacoes_separadas(self):
|
||||||
|
coords = [
|
||||||
|
CoordenacaoCapes(
|
||||||
|
codigo="CA", tipo="Coordenador de Área",
|
||||||
|
area_avaliacao="ÁREA 1", periodo=criar_periodo(2, duracao_anos=2)
|
||||||
|
),
|
||||||
|
CoordenacaoCapes(
|
||||||
|
codigo="CAJ", tipo="Coordenador Adjunto",
|
||||||
|
area_avaliacao="ÁREA 1", periodo=criar_periodo(2, duracao_anos=2)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_a(coords)
|
||||||
|
assert len(resultado.atuacoes) == 2
|
||||||
|
|
||||||
|
def test_codigo_com_hifen_normalizado(self):
|
||||||
|
coord = CoordenacaoCapes(
|
||||||
|
codigo="CAJ-MP",
|
||||||
|
tipo="Coordenador Adjunto de Mestrado Profissionalizante",
|
||||||
|
area_avaliacao="ADMINISTRAÇÃO",
|
||||||
|
periodo=criar_periodo(anos_atras=2, duracao_anos=2),
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
|
||||||
|
assert len(resultado.atuacoes) == 1
|
||||||
|
assert resultado.atuacoes[0].codigo == "CAJ_MP"
|
||||||
|
|
||||||
|
|
||||||
|
class TestBlocoBConsultoria:
|
||||||
|
|
||||||
|
def test_consultoria_vazia_retorna_bloco_vazio(self):
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_b(None)
|
||||||
|
assert resultado.bloco == "B"
|
||||||
|
assert resultado.total == 0
|
||||||
|
|
||||||
|
def test_consultor_ativo_base_150_pontos(self):
|
||||||
|
consultoria = Consultoria(
|
||||||
|
codigo="CONS_ATIVO",
|
||||||
|
situacao="Atividade Contínua",
|
||||||
|
periodo=criar_periodo(anos_atras=1, ativo=True),
|
||||||
|
anos_consecutivos=1,
|
||||||
|
retornos=0,
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
|
||||||
|
assert resultado.total >= 150
|
||||||
|
|
||||||
|
def test_consultor_historico_base_100_pontos(self):
|
||||||
|
consultoria = Consultoria(
|
||||||
|
codigo="CONS_HIST",
|
||||||
|
situacao="Inativo",
|
||||||
|
periodo=criar_periodo(anos_atras=5, duracao_anos=3),
|
||||||
|
anos_consecutivos=3,
|
||||||
|
retornos=0,
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
|
||||||
|
assert resultado.total >= 100
|
||||||
|
|
||||||
|
def test_consultor_falecido_base_100_pontos(self):
|
||||||
|
consultoria = Consultoria(
|
||||||
|
codigo="CONS_FALECIDO",
|
||||||
|
situacao="Falecido",
|
||||||
|
periodo=criar_periodo(anos_atras=10, duracao_anos=8),
|
||||||
|
anos_consecutivos=8,
|
||||||
|
retornos=0,
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
|
||||||
|
assert resultado.total >= 100
|
||||||
|
|
||||||
|
def test_consultoria_5_anos_pontos_tempo(self):
|
||||||
|
consultoria = Consultoria(
|
||||||
|
codigo="CONS_ATIVO",
|
||||||
|
situacao="Atividade Contínua",
|
||||||
|
periodo=criar_periodo(anos_atras=5, ativo=True),
|
||||||
|
periodos=[criar_periodo(anos_atras=5, ativo=True)],
|
||||||
|
anos_consecutivos=5,
|
||||||
|
retornos=0,
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
|
||||||
|
atuacao = resultado.atuacoes[0]
|
||||||
|
assert atuacao.tempo == 25
|
||||||
|
|
||||||
|
def test_consultoria_teto_tempo_50_pontos(self):
|
||||||
|
consultoria = Consultoria(
|
||||||
|
codigo="CONS_ATIVO",
|
||||||
|
situacao="Atividade Contínua",
|
||||||
|
periodo=criar_periodo(anos_atras=15, ativo=True),
|
||||||
|
periodos=[criar_periodo(anos_atras=15, ativo=True)],
|
||||||
|
anos_consecutivos=15,
|
||||||
|
retornos=0,
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
|
||||||
|
atuacao = resultado.atuacoes[0]
|
||||||
|
assert atuacao.tempo == 50
|
||||||
|
|
||||||
|
def test_consultor_ativo_bonus_atualidade_20(self):
|
||||||
|
consultoria = Consultoria(
|
||||||
|
codigo="CONS_ATIVO",
|
||||||
|
situacao="Atividade Contínua",
|
||||||
|
periodo=criar_periodo(anos_atras=1, ativo=True),
|
||||||
|
anos_consecutivos=1,
|
||||||
|
retornos=0,
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
|
||||||
|
atuacao = resultado.atuacoes[0]
|
||||||
|
assert atuacao.bonus >= 20
|
||||||
|
|
||||||
|
def test_consultor_8_anos_bonus_continuidade(self):
|
||||||
|
consultoria = Consultoria(
|
||||||
|
codigo="CONS_ATIVO",
|
||||||
|
situacao="Atividade Contínua",
|
||||||
|
periodo=criar_periodo(anos_atras=8, ativo=True),
|
||||||
|
anos_consecutivos=8,
|
||||||
|
retornos=0,
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
|
||||||
|
atuacao = resultado.atuacoes[0]
|
||||||
|
assert atuacao.bonus >= 40
|
||||||
|
|
||||||
|
def test_consultor_com_retorno_bonus_15(self):
|
||||||
|
consultoria = Consultoria(
|
||||||
|
codigo="CONS_ATIVO",
|
||||||
|
situacao="Atividade Contínua",
|
||||||
|
periodo=criar_periodo(anos_atras=2, ativo=True),
|
||||||
|
periodos=[
|
||||||
|
criar_periodo(anos_atras=8, duracao_anos=3),
|
||||||
|
criar_periodo(anos_atras=2, ativo=True),
|
||||||
|
],
|
||||||
|
anos_consecutivos=2,
|
||||||
|
retornos=1,
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
|
||||||
|
atuacao = resultado.atuacoes[0]
|
||||||
|
assert atuacao.bonus >= 35
|
||||||
|
|
||||||
|
def test_consultoria_teto_maximo_230(self):
|
||||||
|
consultoria = Consultoria(
|
||||||
|
codigo="CONS_ATIVO",
|
||||||
|
situacao="Atividade Contínua",
|
||||||
|
periodo=criar_periodo(anos_atras=20, ativo=True),
|
||||||
|
periodos=[criar_periodo(anos_atras=20, ativo=True)],
|
||||||
|
anos_consecutivos=20,
|
||||||
|
retornos=1,
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
|
||||||
|
atuacao = resultado.atuacoes[0]
|
||||||
|
assert atuacao.total <= 230
|
||||||
|
|
||||||
|
|
||||||
|
class TestBlocoCPremiacoesAvaliacoes:
|
||||||
|
|
||||||
|
def test_bloco_c_vazio(self):
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_c([], [], [], [], [])
|
||||||
|
assert resultado.bloco == "C"
|
||||||
|
assert resultado.total == 0
|
||||||
|
|
||||||
|
def test_inscricao_autor_base_10_pontos(self):
|
||||||
|
inscricao = Inscricao(
|
||||||
|
codigo="INSC_AUTOR", tipo="Autor",
|
||||||
|
premio="PCT", ano=2024
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_c([inscricao], [], [], [], [])
|
||||||
|
atuacao = next((a for a in resultado.atuacoes if a.codigo == "INSC_AUTOR"), None)
|
||||||
|
assert atuacao is not None
|
||||||
|
assert atuacao.base >= 10
|
||||||
|
|
||||||
|
def test_inscricao_institucional_base_20_pontos(self):
|
||||||
|
inscricao = Inscricao(
|
||||||
|
codigo="INSC_INST_AUTOR", tipo="Institucional",
|
||||||
|
premio="PCT", ano=2024
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_c([inscricao], [], [], [], [])
|
||||||
|
atuacao = next((a for a in resultado.atuacoes if a.codigo == "INSC_INST_AUTOR"), None)
|
||||||
|
assert atuacao is not None
|
||||||
|
assert atuacao.base >= 20
|
||||||
|
|
||||||
|
def test_avaliacao_comissao_premio_base_30(self):
|
||||||
|
avaliacao = AvaliacaoComissao(
|
||||||
|
codigo="AVAL_COMIS_PREMIO", tipo="Membro de Comissão",
|
||||||
|
premio="PCT", ano=2024
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_c([], [avaliacao], [], [], [])
|
||||||
|
atuacao = next((a for a in resultado.atuacoes if a.codigo == "AVAL_COMIS_PREMIO"), None)
|
||||||
|
assert atuacao is not None
|
||||||
|
assert atuacao.base >= 30
|
||||||
|
|
||||||
|
def test_avaliacao_comissao_gp_base_40(self):
|
||||||
|
avaliacao = AvaliacaoComissao(
|
||||||
|
codigo="AVAL_COMIS_GP", tipo="Membro de Comissão",
|
||||||
|
premio="Grande Prêmio", ano=2024
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_c([], [avaliacao], [], [], [])
|
||||||
|
atuacao = next((a for a in resultado.atuacoes if a.codigo == "AVAL_COMIS_GP"), None)
|
||||||
|
assert atuacao is not None
|
||||||
|
assert atuacao.base >= 40
|
||||||
|
|
||||||
|
def test_coord_comissao_premio_base_40(self):
|
||||||
|
avaliacao = AvaliacaoComissao(
|
||||||
|
codigo="COORD_COMIS_PREMIO", tipo="Coordenador",
|
||||||
|
premio="PCT", ano=2024
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_c([], [avaliacao], [], [], [])
|
||||||
|
atuacao = next((a for a in resultado.atuacoes if a.codigo == "COORD_COMIS_PREMIO"), None)
|
||||||
|
assert atuacao is not None
|
||||||
|
assert atuacao.base >= 40
|
||||||
|
|
||||||
|
def test_coord_comissao_gp_base_50(self):
|
||||||
|
avaliacao = AvaliacaoComissao(
|
||||||
|
codigo="COORD_COMIS_GP", tipo="Coordenador",
|
||||||
|
premio="Grande Prêmio", ano=2024
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_c([], [avaliacao], [], [], [])
|
||||||
|
atuacao = next((a for a in resultado.atuacoes if a.codigo == "COORD_COMIS_GP"), None)
|
||||||
|
assert atuacao is not None
|
||||||
|
assert atuacao.base >= 50
|
||||||
|
|
||||||
|
def test_premiacao_gp_autor_base_100(self):
|
||||||
|
premiacao = Premiacao(
|
||||||
|
codigo="PREMIACAO_GP_AUTOR", tipo="Grande Prêmio",
|
||||||
|
nome_premio="Grande Prêmio CAPES", ano=2024
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_c([], [], [premiacao], [], [])
|
||||||
|
atuacao = next((a for a in resultado.atuacoes if a.codigo == "PREMIACAO_GP_AUTOR"), None)
|
||||||
|
assert atuacao is not None
|
||||||
|
assert atuacao.base >= 100
|
||||||
|
|
||||||
|
def test_premiacao_autor_base_50(self):
|
||||||
|
premiacao = Premiacao(
|
||||||
|
codigo="PREMIACAO_AUTOR", tipo="Prêmio",
|
||||||
|
nome_premio="Prêmio CAPES de Tese", ano=2024
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_c([], [], [premiacao], [], [])
|
||||||
|
atuacao = next((a for a in resultado.atuacoes if a.codigo == "PREMIACAO_AUTOR"), None)
|
||||||
|
assert atuacao is not None
|
||||||
|
assert atuacao.base >= 50
|
||||||
|
|
||||||
|
def test_mencao_autor_base_30(self):
|
||||||
|
premiacao = Premiacao(
|
||||||
|
codigo="MENCAO_AUTOR", tipo="Menção Honrosa",
|
||||||
|
nome_premio="Prêmio CAPES de Tese", ano=2024
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_c([], [], [premiacao], [], [])
|
||||||
|
atuacao = next((a for a in resultado.atuacoes if a.codigo == "MENCAO_AUTOR"), None)
|
||||||
|
assert atuacao is not None
|
||||||
|
assert atuacao.base >= 30
|
||||||
|
|
||||||
|
def test_avaliacao_recorrente_bonus_anual(self):
|
||||||
|
avaliacoes = [
|
||||||
|
AvaliacaoComissao(
|
||||||
|
codigo="AVAL_COMIS_PREMIO", tipo="Membro",
|
||||||
|
premio="PCT", ano=2022
|
||||||
|
),
|
||||||
|
AvaliacaoComissao(
|
||||||
|
codigo="AVAL_COMIS_PREMIO", tipo="Membro",
|
||||||
|
premio="PCT", ano=2023
|
||||||
|
),
|
||||||
|
AvaliacaoComissao(
|
||||||
|
codigo="AVAL_COMIS_PREMIO", tipo="Membro",
|
||||||
|
premio="PCT", ano=2024
|
||||||
|
),
|
||||||
|
]
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_c([], avaliacoes, [], [], [])
|
||||||
|
atuacao = next((a for a in resultado.atuacoes if a.codigo == "AVAL_COMIS_PREMIO"), None)
|
||||||
|
assert atuacao is not None
|
||||||
|
assert atuacao.bonus > 0
|
||||||
|
assert atuacao.quantidade == 3
|
||||||
|
|
||||||
|
def test_inscricoes_multiplas_acumulam(self):
|
||||||
|
inscricoes = [
|
||||||
|
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2022),
|
||||||
|
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2023),
|
||||||
|
]
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_c(inscricoes, [], [], [], [])
|
||||||
|
atuacao = next((a for a in resultado.atuacoes if a.codigo == "INSC_AUTOR"), None)
|
||||||
|
assert atuacao is not None
|
||||||
|
assert atuacao.quantidade == 2
|
||||||
|
assert atuacao.base == 20
|
||||||
|
|
||||||
|
def test_teto_inscricao_autor_20_pontos(self):
|
||||||
|
inscricoes = [
|
||||||
|
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=i)
|
||||||
|
for i in range(2015, 2025)
|
||||||
|
]
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_c(inscricoes, [], [], [], [])
|
||||||
|
atuacao = next((a for a in resultado.atuacoes if a.codigo == "INSC_AUTOR"), None)
|
||||||
|
assert atuacao is not None
|
||||||
|
assert atuacao.total <= 20
|
||||||
|
|
||||||
|
|
||||||
|
class TestBlocoDParticipacoes:
|
||||||
|
|
||||||
|
def test_bloco_d_vazio(self):
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_d([], [])
|
||||||
|
assert resultado.bloco == "D"
|
||||||
|
assert resultado.total == 0
|
||||||
|
|
||||||
|
def test_bolsa_cnpq_base_30(self):
|
||||||
|
bolsa = BolsaCNPQ(codigo="BOL_BPQ_NIVEL", nivel="1A")
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_d([bolsa], [])
|
||||||
|
atuacao = next((a for a in resultado.atuacoes if a.codigo == "BOL_BPQ_NIVEL"), None)
|
||||||
|
assert atuacao is not None
|
||||||
|
assert atuacao.base >= 30
|
||||||
|
|
||||||
|
def test_evento_base_1_ponto(self):
|
||||||
|
evento = Participacao(codigo="EVENTO", tipo="Evento", ano=2024)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_d([], [evento])
|
||||||
|
atuacao = next((a for a in resultado.atuacoes if a.codigo == "EVENTO"), None)
|
||||||
|
assert atuacao is not None
|
||||||
|
assert atuacao.base >= 1
|
||||||
|
|
||||||
|
def test_projeto_base_10_pontos(self):
|
||||||
|
projeto = Participacao(codigo="PROJ", tipo="Projeto", ano=2024)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_d([], [projeto])
|
||||||
|
atuacao = next((a for a in resultado.atuacoes if a.codigo == "PROJ"), None)
|
||||||
|
assert atuacao is not None
|
||||||
|
assert atuacao.base >= 10
|
||||||
|
|
||||||
|
def test_eventos_multiplos_bonus_recorrencia(self):
|
||||||
|
eventos = [
|
||||||
|
Participacao(codigo="EVENTO", tipo="Evento", ano=i)
|
||||||
|
for i in range(2020, 2025)
|
||||||
|
]
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_d([], eventos)
|
||||||
|
atuacao = next((a for a in resultado.atuacoes if a.codigo == "EVENTO"), None)
|
||||||
|
assert atuacao is not None
|
||||||
|
assert atuacao.quantidade == 5
|
||||||
|
assert atuacao.bonus > 0
|
||||||
|
|
||||||
|
def test_evento_teto_5_pontos(self):
|
||||||
|
eventos = [
|
||||||
|
Participacao(codigo="EVENTO", tipo="Evento", ano=i)
|
||||||
|
for i in range(2000, 2025)
|
||||||
|
]
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_d([], eventos)
|
||||||
|
atuacao = next((a for a in resultado.atuacoes if a.codigo == "EVENTO"), None)
|
||||||
|
assert atuacao is not None
|
||||||
|
assert atuacao.total <= 5
|
||||||
|
|
||||||
|
def test_projeto_teto_30_pontos(self):
|
||||||
|
projetos = [
|
||||||
|
Participacao(codigo="PROJ", tipo="Projeto", ano=i)
|
||||||
|
for i in range(2000, 2025)
|
||||||
|
]
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_d([], projetos)
|
||||||
|
atuacao = next((a for a in resultado.atuacoes if a.codigo == "PROJ"), None)
|
||||||
|
assert atuacao is not None
|
||||||
|
assert atuacao.total <= 30
|
||||||
|
|
||||||
|
|
||||||
|
class TestBlocoECoordPPG:
|
||||||
|
|
||||||
|
def test_bloco_e_sem_coordenador(self):
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_e(False)
|
||||||
|
assert resultado.bloco == "E"
|
||||||
|
assert resultado.total == 0
|
||||||
|
|
||||||
|
def test_bloco_e_com_coordenador(self):
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_e(True)
|
||||||
|
assert resultado.bloco == "E"
|
||||||
|
assert len(resultado.atuacoes) == 1
|
||||||
|
assert resultado.atuacoes[0].codigo == "PPG_COORD"
|
||||||
|
|
||||||
|
|
||||||
|
class TestPontuacaoCompleta:
|
||||||
|
|
||||||
|
def test_consultor_vazio_pontuacao_zero(self, consultor_vazio):
|
||||||
|
resultado = CalculadorPontuacao.calcular_pontuacao_completa(consultor_vazio)
|
||||||
|
assert resultado.total == 0
|
||||||
|
|
||||||
|
def test_consultor_com_coordenacao(self, coordenacao_ca_ativa):
|
||||||
|
consultor = Consultor(
|
||||||
|
id_pessoa=1,
|
||||||
|
nome="Coordenador",
|
||||||
|
coordenacoes_capes=[coordenacao_ca_ativa],
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_pontuacao_completa(consultor)
|
||||||
|
assert resultado.bloco_a.total > 0
|
||||||
|
assert resultado.bloco_b.total == 0
|
||||||
|
|
||||||
|
def test_consultor_completo_todos_blocos(self, consultor_completo):
|
||||||
|
resultado = CalculadorPontuacao.calcular_pontuacao_completa(consultor_completo)
|
||||||
|
assert resultado.bloco_a.total > 0
|
||||||
|
assert resultado.bloco_b.total > 0
|
||||||
|
assert resultado.bloco_c.total > 0
|
||||||
|
assert resultado.bloco_d.total > 0
|
||||||
|
|
||||||
|
def test_pontuacao_total_soma_blocos(self, consultor_completo):
|
||||||
|
resultado = CalculadorPontuacao.calcular_pontuacao_completa(consultor_completo)
|
||||||
|
soma_esperada = (
|
||||||
|
resultado.bloco_a.total
|
||||||
|
+ resultado.bloco_b.total
|
||||||
|
+ resultado.bloco_c.total
|
||||||
|
+ resultado.bloco_d.total
|
||||||
|
+ resultado.bloco_e.total
|
||||||
|
)
|
||||||
|
assert resultado.total == soma_esperada
|
||||||
|
|
||||||
|
def test_detalhamento_retorna_dict(self, consultor_completo):
|
||||||
|
resultado = CalculadorPontuacao.calcular_pontuacao_completa(consultor_completo)
|
||||||
|
detalhes = resultado.detalhamento
|
||||||
|
assert isinstance(detalhes, dict)
|
||||||
|
assert "bloco_a" in detalhes
|
||||||
|
assert "bloco_b" in detalhes
|
||||||
|
assert "bloco_c" in detalhes
|
||||||
|
assert "bloco_d" in detalhes
|
||||||
|
assert "bloco_e" in detalhes
|
||||||
|
assert "pontuacao_total" in detalhes
|
||||||
|
|
||||||
|
|
||||||
|
class TestCriteriosConfigurados:
|
||||||
|
|
||||||
|
def test_criterio_ca_existe(self):
|
||||||
|
assert "CA" in CRITERIOS
|
||||||
|
criterio = CRITERIOS["CA"]
|
||||||
|
assert criterio.base == 200
|
||||||
|
assert criterio.teto == 450
|
||||||
|
|
||||||
|
def test_criterio_caj_existe(self):
|
||||||
|
assert "CAJ" in CRITERIOS
|
||||||
|
criterio = CRITERIOS["CAJ"]
|
||||||
|
assert criterio.base == 150
|
||||||
|
assert criterio.teto == 370
|
||||||
|
|
||||||
|
def test_criterio_cons_ativo_existe(self):
|
||||||
|
assert "CONS_ATIVO" in CRITERIOS
|
||||||
|
criterio = CRITERIOS["CONS_ATIVO"]
|
||||||
|
assert criterio.base == 150
|
||||||
|
assert criterio.teto == 230
|
||||||
|
|
||||||
|
def test_todos_criterios_tem_codigo_valido(self):
|
||||||
|
for codigo, criterio in CRITERIOS.items():
|
||||||
|
assert criterio.codigo == codigo
|
||||||
|
assert criterio.base >= 0
|
||||||
|
assert criterio.teto >= 0
|
||||||
232
backend/tests/domain/services/test_pdf_rules.py
Normal file
232
backend/tests/domain/services/test_pdf_rules.py
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.domain.services.calculador_pontuacao import CalculadorPontuacao
|
||||||
|
from src.domain.value_objects.criterios_pontuacao import CRITERIOS
|
||||||
|
from src.domain.value_objects.periodo import Periodo
|
||||||
|
from src.domain.entities.consultor import (
|
||||||
|
Consultoria,
|
||||||
|
CoordenacaoCapes,
|
||||||
|
Inscricao,
|
||||||
|
AvaliacaoComissao,
|
||||||
|
Participacao,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
PDF_BASE_TETO = {
|
||||||
|
"CA": (200, 450),
|
||||||
|
"CAJ": (150, 370),
|
||||||
|
"CAJ_MP": (120, 315),
|
||||||
|
"CAM": (100, 280),
|
||||||
|
"PPG_COORD": (0, 0),
|
||||||
|
"CONS_ATIVO": (150, 230),
|
||||||
|
"CONS_HIST": (100, 230),
|
||||||
|
"CONS_FALECIDO": (100, 230),
|
||||||
|
"INSC_AUTOR": (10, 20),
|
||||||
|
"INSC_INST_AUTOR": (20, 50),
|
||||||
|
"AVAL_COMIS_PREMIO": (30, 60),
|
||||||
|
"AVAL_COMIS_GP": (40, 80),
|
||||||
|
"COORD_COMIS_PREMIO": (40, 100),
|
||||||
|
"COORD_COMIS_GP": (50, 120),
|
||||||
|
"BOL_BPQ_NIVEL": (30, 60),
|
||||||
|
"PREMIACAO_GP_AUTOR": (100, 300),
|
||||||
|
"PREMIACAO_AUTOR": (50, 150),
|
||||||
|
"MENCAO_AUTOR": (30, 90),
|
||||||
|
"EVENTO": (1, 5),
|
||||||
|
"PROJ": (10, 30),
|
||||||
|
"IDIOMA_BILINGUE": (0, 0),
|
||||||
|
"IDIOMA_MULTILINGUE": (0, 0),
|
||||||
|
"TITULACAO_MESTRE": (0, 0),
|
||||||
|
"TITULACAO_DOUTOR": (0, 0),
|
||||||
|
"TITULACAO_POS_DOUTOR": (0, 0),
|
||||||
|
"ORIENT_POS_DOC": (0, 0),
|
||||||
|
"ORIENT_POS_DOC_PREM": (0, 0),
|
||||||
|
"ORIENT_TESE": (0, 0),
|
||||||
|
"ORIENT_TESE_PREM": (0, 0),
|
||||||
|
"ORIENT_DISS": (0, 0),
|
||||||
|
"ORIENT_DISS_PREM": (0, 0),
|
||||||
|
"CO_ORIENT_POS_DOC": (0, 0),
|
||||||
|
"CO_ORIENT_POS_DOC_PREM": (0, 0),
|
||||||
|
"CO_ORIENT_TESE": (0, 0),
|
||||||
|
"CO_ORIENT_TESE_PREM": (0, 0),
|
||||||
|
"CO_ORIENT_DISS": (0, 0),
|
||||||
|
"CO_ORIENT_DISS_PREM": (0, 0),
|
||||||
|
"MB_BANCA_POS_DOC": (0, 0),
|
||||||
|
"MB_BANCA_POS_DOC_PREM": (0, 0),
|
||||||
|
"MB_BANCA_TESE": (0, 0),
|
||||||
|
"MB_BANCA_TESE_PREM": (0, 0),
|
||||||
|
"MB_BANCA_DISS": (0, 0),
|
||||||
|
"MB_BANCA_DISS_PREM": (0, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
PDF_TEMPO = {
|
||||||
|
"CA": (10, 100),
|
||||||
|
"CAJ": (8, 80),
|
||||||
|
"CAJ_MP": (6, 60),
|
||||||
|
"CAM": (5, 50),
|
||||||
|
"PPG_COORD": (0, 0),
|
||||||
|
"CONS_ATIVO": (5, 50),
|
||||||
|
"CONS_HIST": (5, 50),
|
||||||
|
"CONS_FALECIDO": (5, 50),
|
||||||
|
}
|
||||||
|
|
||||||
|
PDF_BONUS = {
|
||||||
|
"CA": {"atualidade": 30, "retorno": 20},
|
||||||
|
"CAJ": {"atualidade": 20, "retorno": 15},
|
||||||
|
"CAJ_MP": {"atualidade": 15, "retorno": 10},
|
||||||
|
"CAM": {"atualidade": 20, "retorno": 10},
|
||||||
|
"PPG_COORD": {"atualidade": 15, "retorno": 10, "continuidade": 15},
|
||||||
|
"CONS_ATIVO": {"atualidade": 20, "retorno": 15, "continuidade": 20},
|
||||||
|
"CONS_HIST": {"retorno": 20, "continuidade": 20},
|
||||||
|
"CONS_FALECIDO": {"continuidade": 20},
|
||||||
|
}
|
||||||
|
|
||||||
|
PDF_RECORRENCIA = {
|
||||||
|
"INSC_AUTOR": {"por_participacao": 2, "teto_participacao": 10},
|
||||||
|
"INSC_INST_AUTOR": {"por_participacao": 5, "teto_participacao": 10},
|
||||||
|
"AVAL_COMIS_PREMIO": {"por_ano": 2, "teto_ano": 15},
|
||||||
|
"AVAL_COMIS_GP": {"por_ano": 3, "teto_ano": 20},
|
||||||
|
"COORD_COMIS_PREMIO": {"por_ano": 4, "teto_ano": 20},
|
||||||
|
"COORD_COMIS_GP": {"por_ano": 6, "teto_ano": 20},
|
||||||
|
"EVENTO": {"por_participacao": 1, "teto_participacao": 10},
|
||||||
|
"PROJ": {"por_participacao": 2, "teto_participacao": 10},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def periodo_anos(anos: int, ativo: bool = False) -> Periodo:
|
||||||
|
inicio = datetime.now() - relativedelta(years=anos)
|
||||||
|
fim = None if ativo else datetime.now()
|
||||||
|
return Periodo(inicio=inicio, fim=fim)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("codigo,base_teto", PDF_BASE_TETO.items())
|
||||||
|
def test_pdf_base_teto(codigo, base_teto):
|
||||||
|
criterio = CRITERIOS.get(codigo)
|
||||||
|
assert criterio is not None
|
||||||
|
base, teto = base_teto
|
||||||
|
assert criterio.base == base
|
||||||
|
assert criterio.teto == teto
|
||||||
|
|
||||||
|
|
||||||
|
def test_pdf_criterios_cobrem_43_codigos():
|
||||||
|
assert set(CRITERIOS.keys()) == set(PDF_BASE_TETO.keys())
|
||||||
|
assert len(CRITERIOS) == 43
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("codigo,tempo", PDF_TEMPO.items())
|
||||||
|
def test_pdf_regras_tempo(codigo, tempo):
|
||||||
|
criterio = CRITERIOS[codigo]
|
||||||
|
multiplicador, teto_tempo = tempo
|
||||||
|
assert criterio.pontua_tempo is True
|
||||||
|
assert criterio.multiplicador_tempo == multiplicador
|
||||||
|
assert criterio.teto_tempo == teto_tempo
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("codigo", sorted(set(PDF_BASE_TETO) - set(PDF_TEMPO)))
|
||||||
|
def test_pdf_codigos_sem_tempo(codigo):
|
||||||
|
criterio = CRITERIOS[codigo]
|
||||||
|
assert criterio.pontua_tempo is False
|
||||||
|
assert criterio.multiplicador_tempo == 0
|
||||||
|
assert criterio.teto_tempo == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("codigo,bonus", PDF_BONUS.items())
|
||||||
|
def test_pdf_bonus_configurados(codigo, bonus):
|
||||||
|
criterio = CRITERIOS[codigo]
|
||||||
|
assert criterio.bonus_atualidade == bonus.get("atualidade", 0)
|
||||||
|
assert criterio.bonus_retorno == bonus.get("retorno", 0)
|
||||||
|
assert criterio.bonus_continuidade_8anos == bonus.get("continuidade", 0)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("codigo,rec", PDF_RECORRENCIA.items())
|
||||||
|
def test_pdf_regras_recorrencia(codigo, rec):
|
||||||
|
criterio = CRITERIOS[codigo]
|
||||||
|
assert criterio.bonus_recorrencia_anual == rec.get("por_ano", 0)
|
||||||
|
assert criterio.teto_recorrencia == rec.get("teto_ano", 0)
|
||||||
|
assert criterio.bonus_recorrencia_participacao == rec.get("por_participacao", 0)
|
||||||
|
assert criterio.teto_recorrencia_participacao == rec.get("teto_participacao", 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculo_tempo_e_bonus_ca():
|
||||||
|
periodo_historico = Periodo(
|
||||||
|
inicio=datetime.now() - relativedelta(years=12),
|
||||||
|
fim=datetime.now() - relativedelta(years=2),
|
||||||
|
)
|
||||||
|
periodo_ativo = periodo_anos(2, ativo=True)
|
||||||
|
coords = [
|
||||||
|
CoordenacaoCapes(
|
||||||
|
codigo="CA",
|
||||||
|
tipo="Coordenador de Area",
|
||||||
|
area_avaliacao="AREA",
|
||||||
|
periodo=periodo_historico,
|
||||||
|
),
|
||||||
|
CoordenacaoCapes(
|
||||||
|
codigo="CA",
|
||||||
|
tipo="Coordenador de Area",
|
||||||
|
area_avaliacao="AREA",
|
||||||
|
periodo=periodo_ativo,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_a(coords)
|
||||||
|
atuacao = resultado.atuacoes[0]
|
||||||
|
assert atuacao.tempo == 100
|
||||||
|
assert atuacao.bonus == 50
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculo_bonus_consultoria_completo():
|
||||||
|
consultoria = Consultoria(
|
||||||
|
codigo="CONS_ATIVO",
|
||||||
|
situacao="Ativo",
|
||||||
|
periodo=periodo_anos(9, ativo=True),
|
||||||
|
periodos=[periodo_anos(9, ativo=True)],
|
||||||
|
anos_consecutivos=9,
|
||||||
|
retornos=1,
|
||||||
|
)
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
|
||||||
|
atuacao = resultado.atuacoes[0]
|
||||||
|
assert atuacao.bonus == 55
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculo_recorrencia_inscricao_autor_bonus():
|
||||||
|
inscricoes = [
|
||||||
|
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2020),
|
||||||
|
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2021),
|
||||||
|
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2022),
|
||||||
|
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2023),
|
||||||
|
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2024),
|
||||||
|
]
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_c(inscricoes, [], [], [], [])
|
||||||
|
atuacao = next(a for a in resultado.atuacoes if a.codigo == "INSC_AUTOR")
|
||||||
|
assert atuacao.bonus == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculo_recorrencia_avaliacao_gp_bonus():
|
||||||
|
avaliacoes = [
|
||||||
|
AvaliacaoComissao(codigo="AVAL_COMIS_GP", tipo="Membro", premio="GP", ano=2021),
|
||||||
|
AvaliacaoComissao(codigo="AVAL_COMIS_GP", tipo="Membro", premio="GP", ano=2022),
|
||||||
|
AvaliacaoComissao(codigo="AVAL_COMIS_GP", tipo="Membro", premio="GP", ano=2023),
|
||||||
|
]
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_c([], avaliacoes, [], [], [])
|
||||||
|
atuacao = next(a for a in resultado.atuacoes if a.codigo == "AVAL_COMIS_GP")
|
||||||
|
assert atuacao.bonus == 9
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculo_recorrencia_evento_bonus():
|
||||||
|
eventos = [
|
||||||
|
Participacao(codigo="EVENTO", tipo="Evento", ano=2020),
|
||||||
|
Participacao(codigo="EVENTO", tipo="Evento", ano=2021),
|
||||||
|
Participacao(codigo="EVENTO", tipo="Evento", ano=2022),
|
||||||
|
Participacao(codigo="EVENTO", tipo="Evento", ano=2023),
|
||||||
|
Participacao(codigo="EVENTO", tipo="Evento", ano=2024),
|
||||||
|
Participacao(codigo="EVENTO", tipo="Evento", ano=2025),
|
||||||
|
Participacao(codigo="EVENTO", tipo="Evento", ano=2026),
|
||||||
|
Participacao(codigo="EVENTO", tipo="Evento", ano=2027),
|
||||||
|
Participacao(codigo="EVENTO", tipo="Evento", ano=2028),
|
||||||
|
Participacao(codigo="EVENTO", tipo="Evento", ano=2029),
|
||||||
|
Participacao(codigo="EVENTO", tipo="Evento", ano=2030),
|
||||||
|
]
|
||||||
|
resultado = CalculadorPontuacao.calcular_bloco_d([], eventos)
|
||||||
|
atuacao = next(a for a in resultado.atuacoes if a.codigo == "EVENTO")
|
||||||
|
assert atuacao.bonus == 10
|
||||||
0
backend/tests/domain/value_objects/__init__.py
Normal file
0
backend/tests/domain/value_objects/__init__.py
Normal file
169
backend/tests/domain/value_objects/test_periodo.py
Normal file
169
backend/tests/domain/value_objects/test_periodo.py
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import pytest
|
||||||
|
from datetime import datetime
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
from src.domain.value_objects.periodo import (
|
||||||
|
Periodo,
|
||||||
|
mesclar_periodos,
|
||||||
|
anos_completos_periodos,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPeriodo:
|
||||||
|
|
||||||
|
def test_periodo_ativo_sem_fim(self):
|
||||||
|
inicio = datetime(2020, 1, 1)
|
||||||
|
periodo = Periodo(inicio=inicio, fim=None)
|
||||||
|
assert periodo.ativo is True
|
||||||
|
|
||||||
|
def test_periodo_inativo_com_fim(self):
|
||||||
|
inicio = datetime(2020, 1, 1)
|
||||||
|
fim = datetime(2023, 1, 1)
|
||||||
|
periodo = Periodo(inicio=inicio, fim=fim)
|
||||||
|
assert periodo.ativo is False
|
||||||
|
|
||||||
|
def test_anos_decorridos_3_anos(self):
|
||||||
|
inicio = datetime.now() - relativedelta(years=3)
|
||||||
|
fim = datetime.now()
|
||||||
|
periodo = Periodo(inicio=inicio, fim=fim)
|
||||||
|
assert periodo.anos_decorridos >= 2.9
|
||||||
|
assert periodo.anos_decorridos <= 3.1
|
||||||
|
|
||||||
|
def test_anos_decorridos_periodo_ativo(self):
|
||||||
|
inicio = datetime.now() - relativedelta(years=5)
|
||||||
|
periodo = Periodo(inicio=inicio, fim=None)
|
||||||
|
assert periodo.anos_decorridos >= 4.9
|
||||||
|
assert periodo.anos_decorridos <= 5.1
|
||||||
|
|
||||||
|
def test_anos_completos_retorna_inteiro(self):
|
||||||
|
inicio = datetime(2020, 1, 1)
|
||||||
|
fim = datetime(2023, 6, 15)
|
||||||
|
periodo = Periodo(inicio=inicio, fim=fim)
|
||||||
|
assert periodo.anos_completos() == 3
|
||||||
|
|
||||||
|
def test_anos_completos_menos_de_um_ano(self):
|
||||||
|
inicio = datetime.now() - relativedelta(months=6)
|
||||||
|
fim = datetime.now()
|
||||||
|
periodo = Periodo(inicio=inicio, fim=fim)
|
||||||
|
assert periodo.anos_completos() == 0
|
||||||
|
|
||||||
|
def test_anos_completos_com_data_referencia(self):
|
||||||
|
inicio = datetime(2020, 1, 1)
|
||||||
|
periodo = Periodo(inicio=inicio, fim=None)
|
||||||
|
data_ref = datetime(2025, 1, 1)
|
||||||
|
assert periodo.anos_completos(data_ref) == 5
|
||||||
|
|
||||||
|
def test_fim_anterior_inicio_corrige_para_none(self):
|
||||||
|
inicio = datetime(2023, 1, 1)
|
||||||
|
fim = datetime(2020, 1, 1)
|
||||||
|
periodo = Periodo(inicio=inicio, fim=fim)
|
||||||
|
assert periodo.fim is None
|
||||||
|
assert periodo.ativo is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestMesclarPeriodos:
|
||||||
|
|
||||||
|
def test_lista_vazia_retorna_vazia(self):
|
||||||
|
resultado = mesclar_periodos([])
|
||||||
|
assert resultado == []
|
||||||
|
|
||||||
|
def test_um_periodo_retorna_mesmo(self):
|
||||||
|
periodo = Periodo(datetime(2020, 1, 1), datetime(2023, 1, 1))
|
||||||
|
resultado = mesclar_periodos([periodo])
|
||||||
|
assert len(resultado) == 1
|
||||||
|
assert resultado[0].inicio == periodo.inicio
|
||||||
|
|
||||||
|
def test_periodos_consecutivos_mesclados(self):
|
||||||
|
p1 = Periodo(datetime(2020, 1, 1), datetime(2022, 1, 1))
|
||||||
|
p2 = Periodo(datetime(2021, 6, 1), datetime(2024, 1, 1))
|
||||||
|
resultado = mesclar_periodos([p1, p2])
|
||||||
|
assert len(resultado) == 1
|
||||||
|
assert resultado[0].inicio == datetime(2020, 1, 1)
|
||||||
|
assert resultado[0].fim == datetime(2024, 1, 1)
|
||||||
|
|
||||||
|
def test_periodos_separados_nao_mesclados(self):
|
||||||
|
p1 = Periodo(datetime(2015, 1, 1), datetime(2017, 1, 1))
|
||||||
|
p2 = Periodo(datetime(2020, 1, 1), datetime(2023, 1, 1))
|
||||||
|
resultado = mesclar_periodos([p1, p2])
|
||||||
|
assert len(resultado) == 2
|
||||||
|
|
||||||
|
def test_periodo_ativo_preservado(self):
|
||||||
|
p1 = Periodo(datetime(2020, 1, 1), datetime(2022, 1, 1))
|
||||||
|
p2 = Periodo(datetime(2021, 1, 1), None)
|
||||||
|
resultado = mesclar_periodos([p1, p2])
|
||||||
|
assert len(resultado) == 1
|
||||||
|
assert resultado[0].ativo is True
|
||||||
|
|
||||||
|
def test_tres_periodos_mesclados(self):
|
||||||
|
p1 = Periodo(datetime(2018, 1, 1), datetime(2020, 1, 1))
|
||||||
|
p2 = Periodo(datetime(2019, 1, 1), datetime(2021, 1, 1))
|
||||||
|
p3 = Periodo(datetime(2020, 6, 1), datetime(2023, 1, 1))
|
||||||
|
resultado = mesclar_periodos([p1, p2, p3])
|
||||||
|
assert len(resultado) == 1
|
||||||
|
assert resultado[0].inicio == datetime(2018, 1, 1)
|
||||||
|
assert resultado[0].fim == datetime(2023, 1, 1)
|
||||||
|
|
||||||
|
def test_ordenacao_automatica(self):
|
||||||
|
p1 = Periodo(datetime(2022, 1, 1), datetime(2024, 1, 1))
|
||||||
|
p2 = Periodo(datetime(2018, 1, 1), datetime(2020, 1, 1))
|
||||||
|
resultado = mesclar_periodos([p1, p2])
|
||||||
|
assert resultado[0].inicio == datetime(2018, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAnosCompletosPeriodos:
|
||||||
|
|
||||||
|
def test_lista_vazia_retorna_zero(self):
|
||||||
|
resultado = anos_completos_periodos([])
|
||||||
|
assert resultado == 0
|
||||||
|
|
||||||
|
def test_um_periodo_3_anos(self):
|
||||||
|
periodo = Periodo(datetime(2020, 1, 1), datetime(2023, 1, 1))
|
||||||
|
resultado = anos_completos_periodos([periodo])
|
||||||
|
assert resultado == 3
|
||||||
|
|
||||||
|
def test_dois_periodos_soma(self):
|
||||||
|
p1 = Periodo(datetime(2015, 1, 1), datetime(2017, 1, 1))
|
||||||
|
p2 = Periodo(datetime(2020, 1, 1), datetime(2023, 1, 1))
|
||||||
|
resultado = anos_completos_periodos([p1, p2])
|
||||||
|
assert resultado == 5
|
||||||
|
|
||||||
|
def test_com_data_referencia(self):
|
||||||
|
p1 = Periodo(datetime(2020, 1, 1), None)
|
||||||
|
p2 = Periodo(datetime(2015, 1, 1), datetime(2017, 1, 1))
|
||||||
|
data_ref = datetime(2025, 1, 1)
|
||||||
|
resultado = anos_completos_periodos([p1, p2], data_ref)
|
||||||
|
assert resultado == 7
|
||||||
|
|
||||||
|
|
||||||
|
class TestCasosEspeciais:
|
||||||
|
|
||||||
|
def test_periodo_muito_curto(self):
|
||||||
|
inicio = datetime.now() - relativedelta(days=30)
|
||||||
|
fim = datetime.now()
|
||||||
|
periodo = Periodo(inicio=inicio, fim=fim)
|
||||||
|
assert periodo.anos_completos() == 0
|
||||||
|
|
||||||
|
def test_periodo_exatamente_um_ano(self):
|
||||||
|
inicio = datetime(2023, 1, 1)
|
||||||
|
fim = datetime(2024, 1, 1)
|
||||||
|
periodo = Periodo(inicio=inicio, fim=fim)
|
||||||
|
assert periodo.anos_completos() == 1
|
||||||
|
|
||||||
|
def test_periodo_quase_um_ano(self):
|
||||||
|
inicio = datetime(2023, 1, 1)
|
||||||
|
fim = datetime(2023, 12, 31)
|
||||||
|
periodo = Periodo(inicio=inicio, fim=fim)
|
||||||
|
assert periodo.anos_completos() == 0
|
||||||
|
|
||||||
|
def test_mesclagem_periodos_sobrepostos_complexos(self):
|
||||||
|
periodos = [
|
||||||
|
Periodo(datetime(2010, 1, 1), datetime(2012, 1, 1)),
|
||||||
|
Periodo(datetime(2011, 6, 1), datetime(2014, 1, 1)),
|
||||||
|
Periodo(datetime(2013, 1, 1), datetime(2015, 1, 1)),
|
||||||
|
Periodo(datetime(2018, 1, 1), datetime(2020, 1, 1)),
|
||||||
|
Periodo(datetime(2019, 6, 1), None),
|
||||||
|
]
|
||||||
|
resultado = mesclar_periodos(periodos)
|
||||||
|
assert len(resultado) == 2
|
||||||
|
assert resultado[0].fim == datetime(2015, 1, 1)
|
||||||
|
assert resultado[1].ativo is True
|
||||||
0
backend/tests/infrastructure/__init__.py
Normal file
0
backend/tests/infrastructure/__init__.py
Normal file
508
backend/tests/infrastructure/elasticsearch/test_client.py
Normal file
508
backend/tests/infrastructure/elasticsearch/test_client.py
Normal file
@@ -0,0 +1,508 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
from httpx import Response
|
||||||
|
|
||||||
|
from src.infrastructure.elasticsearch.client import ElasticsearchClient
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def es_client():
|
||||||
|
return ElasticsearchClient(
|
||||||
|
url="http://localhost:9200",
|
||||||
|
index="atuacapes_test",
|
||||||
|
user="test_user",
|
||||||
|
password="test_pass"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_httpx_response():
|
||||||
|
def _create_response(json_data, status_code=200):
|
||||||
|
response = MagicMock(spec=Response)
|
||||||
|
response.json.return_value = json_data
|
||||||
|
response.status_code = status_code
|
||||||
|
response.raise_for_status = MagicMock()
|
||||||
|
return response
|
||||||
|
return _create_response
|
||||||
|
|
||||||
|
|
||||||
|
class TestElasticsearchClientConnect:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_connect_cria_cliente(self, es_client):
|
||||||
|
await es_client.connect()
|
||||||
|
assert es_client._client is not None
|
||||||
|
await es_client.close()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_connect_com_auth(self, es_client):
|
||||||
|
await es_client.connect()
|
||||||
|
assert es_client._client is not None
|
||||||
|
await es_client.close()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_close_fecha_cliente(self, es_client):
|
||||||
|
await es_client.connect()
|
||||||
|
await es_client.close()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_client_property_sem_conexao(self, es_client):
|
||||||
|
with pytest.raises(RuntimeError, match="não conectado"):
|
||||||
|
_ = es_client.client
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuscarPorId:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_buscar_por_id_encontrado(self, es_client, mock_httpx_response):
|
||||||
|
es_response = {
|
||||||
|
"hits": {
|
||||||
|
"total": {"value": 1},
|
||||||
|
"hits": [{
|
||||||
|
"_source": {
|
||||||
|
"id": 12345,
|
||||||
|
"dadosPessoais": {"nome": "MARIA SILVA"},
|
||||||
|
"atuacoes": []
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response))
|
||||||
|
|
||||||
|
result = await es_client.buscar_por_id(12345)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert result["id"] == 12345
|
||||||
|
assert result["dadosPessoais"]["nome"] == "MARIA SILVA"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_buscar_por_id_nao_encontrado(self, es_client, mock_httpx_response):
|
||||||
|
es_response = {
|
||||||
|
"hits": {
|
||||||
|
"total": {"value": 0},
|
||||||
|
"hits": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response))
|
||||||
|
|
||||||
|
result = await es_client.buscar_por_id(99999)
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_buscar_por_id_erro(self, es_client):
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(side_effect=Exception("Connection failed"))
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="Erro ao buscar consultor"):
|
||||||
|
await es_client.buscar_por_id(12345)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuscarPorIds:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_buscar_por_ids_multiplos(self, es_client, mock_httpx_response):
|
||||||
|
es_response = {
|
||||||
|
"hits": {
|
||||||
|
"hits": [
|
||||||
|
{"_source": {"id": 1, "dadosPessoais": {"nome": "PESSOA 1"}}},
|
||||||
|
{"_source": {"id": 2, "dadosPessoais": {"nome": "PESSOA 2"}}},
|
||||||
|
{"_source": {"id": 3, "dadosPessoais": {"nome": "PESSOA 3"}}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response))
|
||||||
|
|
||||||
|
result = await es_client.buscar_por_ids([1, 2, 3])
|
||||||
|
|
||||||
|
assert len(result) == 3
|
||||||
|
assert result[0]["id"] == 1
|
||||||
|
assert result[2]["dadosPessoais"]["nome"] == "PESSOA 3"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_buscar_por_ids_vazio(self, es_client):
|
||||||
|
result = await es_client.buscar_por_ids([])
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_buscar_por_ids_com_source_fields(self, es_client, mock_httpx_response):
|
||||||
|
es_response = {"hits": {"hits": [{"_source": {"id": 1}}]}}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response))
|
||||||
|
|
||||||
|
await es_client.buscar_por_ids([1], source_fields=["id", "dadosPessoais.nome"])
|
||||||
|
|
||||||
|
call_args = mock_client.post.call_args
|
||||||
|
query = call_args[1]["json"]
|
||||||
|
assert "_source" in query
|
||||||
|
assert "id" in query["_source"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuscarDocumentoCompleto:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_buscar_documento_completo(self, es_client, mock_httpx_response):
|
||||||
|
es_response = {
|
||||||
|
"hits": {
|
||||||
|
"hits": [{
|
||||||
|
"_index": "atuacapes_test",
|
||||||
|
"_id": "abc123",
|
||||||
|
"_score": 1.0,
|
||||||
|
"_source": {"id": 12345, "dadosPessoais": {"nome": "TESTE"}}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response))
|
||||||
|
|
||||||
|
result = await es_client.buscar_documento_completo(12345)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert result["_index"] == "atuacapes_test"
|
||||||
|
assert result["_id"] == "abc123"
|
||||||
|
assert result["_source"]["id"] == 12345
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuscarComAtuacoes:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_buscar_com_atuacoes(self, es_client, mock_httpx_response):
|
||||||
|
es_response = {
|
||||||
|
"hits": {
|
||||||
|
"hits": [
|
||||||
|
{"_source": {"id": 1, "atuacoes": [{"tipo": "Consultor"}]}},
|
||||||
|
{"_source": {"id": 2, "atuacoes": [{"tipo": "Coordenação de Área de Avaliação"}]}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response))
|
||||||
|
|
||||||
|
result = await es_client.buscar_com_atuacoes(size=100)
|
||||||
|
|
||||||
|
assert len(result) == 2
|
||||||
|
assert result[0]["atuacoes"][0]["tipo"] == "Consultor"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_buscar_com_atuacoes_paginacao(self, es_client, mock_httpx_response):
|
||||||
|
es_response = {"hits": {"hits": []}}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response))
|
||||||
|
|
||||||
|
await es_client.buscar_com_atuacoes(size=50, from_=100)
|
||||||
|
|
||||||
|
call_args = mock_client.post.call_args
|
||||||
|
query = call_args[1]["json"]
|
||||||
|
assert query["size"] == 50
|
||||||
|
assert query["from"] == 100
|
||||||
|
|
||||||
|
|
||||||
|
class TestContarComAtuacoes:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_contar_com_atuacoes(self, es_client, mock_httpx_response):
|
||||||
|
es_response = {"count": 85432}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response))
|
||||||
|
|
||||||
|
result = await es_client.contar_com_atuacoes()
|
||||||
|
|
||||||
|
assert result == 85432
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_contar_com_atuacoes_erro(self, es_client):
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(side_effect=Exception("Timeout"))
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="Erro ao contar consultores"):
|
||||||
|
await es_client.contar_com_atuacoes()
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuscarCandidatosRanking:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_buscar_candidatos_ranking(self, es_client, mock_httpx_response):
|
||||||
|
es_response = {
|
||||||
|
"hits": {
|
||||||
|
"hits": [
|
||||||
|
{"_score": 15.5, "_source": {"id": 1, "dadosPessoais": {"nome": "COORD A"}}},
|
||||||
|
{"_score": 10.2, "_source": {"id": 2, "dadosPessoais": {"nome": "CONSULTOR B"}}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response))
|
||||||
|
|
||||||
|
result = await es_client.buscar_candidatos_ranking(size=100)
|
||||||
|
|
||||||
|
assert len(result) == 2
|
||||||
|
assert result[0]["_score_es"] == 15.5
|
||||||
|
assert result[1]["_score_es"] == 10.2
|
||||||
|
|
||||||
|
|
||||||
|
class TestScrollAPI:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_iniciar_scroll(self, es_client, mock_httpx_response):
|
||||||
|
es_response = {
|
||||||
|
"_scroll_id": "scroll_123abc",
|
||||||
|
"hits": {
|
||||||
|
"total": {"value": 5000},
|
||||||
|
"hits": [
|
||||||
|
{"_source": {"id": 1}},
|
||||||
|
{"_source": {"id": 2}},
|
||||||
|
{"_source": {"id": 3}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response))
|
||||||
|
|
||||||
|
result = await es_client.iniciar_scroll(size=1000)
|
||||||
|
|
||||||
|
assert result["scroll_id"] == "scroll_123abc"
|
||||||
|
assert result["total"] == 5000
|
||||||
|
assert len(result["hits"]) == 3
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_continuar_scroll(self, es_client, mock_httpx_response):
|
||||||
|
es_response = {
|
||||||
|
"_scroll_id": "scroll_456def",
|
||||||
|
"hits": {
|
||||||
|
"hits": [
|
||||||
|
{"_source": {"id": 4}},
|
||||||
|
{"_source": {"id": 5}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response))
|
||||||
|
|
||||||
|
result = await es_client.continuar_scroll("scroll_123abc")
|
||||||
|
|
||||||
|
assert result["scroll_id"] == "scroll_456def"
|
||||||
|
assert len(result["hits"]) == 2
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_limpar_scroll(self, es_client, mock_httpx_response):
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.delete = AsyncMock(return_value=mock_httpx_response({"succeeded": True}))
|
||||||
|
|
||||||
|
await es_client.limpar_scroll("scroll_123abc")
|
||||||
|
|
||||||
|
mock_client.delete.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_limpar_scroll_ignora_erros(self, es_client):
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.delete = AsyncMock(side_effect=Exception("Scroll already expired"))
|
||||||
|
|
||||||
|
await es_client.limpar_scroll("scroll_expired")
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuscarTodosConsultores:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_buscar_todos_consultores_callback(self, es_client, mock_httpx_response):
|
||||||
|
batches_recebidos = []
|
||||||
|
progress_recebido = []
|
||||||
|
|
||||||
|
async def callback(docs, progress):
|
||||||
|
batches_recebidos.append(docs)
|
||||||
|
progress_recebido.append(progress.copy())
|
||||||
|
|
||||||
|
scroll_response_1 = {
|
||||||
|
"_scroll_id": "scroll_1",
|
||||||
|
"hits": {
|
||||||
|
"total": {"value": 6},
|
||||||
|
"hits": [
|
||||||
|
{"_source": {"id": 1}},
|
||||||
|
{"_source": {"id": 2}},
|
||||||
|
{"_source": {"id": 3}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll_response_2 = {
|
||||||
|
"_scroll_id": "scroll_2",
|
||||||
|
"hits": {
|
||||||
|
"hits": [
|
||||||
|
{"_source": {"id": 4}},
|
||||||
|
{"_source": {"id": 5}},
|
||||||
|
{"_source": {"id": 6}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll_response_3 = {
|
||||||
|
"_scroll_id": "scroll_3",
|
||||||
|
"hits": {"hits": []}
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(side_effect=[
|
||||||
|
mock_httpx_response(scroll_response_1),
|
||||||
|
mock_httpx_response(scroll_response_2),
|
||||||
|
mock_httpx_response(scroll_response_3)
|
||||||
|
])
|
||||||
|
mock_client.delete = AsyncMock(return_value=mock_httpx_response({}))
|
||||||
|
|
||||||
|
result = await es_client.buscar_todos_consultores(callback, batch_size=3)
|
||||||
|
|
||||||
|
assert result["total"] == 6
|
||||||
|
assert result["processados"] == 6
|
||||||
|
assert len(batches_recebidos) == 2
|
||||||
|
assert progress_recebido[0]["percentual"] == 50
|
||||||
|
assert progress_recebido[1]["percentual"] == 100
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_buscar_todos_limpa_scroll_ao_final(self, es_client, mock_httpx_response):
|
||||||
|
async def callback(docs, progress):
|
||||||
|
pass
|
||||||
|
|
||||||
|
scroll_response = {
|
||||||
|
"_scroll_id": "scroll_cleanup",
|
||||||
|
"hits": {"total": {"value": 0}, "hits": []}
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=mock_httpx_response(scroll_response))
|
||||||
|
mock_client.delete = AsyncMock(return_value=mock_httpx_response({}))
|
||||||
|
|
||||||
|
await es_client.buscar_todos_consultores(callback)
|
||||||
|
|
||||||
|
mock_client.delete.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
class TestSugerirConsultores:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_sugerir_consultores_por_tema(self, es_client, mock_httpx_response):
|
||||||
|
es_response = {
|
||||||
|
"hits": {
|
||||||
|
"hits": [
|
||||||
|
{"_score": 25.5, "_source": {"id": 1, "dadosPessoais": {"nome": "ESPECIALISTA IA"}}},
|
||||||
|
{"_score": 20.1, "_source": {"id": 2, "dadosPessoais": {"nome": "PESQUISADOR ML"}}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response))
|
||||||
|
|
||||||
|
result = await es_client.sugerir_consultores(tema="inteligência artificial")
|
||||||
|
|
||||||
|
assert len(result) == 2
|
||||||
|
assert result[0]["_score_match"] == 25.5
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_sugerir_consultores_filtro_area(self, es_client, mock_httpx_response):
|
||||||
|
es_response = {"hits": {"hits": []}}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response))
|
||||||
|
|
||||||
|
await es_client.sugerir_consultores(
|
||||||
|
tema="machine learning",
|
||||||
|
area_avaliacao="CIÊNCIA DA COMPUTAÇÃO"
|
||||||
|
)
|
||||||
|
|
||||||
|
call_args = mock_client.post.call_args
|
||||||
|
query = call_args[1]["json"]
|
||||||
|
assert len(query["query"]["bool"]["must"]) >= 2
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_sugerir_consultores_apenas_ativos(self, es_client, mock_httpx_response):
|
||||||
|
es_response = {"hits": {"hits": []}}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response))
|
||||||
|
|
||||||
|
await es_client.sugerir_consultores(tema="biologia", apenas_ativos=True)
|
||||||
|
|
||||||
|
call_args = mock_client.post.call_args
|
||||||
|
query = call_args[1]["json"]
|
||||||
|
assert len(query["query"]["bool"]["must"]) >= 2
|
||||||
|
|
||||||
|
|
||||||
|
class TestListarAreasAvaliacao:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_listar_areas_avaliacao(self, es_client):
|
||||||
|
result = await es_client.listar_areas_avaliacao()
|
||||||
|
|
||||||
|
assert len(result) > 40
|
||||||
|
assert any(a["nome"] == "CIÊNCIA DA COMPUTAÇÃO" for a in result)
|
||||||
|
assert any(a["nome"] == "MEDICINA I" for a in result)
|
||||||
|
assert all("count" in a for a in result)
|
||||||
|
|
||||||
|
|
||||||
|
class TestIntegracaoCompleta:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_fluxo_completo_consultor(self, es_client, mock_httpx_response):
|
||||||
|
doc_consultor = {
|
||||||
|
"id": 12345,
|
||||||
|
"dadosPessoais": {
|
||||||
|
"nome": "MARIA COORDENADORA SILVA",
|
||||||
|
"nascimento": "1970-05-15"
|
||||||
|
},
|
||||||
|
"atuacoes": [
|
||||||
|
{
|
||||||
|
"tipo": "Coordenação de Área de Avaliação",
|
||||||
|
"inicio": "01/01/2020",
|
||||||
|
"fim": None,
|
||||||
|
"dadosCoordenacaoArea": {
|
||||||
|
"tipo": "Coordenador de Área",
|
||||||
|
"areaAvaliacao": {"nome": "CIÊNCIA DA COMPUTAÇÃO"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tipo": "Consultor",
|
||||||
|
"inicio": "01/01/2015",
|
||||||
|
"dadosConsultoria": {
|
||||||
|
"situacaoConsultoria": "Atividade Contínua"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
|
||||||
|
mock_client.post = AsyncMock(return_value=mock_httpx_response({
|
||||||
|
"hits": {"total": {"value": 1}, "hits": [{"_source": doc_consultor}]}
|
||||||
|
}))
|
||||||
|
|
||||||
|
result = await es_client.buscar_por_id(12345)
|
||||||
|
|
||||||
|
assert result["id"] == 12345
|
||||||
|
assert len(result["atuacoes"]) == 2
|
||||||
|
|
||||||
|
coord = next(a for a in result["atuacoes"] if "Coordenação" in a["tipo"])
|
||||||
|
assert coord["dadosCoordenacaoArea"]["tipo"] == "Coordenador de Área"
|
||||||
|
|
||||||
|
cons = next(a for a in result["atuacoes"] if a["tipo"] == "Consultor")
|
||||||
|
assert cons["dadosConsultoria"]["situacaoConsultoria"] == "Atividade Contínua"
|
||||||
208
backend/tests/infrastructure/oracle/test_ranking_repository.py
Normal file
208
backend/tests/infrastructure/oracle/test_ranking_repository.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.infrastructure.oracle.ranking_repository import RankingOracleRepository
|
||||||
|
|
||||||
|
|
||||||
|
class DummyCLOB:
|
||||||
|
def __init__(self, value: str) -> None:
|
||||||
|
self._value = value
|
||||||
|
|
||||||
|
def read(self) -> str:
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def oracle_client():
|
||||||
|
client = MagicMock()
|
||||||
|
client.executar_query.return_value = []
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def repo(oracle_client):
|
||||||
|
return RankingOracleRepository(oracle_client)
|
||||||
|
|
||||||
|
|
||||||
|
def test_buscar_paginado_monta_params_e_parseia_json(repo, oracle_client):
|
||||||
|
oracle_client.executar_query.return_value = [
|
||||||
|
{
|
||||||
|
"ID_PESSOA": 1,
|
||||||
|
"NOME": "Teste",
|
||||||
|
"POSICAO": 10,
|
||||||
|
"PONTUACAO_TOTAL": 200,
|
||||||
|
"COMPONENTE_A": 10,
|
||||||
|
"COMPONENTE_B": 20,
|
||||||
|
"COMPONENTE_C": 30,
|
||||||
|
"COMPONENTE_D": 40,
|
||||||
|
"COMPONENTE_E": 0,
|
||||||
|
"ATIVO": "S",
|
||||||
|
"ANOS_ATUACAO": 5,
|
||||||
|
"DT_CALCULO": datetime(2025, 1, 1),
|
||||||
|
"JSON_DETALHES": DummyCLOB("{\"k\": 1}"),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
resultado = repo.buscar_paginado(page=2, size=20, filtro_ativo=True, filtro_selos=["CA", "EVENTO"])
|
||||||
|
|
||||||
|
assert len(resultado) == 1
|
||||||
|
consultor = resultado[0]
|
||||||
|
assert consultor.id_pessoa == 1
|
||||||
|
assert consultor.ativo is True
|
||||||
|
assert consultor.json_detalhes == "{\"k\": 1}"
|
||||||
|
|
||||||
|
call_args = oracle_client.executar_query.call_args
|
||||||
|
params = call_args[0][1]
|
||||||
|
assert params["offset"] == 20
|
||||||
|
assert params["limit_end"] == 40
|
||||||
|
assert params["ativo"] == "S"
|
||||||
|
assert params["selo_0"] == "CA"
|
||||||
|
assert params["selo_1"] == "EVENTO"
|
||||||
|
|
||||||
|
|
||||||
|
def test_contar_total_com_filtros(repo, oracle_client):
|
||||||
|
oracle_client.executar_query.return_value = [{"TOTAL": 123}]
|
||||||
|
total = repo.contar_total(filtro_ativo=False, filtro_selos=["CONS_ATIVO"])
|
||||||
|
assert total == 123
|
||||||
|
call_args = oracle_client.executar_query.call_args
|
||||||
|
params = call_args[0][1]
|
||||||
|
assert params["ativo"] == "N"
|
||||||
|
assert params["selo_0"] == "CONS_ATIVO"
|
||||||
|
|
||||||
|
|
||||||
|
def test_buscar_por_nome_monta_query(repo, oracle_client):
|
||||||
|
oracle_client.executar_query.return_value = [{"ID_PESSOA": 1, "NOME": "JOAO", "POSICAO": 1, "PONTUACAO_TOTAL": 10}]
|
||||||
|
resultado = repo.buscar_por_nome("Joao Silva", limit=3)
|
||||||
|
assert resultado[0]["ID_PESSOA"] == 1
|
||||||
|
call_args = oracle_client.executar_query.call_args
|
||||||
|
params = call_args[0][1]
|
||||||
|
assert params["limit"] == 3
|
||||||
|
assert params["p0"] == "%JOAO%"
|
||||||
|
assert params["p1"] == "%SILVA%"
|
||||||
|
|
||||||
|
|
||||||
|
def test_buscar_por_id_parseia_json(repo, oracle_client):
|
||||||
|
oracle_client.executar_query.return_value = [
|
||||||
|
{
|
||||||
|
"ID_PESSOA": 5,
|
||||||
|
"NOME": "Teste",
|
||||||
|
"POSICAO": 20,
|
||||||
|
"PONTUACAO_TOTAL": 300,
|
||||||
|
"COMPONENTE_A": 10,
|
||||||
|
"COMPONENTE_B": 20,
|
||||||
|
"COMPONENTE_C": 30,
|
||||||
|
"COMPONENTE_D": 40,
|
||||||
|
"COMPONENTE_E": 0,
|
||||||
|
"ATIVO": "N",
|
||||||
|
"ANOS_ATUACAO": 2,
|
||||||
|
"DT_CALCULO": datetime(2024, 12, 31),
|
||||||
|
"JSON_DETALHES": DummyCLOB("{\"x\": true}"),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
consultor = repo.buscar_por_id(5)
|
||||||
|
assert consultor is not None
|
||||||
|
assert consultor.id_pessoa == 5
|
||||||
|
assert consultor.ativo is False
|
||||||
|
assert consultor.json_detalhes == "{\"x\": true}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_atualizar_posicoes_chama_procedure(repo, oracle_client):
|
||||||
|
cursor = MagicMock()
|
||||||
|
conn = MagicMock()
|
||||||
|
conn.cursor.return_value = cursor
|
||||||
|
oracle_client.get_connection.return_value.__enter__.return_value = conn
|
||||||
|
|
||||||
|
repo.atualizar_posicoes()
|
||||||
|
|
||||||
|
cursor.callproc.assert_called_once_with("SP_ATUALIZAR_POSICOES")
|
||||||
|
conn.commit.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_obter_estatisticas(repo, oracle_client):
|
||||||
|
oracle_client.executar_query.return_value = [
|
||||||
|
{
|
||||||
|
"TOTAL_CONSULTORES": 10,
|
||||||
|
"TOTAL_ATIVOS": 7,
|
||||||
|
"TOTAL_INATIVOS": 3,
|
||||||
|
"ULTIMA_ATUALIZACAO": datetime(2025, 1, 2, 10, 0, 0),
|
||||||
|
"PONTUACAO_MEDIA": 100,
|
||||||
|
"PONTUACAO_MAXIMA": 200,
|
||||||
|
"PONTUACAO_MINIMA": 10,
|
||||||
|
"MEDIA_COMP_A": 10,
|
||||||
|
"MEDIA_COMP_B": 20,
|
||||||
|
"MEDIA_COMP_C": 30,
|
||||||
|
"MEDIA_COMP_D": 40,
|
||||||
|
"MEDIA_COMP_E": 0,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
stats = repo.obter_estatisticas()
|
||||||
|
assert stats["total_consultores"] == 10
|
||||||
|
assert stats["total_ativos"] == 7
|
||||||
|
assert stats["ultima_atualizacao"].startswith("2025-01-02")
|
||||||
|
assert stats["media_componentes"]["a"] == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_obter_distribuicao(repo, oracle_client):
|
||||||
|
oracle_client.executar_query.return_value = [
|
||||||
|
{"FAIXA": "0-199", "QUANTIDADE": 5, "PERCENTUAL": 50},
|
||||||
|
{"FAIXA": "200-399", "QUANTIDADE": 5, "PERCENTUAL": 50},
|
||||||
|
]
|
||||||
|
dist = repo.obter_distribuicao()
|
||||||
|
assert dist[0]["faixa"] == "0-199"
|
||||||
|
assert dist[1]["percentual"] == 50.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_buscar_para_exportacao_usa_cursor(repo, oracle_client):
|
||||||
|
cursor = MagicMock()
|
||||||
|
cursor.description = [("ID_PESSOA",), ("NOME",), ("JSON_DETALHES",)]
|
||||||
|
cursor.fetchmany.side_effect = [
|
||||||
|
[(1, "Teste", DummyCLOB("{\"k\":1}"))],
|
||||||
|
[],
|
||||||
|
]
|
||||||
|
conn = MagicMock()
|
||||||
|
conn.cursor.return_value = cursor
|
||||||
|
oracle_client.get_connection.return_value.__enter__.return_value = conn
|
||||||
|
|
||||||
|
registros = repo.buscar_para_exportacao(filtro_ativo=True, filtro_selos=["CA"], batch_size=1)
|
||||||
|
assert registros[0]["ID_PESSOA"] == 1
|
||||||
|
assert registros[0]["JSON_DETALHES"] == "{\"k\":1}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_contar_para_exportacao_com_filtros(repo, oracle_client):
|
||||||
|
oracle_client.executar_query.return_value = [{"TOTAL": 42}]
|
||||||
|
total = repo.contar_para_exportacao(filtro_ativo=True, filtro_selos=["EVENTO", "PROJ"])
|
||||||
|
assert total == 42
|
||||||
|
call_args = oracle_client.executar_query.call_args
|
||||||
|
params = call_args[0][1]
|
||||||
|
assert params["ativo"] == "S"
|
||||||
|
assert params["selo_0"] == "EVENTO"
|
||||||
|
assert params["selo_1"] == "PROJ"
|
||||||
|
|
||||||
|
|
||||||
|
def test_filtro_ativo_filtra_apenas_consultores(repo, oracle_client):
|
||||||
|
oracle_client.executar_query.return_value = [{"TOTAL": 100}]
|
||||||
|
|
||||||
|
repo.contar_total(filtro_ativo=True)
|
||||||
|
|
||||||
|
call_args = oracle_client.executar_query.call_args
|
||||||
|
query = call_args[0][0]
|
||||||
|
assert "E_CONSULTOR = 'S'" in query
|
||||||
|
assert "ATIVO = :ativo" in query
|
||||||
|
|
||||||
|
|
||||||
|
def test_filtro_ativo_false_filtra_apenas_consultores(repo, oracle_client):
|
||||||
|
oracle_client.executar_query.return_value = [{"TOTAL": 100}]
|
||||||
|
|
||||||
|
repo.contar_total(filtro_ativo=False)
|
||||||
|
|
||||||
|
call_args = oracle_client.executar_query.call_args
|
||||||
|
query = call_args[0][0]
|
||||||
|
assert "E_CONSULTOR = 'S'" in query
|
||||||
|
assert "ATIVO = :ativo" in query
|
||||||
|
params = call_args[0][1]
|
||||||
|
assert params["ativo"] == "N"
|
||||||
@@ -0,0 +1,483 @@
|
|||||||
|
import pytest
|
||||||
|
from datetime import datetime
|
||||||
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
|
from src.infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_es_client():
|
||||||
|
return AsyncMock()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def repository(mock_es_client):
|
||||||
|
return ConsultorRepositoryImpl(es_client=mock_es_client)
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseDate:
|
||||||
|
|
||||||
|
def test_parse_date_formato_brasileiro(self, repository):
|
||||||
|
result = repository._parse_date("15/03/2020")
|
||||||
|
assert result is not None
|
||||||
|
assert result.day == 15
|
||||||
|
assert result.month == 3
|
||||||
|
assert result.year == 2020
|
||||||
|
|
||||||
|
def test_parse_date_formato_iso(self, repository):
|
||||||
|
result = repository._parse_date("2020-03-15")
|
||||||
|
assert result is not None
|
||||||
|
assert result.year == 2020
|
||||||
|
assert result.month == 3
|
||||||
|
assert result.day == 15
|
||||||
|
|
||||||
|
def test_parse_date_none(self, repository):
|
||||||
|
result = repository._parse_date(None)
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_parse_date_string_vazia(self, repository):
|
||||||
|
result = repository._parse_date("")
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_parse_date_invalida(self, repository):
|
||||||
|
result = repository._parse_date("data_invalida")
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestInferirTipoCoordenacao:
|
||||||
|
|
||||||
|
def test_coordenador_area(self, repository):
|
||||||
|
coord = {"dadosCoordenacaoArea": {"tipo": "Coordenador de Área"}}
|
||||||
|
result = repository._inferir_tipo_coordenacao(coord)
|
||||||
|
assert result == "CA"
|
||||||
|
|
||||||
|
def test_coordenador_adjunto(self, repository):
|
||||||
|
coord = {"dadosCoordenacaoArea": {"tipo": "Coordenador Adjunto"}}
|
||||||
|
result = repository._inferir_tipo_coordenacao(coord)
|
||||||
|
assert result == "CAJ"
|
||||||
|
|
||||||
|
def test_coordenador_adjunto_mp(self, repository):
|
||||||
|
coord = {"dadosCoordenacaoArea": {"tipo": "Coordenador Adjunto de Mestrado Profissionalizante"}}
|
||||||
|
result = repository._inferir_tipo_coordenacao(coord)
|
||||||
|
assert result == "CAJ_MP"
|
||||||
|
|
||||||
|
def test_camara_tematica(self, repository):
|
||||||
|
coord = {"dadosCoordenacaoArea": {"tipo": "Câmara Temática"}}
|
||||||
|
result = repository._inferir_tipo_coordenacao(coord)
|
||||||
|
assert result == "CAM"
|
||||||
|
|
||||||
|
def test_inferencia_por_descricao(self, repository):
|
||||||
|
coord = {"dadosCoordenacaoArea": {}, "descricao": "Coordenador Adjunto - MEDICINA"}
|
||||||
|
result = repository._inferir_tipo_coordenacao(coord)
|
||||||
|
assert result == "CAJ"
|
||||||
|
|
||||||
|
def test_fallback_para_ca(self, repository):
|
||||||
|
coord = {"dadosCoordenacaoArea": {"tipo": "Tipo Desconhecido"}}
|
||||||
|
result = repository._inferir_tipo_coordenacao(coord)
|
||||||
|
assert result == "CA"
|
||||||
|
|
||||||
|
|
||||||
|
class TestInferirPremiacaoTipo:
|
||||||
|
|
||||||
|
def test_grande_premio(self):
|
||||||
|
result = ConsultorRepositoryImpl._inferir_premiacao_tipo("Grande Prêmio CAPES")
|
||||||
|
assert result == "GP"
|
||||||
|
|
||||||
|
def test_mencao_honrosa(self):
|
||||||
|
result = ConsultorRepositoryImpl._inferir_premiacao_tipo("Menção Honrosa")
|
||||||
|
assert result == "MENCAO"
|
||||||
|
|
||||||
|
def test_premio_regular(self):
|
||||||
|
result = ConsultorRepositoryImpl._inferir_premiacao_tipo("Prêmio CAPES de Tese")
|
||||||
|
assert result == "PREMIO"
|
||||||
|
|
||||||
|
def test_texto_sem_premiacao(self):
|
||||||
|
result = ConsultorRepositoryImpl._inferir_premiacao_tipo("Texto qualquer")
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_texto_vazio(self):
|
||||||
|
result = ConsultorRepositoryImpl._inferir_premiacao_tipo("")
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtrairCoordenacoesCapes:
|
||||||
|
|
||||||
|
def test_lista_vazia(self, repository):
|
||||||
|
result = repository._extrair_coordenacoes_capes([])
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
def test_atuacao_sem_tipo_coordenacao(self, repository):
|
||||||
|
atuacoes = [{"tipo": "Consultor"}]
|
||||||
|
result = repository._extrair_coordenacoes_capes(atuacoes)
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
def test_coordenacao_ativa(self, repository):
|
||||||
|
atuacoes = [{
|
||||||
|
"tipo": "Coordenação de Área de Avaliação",
|
||||||
|
"inicio": "01/01/2020",
|
||||||
|
"fim": None,
|
||||||
|
"dadosCoordenacaoArea": {
|
||||||
|
"tipo": "Coordenador de Área",
|
||||||
|
"inicioVinculacao": "01/01/2020",
|
||||||
|
"fimVinculacao": None,
|
||||||
|
"areaAvaliacao": {"nome": "CIÊNCIAS AMBIENTAIS"},
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
result = repository._extrair_coordenacoes_capes(atuacoes)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].codigo == "CA"
|
||||||
|
assert result[0].area_avaliacao == "CIÊNCIAS AMBIENTAIS"
|
||||||
|
assert result[0].periodo.ativo is True
|
||||||
|
|
||||||
|
def test_coordenacao_historica(self, repository):
|
||||||
|
atuacoes = [{
|
||||||
|
"tipo": "Histórico de Coordenação de Área de Avaliação",
|
||||||
|
"dadosCoordenacaoArea": {
|
||||||
|
"tipo": "Coordenador Adjunto",
|
||||||
|
"inicioVinculacao": "01/01/2018",
|
||||||
|
"fimVinculacao": "31/12/2020",
|
||||||
|
"areaAvaliacao": {"nome": "MEDICINA I"},
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
result = repository._extrair_coordenacoes_capes(atuacoes)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].codigo == "CAJ"
|
||||||
|
assert result[0].periodo.ativo is False
|
||||||
|
|
||||||
|
def test_coordenacao_sem_inicio_ignorada(self, repository):
|
||||||
|
atuacoes = [{
|
||||||
|
"tipo": "Coordenação de Área de Avaliação",
|
||||||
|
"dadosCoordenacaoArea": {
|
||||||
|
"tipo": "Coordenador de Área",
|
||||||
|
"inicioVinculacao": None,
|
||||||
|
"areaAvaliacao": {"nome": "CIÊNCIAS"},
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
result = repository._extrair_coordenacoes_capes(atuacoes)
|
||||||
|
assert len(result) == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtrairConsultoria:
|
||||||
|
|
||||||
|
def test_sem_consultoria(self, repository):
|
||||||
|
result = repository._extrair_consultoria([])
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_consultor_ativo(self, repository):
|
||||||
|
atuacoes = [{
|
||||||
|
"tipo": "Consultor",
|
||||||
|
"inicio": "01/01/2020",
|
||||||
|
"fim": None,
|
||||||
|
"dadosConsultoria": {
|
||||||
|
"situacaoConsultoria": "Atividade Contínua",
|
||||||
|
"inicioVinculacao": "01/01/2020",
|
||||||
|
"ies": {"id": "1", "nome": "USP", "sigla": "USP"},
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
result = repository._extrair_consultoria(atuacoes)
|
||||||
|
assert result is not None
|
||||||
|
assert result.codigo == "CONS_ATIVO"
|
||||||
|
assert result.periodo.ativo is True
|
||||||
|
|
||||||
|
def test_consultor_historico(self, repository):
|
||||||
|
atuacoes = [{
|
||||||
|
"tipo": "Histórico de Consultoria",
|
||||||
|
"inicio": "01/01/2015",
|
||||||
|
"fim": "31/12/2018",
|
||||||
|
"dadosConsultoria": {
|
||||||
|
"situacaoConsultoria": "Desligado",
|
||||||
|
"inicioVinculacao": "01/01/2015",
|
||||||
|
"inativacaoSituacao": "31/12/2018",
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
result = repository._extrair_consultoria(atuacoes)
|
||||||
|
assert result is not None
|
||||||
|
assert result.codigo == "CONS_HIST"
|
||||||
|
|
||||||
|
def test_consultor_falecido(self, repository):
|
||||||
|
atuacoes = [{
|
||||||
|
"tipo": "Consultor",
|
||||||
|
"inicio": "01/01/2010",
|
||||||
|
"fim": "31/12/2020",
|
||||||
|
"dadosConsultoria": {
|
||||||
|
"situacaoConsultoria": "Falecido",
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
result = repository._extrair_consultoria(atuacoes)
|
||||||
|
assert result is not None
|
||||||
|
assert result.codigo == "CONS_FALECIDO"
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtrairInscricoes:
|
||||||
|
|
||||||
|
def test_sem_inscricoes(self, repository):
|
||||||
|
result = repository._extrair_inscricoes([])
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
def test_inscricao_autor(self, repository):
|
||||||
|
atuacoes = [{
|
||||||
|
"tipo": "Inscrição Prêmio",
|
||||||
|
"inicio": "01/01/2024",
|
||||||
|
"dadosParticipacaoInscricaoPremio": {
|
||||||
|
"tipo": "Autor",
|
||||||
|
"nomePremio": "PCT",
|
||||||
|
"ano": 2024,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
result = repository._extrair_inscricoes(atuacoes)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].codigo == "INSC_AUTOR"
|
||||||
|
assert result[0].ano == 2024
|
||||||
|
|
||||||
|
def test_inscricao_institucional(self, repository):
|
||||||
|
atuacoes = [{
|
||||||
|
"tipo": "Inscrição Prêmio",
|
||||||
|
"dadosParticipacaoInscricaoPremio": {
|
||||||
|
"tipo": "Coordenador PPG",
|
||||||
|
"nomePremio": "PCT",
|
||||||
|
"ano": 2024,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
result = repository._extrair_inscricoes(atuacoes)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].codigo == "INSC_INST_AUTOR"
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtrairAvaliacoesComissao:
|
||||||
|
|
||||||
|
def test_sem_avaliacoes(self, repository):
|
||||||
|
result = repository._extrair_avaliacoes_comissao([])
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
def test_avaliador_premio(self, repository):
|
||||||
|
atuacoes = [{
|
||||||
|
"tipo": "Avaliação Prêmio",
|
||||||
|
"dadosParticipacaoPremio": {
|
||||||
|
"tipo": "Membro de Comissão",
|
||||||
|
"nomePremio": "Prêmio CAPES de Tese",
|
||||||
|
"ano": 2024,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
result = repository._extrair_avaliacoes_comissao(atuacoes)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].codigo == "AVAL_COMIS_PREMIO"
|
||||||
|
|
||||||
|
def test_avaliador_grande_premio(self, repository):
|
||||||
|
atuacoes = [{
|
||||||
|
"tipo": "Avaliação Prêmio",
|
||||||
|
"dadosParticipacaoPremio": {
|
||||||
|
"tipo": "Membro de Comissão",
|
||||||
|
"nomePremio": "Grande Prêmio CAPES",
|
||||||
|
"ano": 2024,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
result = repository._extrair_avaliacoes_comissao(atuacoes)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].codigo == "AVAL_COMIS_GP"
|
||||||
|
|
||||||
|
def test_coordenador_comissao(self, repository):
|
||||||
|
atuacoes = [{
|
||||||
|
"tipo": "Avaliação Prêmio",
|
||||||
|
"dadosParticipacaoPremio": {
|
||||||
|
"tipo": "Coordenador/Presidente",
|
||||||
|
"nomePremio": "Prêmio CAPES",
|
||||||
|
"ano": 2024,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
result = repository._extrair_avaliacoes_comissao(atuacoes)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].codigo == "COORD_COMIS_PREMIO"
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtrairPremiacoes:
|
||||||
|
|
||||||
|
def test_sem_premiacoes(self, repository):
|
||||||
|
result = repository._extrair_premiacoes([])
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
def test_grande_premio(self, repository):
|
||||||
|
atuacoes = [{
|
||||||
|
"tipo": "Premiação Prêmio",
|
||||||
|
"dadosPremiacaoPremio": {
|
||||||
|
"tipoPremiacao": "Grande Prêmio",
|
||||||
|
"nomePremio": "Grande Prêmio CAPES de Tese",
|
||||||
|
"ano": 2024,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
result = repository._extrair_premiacoes(atuacoes)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].codigo == "PREMIACAO_GP_AUTOR"
|
||||||
|
|
||||||
|
def test_premio_regular(self, repository):
|
||||||
|
atuacoes = [{
|
||||||
|
"tipo": "Premiação Prêmio",
|
||||||
|
"dadosPremiacaoPremio": {
|
||||||
|
"tipoPremiacao": "Prêmio",
|
||||||
|
"nomePremio": "Prêmio CAPES de Tese",
|
||||||
|
"ano": 2024,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
result = repository._extrair_premiacoes(atuacoes)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].codigo == "PREMIACAO_AUTOR"
|
||||||
|
|
||||||
|
def test_mencao_honrosa(self, repository):
|
||||||
|
atuacoes = [{
|
||||||
|
"tipo": "Premiação Prêmio",
|
||||||
|
"dadosPremiacaoPremio": {
|
||||||
|
"tipoPremiacao": "Menção Honrosa",
|
||||||
|
"nomePremio": "PCT",
|
||||||
|
"ano": 2024,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
result = repository._extrair_premiacoes(atuacoes)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].codigo == "MENCAO_AUTOR"
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtrairBolsasCNPQ:
|
||||||
|
|
||||||
|
def test_sem_bolsas(self, repository):
|
||||||
|
result = repository._extrair_bolsas_cnpq([])
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
def test_bolsa_cnpq(self, repository):
|
||||||
|
atuacoes = [{
|
||||||
|
"tipo": "Bolsa CNPQ",
|
||||||
|
"dadosBolsa": {
|
||||||
|
"nivel": "1A",
|
||||||
|
"areaConhecimento": "Ciências Exatas",
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
result = repository._extrair_bolsas_cnpq(atuacoes)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].codigo == "BOL_BPQ_NIVEL"
|
||||||
|
assert result[0].nivel == "1A"
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtrairParticipacoes:
|
||||||
|
|
||||||
|
def test_sem_participacoes(self, repository):
|
||||||
|
result = repository._extrair_participacoes([])
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
def test_evento(self, repository):
|
||||||
|
atuacoes = [{
|
||||||
|
"tipo": "Evento",
|
||||||
|
"descricao": "Seminário CAPES 2024",
|
||||||
|
"inicio": "01/06/2024",
|
||||||
|
}]
|
||||||
|
result = repository._extrair_participacoes(atuacoes)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].codigo == "EVENTO"
|
||||||
|
assert result[0].ano == 2024
|
||||||
|
|
||||||
|
def test_projeto(self, repository):
|
||||||
|
atuacoes = [{
|
||||||
|
"tipo": "Projeto",
|
||||||
|
"descricao": "Projeto de Pesquisa",
|
||||||
|
"inicio": "01/01/2024",
|
||||||
|
}]
|
||||||
|
result = repository._extrair_participacoes(atuacoes)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].codigo == "PROJ"
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtrairOrientacoes:
|
||||||
|
|
||||||
|
def test_sem_orientacoes(self, repository):
|
||||||
|
result = repository._extrair_orientacoes([])
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
def test_orientacao_discentes_contagem(self, repository):
|
||||||
|
atuacoes = [{
|
||||||
|
"tipo": "Orientação de Discentes",
|
||||||
|
"dadosOrientacaoDiscente": {
|
||||||
|
"totalOrientacaoFinalizadaMestrado": 5,
|
||||||
|
"totalOrientacaoFinalizadaDoutorado": 3,
|
||||||
|
"totalAcompanhamentoPosDoutorado": 2,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
result = repository._extrair_orientacoes(atuacoes)
|
||||||
|
assert len(result) == 10
|
||||||
|
pos_doc = [o for o in result if o.codigo == "ORIENT_POS_DOC"]
|
||||||
|
tese = [o for o in result if o.codigo == "ORIENT_TESE"]
|
||||||
|
diss = [o for o in result if o.codigo == "ORIENT_DISS"]
|
||||||
|
assert len(pos_doc) == 2
|
||||||
|
assert len(tese) == 3
|
||||||
|
assert len(diss) == 5
|
||||||
|
|
||||||
|
|
||||||
|
class TestConstruirConsultor:
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_construir_consultor_completo(self, repository):
|
||||||
|
doc = {
|
||||||
|
"id": 12345,
|
||||||
|
"dadosPessoais": {"nome": "João da Silva", "cpf": "12345678900"},
|
||||||
|
"atuacoes": [
|
||||||
|
{
|
||||||
|
"tipo": "Coordenação de Área de Avaliação",
|
||||||
|
"inicio": "01/01/2020",
|
||||||
|
"dadosCoordenacaoArea": {
|
||||||
|
"tipo": "Coordenador de Área",
|
||||||
|
"inicioVinculacao": "01/01/2020",
|
||||||
|
"areaAvaliacao": {"nome": "CIÊNCIAS AMBIENTAIS"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tipo": "Consultor",
|
||||||
|
"inicio": "01/01/2018",
|
||||||
|
"dadosConsultoria": {
|
||||||
|
"situacaoConsultoria": "Atividade Contínua",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
consultor = await repository._construir_consultor(doc)
|
||||||
|
assert consultor.id_pessoa == 12345
|
||||||
|
assert consultor.nome == "João da Silva"
|
||||||
|
assert len(consultor.coordenacoes_capes) == 1
|
||||||
|
assert consultor.consultoria is not None
|
||||||
|
assert consultor.pontuacao is not None
|
||||||
|
assert consultor.pontuacao_total > 0
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_construir_consultor_vazio(self, repository):
|
||||||
|
doc = {
|
||||||
|
"id": 99999,
|
||||||
|
"dadosPessoais": {"nome": "Consultor Vazio"},
|
||||||
|
"atuacoes": []
|
||||||
|
}
|
||||||
|
consultor = await repository._construir_consultor(doc)
|
||||||
|
assert consultor.id_pessoa == 99999
|
||||||
|
assert consultor.nome == "Consultor Vazio"
|
||||||
|
assert len(consultor.coordenacoes_capes) == 0
|
||||||
|
assert consultor.consultoria is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuscarPorId:
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_buscar_por_id_encontrado(self, repository, mock_es_client):
|
||||||
|
mock_es_client.buscar_por_id.return_value = {
|
||||||
|
"id": 12345,
|
||||||
|
"dadosPessoais": {"nome": "João"},
|
||||||
|
"atuacoes": []
|
||||||
|
}
|
||||||
|
result = await repository.buscar_por_id(12345)
|
||||||
|
assert result is not None
|
||||||
|
assert result.id_pessoa == 12345
|
||||||
|
mock_es_client.buscar_por_id.assert_called_once_with(12345)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_buscar_por_id_nao_encontrado(self, repository, mock_es_client):
|
||||||
|
mock_es_client.buscar_por_id.return_value = None
|
||||||
|
result = await repository.buscar_por_id(99999)
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_buscar_por_id_erro_es(self, repository, mock_es_client):
|
||||||
|
mock_es_client.buscar_por_id.side_effect = Exception("ES error")
|
||||||
|
result = await repository.buscar_por_id(12345)
|
||||||
|
assert result is None
|
||||||
53
backend/tests/infrastructure/test_pdf_selos.py
Normal file
53
backend/tests/infrastructure/test_pdf_selos.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
from src.infrastructure.ranking_store import SELOS_DISPONIVEIS
|
||||||
|
|
||||||
|
|
||||||
|
PDF_SELOS = {
|
||||||
|
"CA",
|
||||||
|
"CAJ",
|
||||||
|
"CAJ_MP",
|
||||||
|
"CAM",
|
||||||
|
"PRESID_CAMARA",
|
||||||
|
"CONS_ATIVO",
|
||||||
|
"AVAL_COMIS",
|
||||||
|
"COORD_COMIS",
|
||||||
|
"AUTOR_GP",
|
||||||
|
"AUTOR_PREMIO",
|
||||||
|
"AUTOR_MENCAO",
|
||||||
|
"ORIENT_GP",
|
||||||
|
"ORIENT_PREMIO",
|
||||||
|
"ORIENT_MENCAO",
|
||||||
|
"COORIENT_GP",
|
||||||
|
"COORIENT_PREMIO",
|
||||||
|
"COORIENT_MENCAO",
|
||||||
|
"ORIENT_POS_DOC",
|
||||||
|
"ORIENT_POS_DOC_PREM",
|
||||||
|
"ORIENT_TESE",
|
||||||
|
"ORIENT_TESE_PREM",
|
||||||
|
"ORIENT_DISS",
|
||||||
|
"ORIENT_DISS_PREM",
|
||||||
|
"CO_ORIENT_POS_DOC",
|
||||||
|
"CO_ORIENT_POS_DOC_PREM",
|
||||||
|
"CO_ORIENT_TESE",
|
||||||
|
"CO_ORIENT_TESE_PREM",
|
||||||
|
"CO_ORIENT_DISS",
|
||||||
|
"CO_ORIENT_DISS_PREM",
|
||||||
|
"MB_BANCA_POS_DOC",
|
||||||
|
"MB_BANCA_POS_DOC_PREM",
|
||||||
|
"MB_BANCA_TESE",
|
||||||
|
"MB_BANCA_TESE_PREM",
|
||||||
|
"MB_BANCA_DISS",
|
||||||
|
"MB_BANCA_DISS_PREM",
|
||||||
|
"IDIOMA_BILINGUE",
|
||||||
|
"IDIOMA_MULTILINGUE",
|
||||||
|
"TITULACAO_MESTRE",
|
||||||
|
"TITULACAO_DOUTOR",
|
||||||
|
"TITULACAO_POS_DOUTOR",
|
||||||
|
"BOL_BPQ_NIVEL",
|
||||||
|
"PPG_COORD",
|
||||||
|
"EVENTO",
|
||||||
|
"PROJ",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_pdf_selos_disponiveis():
|
||||||
|
assert set(SELOS_DISPONIVEIS) == PDF_SELOS
|
||||||
120
backend/tests/infrastructure/test_ranking_store.py
Normal file
120
backend/tests/infrastructure/test_ranking_store.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import pytest
|
||||||
|
from src.infrastructure.ranking_store import RankingStore, RankingEntry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def store():
|
||||||
|
return RankingStore()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def entries_com_consultores_e_nao_consultores():
|
||||||
|
return [
|
||||||
|
RankingEntry(
|
||||||
|
id_pessoa=1,
|
||||||
|
nome="Consultor Ativo",
|
||||||
|
posicao=1,
|
||||||
|
pontuacao_total=500,
|
||||||
|
bloco_a=200,
|
||||||
|
bloco_b=0,
|
||||||
|
bloco_c=150,
|
||||||
|
bloco_d=100,
|
||||||
|
bloco_e=50,
|
||||||
|
ativo=True,
|
||||||
|
e_consultor=True,
|
||||||
|
anos_atuacao=5.0,
|
||||||
|
detalhes={},
|
||||||
|
),
|
||||||
|
RankingEntry(
|
||||||
|
id_pessoa=2,
|
||||||
|
nome="Consultor Inativo",
|
||||||
|
posicao=2,
|
||||||
|
pontuacao_total=400,
|
||||||
|
bloco_a=200,
|
||||||
|
bloco_b=0,
|
||||||
|
bloco_c=100,
|
||||||
|
bloco_d=50,
|
||||||
|
bloco_e=50,
|
||||||
|
ativo=False,
|
||||||
|
e_consultor=True,
|
||||||
|
anos_atuacao=3.0,
|
||||||
|
detalhes={},
|
||||||
|
),
|
||||||
|
RankingEntry(
|
||||||
|
id_pessoa=3,
|
||||||
|
nome="Nao Consultor (Coordenador)",
|
||||||
|
posicao=3,
|
||||||
|
pontuacao_total=300,
|
||||||
|
bloco_a=250,
|
||||||
|
bloco_b=0,
|
||||||
|
bloco_c=0,
|
||||||
|
bloco_d=50,
|
||||||
|
bloco_e=0,
|
||||||
|
ativo=False,
|
||||||
|
e_consultor=False,
|
||||||
|
anos_atuacao=2.0,
|
||||||
|
detalhes={},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_total_sem_filtro_traz_todos(store, entries_com_consultores_e_nao_consultores):
|
||||||
|
await store.set_entries(entries_com_consultores_e_nao_consultores)
|
||||||
|
assert store.total() == 3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_total_filtro_ativo_somente_consultores_ativos(store, entries_com_consultores_e_nao_consultores):
|
||||||
|
await store.set_entries(entries_com_consultores_e_nao_consultores)
|
||||||
|
assert store.total(filtro_ativo=True) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_total_filtro_inativo_somente_consultores_inativos(store, entries_com_consultores_e_nao_consultores):
|
||||||
|
await store.set_entries(entries_com_consultores_e_nao_consultores)
|
||||||
|
assert store.total(filtro_ativo=False) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_page_filtro_ativo_somente_consultores_ativos(store, entries_com_consultores_e_nao_consultores):
|
||||||
|
await store.set_entries(entries_com_consultores_e_nao_consultores)
|
||||||
|
total, entries = store.get_page(page=1, size=10, filtro_ativo=True)
|
||||||
|
assert total == 1
|
||||||
|
ids = [e.id_pessoa for e in entries]
|
||||||
|
assert 1 in ids
|
||||||
|
assert 2 not in ids
|
||||||
|
assert 3 not in ids
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_page_filtro_inativo_somente_consultores_inativos(store, entries_com_consultores_e_nao_consultores):
|
||||||
|
await store.set_entries(entries_com_consultores_e_nao_consultores)
|
||||||
|
total, entries = store.get_page(page=1, size=10, filtro_ativo=False)
|
||||||
|
assert total == 1
|
||||||
|
ids = [e.id_pessoa for e in entries]
|
||||||
|
assert 2 in ids
|
||||||
|
assert 1 not in ids
|
||||||
|
assert 3 not in ids
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_slice_filtro_ativo_somente_consultores_ativos(store, entries_com_consultores_e_nao_consultores):
|
||||||
|
await store.set_entries(entries_com_consultores_e_nao_consultores)
|
||||||
|
total, entries = store.get_slice(offset=0, limit=10, filtro_ativo=True)
|
||||||
|
assert total == 1
|
||||||
|
ids = [e.id_pessoa for e in entries]
|
||||||
|
assert 1 in ids
|
||||||
|
assert 2 not in ids
|
||||||
|
assert 3 not in ids
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_page_sem_filtro_traz_todos(store, entries_com_consultores_e_nao_consultores):
|
||||||
|
await store.set_entries(entries_com_consultores_e_nao_consultores)
|
||||||
|
total, entries = store.get_page(page=1, size=10)
|
||||||
|
assert total == 3
|
||||||
|
ids = [e.id_pessoa for e in entries]
|
||||||
|
assert 1 in ids
|
||||||
|
assert 2 in ids
|
||||||
|
assert 3 in ids
|
||||||
125
backend/tests/integration/test_es_repository_integration.py
Normal file
125
backend/tests/integration/test_es_repository_integration.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
from httpx import Response
|
||||||
|
|
||||||
|
from src.infrastructure.elasticsearch.client import ElasticsearchClient
|
||||||
|
from src.infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
|
||||||
|
from src.domain.services.calculador_pontuacao import CalculadorPontuacao
|
||||||
|
|
||||||
|
|
||||||
|
def create_mock_response(json_data, status_code=200):
|
||||||
|
response = MagicMock(spec=Response)
|
||||||
|
response.json.return_value = json_data
|
||||||
|
response.status_code = status_code
|
||||||
|
response.raise_for_status = MagicMock()
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def es_client():
|
||||||
|
return ElasticsearchClient(
|
||||||
|
url="http://localhost:9200",
|
||||||
|
index="atuacapes_test",
|
||||||
|
user="test",
|
||||||
|
password="test"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def repository(es_client):
|
||||||
|
return ConsultorRepositoryImpl(es_client)
|
||||||
|
|
||||||
|
|
||||||
|
class TestIntegracaoCoordenadorDeArea:
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_coordenador_area_ativo_pontuacao_completa(self, es_client, repository):
|
||||||
|
doc_es = {
|
||||||
|
"id": 1001,
|
||||||
|
"dadosPessoais": {"nome": "COORDENADOR ATIVO SILVA"},
|
||||||
|
"atuacoes": [{
|
||||||
|
"tipo": "Coordenação de Área de Avaliação",
|
||||||
|
"inicio": "01/01/2020",
|
||||||
|
"fim": None,
|
||||||
|
"dadosCoordenacaoArea": {
|
||||||
|
"tipo": "Coordenador de Área",
|
||||||
|
"areaAvaliacao": {"nome": "CIÊNCIA DA COMPUTAÇÃO", "id": 1}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=create_mock_response({
|
||||||
|
"hits": {"total": {"value": 1}, "hits": [{"_source": doc_es}]}
|
||||||
|
}))
|
||||||
|
|
||||||
|
consultor = await repository.buscar_por_id(1001)
|
||||||
|
|
||||||
|
assert consultor is not None
|
||||||
|
assert consultor.nome == "COORDENADOR ATIVO SILVA"
|
||||||
|
assert len(consultor.coordenacoes_capes) == 1
|
||||||
|
|
||||||
|
pontuacao = CalculadorPontuacao.calcular_pontuacao_completa(consultor)
|
||||||
|
assert pontuacao.bloco_a.total > 0
|
||||||
|
|
||||||
|
coord = consultor.coordenacoes_capes[0]
|
||||||
|
assert coord.codigo == "CA"
|
||||||
|
assert pontuacao.bloco_a.total >= 200
|
||||||
|
|
||||||
|
|
||||||
|
class TestCenariosBorda:
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_consultor_sem_atuacoes(self, es_client, repository):
|
||||||
|
doc_es = {
|
||||||
|
"id": 8001,
|
||||||
|
"dadosPessoais": {"nome": "SEM ATUACOES"},
|
||||||
|
"atuacoes": []
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=create_mock_response({
|
||||||
|
"hits": {"total": {"value": 1}, "hits": [{"_source": doc_es}]}
|
||||||
|
}))
|
||||||
|
|
||||||
|
consultor = await repository.buscar_por_id(8001)
|
||||||
|
pontuacao = CalculadorPontuacao.calcular_pontuacao_completa(consultor)
|
||||||
|
|
||||||
|
assert consultor.nome == "SEM ATUACOES"
|
||||||
|
assert pontuacao.total == 0
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_atuacao_tipo_desconhecido(self, es_client, repository):
|
||||||
|
doc_es = {
|
||||||
|
"id": 8002,
|
||||||
|
"dadosPessoais": {"nome": "TIPO ESTRANHO"},
|
||||||
|
"atuacoes": [{
|
||||||
|
"tipo": "Tipo Inexistente No Sistema",
|
||||||
|
"dados": {"campo": "valor"}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=create_mock_response({
|
||||||
|
"hits": {"total": {"value": 1}, "hits": [{"_source": doc_es}]}
|
||||||
|
}))
|
||||||
|
|
||||||
|
consultor = await repository.buscar_por_id(8002)
|
||||||
|
pontuacao = CalculadorPontuacao.calcular_pontuacao_completa(consultor)
|
||||||
|
|
||||||
|
assert pontuacao.total == 0
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_consultor_nao_encontrado(self, es_client, repository):
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(return_value=create_mock_response({
|
||||||
|
"hits": {"total": {"value": 0}, "hits": []}
|
||||||
|
}))
|
||||||
|
|
||||||
|
consultor = await repository.buscar_por_id(99999)
|
||||||
|
|
||||||
|
assert consultor is None
|
||||||
142
backend/tests/integration/test_scroll_flow.py
Normal file
142
backend/tests/integration/test_scroll_flow.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
from httpx import Response
|
||||||
|
|
||||||
|
from src.infrastructure.elasticsearch.client import ElasticsearchClient
|
||||||
|
from src.infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
|
||||||
|
from src.domain.services.calculador_pontuacao import CalculadorPontuacao
|
||||||
|
|
||||||
|
|
||||||
|
def create_mock_response(json_data, status_code=200):
|
||||||
|
response = MagicMock(spec=Response)
|
||||||
|
response.json.return_value = json_data
|
||||||
|
response.status_code = status_code
|
||||||
|
response.raise_for_status = MagicMock()
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def criar_doc_consultor(id_pessoa, nome, atuacoes):
|
||||||
|
return {
|
||||||
|
"id": id_pessoa,
|
||||||
|
"dadosPessoais": {"nome": nome},
|
||||||
|
"atuacoes": atuacoes
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def es_client():
|
||||||
|
return ElasticsearchClient(
|
||||||
|
url="http://localhost:9200",
|
||||||
|
index="atuacapes_test"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def repository(es_client):
|
||||||
|
return ConsultorRepositoryImpl(es_client)
|
||||||
|
|
||||||
|
|
||||||
|
class TestScrollFlowCompleto:
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_processamento_scroll_com_ranking(self, es_client, repository):
|
||||||
|
docs_batch_1 = [
|
||||||
|
criar_doc_consultor(1, "COORD AREA", [{
|
||||||
|
"tipo": "Coordenação de Área de Avaliação",
|
||||||
|
"inicio": "01/01/2018",
|
||||||
|
"dadosCoordenacaoArea": {"tipo": "Coordenador de Área"}
|
||||||
|
}]),
|
||||||
|
criar_doc_consultor(2, "COORD ADJUNTO", [{
|
||||||
|
"tipo": "Coordenação de Área de Avaliação",
|
||||||
|
"inicio": "01/01/2020",
|
||||||
|
"dadosCoordenacaoArea": {"tipo": "Coordenador Adjunto"}
|
||||||
|
}]),
|
||||||
|
]
|
||||||
|
|
||||||
|
docs_batch_2 = [
|
||||||
|
criar_doc_consultor(3, "CONSULTOR ATIVO", [{
|
||||||
|
"tipo": "Consultor",
|
||||||
|
"inicio": "01/01/2015",
|
||||||
|
"dadosConsultoria": {"situacaoConsultoria": "Atividade Contínua"}
|
||||||
|
}]),
|
||||||
|
criar_doc_consultor(4, "PESSOA SEM ATUACAO", []),
|
||||||
|
]
|
||||||
|
|
||||||
|
scroll_1 = {
|
||||||
|
"_scroll_id": "scroll_1",
|
||||||
|
"hits": {
|
||||||
|
"total": {"value": 4},
|
||||||
|
"hits": [{"_source": d} for d in docs_batch_1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll_2 = {
|
||||||
|
"_scroll_id": "scroll_2",
|
||||||
|
"hits": {"hits": [{"_source": d} for d in docs_batch_2]}
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll_empty = {"_scroll_id": "scroll_3", "hits": {"hits": []}}
|
||||||
|
|
||||||
|
ranking_results = []
|
||||||
|
|
||||||
|
async def processar_batch(docs, progress):
|
||||||
|
for doc in docs:
|
||||||
|
consultor = await repository._construir_consultor(doc)
|
||||||
|
pontuacao = CalculadorPontuacao.calcular_pontuacao_completa(consultor)
|
||||||
|
ranking_results.append({
|
||||||
|
"id": consultor.id_pessoa,
|
||||||
|
"nome": consultor.nome,
|
||||||
|
"total": pontuacao.total
|
||||||
|
})
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(side_effect=[
|
||||||
|
create_mock_response(scroll_1),
|
||||||
|
create_mock_response(scroll_2),
|
||||||
|
create_mock_response(scroll_empty)
|
||||||
|
])
|
||||||
|
mock_client.delete = AsyncMock(return_value=create_mock_response({}))
|
||||||
|
|
||||||
|
result = await es_client.buscar_todos_consultores(processar_batch, batch_size=2)
|
||||||
|
|
||||||
|
assert result["total"] == 4
|
||||||
|
assert result["processados"] == 4
|
||||||
|
assert len(ranking_results) == 4
|
||||||
|
|
||||||
|
|
||||||
|
class TestProgressoScroll:
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_progress_callback(self, es_client):
|
||||||
|
progress_history = []
|
||||||
|
|
||||||
|
async def callback(docs, progress):
|
||||||
|
progress_history.append(progress.copy())
|
||||||
|
|
||||||
|
scroll_1 = {
|
||||||
|
"_scroll_id": "s1",
|
||||||
|
"hits": {"total": {"value": 100}, "hits": [{"_source": {"id": i}} for i in range(50)]}
|
||||||
|
}
|
||||||
|
scroll_2 = {
|
||||||
|
"_scroll_id": "s2",
|
||||||
|
"hits": {"hits": [{"_source": {"id": i}} for i in range(50, 100)]}
|
||||||
|
}
|
||||||
|
scroll_empty = {"_scroll_id": "s3", "hits": {"hits": []}}
|
||||||
|
|
||||||
|
with patch.object(es_client, '_client') as mock_client:
|
||||||
|
mock_client.is_closed = False
|
||||||
|
mock_client.post = AsyncMock(side_effect=[
|
||||||
|
create_mock_response(scroll_1),
|
||||||
|
create_mock_response(scroll_2),
|
||||||
|
create_mock_response(scroll_empty)
|
||||||
|
])
|
||||||
|
mock_client.delete = AsyncMock(return_value=create_mock_response({}))
|
||||||
|
|
||||||
|
await es_client.buscar_todos_consultores(callback, batch_size=50)
|
||||||
|
|
||||||
|
assert len(progress_history) == 2
|
||||||
|
assert progress_history[0]["percentual"] == 50
|
||||||
|
assert progress_history[0]["processados"] == 50
|
||||||
|
assert progress_history[1]["percentual"] == 100
|
||||||
|
assert progress_history[1]["processados"] == 100
|
||||||
0
backend/tests/interface/__init__.py
Normal file
0
backend/tests/interface/__init__.py
Normal file
0
backend/tests/interface/api/__init__.py
Normal file
0
backend/tests/interface/api/__init__.py
Normal file
301
backend/tests/interface/api/test_routes.py
Normal file
301
backend/tests/interface/api/test_routes.py
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from src.interface.api.routes import router
|
||||||
|
from src.interface.api.dependencies import (
|
||||||
|
get_repository,
|
||||||
|
get_ranking_store,
|
||||||
|
get_processar_job,
|
||||||
|
get_es_client,
|
||||||
|
get_ranking_oracle_repo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_repository():
|
||||||
|
repo = AsyncMock()
|
||||||
|
repo.contar_total.return_value = 100
|
||||||
|
return repo
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_ranking_store():
|
||||||
|
store = MagicMock()
|
||||||
|
store.is_ready.return_value = False
|
||||||
|
return store
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_oracle_repo():
|
||||||
|
repo = MagicMock()
|
||||||
|
repo.contar_total.return_value = 1000
|
||||||
|
return repo
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_es_client():
|
||||||
|
return AsyncMock()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_job():
|
||||||
|
return AsyncMock()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app(mock_repository, mock_ranking_store, mock_oracle_repo, mock_es_client, mock_job):
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(router)
|
||||||
|
|
||||||
|
app.dependency_overrides[get_repository] = lambda: mock_repository
|
||||||
|
app.dependency_overrides[get_ranking_store] = lambda: mock_ranking_store
|
||||||
|
app.dependency_overrides[get_ranking_oracle_repo] = lambda: mock_oracle_repo
|
||||||
|
app.dependency_overrides[get_es_client] = lambda: mock_es_client
|
||||||
|
app.dependency_overrides[get_processar_job] = lambda: mock_job
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client(app):
|
||||||
|
return TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
class TestHealthCheck:
|
||||||
|
|
||||||
|
def test_health_check_ok(self, client):
|
||||||
|
response = client.get("/api/v1/health")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["status"] == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
class TestRankingStatus:
|
||||||
|
|
||||||
|
def test_status_inicial(self, client):
|
||||||
|
with patch("src.interface.api.routes.job_status") as mock_status:
|
||||||
|
mock_status.to_dict.return_value = {
|
||||||
|
"running": False,
|
||||||
|
"progress": 0,
|
||||||
|
"processados": 0,
|
||||||
|
"total": 0,
|
||||||
|
"mensagem": "",
|
||||||
|
"batch_atual": 0,
|
||||||
|
"total_batches": 0,
|
||||||
|
"tempo_decorrido": None,
|
||||||
|
"tempo_estimado": None,
|
||||||
|
"inicio": None,
|
||||||
|
"fim": None,
|
||||||
|
"erro": None,
|
||||||
|
}
|
||||||
|
response = client.get("/api/v1/ranking/status")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["running"] is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestRankingPaginado:
|
||||||
|
|
||||||
|
def test_ranking_paginado_sem_oracle(self, client, mock_oracle_repo):
|
||||||
|
mock_oracle_repo.contar_total.return_value = 0
|
||||||
|
response = client.get("/api/v1/ranking/paginado")
|
||||||
|
assert response.status_code == 503
|
||||||
|
|
||||||
|
def test_ranking_paginado_com_dados(self, client, mock_oracle_repo):
|
||||||
|
mock_oracle_repo.contar_total.return_value = 100
|
||||||
|
mock_oracle_repo.buscar_paginado.return_value = [
|
||||||
|
MagicMock(
|
||||||
|
id_pessoa=1,
|
||||||
|
nome="Consultor 1",
|
||||||
|
posicao=1,
|
||||||
|
pontuacao_total=500,
|
||||||
|
componente_a=200,
|
||||||
|
componente_b=100,
|
||||||
|
componente_c=100,
|
||||||
|
componente_d=50,
|
||||||
|
componente_e=50,
|
||||||
|
ativo=True,
|
||||||
|
anos_atuacao=5.0,
|
||||||
|
json_detalhes="{}"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
response = client.get("/api/v1/ranking/paginado?page=1&size=10")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["total"] == 100
|
||||||
|
assert len(data["consultores"]) == 1
|
||||||
|
|
||||||
|
def test_ranking_paginado_filtro_ativo(self, client, mock_oracle_repo):
|
||||||
|
mock_oracle_repo.contar_total.return_value = 50
|
||||||
|
mock_oracle_repo.buscar_paginado.return_value = []
|
||||||
|
response = client.get("/api/v1/ranking/paginado?ativo=true")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
def test_ranking_paginado_filtro_selos(self, client, mock_oracle_repo):
|
||||||
|
mock_oracle_repo.contar_total.return_value = 0
|
||||||
|
response = client.get("/api/v1/ranking/paginado?selos=COORD_PPG,BPQ")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["total"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuscaPorNome:
|
||||||
|
|
||||||
|
def test_busca_por_nome_encontrado(self, client, mock_oracle_repo):
|
||||||
|
mock_oracle_repo.buscar_por_nome.return_value = [
|
||||||
|
{"ID_PESSOA": 1, "NOME": "João Silva", "POSICAO": 10, "PONTUACAO_TOTAL": 500}
|
||||||
|
]
|
||||||
|
response = client.get("/api/v1/ranking/busca?nome=João")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert len(data) == 1
|
||||||
|
assert data[0]["nome"] == "João Silva"
|
||||||
|
|
||||||
|
def test_busca_por_nome_nao_encontrado(self, client, mock_oracle_repo):
|
||||||
|
mock_oracle_repo.buscar_por_nome.return_value = []
|
||||||
|
response = client.get("/api/v1/ranking/busca?nome=XYZ")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert len(data) == 0
|
||||||
|
|
||||||
|
def test_busca_por_nome_minimo_3_chars(self, client):
|
||||||
|
response = client.get("/api/v1/ranking/busca?nome=AB")
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
class TestRankingEstatisticas:
|
||||||
|
|
||||||
|
def test_estatisticas_sem_dados(self, client, mock_oracle_repo):
|
||||||
|
mock_oracle_repo.contar_total.return_value = 0
|
||||||
|
response = client.get("/api/v1/ranking/estatisticas")
|
||||||
|
assert response.status_code == 503
|
||||||
|
|
||||||
|
def test_estatisticas_com_dados(self, client, mock_oracle_repo):
|
||||||
|
mock_oracle_repo.contar_total.return_value = 1000
|
||||||
|
mock_oracle_repo.obter_estatisticas.return_value = {
|
||||||
|
"total_consultores": 1000,
|
||||||
|
"total_ativos": 800,
|
||||||
|
"total_inativos": 200,
|
||||||
|
"ultima_atualizacao": "2024-01-01",
|
||||||
|
"pontuacao_media": 150.5,
|
||||||
|
"pontuacao_maxima": 850,
|
||||||
|
"pontuacao_minima": 10,
|
||||||
|
"media_componentes": {"a": 50, "b": 30, "c": 40, "d": 20, "e": 10}
|
||||||
|
}
|
||||||
|
mock_oracle_repo.obter_distribuicao.return_value = [
|
||||||
|
{"faixa": "0-100", "quantidade": 500},
|
||||||
|
{"faixa": "100-200", "quantidade": 300},
|
||||||
|
]
|
||||||
|
response = client.get("/api/v1/ranking/estatisticas")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["total_consultores"] == 1000
|
||||||
|
assert data["total_ativos"] == 800
|
||||||
|
|
||||||
|
|
||||||
|
class TestPosicaoRanking:
|
||||||
|
|
||||||
|
def test_posicao_encontrada(self, client, mock_oracle_repo):
|
||||||
|
mock_oracle_repo.contar_total.return_value = 1000
|
||||||
|
mock_oracle_repo.buscar_por_id.return_value = MagicMock(
|
||||||
|
id_pessoa=123,
|
||||||
|
nome="João Silva",
|
||||||
|
posicao=42,
|
||||||
|
pontuacao_total=500,
|
||||||
|
componente_a=200,
|
||||||
|
componente_b=100,
|
||||||
|
componente_c=100,
|
||||||
|
componente_d=50,
|
||||||
|
componente_e=50,
|
||||||
|
ativo=True,
|
||||||
|
)
|
||||||
|
response = client.get("/api/v1/ranking/posicao/123")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["id_pessoa"] == 123
|
||||||
|
assert data["posicao"] == 42
|
||||||
|
assert data["encontrado"] is True
|
||||||
|
|
||||||
|
def test_posicao_nao_encontrada(self, client, mock_oracle_repo):
|
||||||
|
mock_oracle_repo.contar_total.return_value = 1000
|
||||||
|
mock_oracle_repo.buscar_por_id.return_value = None
|
||||||
|
response = client.get("/api/v1/ranking/posicao/99999")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["encontrado"] is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestProcessarRanking:
|
||||||
|
|
||||||
|
def test_processar_ranking_inicia(self, client, mock_job):
|
||||||
|
with patch("src.interface.api.routes.job_status") as mock_status:
|
||||||
|
mock_status.is_running = False
|
||||||
|
response = client.post("/api/v1/ranking/processar")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["sucesso"] is True
|
||||||
|
|
||||||
|
def test_processar_ranking_ja_executando(self, client):
|
||||||
|
with patch("src.interface.api.routes.job_status") as mock_status:
|
||||||
|
mock_status.is_running = True
|
||||||
|
response = client.post("/api/v1/ranking/processar")
|
||||||
|
assert response.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
class TestListarSelos:
|
||||||
|
|
||||||
|
def test_listar_selos(self, client):
|
||||||
|
response = client.get("/api/v1/ranking/selos")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "selos" in data
|
||||||
|
assert isinstance(data["selos"], list)
|
||||||
|
|
||||||
|
|
||||||
|
class TestExportarInfo:
|
||||||
|
|
||||||
|
def test_info_exportacao(self, client, mock_oracle_repo):
|
||||||
|
mock_oracle_repo.contar_para_exportacao.return_value = 500
|
||||||
|
response = client.get("/api/v1/ranking/exportar/info")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["total_consultores"] == 500
|
||||||
|
assert "estimativa_tamanho_mb" in data
|
||||||
|
|
||||||
|
|
||||||
|
class TestCorrigirEncoding:
|
||||||
|
|
||||||
|
def test_corrigir_encoding_sem_problemas(self):
|
||||||
|
from src.interface.api.routes import corrigir_encoding
|
||||||
|
result = corrigir_encoding("Texto normal")
|
||||||
|
assert result == "Texto normal"
|
||||||
|
|
||||||
|
def test_corrigir_encoding_none(self):
|
||||||
|
from src.interface.api.routes import corrigir_encoding
|
||||||
|
result = corrigir_encoding(None)
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_corrigir_encoding_vazio(self):
|
||||||
|
from src.interface.api.routes import corrigir_encoding
|
||||||
|
result = corrigir_encoding("")
|
||||||
|
assert result == ""
|
||||||
|
|
||||||
|
|
||||||
|
class TestNormalizarTexto:
|
||||||
|
|
||||||
|
def test_normalizar_texto_acentos(self):
|
||||||
|
from src.interface.api.routes import normalizar_texto
|
||||||
|
result = normalizar_texto("Ciências Ambientais")
|
||||||
|
assert result == "ciencias ambientais"
|
||||||
|
|
||||||
|
def test_normalizar_texto_html_entities(self):
|
||||||
|
from src.interface.api.routes import normalizar_texto
|
||||||
|
result = normalizar_texto("Ciências")
|
||||||
|
assert "ciencias" in result.lower()
|
||||||
|
|
||||||
|
def test_normalizar_texto_vazio(self):
|
||||||
|
from src.interface.api.routes import normalizar_texto
|
||||||
|
result = normalizar_texto("")
|
||||||
|
assert result == ""
|
||||||
@@ -6,7 +6,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
@@ -17,6 +18,10 @@
|
|||||||
"@types/react": "^18.2.0",
|
"@types/react": "^18.2.0",
|
||||||
"@types/react-dom": "^18.2.0",
|
"@types/react-dom": "^18.2.0",
|
||||||
"@vitejs/plugin-react": "^4.2.0",
|
"@vitejs/plugin-react": "^4.2.0",
|
||||||
"vite": "^5.0.0"
|
"@testing-library/jest-dom": "^6.2.0",
|
||||||
|
"@testing-library/react": "^14.1.2",
|
||||||
|
"jsdom": "^24.0.0",
|
||||||
|
"vite": "^5.0.0",
|
||||||
|
"vitest": "^1.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,10 +145,39 @@
|
|||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-exportar {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(16, 185, 129, 0.15));
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.4);
|
||||||
|
color: #86efac;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-exportar:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, rgba(34, 197, 94, 0.3), rgba(16, 185, 129, 0.2));
|
||||||
|
border-color: rgba(34, 197, 94, 0.6);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-exportar:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination button {
|
.pagination button {
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import Header from './components/Header';
|
|||||||
import ConsultorCard from './components/ConsultorCard';
|
import ConsultorCard from './components/ConsultorCard';
|
||||||
import CompararModal from './components/CompararModal';
|
import CompararModal from './components/CompararModal';
|
||||||
import FiltroSelos from './components/FiltroSelos';
|
import FiltroSelos from './components/FiltroSelos';
|
||||||
|
import FiltroAtivo from './components/FiltroAtivo';
|
||||||
import SugerirConsultores from './components/SugerirConsultores';
|
import SugerirConsultores from './components/SugerirConsultores';
|
||||||
|
import ExportProgress from './components/ExportProgress';
|
||||||
import { rankingService } from './services/api';
|
import { rankingService } from './services/api';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
@@ -24,7 +26,13 @@ function App() {
|
|||||||
const [selecionados, setSelecionados] = useState([]);
|
const [selecionados, setSelecionados] = useState([]);
|
||||||
const [modalAberto, setModalAberto] = useState(false);
|
const [modalAberto, setModalAberto] = useState(false);
|
||||||
const [filtroSelos, setFiltroSelos] = useState([]);
|
const [filtroSelos, setFiltroSelos] = useState([]);
|
||||||
|
const [filtroAtivo, setFiltroAtivo] = useState(null);
|
||||||
const [sugerirAberto, setSugerirAberto] = useState(false);
|
const [sugerirAberto, setSugerirAberto] = useState(false);
|
||||||
|
const [exportando, setExportando] = useState(false);
|
||||||
|
const [exportProgress, setExportProgress] = useState({ loaded: 0, total: 0, percent: 0 });
|
||||||
|
const [exportStatus, setExportStatus] = useState('preparing');
|
||||||
|
const abortControllerRef = useRef(null);
|
||||||
|
const loadingRef = useRef(false);
|
||||||
|
|
||||||
const toggleSelecionado = (consultor) => {
|
const toggleSelecionado = (consultor) => {
|
||||||
setSelecionados((prev) => {
|
setSelecionados((prev) => {
|
||||||
@@ -60,9 +68,54 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExportarExcel = async () => {
|
||||||
|
if (exportando) return;
|
||||||
|
try {
|
||||||
|
setExportando(true);
|
||||||
|
setExportStatus('preparing');
|
||||||
|
setExportProgress({ loaded: 0, total: 0, percent: 0 });
|
||||||
|
|
||||||
|
abortControllerRef.current = new AbortController();
|
||||||
|
|
||||||
|
await rankingService.downloadRankingExcel(
|
||||||
|
filtroSelos,
|
||||||
|
filtroAtivo,
|
||||||
|
(progress) => {
|
||||||
|
setExportStatus('downloading');
|
||||||
|
setExportProgress(progress);
|
||||||
|
},
|
||||||
|
abortControllerRef.current
|
||||||
|
);
|
||||||
|
|
||||||
|
setExportStatus('complete');
|
||||||
|
setTimeout(() => setExportando(false), 1000);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name === 'CanceledError' || err.code === 'ERR_CANCELED') {
|
||||||
|
console.log('Exportação cancelada pelo usuário');
|
||||||
|
} else {
|
||||||
|
console.error('Erro ao exportar Excel:', err);
|
||||||
|
setExportStatus('error');
|
||||||
|
setTimeout(() => setExportando(false), 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelExport = () => {
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
abortControllerRef.current = null;
|
||||||
|
}
|
||||||
|
setExportando(false);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const requestKey = `${page}-${pageSize}-${filtroSelos.join(',')}-${filtroAtivo}`;
|
||||||
|
if (loadingRef.current === requestKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadingRef.current = requestKey;
|
||||||
loadRanking();
|
loadRanking();
|
||||||
}, [page, pageSize, filtroSelos]);
|
}, [page, pageSize, filtroSelos, filtroAtivo]);
|
||||||
|
|
||||||
const loadRanking = async (retryCount = 0) => {
|
const loadRanking = async (retryCount = 0) => {
|
||||||
const MAX_RETRIES = 10;
|
const MAX_RETRIES = 10;
|
||||||
@@ -72,7 +125,7 @@ function App() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setProcessMessage('');
|
setProcessMessage('');
|
||||||
const response = await rankingService.getRanking(page, pageSize, filtroSelos);
|
const response = await rankingService.getRanking(page, pageSize, filtroSelos, filtroAtivo);
|
||||||
setConsultores(response.consultores);
|
setConsultores(response.consultores);
|
||||||
setTotal(response.total);
|
setTotal(response.total);
|
||||||
setTotalPages(response.total_pages || 0);
|
setTotalPages(response.total_pages || 0);
|
||||||
@@ -125,7 +178,7 @@ function App() {
|
|||||||
throw new Error('Timeout: processamento demorou mais que 45 minutos');
|
throw new Error('Timeout: processamento demorou mais que 45 minutos');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await rankingService.getRanking(page, pageSize, filtroSelos);
|
const response = await rankingService.getRanking(page, pageSize, filtroSelos, filtroAtivo);
|
||||||
setConsultores(response.consultores);
|
setConsultores(response.consultores);
|
||||||
setTotal(response.total);
|
setTotal(response.total);
|
||||||
setTotalPages(response.total_pages || 0);
|
setTotalPages(response.total_pages || 0);
|
||||||
@@ -216,10 +269,32 @@ function App() {
|
|||||||
onChange={(selos) => { setFiltroSelos(selos); setPage(1); }}
|
onChange={(selos) => { setFiltroSelos(selos); setPage(1); }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FiltroAtivo
|
||||||
|
valor={filtroAtivo}
|
||||||
|
onChange={(valor) => { setFiltroAtivo(valor); setPage(1); }}
|
||||||
|
/>
|
||||||
|
|
||||||
<button className="btn-sugerir" onClick={() => setSugerirAberto(true)}>
|
<button className="btn-sugerir" onClick={() => setSugerirAberto(true)}>
|
||||||
Sugerir por Tema
|
Sugerir por Tema
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn-exportar"
|
||||||
|
onClick={handleExportarExcel}
|
||||||
|
disabled={exportando || loading}
|
||||||
|
title="Exportar ranking para Excel (apenas consultores com pontuação)"
|
||||||
|
>
|
||||||
|
{exportando ? 'Exportando...' : '📊 Exportar Excel'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="pagination">
|
||||||
|
<button onClick={() => setPage(1)} disabled={page <= 1}>«</button>
|
||||||
|
<button onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page <= 1}>‹</button>
|
||||||
|
<span className="page-info">{page} / {totalPages || '?'}</span>
|
||||||
|
<button onClick={() => setPage((p) => (totalPages ? Math.min(totalPages, p + 1) : p + 1))} disabled={totalPages && page >= totalPages}>›</button>
|
||||||
|
<button onClick={() => totalPages && setPage(totalPages)} disabled={totalPages && page >= totalPages}>»</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form className="search-box" onSubmit={handleSubmitBuscar}>
|
<form className="search-box" onSubmit={handleSubmitBuscar}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -231,14 +306,6 @@ function App() {
|
|||||||
{buscando ? '...' : 'Buscar'}
|
{buscando ? '...' : 'Buscar'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="pagination">
|
|
||||||
<button onClick={() => setPage(1)} disabled={page <= 1}>«</button>
|
|
||||||
<button onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page <= 1}>‹</button>
|
|
||||||
<span className="page-info">{page} / {totalPages || '?'}</span>
|
|
||||||
<button onClick={() => setPage((p) => (totalPages ? Math.min(totalPages, p + 1) : p + 1))} disabled={totalPages && page >= totalPages}>›</button>
|
|
||||||
<button onClick={() => totalPages && setPage(totalPages)} disabled={totalPages && page >= totalPages}>»</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ranking-list">
|
<div className="ranking-list">
|
||||||
@@ -290,6 +357,14 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{exportando && (
|
||||||
|
<ExportProgress
|
||||||
|
progress={exportProgress}
|
||||||
|
status={exportStatus}
|
||||||
|
onCancel={handleCancelExport}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>Dados: ATUACAPES (Elasticsearch) + Oracle</p>
|
<p>Dados: ATUACAPES (Elasticsearch) + Oracle</p>
|
||||||
<p>Clique em qualquer consultor para ver detalhes</p>
|
<p>Clique em qualquer consultor para ver detalhes</p>
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ const SelosBadges = ({ selos, compacto = false, onSeloClick }) => {
|
|||||||
<span
|
<span
|
||||||
key={idx}
|
key={idx}
|
||||||
className={`selo ${selo.cor} ${onSeloClick && temDados ? 'selo-clicavel' : ''}`}
|
className={`selo ${selo.cor} ${onSeloClick && temDados ? 'selo-clicavel' : ''}`}
|
||||||
title={temDados ? `Clique para ver detalhes` : (selo.hint || `${selo.label}${selo.qtd > 1 ? ` (${selo.qtd}x)` : ''}`)}
|
title={selo.hint || `${selo.label}${selo.qtd > 1 ? ` (${selo.qtd}x)` : ''}`}
|
||||||
onMouseDown={(e) => onSeloClick && temDados && e.stopPropagation()}
|
onMouseDown={(e) => onSeloClick && temDados && e.stopPropagation()}
|
||||||
onClick={(e) => handleClick(e, selo)}
|
onClick={(e) => handleClick(e, selo)}
|
||||||
>
|
>
|
||||||
|
|||||||
196
frontend/src/components/ExportProgress.css
Normal file
196
frontend/src/components/ExportProgress.css
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
.export-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1100;
|
||||||
|
animation: fadeIn 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-modal {
|
||||||
|
background: linear-gradient(155deg, rgba(15, 23, 42, 0.98), rgba(30, 41, 59, 0.95));
|
||||||
|
border: 1px solid var(--stroke);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2rem 2.5rem;
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 400px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.6), 0 0 40px rgba(34, 197, 94, 0.1);
|
||||||
|
animation: slideUp 300ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(16, 185, 129, 0.15));
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-icon.generating {
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
background: linear-gradient(120deg, #86efac, #22c55e);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-status {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #22c55e, #16a34a);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 150ms ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.3),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
100% { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill.complete {
|
||||||
|
background: linear-gradient(90deg, #22c55e, #16a34a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill.complete::after {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill.error {
|
||||||
|
background: linear-gradient(90deg, #ef4444, #dc2626);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill.indeterminate {
|
||||||
|
width: 30%;
|
||||||
|
animation: indeterminate 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes indeterminate {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(230%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-generating {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--muted);
|
||||||
|
animation: blink 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 100% { opacity: 0.5; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-percent {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #86efac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bytes {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-cancel {
|
||||||
|
padding: 0.6rem 1.5rem;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border: 1px solid var(--stroke);
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-cancel:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
border-color: rgba(239, 68, 68, 0.4);
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
82
frontend/src/components/ExportProgress.jsx
Normal file
82
frontend/src/components/ExportProgress.jsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import './ExportProgress.css';
|
||||||
|
|
||||||
|
function ExportProgress({ progress, status, onCancel }) {
|
||||||
|
const formatBytes = (bytes) => {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
const isGenerating = status === 'preparing' || (status === 'downloading' && progress.total === 0);
|
||||||
|
const isDownloading = status === 'downloading' && progress.total > 0;
|
||||||
|
|
||||||
|
const getStatusText = () => {
|
||||||
|
if (isGenerating) {
|
||||||
|
return 'Gerando arquivo no servidor...';
|
||||||
|
}
|
||||||
|
switch (status) {
|
||||||
|
case 'downloading':
|
||||||
|
return 'Baixando arquivo...';
|
||||||
|
case 'complete':
|
||||||
|
return 'Concluído!';
|
||||||
|
case 'error':
|
||||||
|
return 'Erro na exportação';
|
||||||
|
default:
|
||||||
|
return 'Exportando...';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const percent = Math.min(Math.round(progress.percent || 0), 100);
|
||||||
|
const downloaded = progress.loaded || 0;
|
||||||
|
const total = progress.total || 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="export-overlay">
|
||||||
|
<div className="export-modal">
|
||||||
|
<div className={`export-icon ${isGenerating ? 'generating' : ''}`}>
|
||||||
|
{status === 'complete' ? '✓' : status === 'error' ? '✕' : '📊'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="export-title">Exportação Excel</h3>
|
||||||
|
<p className="export-status">{getStatusText()}</p>
|
||||||
|
|
||||||
|
<div className="progress-container">
|
||||||
|
<div className="progress-bar">
|
||||||
|
{isGenerating ? (
|
||||||
|
<div className="progress-fill indeterminate" />
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={`progress-fill ${status === 'complete' ? 'complete' : ''} ${status === 'error' ? 'error' : ''}`}
|
||||||
|
style={{ width: `${percent}%` }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="progress-info">
|
||||||
|
{isGenerating ? (
|
||||||
|
<span className="progress-generating">Processando ~300k registros...</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="progress-percent">{percent}%</span>
|
||||||
|
{total > 0 && (
|
||||||
|
<span className="progress-bytes">
|
||||||
|
{formatBytes(downloaded)} / {formatBytes(total)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status !== 'complete' && status !== 'error' && (
|
||||||
|
<button className="export-cancel" onClick={onCancel}>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExportProgress;
|
||||||
153
frontend/src/components/FiltroAtivo.css
Normal file
153
frontend/src/components/FiltroAtivo.css
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
.filtro-ativo {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtro-ativo-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0.9rem;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid var(--stroke);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtro-ativo-trigger:hover {
|
||||||
|
border-color: var(--accent-2);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtro-ativo-trigger.ativo {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: rgba(79, 70, 229, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtro-ativo .filtro-icone {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtro-ativo .filtro-label {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtro-ativo .filtro-seta {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--muted);
|
||||||
|
transition: transform 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtro-ativo .filtro-seta.aberto {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtro-ativo .filtro-limpar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
background: rgba(255, 59, 48, 0.2);
|
||||||
|
color: #ff6b6b;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtro-ativo .filtro-limpar:hover {
|
||||||
|
background: rgba(255, 59, 48, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtro-ativo-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
left: 0;
|
||||||
|
min-width: 180px;
|
||||||
|
background: linear-gradient(165deg, rgba(15, 23, 42, 0.98), rgba(30, 41, 59, 0.98));
|
||||||
|
border: 1px solid var(--stroke);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.4), 0 0 20px rgba(79, 70, 229, 0.1);
|
||||||
|
z-index: 100;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
animation: dropdownSlide 200ms ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dropdownSlide {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtro-ativo-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-bottom: 1px solid var(--stroke);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtro-ativo-opcoes {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtro-opcao-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtro-opcao-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: var(--stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtro-opcao-item.selecionado {
|
||||||
|
background: rgba(79, 70, 229, 0.2);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtro-opcao-item .opcao-icone {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtro-opcao-item .opcao-label {
|
||||||
|
color: var(--text);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtro-opcao-item .opcao-check {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.filtro-ativo-dropdown {
|
||||||
|
position: fixed;
|
||||||
|
top: auto;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
min-width: 100%;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
78
frontend/src/components/FiltroAtivo.jsx
Normal file
78
frontend/src/components/FiltroAtivo.jsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import './FiltroAtivo.css';
|
||||||
|
|
||||||
|
const OPCOES = [
|
||||||
|
{ value: null, label: 'Todos', icone: '👥' },
|
||||||
|
{ value: true, label: 'Ativos', icone: '✅' },
|
||||||
|
{ value: false, label: 'Inativos', icone: '📋' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function FiltroAtivo({ valor, onChange }) {
|
||||||
|
const [aberto, setAberto] = useState(false);
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e) => {
|
||||||
|
if (ref.current && !ref.current.contains(e.target)) {
|
||||||
|
setAberto(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const selecionarOpcao = (novoValor) => {
|
||||||
|
onChange(novoValor);
|
||||||
|
setAberto(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const limparFiltro = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onChange(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const opcaoAtual = OPCOES.find((o) => o.value === valor) || OPCOES[0];
|
||||||
|
const filtroAtivo = valor !== null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="filtro-ativo" ref={ref}>
|
||||||
|
<button
|
||||||
|
className={`filtro-ativo-trigger ${filtroAtivo ? 'ativo' : ''}`}
|
||||||
|
onClick={() => setAberto(!aberto)}
|
||||||
|
>
|
||||||
|
<span className="filtro-icone">{opcaoAtual.icone}</span>
|
||||||
|
<span className="filtro-label">{opcaoAtual.label}</span>
|
||||||
|
<span className={`filtro-seta ${aberto ? 'aberto' : ''}`}>▼</span>
|
||||||
|
{filtroAtivo && (
|
||||||
|
<span className="filtro-limpar" onClick={limparFiltro} title="Mostrar todos">
|
||||||
|
✕
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{aberto && (
|
||||||
|
<div className="filtro-ativo-dropdown">
|
||||||
|
<div className="filtro-ativo-header">
|
||||||
|
<span>Filtrar por status</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="filtro-ativo-opcoes">
|
||||||
|
{OPCOES.map((opcao) => (
|
||||||
|
<label
|
||||||
|
key={String(opcao.value)}
|
||||||
|
className={`filtro-opcao-item ${valor === opcao.value ? 'selecionado' : ''}`}
|
||||||
|
onClick={() => selecionarOpcao(opcao.value)}
|
||||||
|
>
|
||||||
|
<span className="opcao-icone">{opcao.icone}</span>
|
||||||
|
<span className="opcao-label">{opcao.label}</span>
|
||||||
|
{valor === opcao.value && <span className="opcao-check">✓</span>}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FiltroAtivo;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { rankingService } from '../services/api';
|
import { rankingService } from '../services/api';
|
||||||
import './RawDataModal.css';
|
import './RawDataModal.css';
|
||||||
@@ -473,8 +473,13 @@ const RawDataModal = ({ idPessoa, nome, onClose }) => {
|
|||||||
const [filterType, setFilterType] = useState('all');
|
const [filterType, setFilterType] = useState('all');
|
||||||
const [copyFeedback, setCopyFeedback] = useState(false);
|
const [copyFeedback, setCopyFeedback] = useState(false);
|
||||||
const [downloadingPDF, setDownloadingPDF] = useState(false);
|
const [downloadingPDF, setDownloadingPDF] = useState(false);
|
||||||
|
const fetchedRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (fetchedRef.current === idPessoa) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchedRef.current = idPessoa;
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
27
frontend/src/components/__tests__/FiltroSelos.test.jsx
Normal file
27
frontend/src/components/__tests__/FiltroSelos.test.jsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import FiltroSelos from '../FiltroSelos';
|
||||||
|
|
||||||
|
describe('FiltroSelos', () => {
|
||||||
|
it('applies selected seals', () => {
|
||||||
|
const handleChange = vi.fn();
|
||||||
|
render(<FiltroSelos selecionados={[]} onChange={handleChange} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /Filtrar por selos/i }));
|
||||||
|
|
||||||
|
const checkbox = screen.getByLabelText(/Coord\./i);
|
||||||
|
fireEvent.click(checkbox);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /Aplicar/i }));
|
||||||
|
|
||||||
|
expect(handleChange).toHaveBeenCalledWith(['CA']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears filters from the trigger when active', () => {
|
||||||
|
const handleChange = vi.fn();
|
||||||
|
render(<FiltroSelos selecionados={['CA']} onChange={handleChange} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTitle('Limpar filtros'));
|
||||||
|
|
||||||
|
expect(handleChange).toHaveBeenCalledWith([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
19
frontend/src/components/__tests__/Header.test.jsx
Normal file
19
frontend/src/components/__tests__/Header.test.jsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import Header from '../Header';
|
||||||
|
|
||||||
|
describe('Header', () => {
|
||||||
|
it('renders the title and total count', () => {
|
||||||
|
render(<Header total={1000} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Ranking de Consultores CAPES')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Total:\s+1\.000 consultores/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens the criteria modal when a block is clicked', () => {
|
||||||
|
render(<Header total={0} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('A - Coordenacao CAPES'));
|
||||||
|
|
||||||
|
expect(screen.getByText(/Coordena/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,10 +9,18 @@ const api = axios.create({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const rankingService = {
|
export const rankingService = {
|
||||||
async getRanking(page = 1, size = 100, selos = []) {
|
async getRanking(page = 1, size = 100, selos = [], ativo = null) {
|
||||||
const params = { page, size };
|
const params = { page, size };
|
||||||
if (selos && selos.length > 0) {
|
if (selos && selos.length > 0) {
|
||||||
params.selos = selos.join(',');
|
const normalizados = selos
|
||||||
|
.map((s) => String(s || '').trim().toUpperCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (normalizados.length > 0) {
|
||||||
|
params.selos = normalizados.join(',');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ativo !== null) {
|
||||||
|
params.ativo = ativo;
|
||||||
}
|
}
|
||||||
const response = await api.get('/ranking/paginado', { params });
|
const response = await api.get('/ranking/paginado', { params });
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
@@ -181,6 +189,79 @@ export const rankingService = {
|
|||||||
const response = await api.get(`/consultor/${idPessoa}/lattes`);
|
const response = await api.get(`/consultor/${idPessoa}/lattes`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getExportInfo(selos = [], ativo = null) {
|
||||||
|
const params = {};
|
||||||
|
if (selos && selos.length > 0) {
|
||||||
|
const normalizados = selos
|
||||||
|
.map((s) => String(s || '').trim().toUpperCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (normalizados.length > 0) {
|
||||||
|
params.selos = normalizados.join(',');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ativo !== null) {
|
||||||
|
params.ativo = ativo;
|
||||||
|
}
|
||||||
|
const response = await api.get('/ranking/exportar/info', { params });
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async downloadRankingExcel(selos = [], ativo = null, onProgress = null, abortController = null) {
|
||||||
|
const params = {};
|
||||||
|
if (selos && selos.length > 0) {
|
||||||
|
const normalizados = selos
|
||||||
|
.map((s) => String(s || '').trim().toUpperCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (normalizados.length > 0) {
|
||||||
|
params.selos = normalizados.join(',');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ativo !== null) {
|
||||||
|
params.ativo = ativo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
params,
|
||||||
|
responseType: 'blob',
|
||||||
|
timeout: 300000,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (onProgress) {
|
||||||
|
config.onDownloadProgress = (progressEvent) => {
|
||||||
|
const { loaded, total } = progressEvent;
|
||||||
|
const percent = total ? Math.round((loaded * 100) / total) : 0;
|
||||||
|
onProgress({ loaded, total, percent });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abortController) {
|
||||||
|
config.signal = abortController.signal;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.get('/ranking/exportar/excel', config);
|
||||||
|
|
||||||
|
const contentDisposition = response.headers['content-disposition'];
|
||||||
|
let nomeArquivo = 'ranking_consultores_capes.xlsx';
|
||||||
|
if (contentDisposition) {
|
||||||
|
const match = contentDisposition.match(/filename="(.+)"/);
|
||||||
|
if (match) {
|
||||||
|
nomeArquivo = match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([response.data], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
});
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = nomeArquivo;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
1
frontend/src/setupTests.js
Normal file
1
frontend/src/setupTests.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
@@ -3,6 +3,11 @@ import react from '@vitejs/plugin-react';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: './src/setupTests.js',
|
||||||
|
globals: true,
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 5173,
|
port: 5173,
|
||||||
|
|||||||
Reference in New Issue
Block a user