Skip re-fetching a CPF from the per-pessoa Gennera loop when the bulk ingressantes step already resolved it for the same ano, cutting redundant HTTP fan-out. Add bounded retry with backoff on transient connection errors (never on HTTP status errors) across the four Gennera fetch functions, backed by a shared requests.Session for connection reuse instead of one-off urllib calls. Also stop syncing carga_horaria_integralizada from the API once a vinculo is FORMADO, matching the existing sticky-situacao rule, in both the vinculo import and the standalone carga-horaria command. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2491 lines
94 KiB
Python
2491 lines
94 KiB
Python
from datetime import date
|
|
from pathlib import Path
|
|
from tempfile import TemporaryDirectory
|
|
from unittest.mock import patch
|
|
|
|
import requests
|
|
from django.db import IntegrityError
|
|
from django.test import SimpleTestCase, TestCase, override_settings
|
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
from django.urls import reverse
|
|
from django.contrib.auth import get_user_model
|
|
from django.utils import timezone
|
|
|
|
from .importers import (
|
|
GENNERA_USER_AGENT,
|
|
build_aluno_export_file,
|
|
export_pessoa_record,
|
|
export_vinculo_record,
|
|
fetch_gennera_pessoas_ingressantes_page,
|
|
fetch_gennera_vinculos_censup_page,
|
|
import_alunos,
|
|
import_carga_horaria_from_gennera,
|
|
import_cursos,
|
|
import_pessoa_record,
|
|
import_pessoas_ingressantes_from_gennera,
|
|
import_vinculo_record,
|
|
import_vinculos_from_gennera,
|
|
import_vinculos_ingressantes_from_gennera,
|
|
import_polos,
|
|
load_max_carga_horaria_by_curso,
|
|
)
|
|
from .models import AnoCenso, Curso, Instituicao, Municipio, Pais, Pessoa, Polo, UF, Vinculo
|
|
from .views import CRUD_CONFIGS
|
|
|
|
|
|
class FakeGenneraResponse:
|
|
def __init__(self, json_data=None, headers=None, status_code=200, text=""):
|
|
self._json = json_data
|
|
self.headers = headers or {}
|
|
self.status_code = status_code
|
|
self.text = text
|
|
|
|
def raise_for_status(self) -> None:
|
|
if self.status_code >= 400:
|
|
raise requests.HTTPError(f"{self.status_code} error", response=self)
|
|
|
|
def json(self):
|
|
return self._json
|
|
|
|
|
|
class AuthenticatedViewTestCase(TestCase):
|
|
def setUp(self) -> None:
|
|
self.user = get_user_model().objects.create_user(
|
|
username="tester",
|
|
password="secret",
|
|
)
|
|
self.client.force_login(self.user)
|
|
|
|
|
|
class HomeViewTests(AuthenticatedViewTestCase):
|
|
def test_home_page_requires_login(self) -> None:
|
|
self.client.logout()
|
|
|
|
response = self.client.get(reverse("home"))
|
|
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertIn(reverse("login"), response["Location"])
|
|
|
|
def test_home_page_returns_ok(self) -> None:
|
|
response = self.client.get(reverse("home"))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertTemplateUsed(response, "base.html")
|
|
self.assertTemplateUsed(response, "core/home.html")
|
|
self.assertContains(response, "Autocensup")
|
|
self.assertContains(response, "cdn.jsdelivr.net/npm/@tabler/core")
|
|
|
|
def test_home_page_displays_course_vinculo_summary(self) -> None:
|
|
year = timezone.localdate().year - 1
|
|
uf = UF.objects.create(codigo=22, nome="Piaui")
|
|
municipio = Municipio.objects.create(codigo=2211001, nome="Teresina", uf=uf)
|
|
pais = Pais.objects.create(codigo="BRA", nome="Brasil")
|
|
curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
situacao_funcionamento_curso="Em atividade",
|
|
carga_horaria_total=3200,
|
|
)
|
|
|
|
statuses = [
|
|
Vinculo.Situacao.DESVINCULADO,
|
|
Vinculo.Situacao.CURSANDO,
|
|
Vinculo.Situacao.TRANSFERENCIA_INTERNA,
|
|
Vinculo.Situacao.FALECIDO,
|
|
Vinculo.Situacao.FORMADO,
|
|
Vinculo.Situacao.NAO_INFORMADA,
|
|
]
|
|
for index, status in enumerate(statuses, start=1):
|
|
pessoa = Pessoa.objects.create(
|
|
nome=f"Aluno {index}",
|
|
cpf=f"{index:011d}",
|
|
data_nascimento=date(2000, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.PARDA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
uf_nascimento=uf,
|
|
municipio_nascimento=municipio,
|
|
pais_origem=pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
Vinculo.objects.create(
|
|
pessoa=pessoa,
|
|
ano_censo_id=year,
|
|
periodo_referencia=Vinculo.PeriodoReferencia.PRIMEIRO_SEMESTRE,
|
|
curso=curso,
|
|
turno=Vinculo.Turno.NOTURNO,
|
|
situacao=status,
|
|
semestre_ingresso=f"01{year}",
|
|
ingresso_vestibular=True,
|
|
ingresso_enem=False,
|
|
ingresso_avaliacao_seriada=False,
|
|
ingresso_selecao_simplificada=False,
|
|
ingresso_egresso_bi_li=False,
|
|
ingresso_pec_g=False,
|
|
ingresso_transferencia_ex_officio=False,
|
|
ingresso_decisao_judicial=False,
|
|
ingresso_vagas_remanescentes=False,
|
|
ingresso_programas_especiais=False,
|
|
apoio_social=False,
|
|
atividade_extracurricular=False,
|
|
carga_horaria_total_aluno=3200,
|
|
carga_horaria_integralizada=3200,
|
|
ingressou_acao_afirmativa=False,
|
|
)
|
|
|
|
response = self.client.get(reverse("home"))
|
|
|
|
self.assertContains(response, "Vinculos por curso")
|
|
self.assertContains(response, "Pedagogia")
|
|
self.assertContains(response, "Desvinculado")
|
|
self.assertContains(response, "Cursando")
|
|
self.assertContains(response, "Transferido")
|
|
self.assertContains(response, "Falecido")
|
|
self.assertContains(response, "Formado")
|
|
self.assertContains(response, "Pendente")
|
|
self.assertContains(response, "Quantidade de vinculos")
|
|
self.assertContains(response, "<td class=\"text-end\">1</td>", count=5)
|
|
self.assertContains(response, "bg-yellow-lt text-yellow")
|
|
self.assertContains(response, "<td class=\"text-end font-weight-medium\">6</td>")
|
|
|
|
|
|
class ModelTests(SimpleTestCase):
|
|
def test_uf_string_representation(self) -> None:
|
|
uf = UF(codigo=22, nome="Piaui")
|
|
|
|
self.assertEqual(str(uf), "Piaui")
|
|
|
|
def test_municipio_string_representation(self) -> None:
|
|
uf = UF(codigo=22, nome="Piaui")
|
|
municipio = Municipio(codigo=2211001, nome="Teresina", uf=uf)
|
|
|
|
self.assertEqual(str(municipio), "Teresina - Piaui")
|
|
|
|
def test_pais_string_representation(self) -> None:
|
|
pais = Pais(codigo="BRA", nome="Brasil")
|
|
|
|
self.assertEqual(str(pais), "Brasil")
|
|
|
|
def test_curso_string_representation(self) -> None:
|
|
curso = Curso(
|
|
codigo_mec=123,
|
|
descricao="Analise e Desenvolvimento de Sistemas",
|
|
carga_horaria_total=2400,
|
|
)
|
|
|
|
self.assertEqual(str(curso), "Analise e Desenvolvimento de Sistemas")
|
|
|
|
def test_instituicao_string_representation(self) -> None:
|
|
instituicao = Instituicao(codigo_inep=123456, nome="Centro Universitario")
|
|
|
|
self.assertEqual(str(instituicao), "Centro Universitario")
|
|
|
|
def test_polo_string_representation(self) -> None:
|
|
polo = Polo(codigo=654321, descricao="Polo Centro")
|
|
|
|
self.assertEqual(str(polo), "Polo Centro")
|
|
|
|
def test_pessoa_string_representation(self) -> None:
|
|
pais = Pais(codigo="BRA", nome="Brasil")
|
|
pessoa = Pessoa(
|
|
nome="Maria Silva",
|
|
cpf="12345678901",
|
|
data_nascimento=date(2000, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.PARDA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
|
|
self.assertEqual(str(pessoa), "Maria Silva")
|
|
|
|
|
|
class ModelDatabaseTests(TestCase):
|
|
def setUp(self) -> None:
|
|
self.pais = Pais.objects.create(codigo="BRA", nome="Brasil")
|
|
self.pessoa = Pessoa.objects.create(
|
|
nome="Maria Silva",
|
|
cpf="12345678901",
|
|
data_nascimento=date(2000, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.PARDA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=self.pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
self.curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Analise e Desenvolvimento de Sistemas",
|
|
carga_horaria_total=2400,
|
|
)
|
|
|
|
def test_cpf_must_be_unique(self) -> None:
|
|
with self.assertRaises(IntegrityError):
|
|
Pessoa.objects.create(
|
|
nome="Outra Pessoa",
|
|
cpf=self.pessoa.cpf,
|
|
data_nascimento=date(2001, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.BRANCA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=self.pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
|
|
def test_vinculo_string_representation(self) -> None:
|
|
vinculo = self.create_vinculo()
|
|
|
|
self.assertEqual(
|
|
str(vinculo),
|
|
"Maria Silva - Analise e Desenvolvimento de Sistemas",
|
|
)
|
|
|
|
def test_pessoa_can_have_more_than_one_vinculo(self) -> None:
|
|
self.create_vinculo()
|
|
outro_curso = Curso.objects.create(
|
|
codigo_mec=456,
|
|
descricao="Sistemas de Informacao",
|
|
carga_horaria_total=3000,
|
|
)
|
|
vinculo = self.create_vinculo(curso=outro_curso)
|
|
|
|
self.assertEqual(vinculo.pessoa, self.pessoa)
|
|
|
|
def test_pessoa_can_have_same_course_vinculo_in_different_years(self) -> None:
|
|
self.create_vinculo(ano_censo=2025)
|
|
vinculo = self.create_vinculo(ano_censo=2026)
|
|
|
|
self.assertEqual(vinculo.curso, self.curso)
|
|
|
|
def test_vinculo_must_be_unique_by_pessoa_curso_and_year(self) -> None:
|
|
self.create_vinculo()
|
|
|
|
with self.assertRaises(IntegrityError):
|
|
self.create_vinculo()
|
|
|
|
def create_vinculo(
|
|
self,
|
|
curso: Curso | None = None,
|
|
ano_censo: int = 2025,
|
|
) -> Vinculo:
|
|
return Vinculo.objects.create(
|
|
pessoa=self.pessoa,
|
|
ano_censo_id=ano_censo,
|
|
periodo_referencia=Vinculo.PeriodoReferencia.PRIMEIRO_SEMESTRE,
|
|
curso=curso or self.curso,
|
|
situacao=Vinculo.Situacao.CURSANDO,
|
|
semestre_ingresso="012025",
|
|
ingresso_vestibular=True,
|
|
ingresso_enem=False,
|
|
ingresso_avaliacao_seriada=False,
|
|
ingresso_selecao_simplificada=False,
|
|
ingresso_egresso_bi_li=False,
|
|
ingresso_pec_g=False,
|
|
ingresso_transferencia_ex_officio=False,
|
|
ingresso_decisao_judicial=False,
|
|
ingresso_vagas_remanescentes=False,
|
|
ingresso_programas_especiais=False,
|
|
apoio_social=False,
|
|
atividade_extracurricular=False,
|
|
carga_horaria_total_aluno=2400,
|
|
carga_horaria_integralizada=120,
|
|
ingressou_acao_afirmativa=False,
|
|
)
|
|
|
|
|
|
class CrudViewTests(AuthenticatedViewTestCase):
|
|
def test_menu_defaults_to_previous_year(self) -> None:
|
|
expected_year = timezone.localdate().year - 1
|
|
|
|
response = self.client.get(reverse("home"))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, f'value="{expected_year}" selected')
|
|
|
|
def test_selected_year_is_persisted_from_menu(self) -> None:
|
|
selected_year = timezone.localdate().year - 2
|
|
AnoCenso.objects.create(ano=selected_year)
|
|
|
|
response = self.client.post(
|
|
reverse("set_ano_censo"),
|
|
{"ano_censo": selected_year, "next": reverse("home")},
|
|
)
|
|
|
|
self.assertRedirects(response, reverse("home"))
|
|
self.assertEqual(self.client.session["ano_censo"], selected_year)
|
|
|
|
def test_vinculos_list_uses_selected_year(self) -> None:
|
|
current_year = timezone.localdate().year - 1
|
|
previous_year = current_year - 1
|
|
AnoCenso.objects.get_or_create(ano=current_year)
|
|
AnoCenso.objects.get_or_create(ano=previous_year)
|
|
pais = Pais.objects.create(codigo="BRA", nome="Brasil")
|
|
pessoa_atual = Pessoa.objects.create(
|
|
nome="Aluno Atual",
|
|
cpf="12345678901",
|
|
data_nascimento=date(2000, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.PARDA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
pessoa_anterior = Pessoa.objects.create(
|
|
nome="Aluno Anterior",
|
|
cpf="12345678902",
|
|
data_nascimento=date(2001, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.PARDA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
carga_horaria_total=3200,
|
|
)
|
|
self.create_test_vinculo(pessoa_atual, curso, current_year)
|
|
self.create_test_vinculo(pessoa_anterior, curso, previous_year)
|
|
|
|
response = self.client.get(reverse("crud_list", kwargs={"slug": "vinculos"}))
|
|
|
|
self.assertContains(response, "Aluno Atual")
|
|
self.assertNotContains(response, "Aluno Anterior")
|
|
|
|
session = self.client.session
|
|
session["ano_censo"] = previous_year
|
|
session.save()
|
|
response = self.client.get(reverse("crud_list", kwargs={"slug": "vinculos"}))
|
|
|
|
self.assertContains(response, "Aluno Anterior")
|
|
self.assertNotContains(response, "Aluno Atual")
|
|
|
|
def test_vinculos_list_searches_by_matricula(self) -> None:
|
|
current_year = timezone.localdate().year - 1
|
|
AnoCenso.objects.get_or_create(ano=current_year)
|
|
pais = Pais.objects.create(codigo="BRA", nome="Brasil")
|
|
pessoa = Pessoa.objects.create(
|
|
nome="Aluno Matricula",
|
|
cpf="12345678901",
|
|
data_nascimento=date(2000, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.PARDA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
outra_pessoa = Pessoa.objects.create(
|
|
nome="Aluno Fora",
|
|
cpf="12345678902",
|
|
data_nascimento=date(2001, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.PARDA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
carga_horaria_total=3200,
|
|
)
|
|
vinculo = self.create_test_vinculo(pessoa, curso, current_year)
|
|
vinculo.matricula = "MAT-953"
|
|
vinculo.save(update_fields=["matricula"])
|
|
self.create_test_vinculo(outra_pessoa, curso, current_year)
|
|
|
|
response = self.client.get(
|
|
reverse("crud_list", kwargs={"slug": "vinculos"}),
|
|
{"q": "MAT-953"},
|
|
)
|
|
|
|
self.assertContains(response, "Aluno Matricula")
|
|
self.assertNotContains(response, "Aluno Fora")
|
|
|
|
def test_vinculo_detail_shows_person_vinculos_navigation(self) -> None:
|
|
AnoCenso.objects.get_or_create(ano=2025)
|
|
AnoCenso.objects.get_or_create(ano=2026)
|
|
pais = Pais.objects.create(codigo="BRA", nome="Brasil")
|
|
pessoa = Pessoa.objects.create(
|
|
nome="Aluno Navegacao",
|
|
cpf="12345678901",
|
|
data_nascimento=date(2000, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.PARDA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
pedagogia = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
carga_horaria_total=3200,
|
|
)
|
|
sistemas = Curso.objects.create(
|
|
codigo_mec=456,
|
|
descricao="Sistemas de Informacao",
|
|
carga_horaria_total=3000,
|
|
)
|
|
vinculo_atual = self.create_test_vinculo(pessoa, pedagogia, 2026)
|
|
outro_vinculo = self.create_test_vinculo(pessoa, sistemas, 2025)
|
|
|
|
response = self.client.get(
|
|
reverse("crud_detail", kwargs={"slug": "vinculos", "pk": vinculo_atual.pk})
|
|
)
|
|
|
|
self.assertContains(response, "Vínculos de Aluno Navegacao")
|
|
self.assertContains(response, "Pedagogia")
|
|
self.assertContains(response, "Sistemas de Informacao")
|
|
self.assertContains(response, "Atual")
|
|
self.assertContains(response, "Próximo vínculo")
|
|
self.assertContains(response, "Ver vínculo")
|
|
self.assertContains(
|
|
response,
|
|
reverse("crud_detail", kwargs={"slug": "vinculos", "pk": outro_vinculo.pk}),
|
|
)
|
|
|
|
def create_test_vinculo(
|
|
self,
|
|
pessoa: Pessoa,
|
|
curso: Curso,
|
|
ano_censo: int,
|
|
) -> Vinculo:
|
|
return Vinculo.objects.create(
|
|
pessoa=pessoa,
|
|
ano_censo_id=ano_censo,
|
|
periodo_referencia=Vinculo.PeriodoReferencia.PRIMEIRO_SEMESTRE,
|
|
curso=curso,
|
|
situacao=Vinculo.Situacao.CURSANDO,
|
|
semestre_ingresso=f"01{ano_censo}",
|
|
ingresso_vestibular=True,
|
|
ingresso_enem=False,
|
|
ingresso_avaliacao_seriada=False,
|
|
ingresso_selecao_simplificada=False,
|
|
ingresso_egresso_bi_li=False,
|
|
ingresso_pec_g=False,
|
|
ingresso_transferencia_ex_officio=False,
|
|
ingresso_decisao_judicial=False,
|
|
ingresso_vagas_remanescentes=False,
|
|
ingresso_programas_especiais=False,
|
|
apoio_social=False,
|
|
atividade_extracurricular=False,
|
|
carga_horaria_total_aluno=curso.carga_horaria_total,
|
|
carga_horaria_integralizada=120,
|
|
ingressou_acao_afirmativa=False,
|
|
)
|
|
|
|
def test_all_crud_lists_return_ok(self) -> None:
|
|
for slug in CRUD_CONFIGS:
|
|
with self.subTest(slug=slug):
|
|
response = self.client.get(reverse("crud_list", kwargs={"slug": slug}))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_crud_search_and_pagination(self) -> None:
|
|
for index in range(25):
|
|
UF.objects.create(codigo=index + 1, nome=f"Estado {index + 1}")
|
|
|
|
response = self.client.get(reverse("crud_list", kwargs={"slug": "ufs"}))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Pagina 1 de 2")
|
|
|
|
response = self.client.get(
|
|
reverse("crud_list", kwargs={"slug": "ufs"}),
|
|
{"q": "Estado 25"},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Estado 25")
|
|
self.assertNotContains(response, "Estado 24")
|
|
|
|
def test_crud_create_detail_update_and_delete(self) -> None:
|
|
create_response = self.client.post(
|
|
reverse("crud_create", kwargs={"slug": "ufs"}),
|
|
{"codigo": 22, "nome": "Piaui"},
|
|
)
|
|
uf = UF.objects.get(codigo=22)
|
|
|
|
self.assertRedirects(
|
|
create_response,
|
|
reverse("crud_detail", kwargs={"slug": "ufs", "pk": uf.pk}),
|
|
)
|
|
|
|
detail_response = self.client.get(
|
|
reverse("crud_detail", kwargs={"slug": "ufs", "pk": uf.pk}),
|
|
)
|
|
|
|
self.assertContains(detail_response, "Piaui")
|
|
|
|
update_response = self.client.post(
|
|
reverse("crud_update", kwargs={"slug": "ufs", "pk": uf.pk}),
|
|
{"codigo": 22, "nome": "Piauí"},
|
|
)
|
|
|
|
self.assertRedirects(
|
|
update_response,
|
|
reverse("crud_detail", kwargs={"slug": "ufs", "pk": uf.pk}),
|
|
)
|
|
self.assertEqual(UF.objects.get(codigo=22).nome, "Piauí")
|
|
|
|
delete_response = self.client.post(
|
|
reverse("crud_delete", kwargs={"slug": "ufs", "pk": uf.pk}),
|
|
)
|
|
|
|
self.assertRedirects(delete_response, reverse("crud_list", kwargs={"slug": "ufs"}))
|
|
self.assertFalse(UF.objects.filter(codigo=22).exists())
|
|
|
|
def test_crud_list_sorts_by_clicked_column(self) -> None:
|
|
UF.objects.create(codigo=1, nome="Zeta")
|
|
UF.objects.create(codigo=2, nome="Alfa")
|
|
|
|
response_asc = self.client.get(
|
|
reverse("crud_list", kwargs={"slug": "ufs"}), {"sort": "nome", "dir": "asc"}
|
|
)
|
|
response_desc = self.client.get(
|
|
reverse("crud_list", kwargs={"slug": "ufs"}), {"sort": "nome", "dir": "desc"}
|
|
)
|
|
|
|
self.assertEqual(
|
|
[uf.nome for uf in response_asc.context["page_obj"].object_list],
|
|
["Alfa", "Zeta"],
|
|
)
|
|
self.assertEqual(
|
|
[uf.nome for uf in response_desc.context["page_obj"].object_list],
|
|
["Zeta", "Alfa"],
|
|
)
|
|
|
|
def test_crud_list_ignores_unknown_sort_field(self) -> None:
|
|
UF.objects.create(codigo=1, nome="Piaui")
|
|
|
|
response = self.client.get(
|
|
reverse("crud_list", kwargs={"slug": "ufs"}), {"sort": "bogus"}
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response.context["sort"], "")
|
|
|
|
def test_crud_list_pagination_links_preserve_sort(self) -> None:
|
|
for index in range(25):
|
|
UF.objects.create(codigo=index + 1, nome=f"UF {index:02d}")
|
|
|
|
response = self.client.get(
|
|
reverse("crud_list", kwargs={"slug": "ufs"}), {"sort": "nome", "dir": "desc"}
|
|
)
|
|
|
|
self.assertContains(response, "sort=nome&dir=desc&page=2")
|
|
|
|
def test_crud_list_sorts_vinculos_by_related_pessoa_name(self) -> None:
|
|
pais = Pais.objects.create(codigo="BRA", nome="Brasil")
|
|
curso = Curso.objects.create(
|
|
codigo_mec=123, descricao="Pedagogia", carga_horaria_total=3200
|
|
)
|
|
for nome, cpf in (("Zeta Aluno", "11111111111"), ("Alfa Aluno", "22222222222")):
|
|
pessoa = Pessoa.objects.create(
|
|
nome=nome,
|
|
cpf=cpf,
|
|
data_nascimento=date(2000, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.PARDA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
Vinculo.objects.create(
|
|
pessoa=pessoa,
|
|
ano_censo_id=timezone.localdate().year - 1,
|
|
curso=curso,
|
|
situacao=Vinculo.Situacao.CURSANDO,
|
|
semestre_ingresso="012025",
|
|
carga_horaria_total_aluno=3200,
|
|
carga_horaria_integralizada=0,
|
|
**REQUIRED_VINCULO_BOOL_DEFAULTS,
|
|
)
|
|
|
|
response = self.client.get(
|
|
reverse("crud_list", kwargs={"slug": "vinculos"}), {"sort": "pessoa", "dir": "asc"}
|
|
)
|
|
|
|
self.assertEqual(
|
|
[v.pessoa.nome for v in response.context["page_obj"].object_list],
|
|
["Alfa Aluno", "Zeta Aluno"],
|
|
)
|
|
|
|
def test_crud_update_marks_pessoa_as_atualizado_manualmente(self) -> None:
|
|
pais = Pais.objects.create(codigo="BRA", nome="Brasil")
|
|
pessoa = Pessoa.objects.create(
|
|
nome="Maria Silva",
|
|
cpf="12345678901",
|
|
cor_raca=Pessoa.CorRaca.PARDA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
self.assertFalse(pessoa.atualizado_manualmente)
|
|
|
|
form_url = reverse("crud_update", kwargs={"slug": "pessoas", "pk": pessoa.pk})
|
|
form_page = self.client.get(form_url)
|
|
self.assertNotContains(form_page, "atualizado_manualmente")
|
|
|
|
response = self.client.post(
|
|
form_url,
|
|
{
|
|
"nome": "Maria Silva Editada",
|
|
"cpf": "12345678901",
|
|
"cor_raca": Pessoa.CorRaca.PARDA,
|
|
"nacionalidade": Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
"pais_origem": pais.pk,
|
|
"aluno_com_deficiencia": Pessoa.SimNaoNaoDispoe.NAO,
|
|
"tipo_escola_ensino_medio": Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
},
|
|
)
|
|
|
|
self.assertRedirects(
|
|
response,
|
|
reverse("crud_detail", kwargs={"slug": "pessoas", "pk": pessoa.pk}),
|
|
)
|
|
pessoa.refresh_from_db()
|
|
self.assertEqual(pessoa.nome, "Maria Silva Editada")
|
|
self.assertTrue(pessoa.atualizado_manualmente)
|
|
|
|
def test_pending_required_fields_view_lists_courses_without_carga(self) -> None:
|
|
Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Curso sem carga",
|
|
carga_horaria_total=0,
|
|
)
|
|
|
|
response = self.client.get(reverse("pending_required_fields"))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Curso sem carga")
|
|
self.assertContains(response, "Carga Horaria Total")
|
|
|
|
def test_course_list_uses_check_icon_for_true_boolean(self) -> None:
|
|
Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Curso EaD",
|
|
situacao_funcionamento_curso="Em atividade",
|
|
ead=True,
|
|
carga_horaria_total=1000,
|
|
)
|
|
|
|
response = self.client.get(reverse("crud_list", kwargs={"slug": "cursos"}))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "ti ti-check")
|
|
self.assertContains(response, "bg-green-lt text-green")
|
|
self.assertContains(response, "Em atividade")
|
|
|
|
def test_course_list_uses_yellow_badge_for_extinction_status(self) -> None:
|
|
Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Curso em extincao",
|
|
situacao_funcionamento_curso="Em extinção",
|
|
carga_horaria_total=1000,
|
|
)
|
|
|
|
response = self.client.get(reverse("crud_list", kwargs={"slug": "cursos"}))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "bg-yellow-lt text-yellow")
|
|
self.assertContains(response, "Em extinção")
|
|
|
|
|
|
class ImportCursosTests(TestCase):
|
|
def test_load_max_carga_horaria_by_curso(self) -> None:
|
|
with TemporaryDirectory() as tmp_dir:
|
|
alunos_path = Path(tmp_dir) / "alunos.txt"
|
|
alunos_path.write_text(
|
|
"\n".join(
|
|
[
|
|
"40|1131",
|
|
self.vinculo_line(codigo_curso=123, carga_horaria=2400),
|
|
self.vinculo_line(codigo_curso=123, carga_horaria=2600),
|
|
self.vinculo_line(codigo_curso=456, carga_horaria=1800),
|
|
]
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
cargas = load_max_carga_horaria_by_curso(alunos_path)
|
|
|
|
self.assertEqual(cargas[123], 2600)
|
|
self.assertEqual(cargas[456], 1800)
|
|
|
|
def test_import_cursos_creates_and_updates_courses(self) -> None:
|
|
Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Descricao antiga",
|
|
carga_horaria_total=1,
|
|
)
|
|
|
|
with TemporaryDirectory() as tmp_dir:
|
|
tmp_path = Path(tmp_dir)
|
|
csv_path = tmp_path / "cursos.csv"
|
|
alunos_path = tmp_path / "alunos.txt"
|
|
csv_path.write_text(
|
|
"\ufeffCODIGO_CURSO;NOME_CURSO;SITUACAO_FUNCIONAMENTO_CURSO;FORMATO_OFERTA;GRAU_ACADEMICO\n"
|
|
"123;PEDAGOGIA;Em atividade;EaD;Licenciatura\n"
|
|
"456;ENGENHARIA;Em extincao;Presencial;Bacharelado\n"
|
|
"789;GESTAO;Em atividade;Semipresencial;Tecnológico\n",
|
|
encoding="utf-8",
|
|
)
|
|
alunos_path.write_text(
|
|
"\n".join(
|
|
[
|
|
self.vinculo_line(codigo_curso=123, carga_horaria=3000),
|
|
self.vinculo_line(codigo_curso=123, carga_horaria=3200),
|
|
self.vinculo_line(codigo_curso=456, carga_horaria=4000),
|
|
]
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = import_cursos(csv_path=csv_path, alunos_path=alunos_path)
|
|
|
|
curso_atualizado = Curso.objects.get(codigo_mec=123)
|
|
curso_criado = Curso.objects.get(codigo_mec=456)
|
|
curso_sem_carga = Curso.objects.get(codigo_mec=789)
|
|
|
|
self.assertEqual(result.total, 3)
|
|
self.assertEqual(result.created, 2)
|
|
self.assertEqual(result.updated, 1)
|
|
self.assertEqual(result.without_carga_horaria, 1)
|
|
self.assertEqual(curso_atualizado.descricao, "PEDAGOGIA")
|
|
self.assertEqual(curso_atualizado.situacao_funcionamento_curso, "Em atividade")
|
|
self.assertTrue(curso_atualizado.licenciatura)
|
|
self.assertTrue(curso_atualizado.ead)
|
|
self.assertEqual(curso_atualizado.carga_horaria_total, 3200)
|
|
self.assertFalse(curso_criado.licenciatura)
|
|
self.assertFalse(curso_criado.ead)
|
|
self.assertEqual(curso_criado.carga_horaria_total, 4000)
|
|
self.assertEqual(curso_sem_carga.carga_horaria_total, 0)
|
|
|
|
def vinculo_line(self, codigo_curso: int, carga_horaria: int) -> str:
|
|
fields = [""] * 73
|
|
fields[0] = "42"
|
|
fields[3] = str(codigo_curso)
|
|
fields[55] = str(carga_horaria)
|
|
return "|".join(fields)
|
|
|
|
|
|
class ImportPolosTests(TestCase):
|
|
def test_import_polos_creates_polos_and_links_ead_courses(self) -> None:
|
|
Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Curso EaD",
|
|
ead=True,
|
|
carga_horaria_total=2000,
|
|
)
|
|
Curso.objects.create(
|
|
codigo_mec=456,
|
|
descricao="Curso Presencial",
|
|
ead=False,
|
|
carga_horaria_total=2000,
|
|
)
|
|
|
|
with TemporaryDirectory() as tmp_dir:
|
|
csv_path = Path(tmp_dir) / "cursos_locais.csv"
|
|
csv_path.write_text(
|
|
"\ufeffCODIGOCURSO;CODIGOLOCALOFERTA;NOMELOCALOFERTA\n"
|
|
"123;10;Polo Centro\n"
|
|
"456;20;Unidade Presencial\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = import_polos(csv_path)
|
|
|
|
curso_ead = Curso.objects.get(codigo_mec=123)
|
|
curso_presencial = Curso.objects.get(codigo_mec=456)
|
|
|
|
self.assertEqual(result.created, 2)
|
|
self.assertEqual(result.cursos_vinculados, 1)
|
|
self.assertEqual(curso_ead.polo.descricao, "Polo Centro")
|
|
self.assertIsNone(curso_presencial.polo)
|
|
|
|
|
|
class ProcessarSituacaoTests(AuthenticatedViewTestCase):
|
|
def setUp(self) -> None:
|
|
super().setUp()
|
|
self.year = timezone.localdate().year - 1
|
|
self.pais = Pais.objects.create(codigo="BRA", nome="Brasil")
|
|
self.curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
situacao_funcionamento_curso="Em atividade",
|
|
carga_horaria_total=3200,
|
|
)
|
|
|
|
def test_processar_situacao_link_is_in_import_menu(self) -> None:
|
|
response = self.client.get(reverse("home"))
|
|
|
|
self.assertContains(response, "Importacoes")
|
|
self.assertContains(response, "Processar Situacao")
|
|
self.assertContains(response, reverse("processar_situacao"))
|
|
|
|
def test_processar_situacao_updates_selected_year_by_matricula(self) -> None:
|
|
selected_year_vinculo = self.create_vinculo(
|
|
matricula="IES-1",
|
|
cpf="12345678901",
|
|
ano_censo=self.year,
|
|
)
|
|
second_selected_year_vinculo = self.create_vinculo(
|
|
matricula="IES-2",
|
|
cpf="12345678902",
|
|
ano_censo=self.year,
|
|
)
|
|
other_year_vinculo = self.create_vinculo(
|
|
matricula="IES-1",
|
|
cpf="12345678903",
|
|
ano_censo=self.year - 1,
|
|
)
|
|
|
|
response = self.client.post(
|
|
reverse("processar_situacao"),
|
|
{
|
|
"matriculas": "IES-1\nIES-2\nNAO-EXISTE",
|
|
"situacao": Vinculo.Situacao.FORMADO,
|
|
},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Vinculos atualizados")
|
|
self.assertContains(response, "NAO-EXISTE")
|
|
selected_year_vinculo.refresh_from_db()
|
|
second_selected_year_vinculo.refresh_from_db()
|
|
other_year_vinculo.refresh_from_db()
|
|
self.assertEqual(selected_year_vinculo.situacao, Vinculo.Situacao.FORMADO)
|
|
self.assertEqual(second_selected_year_vinculo.situacao, Vinculo.Situacao.FORMADO)
|
|
self.assertEqual(other_year_vinculo.situacao, Vinculo.Situacao.CURSANDO)
|
|
|
|
def create_vinculo(
|
|
self,
|
|
matricula: str,
|
|
cpf: str,
|
|
ano_censo: int,
|
|
) -> Vinculo:
|
|
pessoa = Pessoa.objects.create(
|
|
nome=f"Aluno {cpf}",
|
|
cpf=cpf,
|
|
data_nascimento=date(2000, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.PARDA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=self.pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
return Vinculo.objects.create(
|
|
pessoa=pessoa,
|
|
ano_censo_id=ano_censo,
|
|
matricula=matricula,
|
|
periodo_referencia=Vinculo.PeriodoReferencia.PRIMEIRO_SEMESTRE,
|
|
curso=self.curso,
|
|
turno=Vinculo.Turno.NOTURNO,
|
|
situacao=Vinculo.Situacao.CURSANDO,
|
|
semestre_ingresso=f"01{ano_censo}",
|
|
ingresso_vestibular=True,
|
|
ingresso_enem=False,
|
|
ingresso_avaliacao_seriada=False,
|
|
ingresso_selecao_simplificada=False,
|
|
ingresso_egresso_bi_li=False,
|
|
ingresso_pec_g=False,
|
|
ingresso_transferencia_ex_officio=False,
|
|
ingresso_decisao_judicial=False,
|
|
ingresso_vagas_remanescentes=False,
|
|
ingresso_programas_especiais=False,
|
|
apoio_social=False,
|
|
atividade_extracurricular=False,
|
|
carga_horaria_total_aluno=3200,
|
|
carga_horaria_integralizada=120,
|
|
ingressou_acao_afirmativa=False,
|
|
)
|
|
|
|
|
|
class ImportAlunosTests(AuthenticatedViewTestCase):
|
|
def setUp(self) -> None:
|
|
super().setUp()
|
|
self.uf = UF.objects.create(codigo=22, nome="Piaui")
|
|
self.municipio = Municipio.objects.create(
|
|
codigo=2211001,
|
|
nome="Teresina",
|
|
uf=self.uf,
|
|
)
|
|
self.pais = Pais.objects.create(codigo="BRA", nome="Brasil")
|
|
self.curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
situacao_funcionamento_curso="Em atividade",
|
|
licenciatura=True,
|
|
ead=False,
|
|
carga_horaria_total=3200,
|
|
)
|
|
|
|
def test_import_alunos_creates_pessoa_and_vinculo(self) -> None:
|
|
result = import_alunos(
|
|
[
|
|
self.pessoa_line(nome="Maria Silva", nascimento="01012000"),
|
|
self.vinculo_line(codigo_curso=123, carga_horaria=9999),
|
|
],
|
|
ano_censo=2025,
|
|
)
|
|
|
|
pessoa = Pessoa.objects.get(cpf="12345678901")
|
|
vinculo = Vinculo.objects.get(pessoa=pessoa, curso=self.curso, ano_censo=2025)
|
|
|
|
self.assertEqual(result.pessoas_created, 1)
|
|
self.assertEqual(result.vinculos_created, 1)
|
|
self.assertTrue(pessoa.anos_censo.filter(ano=2025).exists())
|
|
self.assertEqual(vinculo.carga_horaria_total_aluno, 3200)
|
|
self.assertEqual(vinculo.situacao, Vinculo.Situacao.NAO_INFORMADA)
|
|
|
|
def test_import_alunos_updates_pessoa_but_preserves_name_and_birth_date(self) -> None:
|
|
Pessoa.objects.create(
|
|
nome="Nome Original",
|
|
cpf="12345678901",
|
|
data_nascimento=date(1999, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.BRANCA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=self.pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PRIVADA,
|
|
)
|
|
|
|
result = import_alunos(
|
|
[
|
|
self.pessoa_line(nome="Nome Novo", nascimento="02022000"),
|
|
],
|
|
ano_censo=2025,
|
|
)
|
|
|
|
pessoa = Pessoa.objects.get(cpf="12345678901")
|
|
|
|
self.assertEqual(result.pessoas_updated, 1)
|
|
self.assertTrue(pessoa.anos_censo.filter(ano=2025).exists())
|
|
self.assertEqual(pessoa.nome, "Nome Original")
|
|
self.assertEqual(pessoa.data_nascimento, date(1999, 1, 1))
|
|
self.assertEqual(pessoa.cor_raca, Pessoa.CorRaca.PARDA)
|
|
|
|
def test_import_alunos_does_not_overwrite_manually_edited_pessoa(self) -> None:
|
|
Pessoa.objects.create(
|
|
nome="Nome Original",
|
|
cpf="12345678901",
|
|
data_nascimento=date(1999, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.BRANCA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=self.pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PRIVADA,
|
|
atualizado_manualmente=True,
|
|
)
|
|
|
|
result = import_alunos(
|
|
[self.pessoa_line(nome="Nome Novo", nascimento="02022000")],
|
|
ano_censo=2025,
|
|
)
|
|
|
|
pessoa = Pessoa.objects.get(cpf="12345678901")
|
|
|
|
self.assertEqual(result.pessoas_updated, 1)
|
|
self.assertEqual(pessoa.cor_raca, Pessoa.CorRaca.BRANCA)
|
|
self.assertEqual(pessoa.tipo_escola_ensino_medio, Pessoa.TipoEscolaEnsinoMedio.PRIVADA)
|
|
|
|
def test_import_alunos_does_not_overwrite_manually_edited_vinculo(self) -> None:
|
|
pessoa = Pessoa.objects.create(
|
|
nome="Maria Silva",
|
|
cpf="12345678901",
|
|
data_nascimento=date(2000, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.PARDA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=self.pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
Vinculo.objects.create(
|
|
pessoa=pessoa,
|
|
ano_censo_id=2025,
|
|
curso=self.curso,
|
|
situacao=Vinculo.Situacao.FORMADO,
|
|
semestre_ingresso="012020",
|
|
carga_horaria_total_aluno=3200,
|
|
carga_horaria_integralizada=3200,
|
|
atualizado_manualmente=True,
|
|
ingresso_vestibular=True,
|
|
ingresso_enem=False,
|
|
ingresso_avaliacao_seriada=False,
|
|
ingresso_selecao_simplificada=False,
|
|
ingresso_egresso_bi_li=False,
|
|
ingresso_pec_g=False,
|
|
ingresso_transferencia_ex_officio=False,
|
|
ingresso_decisao_judicial=False,
|
|
ingresso_vagas_remanescentes=False,
|
|
ingresso_programas_especiais=False,
|
|
apoio_social=False,
|
|
atividade_extracurricular=False,
|
|
ingressou_acao_afirmativa=False,
|
|
)
|
|
|
|
import_alunos(
|
|
[
|
|
self.pessoa_line(nome="Maria Silva", nascimento="01012000"),
|
|
self.vinculo_line(codigo_curso=123, carga_horaria=1),
|
|
],
|
|
ano_censo=2025,
|
|
)
|
|
|
|
vinculo = Vinculo.objects.get(pessoa=pessoa, curso=self.curso, ano_censo=2025)
|
|
self.assertEqual(vinculo.situacao, Vinculo.Situacao.FORMADO)
|
|
self.assertEqual(vinculo.carga_horaria_integralizada, 3200)
|
|
|
|
def test_import_alunos_view_accepts_upload(self) -> None:
|
|
uploaded = SimpleUploadedFile(
|
|
"aluno_exportacao.txt",
|
|
(
|
|
self.pessoa_line(nome="Maria Silva", nascimento="01012000")
|
|
+ "\n"
|
|
+ self.vinculo_line(codigo_curso=123, carga_horaria=9999)
|
|
).encode("utf-8"),
|
|
content_type="text/plain",
|
|
)
|
|
|
|
response = self.client.post(
|
|
reverse("import_alunos"),
|
|
{"ano_censo": 2025, "arquivo": uploaded},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Pessoas criadas")
|
|
self.assertEqual(Pessoa.objects.count(), 1)
|
|
self.assertEqual(Vinculo.objects.count(), 1)
|
|
|
|
def pessoa_line(self, nome: str, nascimento: str) -> str:
|
|
return "|".join(
|
|
[
|
|
"41",
|
|
"1",
|
|
nome,
|
|
"12345678901",
|
|
"",
|
|
nascimento,
|
|
"3",
|
|
"1",
|
|
"22",
|
|
"2211001",
|
|
"BRA",
|
|
"0",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
"1",
|
|
]
|
|
)
|
|
|
|
def vinculo_line(self, codigo_curso: int, carga_horaria: int) -> str:
|
|
fields = [""] * 73
|
|
fields[0] = "42"
|
|
fields[1] = "IES-1"
|
|
fields[3] = str(codigo_curso)
|
|
fields[6] = "-1"
|
|
fields[12] = "012025"
|
|
for index in range(13, 23):
|
|
fields[index] = "0"
|
|
fields[46] = "0"
|
|
fields[55] = str(carga_horaria)
|
|
fields[57] = "0"
|
|
return "|".join(fields)
|
|
|
|
|
|
REQUIRED_VINCULO_BOOL_DEFAULTS = {
|
|
"ingresso_vestibular": True,
|
|
"ingresso_enem": False,
|
|
"ingresso_avaliacao_seriada": False,
|
|
"ingresso_selecao_simplificada": False,
|
|
"ingresso_egresso_bi_li": False,
|
|
"ingresso_pec_g": False,
|
|
"ingresso_transferencia_ex_officio": False,
|
|
"ingresso_decisao_judicial": False,
|
|
"ingresso_vagas_remanescentes": False,
|
|
"ingresso_programas_especiais": False,
|
|
"apoio_social": False,
|
|
"atividade_extracurricular": False,
|
|
"ingressou_acao_afirmativa": False,
|
|
}
|
|
|
|
|
|
class ExportAlunoRecordTests(TestCase):
|
|
def setUp(self) -> None:
|
|
self.uf = UF.objects.create(codigo=22, nome="Piaui")
|
|
self.municipio = Municipio.objects.create(
|
|
codigo=2211001,
|
|
nome="Teresina",
|
|
uf=self.uf,
|
|
)
|
|
self.pais = Pais.objects.create(codigo="BRA", nome="Brasil")
|
|
self.curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
situacao_funcionamento_curso="Em atividade",
|
|
licenciatura=False,
|
|
ead=False,
|
|
carga_horaria_total=3200,
|
|
)
|
|
AnoCenso.objects.get_or_create(ano=2025)
|
|
|
|
def make_pessoa(self, **overrides: object) -> Pessoa:
|
|
defaults = dict(
|
|
nome="Maria Silva",
|
|
cpf="12345678901",
|
|
data_nascimento=date(2000, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.PARDA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
uf_nascimento=self.uf,
|
|
municipio_nascimento=self.municipio,
|
|
pais_origem=self.pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
defaults.update(overrides)
|
|
return Pessoa.objects.create(**defaults)
|
|
|
|
def make_vinculo(self, pessoa: Pessoa, **overrides: object) -> Vinculo:
|
|
defaults = dict(
|
|
pessoa=pessoa,
|
|
ano_censo_id=2025,
|
|
curso=self.curso,
|
|
matricula="MAT-1",
|
|
situacao=Vinculo.Situacao.CURSANDO,
|
|
semestre_ingresso="012025",
|
|
carga_horaria_total_aluno=3200,
|
|
carga_horaria_integralizada=100,
|
|
**REQUIRED_VINCULO_BOOL_DEFAULTS,
|
|
)
|
|
defaults.update(overrides)
|
|
return Vinculo.objects.create(**defaults)
|
|
|
|
def test_export_pessoa_record_round_trips_through_import(self) -> None:
|
|
pessoa = self.make_pessoa()
|
|
line = export_pessoa_record(pessoa)
|
|
|
|
reimported, created = import_pessoa_record(line.split("|"))
|
|
|
|
self.assertFalse(created)
|
|
self.assertEqual(reimported.pk, pessoa.pk)
|
|
self.assertEqual(reimported.cor_raca, pessoa.cor_raca)
|
|
self.assertEqual(reimported.nacionalidade, pessoa.nacionalidade)
|
|
self.assertEqual(reimported.uf_nascimento_id, pessoa.uf_nascimento_id)
|
|
self.assertEqual(reimported.municipio_nascimento_id, pessoa.municipio_nascimento_id)
|
|
self.assertEqual(reimported.pais_origem_id, pessoa.pais_origem_id)
|
|
self.assertEqual(reimported.aluno_com_deficiencia, pessoa.aluno_com_deficiencia)
|
|
self.assertEqual(reimported.tipo_escola_ensino_medio, pessoa.tipo_escola_ensino_medio)
|
|
|
|
def test_export_pessoa_record_handles_null_data_nascimento(self) -> None:
|
|
pessoa = self.make_pessoa(data_nascimento=None)
|
|
line = export_pessoa_record(pessoa)
|
|
|
|
self.assertEqual(line.split("|")[5], "")
|
|
|
|
def test_export_vinculo_record_round_trips_through_import(self) -> None:
|
|
pessoa = self.make_pessoa()
|
|
vinculo = self.make_vinculo(pessoa)
|
|
line = export_vinculo_record(vinculo)
|
|
|
|
reimported, created = import_vinculo_record(line.split("|"), pessoa, 2025)
|
|
|
|
self.assertFalse(created)
|
|
self.assertEqual(reimported.pk, vinculo.pk)
|
|
self.assertEqual(reimported.matricula, vinculo.matricula)
|
|
self.assertEqual(reimported.situacao, vinculo.situacao)
|
|
self.assertEqual(reimported.semestre_ingresso, vinculo.semestre_ingresso)
|
|
self.assertEqual(reimported.carga_horaria_integralizada, vinculo.carga_horaria_integralizada)
|
|
self.assertEqual(reimported.ingresso_vestibular, vinculo.ingresso_vestibular)
|
|
|
|
def test_export_vinculo_record_writes_polo_codigo_not_polo_pk(self) -> None:
|
|
# Two polos so the auto PK is guaranteed to differ from `codigo`.
|
|
Polo.objects.create(codigo=999, descricao="Polo Descartavel")
|
|
polo = Polo.objects.create(codigo=555, descricao="Polo Real")
|
|
self.assertNotEqual(polo.pk, polo.codigo)
|
|
|
|
pessoa = self.make_pessoa()
|
|
vinculo = self.make_vinculo(pessoa, polo=polo)
|
|
line = export_vinculo_record(vinculo)
|
|
|
|
self.assertEqual(line.split("|")[4], str(polo.codigo))
|
|
|
|
def test_export_vinculo_record_falls_back_to_curso_polo_when_vinculo_polo_is_none(
|
|
self,
|
|
) -> None:
|
|
# Vinculos importados via API Gennera nunca preenchem Vinculo.polo diretamente
|
|
# (ver import_gennera_vinculo_group) -- o polo EaD fica so em Curso.polo.
|
|
Polo.objects.create(codigo=999, descricao="Polo Descartavel")
|
|
polo_do_curso = Polo.objects.create(codigo=777, descricao="Polo do Curso EaD")
|
|
self.curso.ead = True
|
|
self.curso.polo = polo_do_curso
|
|
self.curso.save(update_fields=["ead", "polo"])
|
|
|
|
pessoa = self.make_pessoa()
|
|
vinculo = self.make_vinculo(pessoa, polo=None)
|
|
line = export_vinculo_record(vinculo)
|
|
|
|
self.assertEqual(line.split("|")[4], str(polo_do_curso.codigo))
|
|
|
|
def test_export_vinculo_record_writes_minus_one_for_nao_informada_situacao(self) -> None:
|
|
pessoa = self.make_pessoa()
|
|
vinculo = self.make_vinculo(pessoa, situacao=Vinculo.Situacao.NAO_INFORMADA)
|
|
line = export_vinculo_record(vinculo)
|
|
|
|
self.assertEqual(line.split("|")[6], "-1")
|
|
|
|
def test_export_vinculo_record_writes_situacao_int_for_other_values(self) -> None:
|
|
pessoa = self.make_pessoa()
|
|
vinculo = self.make_vinculo(pessoa, situacao=Vinculo.Situacao.FORMADO)
|
|
line = export_vinculo_record(vinculo)
|
|
|
|
self.assertEqual(line.split("|")[6], str(int(Vinculo.Situacao.FORMADO)))
|
|
|
|
def test_build_aluno_export_file_emits_header_once_and_groups_by_pessoa(self) -> None:
|
|
curso_2 = Curso.objects.create(
|
|
codigo_mec=456,
|
|
descricao="Direito",
|
|
carga_horaria_total=3600,
|
|
)
|
|
pessoa_a = self.make_pessoa(cpf="11111111111", nome="Ana")
|
|
pessoa_b = self.make_pessoa(cpf="22222222222", nome="Bruno")
|
|
self.make_vinculo(pessoa_a)
|
|
self.make_vinculo(pessoa_a, curso=curso_2, carga_horaria_total_aluno=3600)
|
|
self.make_vinculo(pessoa_b)
|
|
|
|
vinculos = list(
|
|
Vinculo.objects.filter(ano_censo=2025).order_by("pessoa__nome", "pessoa_id")
|
|
)
|
|
content = build_aluno_export_file(vinculos, ies_codigo=1131)
|
|
lines = content.splitlines()
|
|
|
|
self.assertEqual(lines[0], "40|1131")
|
|
record_types = [line.split("|")[0] for line in lines[1:]]
|
|
self.assertEqual(record_types, ["41", "42", "42", "41", "42"])
|
|
self.assertTrue(content.endswith("\n"))
|
|
|
|
def test_build_aluno_export_file_empty_queryset_is_header_only(self) -> None:
|
|
content = build_aluno_export_file(Vinculo.objects.none(), ies_codigo=1131)
|
|
|
|
self.assertEqual(content, "40|1131\n")
|
|
|
|
|
|
class ExportAlunosViewTests(AuthenticatedViewTestCase):
|
|
def setUp(self) -> None:
|
|
super().setUp()
|
|
self.uf = UF.objects.create(codigo=22, nome="Piaui")
|
|
self.municipio = Municipio.objects.create(
|
|
codigo=2211001,
|
|
nome="Teresina",
|
|
uf=self.uf,
|
|
)
|
|
self.pais = Pais.objects.create(codigo="BRA", nome="Brasil")
|
|
self.curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
carga_horaria_total=3200,
|
|
)
|
|
self.ano_2025, _ = AnoCenso.objects.get_or_create(ano=2025)
|
|
AnoCenso.objects.get_or_create(ano=2024)
|
|
self.client.post(
|
|
reverse("set_ano_censo"),
|
|
{"ano_censo": 2025, "next": reverse("home")},
|
|
)
|
|
|
|
def make_pessoa(self, cpf: str = "12345678901", **overrides: object) -> Pessoa:
|
|
defaults = dict(
|
|
nome="Maria Silva",
|
|
cpf=cpf,
|
|
data_nascimento=date(2000, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.PARDA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=self.pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
defaults.update(overrides)
|
|
return Pessoa.objects.create(**defaults)
|
|
|
|
def make_vinculo(self, pessoa: Pessoa, **overrides: object) -> Vinculo:
|
|
defaults = dict(
|
|
pessoa=pessoa,
|
|
ano_censo_id=2025,
|
|
curso=self.curso,
|
|
situacao=Vinculo.Situacao.CURSANDO,
|
|
semestre_ingresso="012025",
|
|
carga_horaria_total_aluno=3200,
|
|
carga_horaria_integralizada=0,
|
|
**REQUIRED_VINCULO_BOOL_DEFAULTS,
|
|
)
|
|
defaults.update(overrides)
|
|
return Vinculo.objects.create(**defaults)
|
|
|
|
@override_settings(CENSUP_IES_CODIGO="1131")
|
|
def test_export_requires_login(self) -> None:
|
|
self.client.logout()
|
|
response = self.client.get(reverse("export_alunos", args=["todos"]))
|
|
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
@override_settings(CENSUP_IES_CODIGO="1131")
|
|
def test_export_unknown_filtro_returns_404(self) -> None:
|
|
response = self.client.get(reverse("export_alunos", args=["bogus"]))
|
|
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
@override_settings(CENSUP_IES_CODIGO="")
|
|
def test_export_missing_ies_codigo_redirects_with_message(self) -> None:
|
|
response = self.client.get(reverse("export_alunos", args=["todos"]), follow=True)
|
|
|
|
self.assertRedirects(response, reverse("home"))
|
|
self.assertContains(response, "CENSUP_IES_CODIGO")
|
|
|
|
@override_settings(CENSUP_IES_CODIGO="1131")
|
|
def test_export_todos_includes_all_situacoes(self) -> None:
|
|
pessoa_a = self.make_pessoa(cpf="11111111111")
|
|
pessoa_b = self.make_pessoa(cpf="22222222222")
|
|
self.make_vinculo(pessoa_a, situacao=Vinculo.Situacao.CURSANDO)
|
|
self.make_vinculo(pessoa_b, situacao=Vinculo.Situacao.FORMADO)
|
|
|
|
response = self.client.get(reverse("export_alunos", args=["todos"]))
|
|
content = response.content.decode()
|
|
|
|
self.assertIn("11111111111", content)
|
|
self.assertIn("22222222222", content)
|
|
|
|
@override_settings(CENSUP_IES_CODIGO="1131")
|
|
def test_export_filters_by_situacao_cursando(self) -> None:
|
|
pessoa_a = self.make_pessoa(cpf="11111111111")
|
|
pessoa_b = self.make_pessoa(cpf="22222222222")
|
|
self.make_vinculo(pessoa_a, situacao=Vinculo.Situacao.CURSANDO)
|
|
self.make_vinculo(pessoa_b, situacao=Vinculo.Situacao.FORMADO)
|
|
|
|
response = self.client.get(reverse("export_alunos", args=["cursando"]))
|
|
content = response.content.decode()
|
|
|
|
self.assertIn("11111111111", content)
|
|
self.assertNotIn("22222222222", content)
|
|
|
|
@override_settings(CENSUP_IES_CODIGO="1131")
|
|
def test_export_ingressantes_filters_by_semestre_ingresso_year_suffix(self) -> None:
|
|
pessoa_a = self.make_pessoa(cpf="11111111111")
|
|
pessoa_b = self.make_pessoa(cpf="22222222222")
|
|
self.make_vinculo(pessoa_a, semestre_ingresso="012025")
|
|
self.make_vinculo(pessoa_b, semestre_ingresso="012020")
|
|
|
|
response = self.client.get(reverse("export_alunos", args=["ingressantes"]))
|
|
content = response.content.decode()
|
|
|
|
self.assertIn("11111111111", content)
|
|
self.assertNotIn("22222222222", content)
|
|
|
|
@override_settings(CENSUP_IES_CODIGO="1131")
|
|
def test_export_ingressantes_accepts_second_semester_suffix(self) -> None:
|
|
pessoa = self.make_pessoa()
|
|
self.make_vinculo(pessoa, semestre_ingresso="022025")
|
|
|
|
response = self.client.get(reverse("export_alunos", args=["ingressantes"]))
|
|
|
|
self.assertIn("12345678901", response.content.decode())
|
|
|
|
@override_settings(CENSUP_IES_CODIGO="1131")
|
|
def test_export_scopes_by_selected_ano_censo(self) -> None:
|
|
pessoa = self.make_pessoa()
|
|
self.make_vinculo(pessoa, ano_censo_id=2024)
|
|
|
|
response = self.client.get(reverse("export_alunos", args=["todos"]))
|
|
|
|
self.assertNotIn("12345678901", response.content.decode())
|
|
|
|
@override_settings(CENSUP_IES_CODIGO="1131")
|
|
def test_export_only_includes_matching_vinculos_not_all_of_pessoa(self) -> None:
|
|
curso_2 = Curso.objects.create(
|
|
codigo_mec=456,
|
|
descricao="Direito",
|
|
carga_horaria_total=3600,
|
|
)
|
|
pessoa = self.make_pessoa()
|
|
self.make_vinculo(pessoa, situacao=Vinculo.Situacao.CURSANDO)
|
|
self.make_vinculo(
|
|
pessoa,
|
|
curso=curso_2,
|
|
situacao=Vinculo.Situacao.DESVINCULADO,
|
|
carga_horaria_total_aluno=3600,
|
|
)
|
|
|
|
response = self.client.get(reverse("export_alunos", args=["cursando"]))
|
|
lines = response.content.decode().splitlines()
|
|
record_types = [line.split("|")[0] for line in lines[1:]]
|
|
|
|
self.assertEqual(record_types, ["41", "42"])
|
|
|
|
@override_settings(CENSUP_IES_CODIGO="1131")
|
|
def test_export_empty_queryset_returns_header_only(self) -> None:
|
|
response = self.client.get(reverse("export_alunos", args=["falecido"]))
|
|
|
|
self.assertEqual(response.content.decode(), "40|1131\n")
|
|
|
|
@override_settings(CENSUP_IES_CODIGO="1131")
|
|
def test_export_content_disposition_header_and_filename(self) -> None:
|
|
response = self.client.get(reverse("export_alunos", args=["cursando"]))
|
|
|
|
self.assertEqual(
|
|
response["Content-Disposition"],
|
|
'attachment; filename="aluno_exportacao_cursando_2025.txt"',
|
|
)
|
|
|
|
@override_settings(CENSUP_IES_CODIGO="1131")
|
|
def test_export_menu_links_present_on_home_page(self) -> None:
|
|
response = self.client.get(reverse("home"))
|
|
|
|
for filtro in (
|
|
"todos",
|
|
"ingressantes",
|
|
"desvinculado",
|
|
"cursando",
|
|
"transferido",
|
|
"falecido",
|
|
"formado",
|
|
):
|
|
self.assertContains(response, reverse("export_alunos", args=[filtro]))
|
|
|
|
@override_settings(CENSUP_IES_CODIGO="1131")
|
|
def test_export_output_is_reimportable(self) -> None:
|
|
pessoa = self.make_pessoa()
|
|
self.make_vinculo(pessoa)
|
|
|
|
response = self.client.get(reverse("export_alunos", args=["todos"]))
|
|
content = response.content.decode()
|
|
lines = [line for line in content.splitlines() if line.split("|")[0] != "40"]
|
|
|
|
result = import_alunos(lines, ano_censo=2025)
|
|
|
|
self.assertEqual(result.errors, [])
|
|
self.assertEqual(result.pessoas_created, 0)
|
|
self.assertEqual(result.pessoas_updated, 1)
|
|
self.assertEqual(result.vinculos_created, 0)
|
|
self.assertEqual(result.vinculos_updated, 1)
|
|
|
|
|
|
class ImportPessoasGenneraTests(AuthenticatedViewTestCase):
|
|
def setUp(self) -> None:
|
|
super().setUp()
|
|
self.pais = Pais.objects.create(codigo="BRA", nome="Brasil")
|
|
AnoCenso.objects.create(ano=2026)
|
|
|
|
def test_import_pessoas_ingressantes_from_gennera_creates_pessoa(self) -> None:
|
|
result = import_pessoas_ingressantes_from_gennera(
|
|
primeiro_ano=2026,
|
|
base_url="http://api.example.test",
|
|
token="token",
|
|
pages=[
|
|
(
|
|
1,
|
|
1,
|
|
1,
|
|
[
|
|
{
|
|
"aluno_com_deficiencia": None,
|
|
"cor_raca": None,
|
|
"cpf": "025.426.183-30",
|
|
"data_nascimento": "Fri, 03 Dec 2004 00:00:00 GMT",
|
|
"idPerson": 2824600,
|
|
"nacionalidade": "Brasil",
|
|
"nome": "Maria Silva",
|
|
"pais_origem": "Brasil",
|
|
"tipo_escola_ensino_medio": "Publico",
|
|
}
|
|
],
|
|
)
|
|
],
|
|
)
|
|
|
|
pessoa = Pessoa.objects.get(cpf="02542618330")
|
|
|
|
self.assertEqual(result.created, 1)
|
|
self.assertTrue(pessoa.anos_censo.filter(ano=2026).exists())
|
|
self.assertEqual(pessoa.nome, "Maria Silva")
|
|
self.assertEqual(pessoa.id_aluno_inep, 2824600)
|
|
self.assertEqual(pessoa.data_nascimento, date(2004, 12, 3))
|
|
self.assertEqual(pessoa.nacionalidade, Pessoa.Nacionalidade.BRASILEIRA_NATA)
|
|
self.assertEqual(pessoa.pais_origem, self.pais)
|
|
self.assertEqual(
|
|
pessoa.tipo_escola_ensino_medio,
|
|
Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
|
|
def test_import_pessoas_ingressantes_from_gennera_skips_existing_pessoa(self) -> None:
|
|
Pessoa.objects.create(
|
|
nome="Nome Existente",
|
|
cpf="02542618330",
|
|
data_nascimento=date(2000, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.PARDA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=self.pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
|
|
result = import_pessoas_ingressantes_from_gennera(
|
|
primeiro_ano=2026,
|
|
base_url="http://api.example.test",
|
|
token="token",
|
|
pages=[
|
|
(
|
|
1,
|
|
1,
|
|
1,
|
|
[
|
|
{
|
|
"aluno_com_deficiencia": None,
|
|
"cor_raca": None,
|
|
"cpf": "025.426.183-30",
|
|
"data_nascimento": "Fri, 03 Dec 2004 00:00:00 GMT",
|
|
"idPerson": 2824600,
|
|
"nacionalidade": "Brasil",
|
|
"nome": "Maria Silva",
|
|
"pais_origem": "Brasil",
|
|
"tipo_escola_ensino_medio": "Publico",
|
|
}
|
|
],
|
|
)
|
|
],
|
|
)
|
|
|
|
pessoa = Pessoa.objects.get(cpf="02542618330")
|
|
|
|
self.assertEqual(result.created, 0)
|
|
self.assertEqual(result.skipped, 1)
|
|
self.assertEqual(result.total, 1)
|
|
self.assertEqual(pessoa.nome, "Nome Existente")
|
|
self.assertIsNone(pessoa.id_aluno_inep)
|
|
|
|
def test_fetch_gennera_pessoas_ingressantes_page_sends_auth_token_header(self) -> None:
|
|
response = FakeGenneraResponse(
|
|
{"data": []}, headers={"X-Total-Count": "0", "X-Total-Pages": "1"}
|
|
)
|
|
|
|
with patch(
|
|
"core.importers._gennera_session.get", return_value=response
|
|
) as get_mock:
|
|
result, total_count, total_pages = fetch_gennera_pessoas_ingressantes_page(
|
|
base_url="http://api.example.test",
|
|
token="env-token",
|
|
primeiro_ano=2026,
|
|
page=1,
|
|
)
|
|
|
|
url = get_mock.call_args.args[0]
|
|
headers = get_mock.call_args.kwargs["headers"]
|
|
|
|
self.assertEqual(result, [])
|
|
self.assertEqual(total_count, 0)
|
|
self.assertEqual(total_pages, 1)
|
|
self.assertIn("/api/censup_person_ingressantes?", url)
|
|
self.assertIn("primeiroAno=2026", url)
|
|
self.assertEqual(headers["auth_token"], "env-token")
|
|
self.assertEqual(headers["User-Agent"], GENNERA_USER_AGENT)
|
|
|
|
def test_fetch_gennera_vinculos_censup_page_uses_censup_vinculos_endpoint(self) -> None:
|
|
response = FakeGenneraResponse(
|
|
{
|
|
"data": [
|
|
{
|
|
"cpf": "02542618330",
|
|
"turno": "Noite",
|
|
"referenceYear": "2026/1",
|
|
"course": "Pedagogia",
|
|
"course_code": "123",
|
|
"ano": 2026,
|
|
"carga_horaria_acumulada": 1600,
|
|
}
|
|
]
|
|
},
|
|
headers={"X-Total-Count": "1", "X-Total-Pages": "1"},
|
|
)
|
|
|
|
with patch(
|
|
"core.importers._gennera_session.get", return_value=response
|
|
) as get_mock:
|
|
result, total_count, total_pages = fetch_gennera_vinculos_censup_page(
|
|
base_url="http://api.example.test",
|
|
token="env-token",
|
|
cpf="02542618330",
|
|
page=1,
|
|
)
|
|
|
|
url = get_mock.call_args.args[0]
|
|
headers = get_mock.call_args.kwargs["headers"]
|
|
|
|
self.assertEqual(result[0]["course"], "Pedagogia")
|
|
self.assertEqual(total_count, 1)
|
|
self.assertEqual(total_pages, 1)
|
|
self.assertIn("/api/censup_vinculos?", url)
|
|
self.assertIn("cpf=02542618330", url)
|
|
self.assertEqual(headers["auth_token"], "env-token")
|
|
self.assertEqual(headers["User-Agent"], GENNERA_USER_AGENT)
|
|
|
|
def test_fetch_gennera_pessoas_ingressantes_page_retries_on_connection_error(
|
|
self,
|
|
) -> None:
|
|
response = FakeGenneraResponse(
|
|
{"data": []}, headers={"X-Total-Count": "0", "X-Total-Pages": "1"}
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"core.importers._gennera_session.get",
|
|
side_effect=[requests.ConnectionError("boom"), response],
|
|
) as get_mock,
|
|
patch("core.importers.time.sleep") as sleep_mock,
|
|
):
|
|
result, total_count, total_pages = fetch_gennera_pessoas_ingressantes_page(
|
|
base_url="http://api.example.test",
|
|
token="env-token",
|
|
primeiro_ano=2026,
|
|
page=1,
|
|
)
|
|
|
|
self.assertEqual(result, [])
|
|
self.assertEqual(total_count, 0)
|
|
self.assertEqual(total_pages, 1)
|
|
self.assertEqual(get_mock.call_count, 2)
|
|
sleep_mock.assert_called_once()
|
|
|
|
def test_fetch_gennera_pessoas_ingressantes_page_does_not_retry_on_http_error(
|
|
self,
|
|
) -> None:
|
|
response = FakeGenneraResponse({}, status_code=500, text="boom")
|
|
|
|
with patch(
|
|
"core.importers._gennera_session.get", return_value=response
|
|
) as get_mock:
|
|
with self.assertRaises(ValueError) as ctx:
|
|
fetch_gennera_pessoas_ingressantes_page(
|
|
base_url="http://api.example.test",
|
|
token="env-token",
|
|
primeiro_ano=2026,
|
|
page=1,
|
|
)
|
|
|
|
self.assertEqual(get_mock.call_count, 1)
|
|
self.assertIn("HTTP 500", str(ctx.exception))
|
|
self.assertIn("boom", str(ctx.exception))
|
|
|
|
def test_import_vinculos_from_gennera_uses_course_code_as_codigo_mec(self) -> None:
|
|
pessoa_com_vinculo = Pessoa.objects.create(
|
|
nome="Maria Silva",
|
|
cpf="02542618330",
|
|
data_nascimento=date(2000, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.PARDA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=self.pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
Pessoa.objects.create(
|
|
nome="Joao Sem Vinculo",
|
|
cpf="11122233344",
|
|
data_nascimento=date(2001, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.PARDA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=self.pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
carga_horaria_total=3200,
|
|
)
|
|
requested_cpfs = []
|
|
|
|
def fetcher(
|
|
base_url: str,
|
|
token: str,
|
|
ano: int,
|
|
cpf: str,
|
|
) -> list[dict[str, object]]:
|
|
requested_cpfs.append(cpf)
|
|
if cpf == pessoa_com_vinculo.cpf:
|
|
return [
|
|
{
|
|
"cpf": cpf,
|
|
"turno": "Noite",
|
|
"referenceYear": "2026/2",
|
|
"course": "Pedagogia",
|
|
"course_code": "123",
|
|
"ano": 2026,
|
|
"carga_horaria_acumulada": 1600,
|
|
}
|
|
]
|
|
return []
|
|
|
|
result = import_vinculos_from_gennera(
|
|
ano=2026,
|
|
base_url="http://api.example.test",
|
|
token="token",
|
|
ingressantes_fetcher=lambda *a, **k: ([], 0, 1),
|
|
fetcher=fetcher,
|
|
)
|
|
|
|
vinculo = Vinculo.objects.get(
|
|
pessoa=pessoa_com_vinculo,
|
|
curso=curso,
|
|
ano_censo=2026,
|
|
)
|
|
|
|
self.assertEqual(requested_cpfs, ["11122233344", "02542618330"])
|
|
self.assertEqual(result.pessoas_processed, 2)
|
|
self.assertEqual(result.pessoas_without_vinculo, 1)
|
|
self.assertEqual(result.created, 1)
|
|
self.assertTrue(pessoa_com_vinculo.anos_censo.filter(ano=2026).exists())
|
|
self.assertEqual(vinculo.situacao, Vinculo.Situacao.CURSANDO)
|
|
self.assertEqual(vinculo.turno, Vinculo.Turno.NOTURNO)
|
|
self.assertEqual(
|
|
vinculo.periodo_referencia,
|
|
Vinculo.PeriodoReferencia.SEGUNDO_SEMESTRE,
|
|
)
|
|
self.assertEqual(vinculo.semestre_ingresso, "022026")
|
|
self.assertEqual(vinculo.carga_horaria_total_aluno, 3200)
|
|
self.assertEqual(vinculo.carga_horaria_integralizada, 1600)
|
|
|
|
def test_import_vinculos_from_gennera_uses_oldest_record_for_semestre_ingresso(self) -> None:
|
|
pessoa = self.create_gennera_pessoa("Aluno Ingresso Antigo", "02542618330")
|
|
curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
carga_horaria_total=3200,
|
|
)
|
|
|
|
import_vinculos_from_gennera(
|
|
ano=2026,
|
|
base_url="http://api.example.test",
|
|
token="token",
|
|
ingressantes_fetcher=lambda *a, **k: ([], 0, 1),
|
|
fetcher=lambda base_url, token, ano, cpf: [
|
|
{
|
|
"cpf": cpf,
|
|
"turno": "Noite",
|
|
"referenceYear": "2026/2",
|
|
"course": "Pedagogia",
|
|
"course_code": "123",
|
|
"ano": 2026,
|
|
"carga_horaria_acumulada": 1600,
|
|
},
|
|
{
|
|
"cpf": cpf,
|
|
"turno": "Noite",
|
|
"referenceYear": "2026/1",
|
|
"course": "Pedagogia",
|
|
"course_code": "123",
|
|
"ano": 2026,
|
|
"carga_horaria_acumulada": 800,
|
|
},
|
|
],
|
|
)
|
|
|
|
vinculo = Vinculo.objects.get(pessoa=pessoa, curso=curso, ano_censo=2026)
|
|
|
|
self.assertEqual(
|
|
vinculo.periodo_referencia,
|
|
Vinculo.PeriodoReferencia.SEGUNDO_SEMESTRE,
|
|
)
|
|
self.assertEqual(vinculo.semestre_ingresso, "012026")
|
|
|
|
def test_import_vinculos_from_gennera_sets_formado_by_workload(self) -> None:
|
|
pessoa = self.create_gennera_pessoa("Aluno Formado", "02542618330")
|
|
curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
carga_horaria_total=3200,
|
|
)
|
|
|
|
result = import_vinculos_from_gennera(
|
|
ano=2026,
|
|
base_url="http://api.example.test",
|
|
token="token",
|
|
ingressantes_fetcher=lambda *a, **k: ([], 0, 1),
|
|
fetcher=lambda base_url, token, ano, cpf: [
|
|
{
|
|
"cpf": cpf,
|
|
"turno": "Noite",
|
|
"referenceYear": "2026/1",
|
|
"course": "Pedagogia",
|
|
"course_code": "123",
|
|
"ano": 2026,
|
|
"carga_horaria_acumulada": 3200,
|
|
},
|
|
{
|
|
"cpf": cpf,
|
|
"turno": "Noite",
|
|
"referenceYear": "2026/2",
|
|
"course": "Pedagogia",
|
|
"course_code": "123",
|
|
"ano": 2026,
|
|
"carga_horaria_acumulada": 3200,
|
|
},
|
|
],
|
|
)
|
|
|
|
vinculo = Vinculo.objects.get(pessoa=pessoa, curso=curso, ano_censo=2026)
|
|
|
|
self.assertEqual(result.created, 1)
|
|
self.assertEqual(vinculo.situacao, Vinculo.Situacao.FORMADO)
|
|
|
|
def test_import_vinculos_from_gennera_sets_desvinculado_for_semester_one_only(self) -> None:
|
|
pessoa = self.create_gennera_pessoa("Aluno Desvinculado", "02542618330")
|
|
curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
carga_horaria_total=3200,
|
|
)
|
|
|
|
import_vinculos_from_gennera(
|
|
ano=2026,
|
|
base_url="http://api.example.test",
|
|
token="token",
|
|
ingressantes_fetcher=lambda *a, **k: ([], 0, 1),
|
|
fetcher=lambda base_url, token, ano, cpf: [
|
|
{
|
|
"cpf": cpf,
|
|
"turno": "Noite",
|
|
"referenceYear": "2026/1",
|
|
"course": "Pedagogia",
|
|
"course_code": "123",
|
|
"ano": 2026,
|
|
"carga_horaria_acumulada": 1600,
|
|
}
|
|
],
|
|
)
|
|
|
|
vinculo = Vinculo.objects.get(pessoa=pessoa, curso=curso, ano_censo=2026)
|
|
|
|
self.assertEqual(vinculo.situacao, Vinculo.Situacao.DESVINCULADO)
|
|
|
|
def test_import_vinculos_from_gennera_requires_course_code(self) -> None:
|
|
self.create_gennera_pessoa("Aluno Sem Codigo Curso", "02542618330")
|
|
Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="ADMINISTRAÇÃO",
|
|
carga_horaria_total=3200,
|
|
)
|
|
|
|
result = import_vinculos_from_gennera(
|
|
ano=2025,
|
|
base_url="http://api.example.test",
|
|
token="token",
|
|
ingressantes_fetcher=lambda *a, **k: ([], 0, 1),
|
|
fetcher=lambda base_url, token, ano, cpf: [
|
|
{
|
|
"cpf": cpf,
|
|
"turno": "Noturno",
|
|
"referenceYear": "2025/2",
|
|
"course": "Bacharelado em Administração",
|
|
"course_code": None,
|
|
"ano": 2025,
|
|
"carga_horaria_acumulada": 774,
|
|
}
|
|
],
|
|
)
|
|
|
|
self.assertEqual(Vinculo.objects.count(), 0)
|
|
self.assertEqual(len(result.errors or []), 1)
|
|
self.assertIn("Codigo MEC do curso obrigatorio", result.errors[0].message)
|
|
|
|
def test_import_vinculos_ingressantes_from_gennera_tracks_only_successful_cpfs(
|
|
self,
|
|
) -> None:
|
|
self.create_gennera_pessoa("Aluno Ok", "02542618330")
|
|
self.create_gennera_pessoa("Aluno Sem Curso", "11122233344")
|
|
Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
carga_horaria_total=3200,
|
|
)
|
|
|
|
def fetcher(base_url, token, ano, page, page_size):
|
|
rows = [
|
|
{
|
|
"cpf": "02542618330",
|
|
"turno": "Noite",
|
|
"referenceYear": "2026/1",
|
|
"course": "Pedagogia",
|
|
"course_code": "123",
|
|
"ano": 2026,
|
|
"carga_horaria_acumulada": 800,
|
|
},
|
|
{
|
|
"cpf": "11122233344",
|
|
"turno": "Noite",
|
|
"referenceYear": "2026/1",
|
|
"course": "Curso Inexistente",
|
|
"course_code": None,
|
|
"ano": 2026,
|
|
"carga_horaria_acumulada": 100,
|
|
},
|
|
{
|
|
"cpf": "99988877766",
|
|
"turno": "Noite",
|
|
"referenceYear": "2026/1",
|
|
"course": "Pedagogia",
|
|
"course_code": "123",
|
|
"ano": 2026,
|
|
"carga_horaria_acumulada": 100,
|
|
},
|
|
]
|
|
return rows, len(rows), 1
|
|
|
|
result = import_vinculos_ingressantes_from_gennera(
|
|
ano=2026,
|
|
base_url="http://api.example.test",
|
|
token="token",
|
|
fetcher=fetcher,
|
|
)
|
|
|
|
self.assertEqual(result.cpfs_resolvidos, {"02542618330"})
|
|
self.assertEqual(result.pessoas_nao_encontradas, 1)
|
|
error_identifiers = {error.identifier for error in result.errors}
|
|
self.assertIn("99988877766", error_identifiers)
|
|
self.assertIn("11122233344", error_identifiers)
|
|
|
|
def test_import_vinculos_from_gennera_skips_per_cpf_refetch_for_bulk_resolved_cpf(
|
|
self,
|
|
) -> None:
|
|
self.create_gennera_pessoa("Aluno Bulk", "02542618330")
|
|
self.create_gennera_pessoa("Aluno Per Cpf", "11122233344")
|
|
Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
carga_horaria_total=3200,
|
|
)
|
|
|
|
def ingressantes_fetcher(base_url, token, ano, page, page_size):
|
|
rows = [
|
|
{
|
|
"cpf": "02542618330",
|
|
"turno": "Noite",
|
|
"referenceYear": "2026/1",
|
|
"course": "Pedagogia",
|
|
"course_code": "123",
|
|
"ano": 2026,
|
|
"carga_horaria_acumulada": 800,
|
|
}
|
|
]
|
|
return rows, len(rows), 1
|
|
|
|
requested_cpfs = []
|
|
|
|
def fetcher(base_url, token, ano, cpf):
|
|
requested_cpfs.append(cpf)
|
|
return []
|
|
|
|
import_vinculos_from_gennera(
|
|
ano=2026,
|
|
base_url="http://api.example.test",
|
|
token="token",
|
|
ingressantes_fetcher=ingressantes_fetcher,
|
|
fetcher=fetcher,
|
|
)
|
|
|
|
self.assertNotIn("02542618330", requested_cpfs)
|
|
self.assertIn("11122233344", requested_cpfs)
|
|
|
|
def test_import_vinculos_from_gennera_retries_bulk_cpf_that_failed_ingressantes_step(
|
|
self,
|
|
) -> None:
|
|
self.create_gennera_pessoa("Aluno Falha Bulk", "02542618330")
|
|
|
|
def ingressantes_fetcher(base_url, token, ano, page, page_size):
|
|
rows = [
|
|
{
|
|
"cpf": "02542618330",
|
|
"turno": "Noite",
|
|
"referenceYear": "2026/1",
|
|
"course": "Curso Inexistente",
|
|
"course_code": None,
|
|
"ano": 2026,
|
|
"carga_horaria_acumulada": 100,
|
|
}
|
|
]
|
|
return rows, len(rows), 1
|
|
|
|
requested_cpfs = []
|
|
|
|
def fetcher(base_url, token, ano, cpf):
|
|
requested_cpfs.append(cpf)
|
|
return []
|
|
|
|
import_vinculos_from_gennera(
|
|
ano=2026,
|
|
base_url="http://api.example.test",
|
|
token="token",
|
|
ingressantes_fetcher=ingressantes_fetcher,
|
|
fetcher=fetcher,
|
|
)
|
|
|
|
self.assertIn("02542618330", requested_cpfs)
|
|
|
|
def test_import_vinculos_from_gennera_keeps_existing_formado(self) -> None:
|
|
pessoa = self.create_gennera_pessoa("Aluno Formado", "02542618330")
|
|
curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
carga_horaria_total=3200,
|
|
)
|
|
Vinculo.objects.create(
|
|
pessoa=pessoa,
|
|
ano_censo_id=2026,
|
|
periodo_referencia=Vinculo.PeriodoReferencia.PRIMEIRO_SEMESTRE,
|
|
curso=curso,
|
|
situacao=Vinculo.Situacao.FORMADO,
|
|
semestre_ingresso="012026",
|
|
ingresso_vestibular=False,
|
|
ingresso_enem=False,
|
|
ingresso_avaliacao_seriada=False,
|
|
ingresso_selecao_simplificada=False,
|
|
ingresso_egresso_bi_li=False,
|
|
ingresso_pec_g=False,
|
|
ingresso_transferencia_ex_officio=False,
|
|
ingresso_decisao_judicial=False,
|
|
ingresso_vagas_remanescentes=False,
|
|
ingresso_programas_especiais=False,
|
|
apoio_social=False,
|
|
atividade_extracurricular=False,
|
|
carga_horaria_total_aluno=3200,
|
|
carga_horaria_integralizada=3200,
|
|
ingressou_acao_afirmativa=False,
|
|
)
|
|
|
|
import_vinculos_from_gennera(
|
|
ano=2026,
|
|
base_url="http://api.example.test",
|
|
token="token",
|
|
ingressantes_fetcher=lambda *a, **k: ([], 0, 1),
|
|
fetcher=lambda base_url, token, ano, cpf: [
|
|
{
|
|
"cpf": cpf,
|
|
"turno": "Noite",
|
|
"referenceYear": "2026/1",
|
|
"course": "Pedagogia",
|
|
"course_code": "123",
|
|
"ano": 2026,
|
|
"carga_horaria_acumulada": 1600,
|
|
}
|
|
],
|
|
)
|
|
|
|
vinculo = Vinculo.objects.get(pessoa=pessoa, curso=curso, ano_censo=2026)
|
|
|
|
self.assertEqual(vinculo.situacao, Vinculo.Situacao.FORMADO)
|
|
self.assertEqual(vinculo.carga_horaria_integralizada, 3200)
|
|
|
|
def test_import_carga_horaria_from_gennera_skips_formado_vinculo(self) -> None:
|
|
pessoa = self.create_gennera_pessoa("Aluno Formado", "02542618330")
|
|
curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
carga_horaria_total=3200,
|
|
)
|
|
vinculo = Vinculo.objects.create(
|
|
pessoa=pessoa,
|
|
ano_censo_id=2026,
|
|
matricula="MAT-1",
|
|
periodo_referencia=Vinculo.PeriodoReferencia.PRIMEIRO_SEMESTRE,
|
|
curso=curso,
|
|
situacao=Vinculo.Situacao.FORMADO,
|
|
semestre_ingresso="012026",
|
|
ingresso_vestibular=False,
|
|
ingresso_enem=False,
|
|
ingresso_avaliacao_seriada=False,
|
|
ingresso_selecao_simplificada=False,
|
|
ingresso_egresso_bi_li=False,
|
|
ingresso_pec_g=False,
|
|
ingresso_transferencia_ex_officio=False,
|
|
ingresso_decisao_judicial=False,
|
|
ingresso_vagas_remanescentes=False,
|
|
ingresso_programas_especiais=False,
|
|
apoio_social=False,
|
|
atividade_extracurricular=False,
|
|
carga_horaria_total_aluno=3200,
|
|
carga_horaria_integralizada=3200,
|
|
ingressou_acao_afirmativa=False,
|
|
)
|
|
|
|
result = import_carga_horaria_from_gennera(
|
|
base_url="http://api.example.test",
|
|
token="token",
|
|
fetcher=lambda base_url, token, matricula, page_size: [
|
|
{"ano": 2026, "carga_horaria": 999}
|
|
],
|
|
)
|
|
|
|
vinculo.refresh_from_db()
|
|
|
|
self.assertEqual(vinculo.carga_horaria_integralizada, 3200)
|
|
self.assertEqual(result.vinculos_atualizados, 0)
|
|
|
|
def test_import_vinculos_from_gennera_sets_desvinculado_when_no_remote_vinculo(self) -> None:
|
|
pessoa = self.create_gennera_pessoa("Aluno Sem Remoto", "02542618330")
|
|
curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
carga_horaria_total=3200,
|
|
)
|
|
Vinculo.objects.create(
|
|
pessoa=pessoa,
|
|
ano_censo_id=2026,
|
|
periodo_referencia=Vinculo.PeriodoReferencia.SEGUNDO_SEMESTRE,
|
|
curso=curso,
|
|
situacao=Vinculo.Situacao.CURSANDO,
|
|
semestre_ingresso="022026",
|
|
ingresso_vestibular=False,
|
|
ingresso_enem=False,
|
|
ingresso_avaliacao_seriada=False,
|
|
ingresso_selecao_simplificada=False,
|
|
ingresso_egresso_bi_li=False,
|
|
ingresso_pec_g=False,
|
|
ingresso_transferencia_ex_officio=False,
|
|
ingresso_decisao_judicial=False,
|
|
ingresso_vagas_remanescentes=False,
|
|
ingresso_programas_especiais=False,
|
|
apoio_social=False,
|
|
atividade_extracurricular=False,
|
|
carga_horaria_total_aluno=3200,
|
|
carga_horaria_integralizada=1600,
|
|
ingressou_acao_afirmativa=False,
|
|
)
|
|
|
|
result = import_vinculos_from_gennera(
|
|
ano=2026,
|
|
base_url="http://api.example.test",
|
|
token="token",
|
|
ingressantes_fetcher=lambda *a, **k: ([], 0, 1),
|
|
fetcher=lambda base_url, token, ano, cpf: [],
|
|
)
|
|
|
|
vinculo = Vinculo.objects.get(pessoa=pessoa, curso=curso, ano_censo=2026)
|
|
|
|
self.assertEqual(result.pessoas_without_vinculo, 1)
|
|
self.assertEqual(vinculo.situacao, Vinculo.Situacao.DESVINCULADO)
|
|
|
|
def test_import_vinculos_from_gennera_ignores_remote_vinculo_from_other_year(self) -> None:
|
|
pessoa = self.create_gennera_pessoa("Aluno Outro Ano", "02542618330")
|
|
curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
carga_horaria_total=3200,
|
|
)
|
|
Vinculo.objects.create(
|
|
pessoa=pessoa,
|
|
ano_censo_id=2026,
|
|
periodo_referencia=Vinculo.PeriodoReferencia.SEGUNDO_SEMESTRE,
|
|
curso=curso,
|
|
situacao=Vinculo.Situacao.CURSANDO,
|
|
semestre_ingresso="022026",
|
|
ingresso_vestibular=False,
|
|
ingresso_enem=False,
|
|
ingresso_avaliacao_seriada=False,
|
|
ingresso_selecao_simplificada=False,
|
|
ingresso_egresso_bi_li=False,
|
|
ingresso_pec_g=False,
|
|
ingresso_transferencia_ex_officio=False,
|
|
ingresso_decisao_judicial=False,
|
|
ingresso_vagas_remanescentes=False,
|
|
ingresso_programas_especiais=False,
|
|
apoio_social=False,
|
|
atividade_extracurricular=False,
|
|
carga_horaria_total_aluno=3200,
|
|
carga_horaria_integralizada=1600,
|
|
ingressou_acao_afirmativa=False,
|
|
)
|
|
|
|
result = import_vinculos_from_gennera(
|
|
ano=2026,
|
|
base_url="http://api.example.test",
|
|
token="token",
|
|
ingressantes_fetcher=lambda *a, **k: ([], 0, 1),
|
|
fetcher=lambda base_url, token, ano, cpf: [
|
|
{
|
|
"cpf": cpf,
|
|
"turno": "Noite",
|
|
"referenceYear": "2025/2",
|
|
"course": "Pedagogia",
|
|
"course_code": "123",
|
|
"ano": 2025,
|
|
"carga_horaria_acumulada": 3200,
|
|
}
|
|
],
|
|
)
|
|
|
|
vinculo = Vinculo.objects.get(pessoa=pessoa, curso=curso, ano_censo=2026)
|
|
|
|
self.assertEqual(result.pessoas_without_vinculo, 1)
|
|
self.assertEqual(vinculo.situacao, Vinculo.Situacao.DESVINCULADO)
|
|
|
|
def test_import_vinculos_from_gennera_does_not_overwrite_manually_edited_vinculo(
|
|
self,
|
|
) -> None:
|
|
pessoa = self.create_gennera_pessoa("Aluno Editado Manualmente", "02542618330")
|
|
curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
carga_horaria_total=3200,
|
|
)
|
|
Vinculo.objects.create(
|
|
pessoa=pessoa,
|
|
ano_censo_id=2026,
|
|
curso=curso,
|
|
situacao=Vinculo.Situacao.TRANSFERENCIA_INTERNA,
|
|
semestre_ingresso="012026",
|
|
atualizado_manualmente=True,
|
|
ingresso_vestibular=False,
|
|
ingresso_enem=False,
|
|
ingresso_avaliacao_seriada=False,
|
|
ingresso_selecao_simplificada=False,
|
|
ingresso_egresso_bi_li=False,
|
|
ingresso_pec_g=False,
|
|
ingresso_transferencia_ex_officio=False,
|
|
ingresso_decisao_judicial=False,
|
|
ingresso_vagas_remanescentes=False,
|
|
ingresso_programas_especiais=False,
|
|
apoio_social=False,
|
|
atividade_extracurricular=False,
|
|
carga_horaria_total_aluno=3200,
|
|
carga_horaria_integralizada=1600,
|
|
ingressou_acao_afirmativa=False,
|
|
)
|
|
|
|
import_vinculos_from_gennera(
|
|
ano=2026,
|
|
base_url="http://api.example.test",
|
|
token="token",
|
|
ingressantes_fetcher=lambda *a, **k: ([], 0, 1),
|
|
fetcher=lambda base_url, token, ano, cpf: [
|
|
{
|
|
"cpf": cpf,
|
|
"turno": "Noite",
|
|
"referenceYear": "2026/2",
|
|
"course": "Pedagogia",
|
|
"course_code": "123",
|
|
"ano": 2026,
|
|
"carga_horaria_acumulada": 3200,
|
|
}
|
|
],
|
|
)
|
|
|
|
vinculo = Vinculo.objects.get(pessoa=pessoa, curso=curso, ano_censo=2026)
|
|
self.assertEqual(vinculo.situacao, Vinculo.Situacao.TRANSFERENCIA_INTERNA)
|
|
self.assertEqual(vinculo.carga_horaria_integralizada, 1600)
|
|
|
|
def test_import_vinculos_from_gennera_desvinculado_skips_manually_edited(self) -> None:
|
|
pessoa = self.create_gennera_pessoa("Aluno Sem Remoto Manual", "02542618330")
|
|
curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
carga_horaria_total=3200,
|
|
)
|
|
Vinculo.objects.create(
|
|
pessoa=pessoa,
|
|
ano_censo_id=2026,
|
|
curso=curso,
|
|
situacao=Vinculo.Situacao.CURSANDO,
|
|
semestre_ingresso="012026",
|
|
atualizado_manualmente=True,
|
|
ingresso_vestibular=False,
|
|
ingresso_enem=False,
|
|
ingresso_avaliacao_seriada=False,
|
|
ingresso_selecao_simplificada=False,
|
|
ingresso_egresso_bi_li=False,
|
|
ingresso_pec_g=False,
|
|
ingresso_transferencia_ex_officio=False,
|
|
ingresso_decisao_judicial=False,
|
|
ingresso_vagas_remanescentes=False,
|
|
ingresso_programas_especiais=False,
|
|
apoio_social=False,
|
|
atividade_extracurricular=False,
|
|
carga_horaria_total_aluno=3200,
|
|
carga_horaria_integralizada=1600,
|
|
ingressou_acao_afirmativa=False,
|
|
)
|
|
|
|
import_vinculos_from_gennera(
|
|
ano=2026,
|
|
base_url="http://api.example.test",
|
|
token="token",
|
|
ingressantes_fetcher=lambda *a, **k: ([], 0, 1),
|
|
fetcher=lambda base_url, token, ano, cpf: [],
|
|
)
|
|
|
|
vinculo = Vinculo.objects.get(pessoa=pessoa, curso=curso, ano_censo=2026)
|
|
self.assertEqual(vinculo.situacao, Vinculo.Situacao.CURSANDO)
|
|
|
|
def test_import_vinculos_from_gennera_fixes_situacao_nao_informada(self) -> None:
|
|
pessoa = self.create_gennera_pessoa("Aluno Nao Informado", "02542618330")
|
|
curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
carga_horaria_total=3200,
|
|
)
|
|
# Outro curso com dados remotos, so a API nao "zera" o pessoa via
|
|
# mark_person_vinculos_as_desvinculado (que so roda quando a API nao
|
|
# retorna NENHUM vinculo pra pessoa) -- assim o vinculo NAO_INFORMADA
|
|
# abaixo fica intocado pelo loop principal e so a nova etapa final o pega.
|
|
Curso.objects.create(
|
|
codigo_mec=999,
|
|
descricao="Direito",
|
|
carga_horaria_total=3600,
|
|
)
|
|
vinculo = Vinculo.objects.create(
|
|
pessoa=pessoa,
|
|
ano_censo_id=2026,
|
|
curso=curso,
|
|
matricula="MAT-1",
|
|
situacao=Vinculo.Situacao.NAO_INFORMADA,
|
|
semestre_ingresso="012026",
|
|
ingresso_vestibular=False,
|
|
ingresso_enem=False,
|
|
ingresso_avaliacao_seriada=False,
|
|
ingresso_selecao_simplificada=False,
|
|
ingresso_egresso_bi_li=False,
|
|
ingresso_pec_g=False,
|
|
ingresso_transferencia_ex_officio=False,
|
|
ingresso_decisao_judicial=False,
|
|
ingresso_vagas_remanescentes=False,
|
|
ingresso_programas_especiais=False,
|
|
apoio_social=False,
|
|
atividade_extracurricular=False,
|
|
carga_horaria_total_aluno=3200,
|
|
carga_horaria_integralizada=0,
|
|
ingressou_acao_afirmativa=False,
|
|
)
|
|
|
|
with patch("core.importers.fetch_gennera_carga_horaria") as fetch_mock:
|
|
fetch_mock.return_value = [{"ano": 2026, "carga_horaria": 1800}]
|
|
result = import_vinculos_from_gennera(
|
|
ano=2026,
|
|
base_url="http://api.example.test",
|
|
token="token",
|
|
ingressantes_fetcher=lambda *a, **k: ([], 0, 1),
|
|
fetcher=lambda base_url, token, ano, cpf: [
|
|
{
|
|
"cpf": cpf,
|
|
"turno": "Noite",
|
|
"referenceYear": "2026/1",
|
|
"course": "Direito",
|
|
"course_code": "999",
|
|
"ano": 2026,
|
|
"carga_horaria_acumulada": 100,
|
|
}
|
|
],
|
|
)
|
|
|
|
vinculo.refresh_from_db()
|
|
self.assertEqual(vinculo.situacao, Vinculo.Situacao.DESVINCULADO)
|
|
self.assertEqual(vinculo.carga_horaria_integralizada, 1800)
|
|
self.assertEqual(result.situacao_nao_informada_corrigidos, 1)
|
|
fetch_mock.assert_called_once_with("http://api.example.test", "token", "MAT-1")
|
|
|
|
def test_import_vinculos_from_gennera_skips_manually_edited_nao_informada(self) -> None:
|
|
pessoa = self.create_gennera_pessoa("Aluno Nao Informado Manual", "02542618330")
|
|
curso = Curso.objects.create(
|
|
codigo_mec=123,
|
|
descricao="Pedagogia",
|
|
carga_horaria_total=3200,
|
|
)
|
|
vinculo = Vinculo.objects.create(
|
|
pessoa=pessoa,
|
|
ano_censo_id=2026,
|
|
curso=curso,
|
|
matricula="MAT-1",
|
|
situacao=Vinculo.Situacao.NAO_INFORMADA,
|
|
semestre_ingresso="012026",
|
|
atualizado_manualmente=True,
|
|
ingresso_vestibular=False,
|
|
ingresso_enem=False,
|
|
ingresso_avaliacao_seriada=False,
|
|
ingresso_selecao_simplificada=False,
|
|
ingresso_egresso_bi_li=False,
|
|
ingresso_pec_g=False,
|
|
ingresso_transferencia_ex_officio=False,
|
|
ingresso_decisao_judicial=False,
|
|
ingresso_vagas_remanescentes=False,
|
|
ingresso_programas_especiais=False,
|
|
apoio_social=False,
|
|
atividade_extracurricular=False,
|
|
carga_horaria_total_aluno=3200,
|
|
carga_horaria_integralizada=0,
|
|
ingressou_acao_afirmativa=False,
|
|
)
|
|
|
|
with patch("core.importers.fetch_gennera_carga_horaria") as fetch_mock:
|
|
result = import_vinculos_from_gennera(
|
|
ano=2026,
|
|
base_url="http://api.example.test",
|
|
token="token",
|
|
ingressantes_fetcher=lambda *a, **k: ([], 0, 1),
|
|
fetcher=lambda base_url, token, ano, cpf: [],
|
|
)
|
|
|
|
vinculo.refresh_from_db()
|
|
self.assertEqual(vinculo.situacao, Vinculo.Situacao.NAO_INFORMADA)
|
|
self.assertEqual(vinculo.carga_horaria_integralizada, 0)
|
|
self.assertEqual(result.situacao_nao_informada_corrigidos, 0)
|
|
fetch_mock.assert_not_called()
|
|
|
|
def create_gennera_pessoa(self, nome: str, cpf: str) -> Pessoa:
|
|
return Pessoa.objects.create(
|
|
nome=nome,
|
|
cpf=cpf,
|
|
data_nascimento=date(2000, 1, 1),
|
|
cor_raca=Pessoa.CorRaca.PARDA,
|
|
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
|
|
pais_origem=self.pais,
|
|
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
|
|
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
|
|
)
|
|
|
|
def test_task_pages_are_in_menu(self) -> None:
|
|
response = self.client.get(reverse("home"))
|
|
|
|
self.assertContains(response, reverse("task_page", args=["pessoas-ingressantes"]))
|
|
self.assertContains(response, reverse("task_page", args=["vinculos-censup"]))
|
|
self.assertContains(response, reverse("task_page", args=["carga-horaria-censup"]))
|
|
|
|
def test_start_task_view_dispatches_pessoas_ingressantes_task(self) -> None:
|
|
with patch(
|
|
"core.views.import_pessoas_ingressantes_task.delay"
|
|
) as delay_mock:
|
|
delay_mock.return_value.id = "task-123"
|
|
response = self.client.post(
|
|
reverse("start_task", args=["pessoas-ingressantes"]),
|
|
{"primeiro_ano": "2026"},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
delay_mock.assert_called_once_with(primeiro_ano=2026, page_size=100)
|
|
self.assertEqual(response.json(), {"task_id": "task-123", "already_running": False})
|
|
|
|
def test_start_task_blocks_second_start_while_running(self) -> None:
|
|
with patch("core.views.import_carga_horaria_censup_task.delay") as delay_mock:
|
|
delay_mock.return_value.id = "task-abc"
|
|
first = self.client.post(reverse("start_task", args=["carga-horaria-censup"]))
|
|
|
|
with patch("core.views.AsyncResult") as async_result_cls:
|
|
async_result_cls.return_value.state = "PROGRESS"
|
|
second = self.client.post(reverse("start_task", args=["carga-horaria-censup"]))
|
|
|
|
self.assertEqual(first.json()["task_id"], "task-abc")
|
|
self.assertEqual(second.json(), {"task_id": "task-abc", "already_running": True})
|