Reimplementa sistema de ranking com novos critérios V2
Mudanças principais: - Substitui 4 Componentes (A,B,C,D) por 3 Blocos (A,C,D) - Remove Componente B (Coordenação PPG = 0 pts no V1) - Adiciona novos tipos de atuação do Elasticsearch - Implementa critérios de pontuação com tetos individuais Bloco A - Coordenação CAPES: - CA (max 450), CAJ (max 370), CAJ_MP (max 315), CAM (max 280) - Calcula base + tempo + bônus atualidade + bônus retorno Bloco C - Consultoria: - CONS_ATIVO (base 150), CONS_HIST (base 100), CONS_FALECIDO (base 100) - Bônus continuidade: 3anos=+5, 5anos=+10, 8anos=+15 - Bônus retorno: +15 Bloco D - Premiações/Avaliações: - Inscrições (INSC_AUTOR, INSC_INST) - Avaliações (AVAL_COMIS_PREMIO, AVAL_COMIS_GP) - Coordenações (COORD_COMIS_PREMIO, COORD_COMIS_GP) - Premiações (PREMIACAO, PREMIACAO_GP, MENCAO) - Bolsas CNPQ, Participações, Orientações, Membros de Banca Frontend: - Header, ConsultorCard, CompararModal atualizados para 3 blocos - API service atualizado para nova estrutura de dados
This commit is contained in:
@@ -1,44 +1,44 @@
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
from typing import List, Dict
|
||||
from collections import defaultdict
|
||||
|
||||
from ..entities.consultor import (
|
||||
Consultor,
|
||||
CoordenacaoCapes,
|
||||
CoordenacaoPrograma,
|
||||
Consultoria,
|
||||
Inscricao,
|
||||
AvaliacaoComissao,
|
||||
Premiacao,
|
||||
BolsaCNPQ,
|
||||
Participacao,
|
||||
Orientacao,
|
||||
MembroBanca,
|
||||
)
|
||||
from ..value_objects.pontuacao import ComponentePontuacao, PontuacaoCompleta
|
||||
from ..value_objects.pontuacao import PontuacaoAtuacao, PontuacaoBloco, PontuacaoCompleta
|
||||
from ..value_objects.criterios_pontuacao import CRITERIOS, get_criterio, Bloco
|
||||
from ..value_objects.periodo import Periodo
|
||||
|
||||
|
||||
class CalculadorPontuacao:
|
||||
|
||||
@staticmethod
|
||||
def _mesclar_periodos(periodos: List[Periodo]) -> List[Periodo]:
|
||||
"""
|
||||
Mescla períodos sobrepostos/contíguos para evitar contagem dupla.
|
||||
"""
|
||||
if not periodos:
|
||||
return []
|
||||
|
||||
periodos_ordenados = sorted(periodos, key=lambda p: p.inicio)
|
||||
periodos_ordenados = sorted(periodos, key=lambda p: p.inicio if p.inicio else datetime.min)
|
||||
mesclados: List[Periodo] = []
|
||||
|
||||
for p in periodos_ordenados:
|
||||
if not mesclados:
|
||||
mesclados.append(p)
|
||||
continue
|
||||
|
||||
ultimo = mesclados[-1]
|
||||
ultimo_fim = ultimo.fim or datetime.now()
|
||||
atual_fim = p.fim or datetime.now()
|
||||
|
||||
if p.inicio <= ultimo_fim:
|
||||
if p.inicio and p.inicio <= ultimo_fim:
|
||||
novo_fim = max(ultimo_fim, atual_fim)
|
||||
mesclados[-1] = Periodo(inicio=ultimo.inicio, fim=novo_fim if not ultimo.ativo else None)
|
||||
else:
|
||||
mesclados.append(p)
|
||||
|
||||
return mesclados
|
||||
|
||||
@staticmethod
|
||||
@@ -47,164 +47,183 @@ class CalculadorPontuacao:
|
||||
return sum(p.anos_completos(ref) for p in periodos)
|
||||
|
||||
@staticmethod
|
||||
def calcular_componente_a(coordenacoes: List[CoordenacaoCapes]) -> ComponentePontuacao:
|
||||
def calcular_bloco_a(coordenacoes: List[CoordenacaoCapes]) -> PontuacaoBloco:
|
||||
if not coordenacoes:
|
||||
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0, retorno=0)
|
||||
return PontuacaoBloco(bloco="A", atuacoes=[])
|
||||
|
||||
base_map = {"CA": 200, "CAJ": 150, "CAJ-MP": 120, "CAM": 100}
|
||||
tempo_max_map = {"CA": 100, "CAJ": 80, "CAJ-MP": 60, "CAM": 50}
|
||||
bonus_atual_map = {"CA": 30, "CAJ": 20, "CAJ-MP": 15, "CAM": 10}
|
||||
mult_tempo_map = {"CA": 10, "CAJ": 8, "CAJ-MP": 6, "CAM": 5}
|
||||
|
||||
# Agrupa por tipo de coordenação e considera o melhor tipo (hierarquia)
|
||||
tipos_ordenados = ["CA", "CAJ", "CAJ-MP", "CAM"]
|
||||
coord_por_tipo = {t: [] for t in tipos_ordenados}
|
||||
tipos_ordenados = ["CA", "CAJ", "CAJ_MP", "CAM"]
|
||||
coord_por_tipo: Dict[str, List[CoordenacaoCapes]] = defaultdict(list)
|
||||
for c in coordenacoes:
|
||||
coord_por_tipo.setdefault(c.tipo, []).append(c)
|
||||
codigo = c.codigo.replace("-", "_")
|
||||
coord_por_tipo[codigo].append(c)
|
||||
|
||||
coord_escolhida_tipo = None
|
||||
for t in tipos_ordenados:
|
||||
if coord_por_tipo.get(t):
|
||||
coord_escolhida_tipo = t
|
||||
break
|
||||
|
||||
if not coord_escolhida_tipo:
|
||||
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0, retorno=0)
|
||||
|
||||
coord_do_tipo = coord_por_tipo.get(coord_escolhida_tipo, [])
|
||||
coord_por_area = {}
|
||||
for c in coord_do_tipo:
|
||||
area = c.area_avaliacao or "N/A"
|
||||
coord_por_area.setdefault(area, []).append(c.periodo)
|
||||
|
||||
anos_total = 0
|
||||
ativo = False
|
||||
retornos_encontrados = 0
|
||||
|
||||
for _, periodos_area in coord_por_area.items():
|
||||
mesclados = CalculadorPontuacao._mesclar_periodos(periodos_area)
|
||||
anos_total += CalculadorPontuacao._anos_completos_periodos(mesclados)
|
||||
ativo = ativo or any(p.ativo for p in periodos_area)
|
||||
if len(mesclados) > 1:
|
||||
retornos_encontrados += 1
|
||||
|
||||
base = base_map.get(coord_escolhida_tipo, 0)
|
||||
tempo = min(int(anos_total * mult_tempo_map.get(coord_escolhida_tipo, 0)), tempo_max_map.get(coord_escolhida_tipo, 0))
|
||||
|
||||
extras = 0
|
||||
areas_distintas = list(coord_por_area.keys())
|
||||
if len(areas_distintas) > 1:
|
||||
extras = min((len(areas_distintas) - 1) * 20, 100)
|
||||
|
||||
bonus = bonus_atual_map.get(coord_escolhida_tipo, 0) if ativo else 0
|
||||
retorno = 20 if retornos_encontrados > 0 else 0
|
||||
|
||||
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus, retorno=retorno, teto=450)
|
||||
|
||||
@staticmethod
|
||||
def calcular_componente_b(coordenacoes: List[CoordenacaoPrograma]) -> ComponentePontuacao:
|
||||
"""
|
||||
Calcula pontuação do Componente B (Coordenação de Programa PPG).
|
||||
|
||||
Regras (máximo 180 pts):
|
||||
- Base: 70 pts por ser coordenador
|
||||
- Tempo: 5 pts/ano (máx 50)
|
||||
- Programas adicionais: 20 pts/programa (máx 40)
|
||||
- Nota do PPG: usar MAIOR nota (7=20, 6=15, 5=10, 4=5, 3=0)
|
||||
"""
|
||||
if not coordenacoes:
|
||||
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)
|
||||
|
||||
# Base: 70 pts por ser coordenador
|
||||
base = 70
|
||||
|
||||
# Tempo: 5 pts/ano (máx 50)
|
||||
anos_totais = sum(c.periodo.anos_completos(datetime.now()) for c in coordenacoes)
|
||||
tempo = min(int(anos_totais * 5), 50)
|
||||
|
||||
# Programas adicionais: 20 pts/programa extra (máx 40)
|
||||
programas_distintos = len({c.id_programa for c in coordenacoes})
|
||||
extras = min((programas_distintos - 1) * 20, 40) if programas_distintos > 1 else 0
|
||||
|
||||
# Nota do PPG: usar MAIOR nota entre os programas
|
||||
# Escala: 7=20, 6=15, 5=10, 4=5, 3=0
|
||||
maior_nota = 0
|
||||
for c in coordenacoes:
|
||||
try:
|
||||
nota_str = str(c.nota_ppg).strip()
|
||||
# Trata notas válidas (3-7 ou A para programas novos)
|
||||
if nota_str in ['7']:
|
||||
maior_nota = max(maior_nota, 7)
|
||||
elif nota_str in ['6']:
|
||||
maior_nota = max(maior_nota, 6)
|
||||
elif nota_str in ['5']:
|
||||
maior_nota = max(maior_nota, 5)
|
||||
elif nota_str in ['4']:
|
||||
maior_nota = max(maior_nota, 4)
|
||||
elif nota_str in ['3']:
|
||||
maior_nota = max(maior_nota, 3)
|
||||
except Exception:
|
||||
atuacoes = []
|
||||
for tipo in tipos_ordenados:
|
||||
if tipo not in coord_por_tipo:
|
||||
continue
|
||||
|
||||
# Mapeia nota para pontos
|
||||
mapa_nota = {7: 20, 6: 15, 5: 10, 4: 5, 3: 0}
|
||||
bonus = mapa_nota.get(maior_nota, 0)
|
||||
criterio = get_criterio(tipo)
|
||||
if not criterio:
|
||||
continue
|
||||
|
||||
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus, teto=180)
|
||||
coords = coord_por_tipo[tipo]
|
||||
periodos = [c.periodo for c in coords]
|
||||
mesclados = CalculadorPontuacao._mesclar_periodos(periodos)
|
||||
|
||||
anos_total = CalculadorPontuacao._anos_completos_periodos(mesclados)
|
||||
ativo = any(c.periodo.ativo for c in coords)
|
||||
tem_retorno = len(mesclados) > 1
|
||||
|
||||
base = criterio.base
|
||||
tempo = min(anos_total * criterio.multiplicador_tempo, criterio.teto_tempo)
|
||||
bonus_atualidade = criterio.bonus_atualidade if ativo else 0
|
||||
bonus_retorno = criterio.bonus_retorno if tem_retorno else 0
|
||||
bonus = bonus_atualidade + bonus_retorno
|
||||
|
||||
total_bruto = base + tempo + bonus
|
||||
total = min(total_bruto, criterio.teto)
|
||||
|
||||
atuacoes.append(PontuacaoAtuacao(
|
||||
codigo=tipo,
|
||||
base=base,
|
||||
tempo=tempo,
|
||||
bonus=bonus,
|
||||
total=total,
|
||||
quantidade=len(coords),
|
||||
))
|
||||
|
||||
return PontuacaoBloco(bloco="A", atuacoes=atuacoes)
|
||||
|
||||
@staticmethod
|
||||
def calcular_componente_c(consultoria: Consultoria) -> ComponentePontuacao:
|
||||
def calcular_bloco_c(consultoria: Consultoria) -> PontuacaoBloco:
|
||||
if not consultoria:
|
||||
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)
|
||||
return PontuacaoBloco(bloco="C", atuacoes=[])
|
||||
|
||||
base = 150 if consultoria.eventos_recentes > 0 else 100
|
||||
codigo = consultoria.codigo
|
||||
criterio = get_criterio(codigo)
|
||||
if not criterio:
|
||||
return PontuacaoBloco(bloco="C", atuacoes=[])
|
||||
|
||||
anos = consultoria.anos_completos if consultoria.anos_completos else int(
|
||||
((datetime.now() - consultoria.primeiro_evento).days) // 365
|
||||
)
|
||||
tempo = min(int(anos * 5), 50)
|
||||
base = criterio.base
|
||||
|
||||
extras_eventos = min(consultoria.total_eventos * 2, 20)
|
||||
extras_responsavel = min(consultoria.vezes_responsavel * 5, 25)
|
||||
extras_areas = min((len(consultoria.areas) - 1) * 10, 30) if len(consultoria.areas) > 1 else 0
|
||||
extras = extras_eventos + extras_responsavel + extras_areas
|
||||
tempo = 0
|
||||
if criterio.pontua_tempo and consultoria.periodo.inicio:
|
||||
anos = consultoria.periodo.anos_completos(datetime.now())
|
||||
tempo = min(anos * criterio.multiplicador_tempo, criterio.teto_tempo)
|
||||
|
||||
continuidade = consultoria.anos_consecutivos
|
||||
if continuidade >= 8:
|
||||
bonus_continuidade = 15
|
||||
elif continuidade >= 5:
|
||||
bonus_continuidade = 10
|
||||
elif continuidade >= 3:
|
||||
bonus_continuidade = 5
|
||||
else:
|
||||
bonus_continuidade = 0
|
||||
bonus = 0
|
||||
if codigo == "CONS_ATIVO":
|
||||
if consultoria.anos_consecutivos >= 8:
|
||||
bonus += criterio.bonus_continuidade_8anos
|
||||
elif consultoria.anos_consecutivos >= 5:
|
||||
bonus += criterio.bonus_continuidade_5anos
|
||||
elif consultoria.anos_consecutivos >= 3:
|
||||
bonus += criterio.bonus_continuidade_3anos
|
||||
if consultoria.retornos > 0:
|
||||
bonus += criterio.bonus_retorno
|
||||
|
||||
retorno_bonus = 15 if consultoria.retornos > 0 else 0
|
||||
bonus = bonus_continuidade + retorno_bonus
|
||||
total_bruto = base + tempo + bonus
|
||||
total = min(total_bruto, criterio.teto)
|
||||
|
||||
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus, teto=230)
|
||||
atuacoes = [PontuacaoAtuacao(
|
||||
codigo=codigo,
|
||||
base=base,
|
||||
tempo=tempo,
|
||||
bonus=bonus,
|
||||
total=total,
|
||||
quantidade=1,
|
||||
)]
|
||||
|
||||
return PontuacaoBloco(bloco="C", atuacoes=atuacoes)
|
||||
|
||||
@staticmethod
|
||||
def calcular_componente_d(premiacoes: List[Premiacao]) -> ComponentePontuacao:
|
||||
if not premiacoes:
|
||||
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)
|
||||
def calcular_bloco_d(
|
||||
inscricoes: List[Inscricao],
|
||||
avaliacoes: List[AvaliacaoComissao],
|
||||
premiacoes: List[Premiacao],
|
||||
bolsas: List[BolsaCNPQ],
|
||||
participacoes: List[Participacao],
|
||||
orientacoes: List[Orientacao],
|
||||
membros_banca: List[MembroBanca],
|
||||
) -> PontuacaoBloco:
|
||||
atuacoes = []
|
||||
totais_por_codigo: Dict[str, Dict] = defaultdict(lambda: {"base": 0, "qtd": 0})
|
||||
|
||||
avaliador = [p for p in premiacoes if "avaliador" in (p.tipo or "").lower()]
|
||||
outros = [p for p in premiacoes if p not in avaliador]
|
||||
for insc in inscricoes:
|
||||
criterio = get_criterio(insc.codigo)
|
||||
if criterio:
|
||||
totais_por_codigo[insc.codigo]["base"] += criterio.base
|
||||
totais_por_codigo[insc.codigo]["qtd"] += 1
|
||||
|
||||
pontos_avaliador = min(sum(p.pontos for p in avaliador), 20)
|
||||
total_pontos = pontos_avaliador + sum(p.pontos for p in outros)
|
||||
total_pontos = min(total_pontos, 180)
|
||||
for aval in avaliacoes:
|
||||
criterio = get_criterio(aval.codigo)
|
||||
if criterio:
|
||||
totais_por_codigo[aval.codigo]["base"] += criterio.base
|
||||
totais_por_codigo[aval.codigo]["qtd"] += 1
|
||||
|
||||
return ComponentePontuacao(base=total_pontos, tempo=0, extras=0, bonus=0, teto=180)
|
||||
for prem in premiacoes:
|
||||
criterio = get_criterio(prem.codigo)
|
||||
if criterio:
|
||||
totais_por_codigo[prem.codigo]["base"] += criterio.base
|
||||
totais_por_codigo[prem.codigo]["qtd"] += 1
|
||||
|
||||
for bolsa in bolsas:
|
||||
criterio = get_criterio(bolsa.codigo)
|
||||
if criterio:
|
||||
totais_por_codigo[bolsa.codigo]["base"] += criterio.base
|
||||
totais_por_codigo[bolsa.codigo]["qtd"] += 1
|
||||
|
||||
for part in participacoes:
|
||||
criterio = get_criterio(part.codigo)
|
||||
if criterio:
|
||||
totais_por_codigo[part.codigo]["base"] += criterio.base
|
||||
totais_por_codigo[part.codigo]["qtd"] += 1
|
||||
|
||||
for orient in orientacoes:
|
||||
criterio = get_criterio(orient.codigo)
|
||||
if criterio:
|
||||
totais_por_codigo[orient.codigo]["base"] += criterio.base
|
||||
totais_por_codigo[orient.codigo]["qtd"] += 1
|
||||
|
||||
for mb in membros_banca:
|
||||
criterio = get_criterio(mb.codigo)
|
||||
if criterio:
|
||||
totais_por_codigo[mb.codigo]["base"] += criterio.base
|
||||
totais_por_codigo[mb.codigo]["qtd"] += 1
|
||||
|
||||
for codigo, dados in totais_por_codigo.items():
|
||||
criterio = get_criterio(codigo)
|
||||
if not criterio:
|
||||
continue
|
||||
|
||||
total = min(dados["base"], criterio.teto)
|
||||
atuacoes.append(PontuacaoAtuacao(
|
||||
codigo=codigo,
|
||||
base=dados["base"],
|
||||
tempo=0,
|
||||
bonus=0,
|
||||
total=total,
|
||||
quantidade=dados["qtd"],
|
||||
))
|
||||
|
||||
return PontuacaoBloco(bloco="D", atuacoes=atuacoes)
|
||||
|
||||
@classmethod
|
||||
def calcular_pontuacao_completa(cls, consultor: Consultor) -> PontuacaoCompleta:
|
||||
comp_a = cls.calcular_componente_a(consultor.coordenacoes_capes)
|
||||
comp_b = cls.calcular_componente_b(consultor.coordenacoes_programas)
|
||||
comp_c = cls.calcular_componente_c(consultor.consultoria)
|
||||
comp_d = cls.calcular_componente_d(consultor.premiacoes)
|
||||
bloco_a = cls.calcular_bloco_a(consultor.coordenacoes_capes)
|
||||
bloco_c = cls.calcular_bloco_c(consultor.consultoria)
|
||||
bloco_d = cls.calcular_bloco_d(
|
||||
inscricoes=consultor.inscricoes,
|
||||
avaliacoes=consultor.avaliacoes_comissao,
|
||||
premiacoes=consultor.premiacoes,
|
||||
bolsas=consultor.bolsas_cnpq,
|
||||
participacoes=consultor.participacoes,
|
||||
orientacoes=consultor.orientacoes,
|
||||
membros_banca=consultor.membros_banca,
|
||||
)
|
||||
|
||||
return PontuacaoCompleta(
|
||||
componente_a=comp_a, componente_b=comp_b, componente_c=comp_c, componente_d=comp_d
|
||||
bloco_a=bloco_a,
|
||||
bloco_c=bloco_c,
|
||||
bloco_d=bloco_d,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user