autocensup/core/tests.py

2550 lines
96 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 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})