diff --git a/README.md b/README.md index fccb1ccf..ac287654 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,16 @@ Este es el hogar para el desarrollo del nuevo Planner de Ingeniería UC, hecho p Tras varios años en ideación, este proyecto se lanzó como [una propuesta conjunta](https://drive.google.com/file/d/1IxAJ8cCzDkayPwnju5kgc2oKc7g9fvwf/view) entre la Consejería Académica de Ingeniería y Open Source UC, con el propósito de reemplazar el [actual planner de Ingeniería](https://planner.ing.puc.cl/). La propuesta, tras ser aprobada por la Escuela de Ingeniería, dió comienzo al proyecto en modalidad de marcha blanca. A principios del 2023, y con un MVP listo, la Dirección de Pregrado oficialmente aprobó la continuación del desarrollo del proyecto. +## ¿Cómo agregar funcionalidades o modificar Mallas ING? + +1. **Hacer un fork** de este repositorio. +2. Realizar los cambios en tu repo forkeado, trabajando en la rama `dev`. +3. **Crear un Pull Request (PR)** desde la rama `dev` de tu repo hacia la rama `dev` de este repo. +4. Esperar a que el equipo del Nuevo Planner + OSUC acepte tu solicitud. +5. Una vez aceptado, el equipo del Nuevo Planner + OSUC abre un **PR** desde `dev` hacia `main` en este mismo repositorio. +6. Luego, el equipo del Nuevo Planner + OSUC podrá hacer un **PR** desde `main` hacia `main` del repositorio controlado por la universidad: [spavea/mallas.ing.uc.cl](https://github.com/spavea/mallas.ing.uc.cl). +7. Finalmente, solo queda esperar a que la universidad apruebe los cambios. El deploy se hará automáticamente al recibir la aprobación 🚀 + ## Instalación y desarrollo El proyecto está configurado para ser desarrollado en [Visual Studio Code](https://code.visualstudio.com/) con [Dev Containers](https://code.visualstudio.com/docs/remote/containers). Puedes [instalar VSCode aquí](https://code.visualstudio.com/download). Existen 2 maneras de correr Dev Containers: GitHub Codespaces y localmente. diff --git a/backend/app/plan/validation/courses/simplify.py b/backend/app/plan/validation/courses/simplify.py index 20dceefd..af792d0f 100644 --- a/backend/app/plan/validation/courses/simplify.py +++ b/backend/app/plan/validation/courses/simplify.py @@ -242,9 +242,7 @@ def _anihil_rule( new.clear() new.append(Const(value=not op.neutral)) anihilate[0] = True - if anihilate[0]: - return True - return False + return bool(anihilate[0]) def anihil(expr: Operator) -> Expr: @@ -255,10 +253,8 @@ def anihil(expr: Operator) -> Expr: def _ident_rule(ctx: None, op: Operator, new: list[Expr], child: Expr) -> bool: - if isinstance(child, Const) and child.value == op.neutral: - # Skip this child, since it adds nothing to the expression - return True - return False + # Skip this child, since it adds nothing to the expression + return isinstance(child, Const) and child.value == op.neutral def ident(expr: Operator) -> Expr: diff --git a/backend/app/plan/validation/courses/validate.py b/backend/app/plan/validation/courses/validate.py index 4b44510b..550b2f34 100644 --- a/backend/app/plan/validation/courses/validate.py +++ b/backend/app/plan/validation/courses/validate.py @@ -553,6 +553,4 @@ def is_course_indirectly_available(courseinfo: CourseInfo, code: str): info = courseinfo.try_course(code) if info is None: return False - if courseinfo.is_available(info.canonical_equiv): - return True - return False + return courseinfo.is_available(info.canonical_equiv) diff --git a/backend/app/sync/curriculums/scrape/translate.py b/backend/app/sync/curriculums/scrape/translate.py index 615c0477..5290b87b 100644 --- a/backend/app/sync/curriculums/scrape/translate.py +++ b/backend/app/sync/curriculums/scrape/translate.py @@ -66,7 +66,7 @@ def __init__( if bloque.CodSigla is not None: equivalents: list[str] = [bloque.CodSigla] if bloque.Equivalencias is not None: - for equivalent in bloque.Equivalencias.Cursos: + for equivalent in bloque.Equivalencias.Cursos or []: if equivalent.Sigla is not None: equivalents.append(equivalent.Sigla) if len(equivalents) > 1: diff --git a/backend/app/sync/curriculums/siding.py b/backend/app/sync/curriculums/siding.py index df1ef005..55a7c205 100644 --- a/backend/app/sync/curriculums/siding.py +++ b/backend/app/sync/curriculums/siding.py @@ -151,7 +151,7 @@ def _filter_relevant_cyears(cyears: StringArray | None) -> bool: if cyears is None: return False cyears.strings.string = [ - cyear for cyear in cyears.strings.string if cyear >= IGNORE_CYEARS_BEFORE + cyear for cyear in cyears.strings.string or [] if cyear >= IGNORE_CYEARS_BEFORE ] return len(cyears.strings.string) > 0 @@ -161,7 +161,7 @@ async def _fetch_siding_plans(siding: SidingInfo): for major in siding.majors: if major.Curriculum is None: continue - for cyear_str in major.Curriculum.strings.string: + for cyear_str in major.Curriculum.strings.string or []: cyear = cyear_from_str(cyear_str) if cyear is None: log.error( @@ -184,7 +184,7 @@ async def _fetch_siding_plans(siding: SidingInfo): for minor in siding.minors: if minor.Curriculum is None: continue - for cyear_str in minor.Curriculum.strings.string: + for cyear_str in minor.Curriculum.strings.string or []: cyear = cyear_from_str(cyear_str) if cyear is None: log.error( @@ -207,7 +207,7 @@ async def _fetch_siding_plans(siding: SidingInfo): for title in siding.titles: if title.Curriculum is None: continue - for cyear_str in title.Curriculum.strings.string: + for cyear_str in title.Curriculum.strings.string or []: cyear = cyear_from_str(cyear_str) if cyear is None: log.error( @@ -278,7 +278,7 @@ def translate_siding( codes = [main_code] if raw_block.Equivalencias is not None: # Add equivalences to the list - for curso in raw_block.Equivalencias.Cursos: + for curso in raw_block.Equivalencias.Cursos or []: if curso.Sigla is not None and curso.Sigla != main_code: codes.append(curso.Sigla) list_code = ( @@ -385,7 +385,7 @@ def _fill_in_c2022_titles(courses: dict[str, CourseDetails], siding: SidingInfo) if title.Curriculum is None: continue cyears = title.Curriculum.strings.string - if "C2020" in cyears and "C2022" not in cyears: + if cyears and "C2020" in cyears and "C2022" not in cyears: cyears.append("C2022") siding.plans["C2022"].plans[title.CodTitulo] = siding.plans["C2020"].plans[ title.CodTitulo diff --git a/backend/app/sync/siding/client.py b/backend/app/sync/siding/client.py index b5dcea2f..29c7974a 100644 --- a/backend/app/sync/siding/client.py +++ b/backend/app/sync/siding/client.py @@ -5,12 +5,11 @@ from decimal import Decimal from io import BytesIO from pathlib import Path -from typing import Annotated, Any, Final, Generic, Literal, TypeVar +from typing import Annotated, Any, Final, Literal, TypeVar import httpx import zeep from pydantic import BaseModel, ConstrainedStr, Field, parse_obj_as -from pydantic.generics import GenericModel from zeep import AsyncClient from zeep.transports import AsyncTransport @@ -20,7 +19,7 @@ class StringArrayInner(BaseModel): - string: list[str] + string: list[str] | None class StringArray(BaseModel): @@ -80,7 +79,7 @@ class Curso(BaseModel): class ListaCursos(BaseModel): - Cursos: list[Curso] + Cursos: list[Curso] | None class Restriccion(BaseModel): @@ -89,11 +88,11 @@ class Restriccion(BaseModel): class ListaRestricciones(BaseModel): - Restricciones: list[Restriccion] + Restricciones: list[Restriccion] | None class ListaRequisitos(BaseModel): - Cursos: list[Curso] + Cursos: list[Curso] | None class BloqueMalla(BaseModel): @@ -210,22 +209,19 @@ class SeleccionEstudiante(BaseModel): T = TypeVar("T") -class NullableList(GenericModel, Generic[T]): - """ - SIDING has the horrible tendency to return `None` instead of empty lists. - This patches that. - """ - - __root__: list[T] | None - - def to_list(self) -> list[T]: - return [] if self.__root__ is None else self.__root__ +def parse_nullable_list(ty: type[T], value: Any) -> list[T]: # noqa: ANN401 + if value is None: + return [] + return parse_obj_as( + list[ty], + value, + ) def decode_cyears(cyears: StringArray | None) -> list[str]: if cyears is None: return [] - return cyears.strings.string + return cyears.strings.string or [] class SoapClient: @@ -356,38 +352,38 @@ async def get_majors() -> list[Major]: # with open("log.txt", "a") as f: # print(resp.content, file=f) - return parse_obj_as( - NullableList[Major], + return parse_nullable_list( + Major, await client.call_endpoint("getListadoMajor", {}), - ).to_list() + ) async def get_minors() -> list[Minor]: """ Obtain a global list of all minors. """ - return parse_obj_as( - NullableList[Minor], + return parse_nullable_list( + Minor, await client.call_endpoint("getListadoMinor", {}), - ).to_list() + ) async def get_titles() -> list[Titulo]: """ Obtain a global list of all titles. """ - return parse_obj_as( - NullableList[Titulo], + return parse_nullable_list( + Titulo, await client.call_endpoint("getListadoTitulo", {}), - ).to_list() + ) async def get_minors_for_major(major_code: str) -> list[Minor]: """ Obtain a list of minors that are a valid choice for each major. """ - return parse_obj_as( - list[Minor], + return parse_nullable_list( + Minor, await client.call_endpoint("getMajorMinorAsociado", {"CodMajor": major_code}), ) @@ -396,26 +392,26 @@ async def get_courses_for_spec(study_spec: PlanEstudios) -> list[Curso]: """ Get pretty much all the courses that are available to a certain study spec. """ - return parse_obj_as( - NullableList[Curso], + return parse_nullable_list( + Curso, await client.call_endpoint( "getConcentracionCursos", study_spec.dict(), ), - ).to_list() + ) async def get_curriculum_for_spec(study_spec: PlanEstudios) -> list[BloqueMalla]: """ Get a list of curriculum blocks for the given spec. """ - return parse_obj_as( - NullableList[BloqueMalla], + return parse_nullable_list( + BloqueMalla, await client.call_endpoint( "getMallaSugerida", study_spec.dict(), ), - ).to_list() + ) async def get_equivalencies(course_code: str, study_spec: PlanEstudios) -> list[Curso]: @@ -429,8 +425,8 @@ async def get_equivalencies(course_code: str, study_spec: PlanEstudios) -> list[ For example, 'FIS1514' has 3 equivalencies, including 'ICE1514'. However, 'ICE1514' has zero equivalencies. """ - return parse_obj_as( - NullableList[Curso], + return parse_nullable_list( + Curso, await client.call_endpoint( "getCursoEquivalente", { @@ -438,7 +434,7 @@ async def get_equivalencies(course_code: str, study_spec: PlanEstudios) -> list[ } | study_spec.dict(), ), - ).to_list() + ) async def get_requirements(course_code: str, study_spec: PlanEstudios) -> list[Curso]: @@ -448,8 +444,8 @@ async def get_requirements(course_code: str, study_spec: PlanEstudios) -> list[C represented as a logical expression and not as a list. These requirements are only a heuristic. """ - return parse_obj_as( - NullableList[Curso], + return parse_nullable_list( + Curso, await client.call_endpoint( "getRequisito", { @@ -457,7 +453,7 @@ async def get_requirements(course_code: str, study_spec: PlanEstudios) -> list[C } | study_spec.dict(), ), - ).to_list() + ) async def get_restrictions( @@ -469,8 +465,8 @@ async def get_restrictions( This is actually broken, Banner restrictions are represented as a logical expression and not as a list. """ - return parse_obj_as( - NullableList[Restriccion], + return parse_nullable_list( + Restriccion, await client.call_endpoint( "getRestriccion", { @@ -478,7 +474,7 @@ async def get_restrictions( } | study_spec.dict(), ), - ).to_list() + ) async def get_predefined_list(list_code: str) -> list[Curso]: @@ -487,10 +483,10 @@ async def get_predefined_list(list_code: str) -> list[Curso]: Returns `null` if the list is empty. """ - return parse_obj_as( - NullableList[Curso], + return parse_nullable_list( + Curso, await client.call_endpoint("getListaPredefinida", {"CodLista": list_code}), - ).to_list() + ) async def get_student_info(rut: Rut) -> InfoEstudiante: @@ -507,10 +503,10 @@ async def get_student_done_courses(rut: Rut) -> list[CursoHecho]: """ Get the information associated with the given student, by RUT. """ - return parse_obj_as( - NullableList[CursoHecho], + return parse_nullable_list( + CursoHecho, await client.call_endpoint("getCursosHechos", {"rut": rut}), - ).to_list() + ) async def get_student_current_courses(rut: Rut) -> list[CursoInscrito]: @@ -519,10 +515,10 @@ async def get_student_current_courses(rut: Rut) -> list[CursoInscrito]: Not sure when exactly are current courses converted into past courses. """ - return parse_obj_as( - NullableList[CursoInscrito], + return parse_nullable_list( + CursoInscrito, await client.call_endpoint("getCargaAcademica", {"rut": rut}), - ).to_list() + ) async def get_student_selections(rut: Rut) -> list[SeleccionEstudiante]: @@ -531,10 +527,10 @@ async def get_student_selections(rut: Rut) -> list[SeleccionEstudiante]: Not sure when exactly are current courses converted into past courses. """ - return parse_obj_as( - NullableList[SeleccionEstudiante], + return parse_nullable_list( + SeleccionEstudiante, await client.call_endpoint("getSeleccionesEstudiante", {"rut": rut}), - ).to_list() + ) async def get_current_period() -> AcademicPeriod: diff --git a/backend/app/sync/siding/translate.py b/backend/app/sync/siding/translate.py index 5a838dfd..6d2e55d3 100644 --- a/backend/app/sync/siding/translate.py +++ b/backend/app/sync/siding/translate.py @@ -51,7 +51,7 @@ def _decode_curriculum_versions(input: StringArray | None) -> list[str]: # too unreliable logging.warning("null curriculum version list") return [] - return input.strings.string + return input.strings.string or [] def _decode_period(period: str) -> tuple[int, int]: diff --git a/frontend/index.html b/frontend/index.html index 1350468a..89ad258c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -10,23 +10,23 @@ -
- Le informamos que el Nuevo Planner no estará disponible el INSERTAR_FECHA entre las INSERTAR_HORA_INICIO y las INSERTAR_HORA_FIN hrs debido a una mantención programada en nuestros servidores. Agradecemos su comprensión.
+ Le informamos que Mallas ING no estará disponible el INSERTAR_FECHA entre las INSERTAR_HORA_INICIO y las INSERTAR_HORA_FIN hrs debido a una mantención programada en nuestros servidores. Agradecemos su comprensión.Esta malla curricular es la recomendada y está sujeta a modificaciones conforme a la planificación académica vigente al momento de inscribir cursos.
-Los requisitos y planificación de los cursos pueden ir variando semestre a semestre. Por lo que el Nuevo Planner puede tener algunas variaciones. La información oficial de los requisitos y planificación de los cursos se encuentra disponible en el catálogo de cursos (http://catalogo.uc.cl/) y el libro de cursos (http://buscacursos.uc.cl/).
-Es responsabilidad del alumno verificar que el avance curricular determinado por Nuevo Planner sea el correcto. Los Planes de Estudios publicados en Siding corresponden a la información oficial y actualizada.
+Los requisitos y planificación de los cursos pueden ir variando semestre a semestre. Por lo que Mallas ING puede tener algunas variaciones. La información oficial de los requisitos y planificación de los cursos se encuentra disponible en el catálogo de cursos (http://catalogo.uc.cl/) y el libro de cursos (http://buscacursos.uc.cl/).
+Es responsabilidad del alumno verificar que el avance curricular determinado por Mallas ING sea el correcto. Los Planes de Estudios publicados en Siding corresponden a la información oficial y actualizada.
para favourite */} + | Fav | Nombre | Fecha Creación | Fecha Modificación | @@ -89,7 +164,13 @@ const CurriculumList = (): JSX.Element => {|
---|---|---|---|---|---|
+ {handleFavourite !== undefined && + + } + |
{impersonateRut !== undefined
? {curriculum.name}
- : {curriculum.name}
+ {openPlanNameModal !== undefined &&
+
+
+ Editar nombre
+
+ }
+
}
|
{getDateString(curriculum.created_at)} | {getDateString(curriculum.updated_at)} | -
- {impersonateRut !== undefined
- ? Editar
-
- : Editar
-
- }
- {handleDelete !== undefined &&
-
- }
- |
+
+
+ {impersonateRut !== undefined
+ ? Editar
+
+ : Editar
+
+ }
+ {handleDelete !== undefined &&
+
+ }
+
+ |