autocensup/core/tests.py

1833 lines
69 KiB
Python

from datetime import date
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import patch
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_cursos,
import_pessoa_record,
import_pessoas_ingressantes_from_gennera,
import_vinculo_record,
import_vinculos_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 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_pending_required_fields_view_lists_courses_without_carga(self) -> None:
Curso.objects.create(
codigo_mec=123,
descricao="Curso sem carga",
carga_horaria_total=0,
)
response = self.client.get(reverse("pending_required_fields"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Curso sem carga")
self.assertContains(response, "Carga Horaria Total")
def test_course_list_uses_check_icon_for_true_boolean(self) -> None:
Curso.objects.create(
codigo_mec=123,
descricao="Curso EaD",
situacao_funcionamento_curso="Em atividade",
ead=True,
carga_horaria_total=1000,
)
response = self.client.get(reverse("crud_list", kwargs={"slug": "cursos"}))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "ti ti-check")
self.assertContains(response, "bg-green-lt text-green")
self.assertContains(response, "Em atividade")
def test_course_list_uses_yellow_badge_for_extinction_status(self) -> None:
Curso.objects.create(
codigo_mec=123,
descricao="Curso em extincao",
situacao_funcionamento_curso="Em extinção",
carga_horaria_total=1000,
)
response = self.client.get(reverse("crud_list", kwargs={"slug": "cursos"}))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "bg-yellow-lt text-yellow")
self.assertContains(response, "Em extinção")
class ImportCursosTests(TestCase):
def test_load_max_carga_horaria_by_curso(self) -> None:
with TemporaryDirectory() as tmp_dir:
alunos_path = Path(tmp_dir) / "alunos.txt"
alunos_path.write_text(
"\n".join(
[
"40|1131",
self.vinculo_line(codigo_curso=123, carga_horaria=2400),
self.vinculo_line(codigo_curso=123, carga_horaria=2600),
self.vinculo_line(codigo_curso=456, carga_horaria=1800),
]
),
encoding="utf-8",
)
cargas = load_max_carga_horaria_by_curso(alunos_path)
self.assertEqual(cargas[123], 2600)
self.assertEqual(cargas[456], 1800)
def test_import_cursos_creates_and_updates_courses(self) -> None:
Curso.objects.create(
codigo_mec=123,
descricao="Descricao antiga",
carga_horaria_total=1,
)
with TemporaryDirectory() as tmp_dir:
tmp_path = Path(tmp_dir)
csv_path = tmp_path / "cursos.csv"
alunos_path = tmp_path / "alunos.txt"
csv_path.write_text(
"\ufeffCODIGO_CURSO;NOME_CURSO;SITUACAO_FUNCIONAMENTO_CURSO;FORMATO_OFERTA;GRAU_ACADEMICO\n"
"123;PEDAGOGIA;Em atividade;EaD;Licenciatura\n"
"456;ENGENHARIA;Em extincao;Presencial;Bacharelado\n"
"789;GESTAO;Em atividade;Semipresencial;Tecnológico\n",
encoding="utf-8",
)
alunos_path.write_text(
"\n".join(
[
self.vinculo_line(codigo_curso=123, carga_horaria=3000),
self.vinculo_line(codigo_curso=123, carga_horaria=3200),
self.vinculo_line(codigo_curso=456, carga_horaria=4000),
]
),
encoding="utf-8",
)
result = import_cursos(csv_path=csv_path, alunos_path=alunos_path)
curso_atualizado = Curso.objects.get(codigo_mec=123)
curso_criado = Curso.objects.get(codigo_mec=456)
curso_sem_carga = Curso.objects.get(codigo_mec=789)
self.assertEqual(result.total, 3)
self.assertEqual(result.created, 2)
self.assertEqual(result.updated, 1)
self.assertEqual(result.without_carga_horaria, 1)
self.assertEqual(curso_atualizado.descricao, "PEDAGOGIA")
self.assertEqual(curso_atualizado.situacao_funcionamento_curso, "Em atividade")
self.assertTrue(curso_atualizado.licenciatura)
self.assertTrue(curso_atualizado.ead)
self.assertEqual(curso_atualizado.carga_horaria_total, 3200)
self.assertFalse(curso_criado.licenciatura)
self.assertFalse(curso_criado.ead)
self.assertEqual(curso_criado.carga_horaria_total, 4000)
self.assertEqual(curso_sem_carga.carga_horaria_total, 0)
def vinculo_line(self, codigo_curso: int, carga_horaria: int) -> str:
fields = [""] * 73
fields[0] = "42"
fields[3] = str(codigo_curso)
fields[55] = str(carga_horaria)
return "|".join(fields)
class ImportPolosTests(TestCase):
def test_import_polos_creates_polos_and_links_ead_courses(self) -> None:
Curso.objects.create(
codigo_mec=123,
descricao="Curso EaD",
ead=True,
carga_horaria_total=2000,
)
Curso.objects.create(
codigo_mec=456,
descricao="Curso Presencial",
ead=False,
carga_horaria_total=2000,
)
with TemporaryDirectory() as tmp_dir:
csv_path = Path(tmp_dir) / "cursos_locais.csv"
csv_path.write_text(
"\ufeffCODIGOCURSO;CODIGOLOCALOFERTA;NOMELOCALOFERTA\n"
"123;10;Polo Centro\n"
"456;20;Unidade Presencial\n",
encoding="utf-8",
)
result = import_polos(csv_path)
curso_ead = Curso.objects.get(codigo_mec=123)
curso_presencial = Curso.objects.get(codigo_mec=456)
self.assertEqual(result.created, 2)
self.assertEqual(result.cursos_vinculados, 1)
self.assertEqual(curso_ead.polo.descricao, "Polo Centro")
self.assertIsNone(curso_presencial.polo)
class ProcessarSituacaoTests(AuthenticatedViewTestCase):
def setUp(self) -> None:
super().setUp()
self.year = timezone.localdate().year - 1
self.pais = Pais.objects.create(codigo="BRA", nome="Brasil")
self.curso = Curso.objects.create(
codigo_mec=123,
descricao="Pedagogia",
situacao_funcionamento_curso="Em atividade",
carga_horaria_total=3200,
)
def test_processar_situacao_link_is_in_import_menu(self) -> None:
response = self.client.get(reverse("home"))
self.assertContains(response, "Importacoes")
self.assertContains(response, "Processar Situacao")
self.assertContains(response, reverse("processar_situacao"))
def test_processar_situacao_updates_selected_year_by_matricula(self) -> None:
selected_year_vinculo = self.create_vinculo(
matricula="IES-1",
cpf="12345678901",
ano_censo=self.year,
)
second_selected_year_vinculo = self.create_vinculo(
matricula="IES-2",
cpf="12345678902",
ano_censo=self.year,
)
other_year_vinculo = self.create_vinculo(
matricula="IES-1",
cpf="12345678903",
ano_censo=self.year - 1,
)
response = self.client.post(
reverse("processar_situacao"),
{
"matriculas": "IES-1\nIES-2\nNAO-EXISTE",
"situacao": Vinculo.Situacao.FORMADO,
},
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Vinculos atualizados")
self.assertContains(response, "NAO-EXISTE")
selected_year_vinculo.refresh_from_db()
second_selected_year_vinculo.refresh_from_db()
other_year_vinculo.refresh_from_db()
self.assertEqual(selected_year_vinculo.situacao, Vinculo.Situacao.FORMADO)
self.assertEqual(second_selected_year_vinculo.situacao, Vinculo.Situacao.FORMADO)
self.assertEqual(other_year_vinculo.situacao, Vinculo.Situacao.CURSANDO)
def create_vinculo(
self,
matricula: str,
cpf: str,
ano_censo: int,
) -> Vinculo:
pessoa = Pessoa.objects.create(
nome=f"Aluno {cpf}",
cpf=cpf,
data_nascimento=date(2000, 1, 1),
cor_raca=Pessoa.CorRaca.PARDA,
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
pais_origem=self.pais,
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
)
return Vinculo.objects.create(
pessoa=pessoa,
ano_censo_id=ano_censo,
matricula=matricula,
periodo_referencia=Vinculo.PeriodoReferencia.PRIMEIRO_SEMESTRE,
curso=self.curso,
turno=Vinculo.Turno.NOTURNO,
situacao=Vinculo.Situacao.CURSANDO,
semestre_ingresso=f"01{ano_censo}",
ingresso_vestibular=True,
ingresso_enem=False,
ingresso_avaliacao_seriada=False,
ingresso_selecao_simplificada=False,
ingresso_egresso_bi_li=False,
ingresso_pec_g=False,
ingresso_transferencia_ex_officio=False,
ingresso_decisao_judicial=False,
ingresso_vagas_remanescentes=False,
ingresso_programas_especiais=False,
apoio_social=False,
atividade_extracurricular=False,
carga_horaria_total_aluno=3200,
carga_horaria_integralizada=120,
ingressou_acao_afirmativa=False,
)
class ImportAlunosTests(AuthenticatedViewTestCase):
def setUp(self) -> None:
super().setUp()
self.uf = UF.objects.create(codigo=22, nome="Piaui")
self.municipio = Municipio.objects.create(
codigo=2211001,
nome="Teresina",
uf=self.uf,
)
self.pais = Pais.objects.create(codigo="BRA", nome="Brasil")
self.curso = Curso.objects.create(
codigo_mec=123,
descricao="Pedagogia",
situacao_funcionamento_curso="Em atividade",
licenciatura=True,
ead=False,
carga_horaria_total=3200,
)
def test_import_alunos_creates_pessoa_and_vinculo(self) -> None:
result = import_alunos(
[
self.pessoa_line(nome="Maria Silva", nascimento="01012000"),
self.vinculo_line(codigo_curso=123, carga_horaria=9999),
],
ano_censo=2025,
)
pessoa = Pessoa.objects.get(cpf="12345678901")
vinculo = Vinculo.objects.get(pessoa=pessoa, curso=self.curso, ano_censo=2025)
self.assertEqual(result.pessoas_created, 1)
self.assertEqual(result.vinculos_created, 1)
self.assertTrue(pessoa.anos_censo.filter(ano=2025).exists())
self.assertEqual(vinculo.carga_horaria_total_aluno, 3200)
self.assertEqual(vinculo.situacao, Vinculo.Situacao.NAO_INFORMADA)
def test_import_alunos_updates_pessoa_but_preserves_name_and_birth_date(self) -> None:
Pessoa.objects.create(
nome="Nome Original",
cpf="12345678901",
data_nascimento=date(1999, 1, 1),
cor_raca=Pessoa.CorRaca.BRANCA,
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
pais_origem=self.pais,
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PRIVADA,
)
result = import_alunos(
[
self.pessoa_line(nome="Nome Novo", nascimento="02022000"),
],
ano_censo=2025,
)
pessoa = Pessoa.objects.get(cpf="12345678901")
self.assertEqual(result.pessoas_updated, 1)
self.assertTrue(pessoa.anos_censo.filter(ano=2025).exists())
self.assertEqual(pessoa.nome, "Nome Original")
self.assertEqual(pessoa.data_nascimento, date(1999, 1, 1))
self.assertEqual(pessoa.cor_raca, Pessoa.CorRaca.PARDA)
def test_import_alunos_view_accepts_upload(self) -> None:
uploaded = SimpleUploadedFile(
"aluno_exportacao.txt",
(
self.pessoa_line(nome="Maria Silva", nascimento="01012000")
+ "\n"
+ self.vinculo_line(codigo_curso=123, carga_horaria=9999)
).encode("utf-8"),
content_type="text/plain",
)
response = self.client.post(
reverse("import_alunos"),
{"ano_censo": 2025, "arquivo": uploaded},
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Pessoas criadas")
self.assertEqual(Pessoa.objects.count(), 1)
self.assertEqual(Vinculo.objects.count(), 1)
def pessoa_line(self, nome: str, nascimento: str) -> str:
return "|".join(
[
"41",
"1",
nome,
"12345678901",
"",
nascimento,
"3",
"1",
"22",
"2211001",
"BRA",
"0",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"1",
]
)
def vinculo_line(self, codigo_curso: int, carga_horaria: int) -> str:
fields = [""] * 73
fields[0] = "42"
fields[1] = "IES-1"
fields[3] = str(codigo_curso)
fields[6] = "-1"
fields[12] = "012025"
for index in range(13, 23):
fields[index] = "0"
fields[46] = "0"
fields[55] = str(carga_horaria)
fields[57] = "0"
return "|".join(fields)
REQUIRED_VINCULO_BOOL_DEFAULTS = {
"ingresso_vestibular": True,
"ingresso_enem": False,
"ingresso_avaliacao_seriada": False,
"ingresso_selecao_simplificada": False,
"ingresso_egresso_bi_li": False,
"ingresso_pec_g": False,
"ingresso_transferencia_ex_officio": False,
"ingresso_decisao_judicial": False,
"ingresso_vagas_remanescentes": False,
"ingresso_programas_especiais": False,
"apoio_social": False,
"atividade_extracurricular": False,
"ingressou_acao_afirmativa": False,
}
class ExportAlunoRecordTests(TestCase):
def setUp(self) -> None:
self.uf = UF.objects.create(codigo=22, nome="Piaui")
self.municipio = Municipio.objects.create(
codigo=2211001,
nome="Teresina",
uf=self.uf,
)
self.pais = Pais.objects.create(codigo="BRA", nome="Brasil")
self.curso = Curso.objects.create(
codigo_mec=123,
descricao="Pedagogia",
situacao_funcionamento_curso="Em atividade",
licenciatura=False,
ead=False,
carga_horaria_total=3200,
)
AnoCenso.objects.get_or_create(ano=2025)
def make_pessoa(self, **overrides: object) -> Pessoa:
defaults = dict(
nome="Maria Silva",
cpf="12345678901",
data_nascimento=date(2000, 1, 1),
cor_raca=Pessoa.CorRaca.PARDA,
nacionalidade=Pessoa.Nacionalidade.BRASILEIRA_NATA,
uf_nascimento=self.uf,
municipio_nascimento=self.municipio,
pais_origem=self.pais,
aluno_com_deficiencia=Pessoa.SimNaoNaoDispoe.NAO,
tipo_escola_ensino_medio=Pessoa.TipoEscolaEnsinoMedio.PUBLICA,
)
defaults.update(overrides)
return Pessoa.objects.create(**defaults)
def make_vinculo(self, pessoa: Pessoa, **overrides: object) -> Vinculo:
defaults = dict(
pessoa=pessoa,
ano_censo_id=2025,
curso=self.curso,
matricula="MAT-1",
situacao=Vinculo.Situacao.CURSANDO,
semestre_ingresso="012025",
carga_horaria_total_aluno=3200,
carga_horaria_integralizada=100,
**REQUIRED_VINCULO_BOOL_DEFAULTS,
)
defaults.update(overrides)
return Vinculo.objects.create(**defaults)
def test_export_pessoa_record_round_trips_through_import(self) -> None:
pessoa = self.make_pessoa()
line = export_pessoa_record(pessoa)
reimported, created = import_pessoa_record(line.split("|"))
self.assertFalse(created)
self.assertEqual(reimported.pk, pessoa.pk)
self.assertEqual(reimported.cor_raca, pessoa.cor_raca)
self.assertEqual(reimported.nacionalidade, pessoa.nacionalidade)
self.assertEqual(reimported.uf_nascimento_id, pessoa.uf_nascimento_id)
self.assertEqual(reimported.municipio_nascimento_id, pessoa.municipio_nascimento_id)
self.assertEqual(reimported.pais_origem_id, pessoa.pais_origem_id)
self.assertEqual(reimported.aluno_com_deficiencia, pessoa.aluno_com_deficiencia)
self.assertEqual(reimported.tipo_escola_ensino_medio, pessoa.tipo_escola_ensino_medio)
def test_export_pessoa_record_handles_null_data_nascimento(self) -> None:
pessoa = self.make_pessoa(data_nascimento=None)
line = export_pessoa_record(pessoa)
self.assertEqual(line.split("|")[5], "")
def test_export_vinculo_record_round_trips_through_import(self) -> None:
pessoa = self.make_pessoa()
vinculo = self.make_vinculo(pessoa)
line = export_vinculo_record(vinculo)
reimported, created = import_vinculo_record(line.split("|"), pessoa, 2025)
self.assertFalse(created)
self.assertEqual(reimported.pk, vinculo.pk)
self.assertEqual(reimported.matricula, vinculo.matricula)
self.assertEqual(reimported.situacao, vinculo.situacao)
self.assertEqual(reimported.semestre_ingresso, vinculo.semestre_ingresso)
self.assertEqual(reimported.carga_horaria_integralizada, vinculo.carga_horaria_integralizada)
self.assertEqual(reimported.ingresso_vestibular, vinculo.ingresso_vestibular)
def test_export_vinculo_record_writes_polo_codigo_not_polo_pk(self) -> None:
# Two polos so the auto PK is guaranteed to differ from `codigo`.
Polo.objects.create(codigo=999, descricao="Polo Descartavel")
polo = Polo.objects.create(codigo=555, descricao="Polo Real")
self.assertNotEqual(polo.pk, polo.codigo)
pessoa = self.make_pessoa()
vinculo = self.make_vinculo(pessoa, polo=polo)
line = export_vinculo_record(vinculo)
self.assertEqual(line.split("|")[4], str(polo.codigo))
def test_export_vinculo_record_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:
class Response:
headers = {"X-Total-Count": "0", "X-Total-Pages": "1"}
def __enter__(self) -> "Response":
return self
def __exit__(self, *args: object) -> None:
return None
def read(self) -> bytes:
return b'{"data": []}'
with patch("core.importers.urlopen", return_value=Response()) as urlopen_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,
)
request = urlopen_mock.call_args.args[0]
self.assertEqual(result, [])
self.assertEqual(total_count, 0)
self.assertEqual(total_pages, 1)
self.assertIn(
"/api/censup_person_ingressantes?",
request.full_url,
)
self.assertIn("primeiroAno=2026", request.full_url)
self.assertEqual(request.get_header("Auth_token"), "env-token")
self.assertEqual(request.get_header("User-agent"), GENNERA_USER_AGENT)
def test_fetch_gennera_vinculos_censup_page_uses_censup_vinculos_endpoint(self) -> None:
class Response:
headers = {"X-Total-Count": "1", "X-Total-Pages": "1"}
def __enter__(self) -> "Response":
return self
def __exit__(self, *args: object) -> None:
return None
def read(self) -> bytes:
return (
b'{"data": [{"cpf": "02542618330", "turno": "Noite", '
b'"referenceYear": "2026/1", "course": "Pedagogia", '
b'"course_code": "123", "ano": 2026, '
b'"carga_horaria_acumulada": 1600}]}'
)
with patch("core.importers.urlopen", return_value=Response()) as urlopen_mock:
result, total_count, total_pages = fetch_gennera_vinculos_censup_page(
base_url="http://api.example.test",
token="env-token",
cpf="02542618330",
page=1,
)
request = urlopen_mock.call_args.args[0]
self.assertEqual(result[0]["course"], "Pedagogia")
self.assertEqual(total_count, 1)
self.assertEqual(total_pages, 1)
self.assertIn("/api/censup_vinculos?", request.full_url)
self.assertIn("cpf=02542618330", request.full_url)
self.assertEqual(request.get_header("Auth_token"), "env-token")
self.assertEqual(request.get_header("User-agent"), GENNERA_USER_AGENT)
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",
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",
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",
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",
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",
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_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",
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, 1600)
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",
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",
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 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})