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