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, "
1 | ", count=5)
self.assertContains(response, "bg-yellow-lt text-yellow")
self.assertContains(response, "6 | ")
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 test_import_cursos_from_cursos_csv_without_support_file(self) -> None:
Curso.objects.create(
codigo_mec=123,
descricao="Descricao antiga",
carga_horaria_total=3200,
)
csv_text = (
"\ufeffCODIGO_IES;NOME_IES;CODIGO_CURSO;NOME_CURSO;"
"CURSO_ATUALIZADO;SITUACAO_FUNCIONAMENTO_CURSO;FORMATO_OFERTA;GRAU_ACADEMICO\n"
"1131;IES;123;PEDAGOGIA;Sim;Em atividade;EaD;Licenciatura\n"
"1131;IES;456;ENGENHARIA;Sim;Em extinção;Presencial;Bacharelado\n"
)
result = import_cursos(csv_text=csv_text)
curso_atualizado = Curso.objects.get(codigo_mec=123)
curso_criado = Curso.objects.get(codigo_mec=456)
self.assertEqual(result.total, 2)
self.assertEqual(result.created, 1)
self.assertEqual(result.updated, 1)
self.assertEqual(result.without_carga_horaria, 2)
self.assertEqual(curso_atualizado.descricao, "PEDAGOGIA")
self.assertEqual(curso_atualizado.carga_horaria_total, 3200)
self.assertTrue(curso_atualizado.licenciatura)
self.assertTrue(curso_atualizado.ead)
self.assertTrue(curso_atualizado.exportar)
self.assertEqual(curso_criado.carga_horaria_total, 0)
self.assertFalse(curso_criado.licenciatura)
self.assertFalse(curso_criado.ead)
self.assertFalse(curso_criado.exportar)
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)
class ImportCursosViewTests(AuthenticatedViewTestCase):
def test_import_cursos_view_accepts_upload(self) -> None:
uploaded = SimpleUploadedFile(
"cursos.csv",
(
"\ufeffCODIGO_IES;NOME_IES;CODIGO_CURSO;NOME_CURSO;"
"CURSO_ATUALIZADO;SITUACAO_FUNCIONAMENTO_CURSO;FORMATO_OFERTA;GRAU_ACADEMICO\n"
"1131;IES;123;PEDAGOGIA;Sim;Em atividade;EaD;Licenciatura\n"
).encode("utf-8"),
content_type="text/csv",
)
response = self.client.post(
reverse("import_cursos"),
{"arquivo": uploaded},
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Cursos criados")
self.assertEqual(Curso.objects.count(), 1)
curso = Curso.objects.get(codigo_mec=123)
self.assertEqual(curso.descricao, "PEDAGOGIA")
self.assertTrue(curso.licenciatura)
self.assertTrue(curso.ead)
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})