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 @@ - Nuevo Planner para Ingeniería UC - + Mallas ING + + content="Este es el planificador de mallas de Ingeniería UC, hecho por estudiantes para estudiantes." /> - + + content="Este es el planificador de mallas de Ingeniería UC, hecho por estudiantes para estudiantes." /> - + + content="Este es el planificador de mallas de Ingeniería UC, hecho por estudiantes para estudiantes." /> diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 530e082c..145d8216 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2444,11 +2444,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3703,9 +3703,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3747,9 +3747,9 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { @@ -6508,9 +6508,9 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/vite": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", - "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", @@ -8238,11 +8238,11 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "browserslist": { @@ -9152,9 +9152,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "requires": { "to-regex-range": "^5.0.1" } @@ -9184,9 +9184,9 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" }, "follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true }, "for-each": { @@ -11119,9 +11119,9 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "vite": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", - "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", "requires": { "esbuild": "^0.18.10", "fsevents": "~2.3.2", diff --git a/frontend/public/logo.png b/frontend/public/logo.png index d6fe1d57..4c53cc48 100644 Binary files a/frontend/public/logo.png and b/frontend/public/logo.png differ diff --git a/frontend/src/assets/editBlue.svg b/frontend/src/assets/editBlue.svg new file mode 100644 index 00000000..380a0f2d --- /dev/null +++ b/frontend/src/assets/editBlue.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/starFull.svg b/frontend/src/assets/starFull.svg new file mode 100644 index 00000000..0084396f --- /dev/null +++ b/frontend/src/assets/starFull.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/starOutline.svg b/frontend/src/assets/starOutline.svg new file mode 100644 index 00000000..a04b911b --- /dev/null +++ b/frontend/src/assets/starOutline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/layout/Banner.tsx b/frontend/src/layout/Banner.tsx index 0cb12fa4..5056d8e8 100644 --- a/frontend/src/layout/Banner.tsx +++ b/frontend/src/layout/Banner.tsx @@ -3,7 +3,7 @@ const Banner = (): JSX.Element => {

- 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.

) } diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 045ff51d..0e4c59ca 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -58,7 +58,7 @@ const Home = (): JSX.Element => {

- Bienvenido al Nuevo Planner, el lugar donde puedes planificar tu + Bienvenido a Mallas ING, el lugar donde puedes planificar tu carrera universitaria

diff --git a/frontend/src/pages/mod/userViewer.tsx b/frontend/src/pages/mod/userViewer.tsx index ca684973..e17fcc49 100644 --- a/frontend/src/pages/mod/userViewer.tsx +++ b/frontend/src/pages/mod/userViewer.tsx @@ -37,7 +37,7 @@ const UserViewer = (): JSX.Element => { const { status: studentDataStatus, error: studentDataError, data: studentInfo } = useQuery({ queryKey: ['studentInfo', userRut], retry: false, - onSuccess: (data) => { + onSuccess: (data: Student | null) => { if (data !== undefined && data !== null) { setSearchingPlanModalIsOpen(false) if (authState?.setStudent !== null) { diff --git a/frontend/src/pages/planner/dialogs/LegendModal.tsx b/frontend/src/pages/planner/dialogs/LegendModal.tsx index 0f2f8667..bf964b1f 100644 --- a/frontend/src/pages/planner/dialogs/LegendModal.tsx +++ b/frontend/src/pages/planner/dialogs/LegendModal.tsx @@ -34,8 +34,8 @@ const LegendModal = ({ open, onClose }: { open: boolean, onClose: Function }): J

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.

diff --git a/frontend/src/pages/planner/dialogs/SavePlanModal.tsx b/frontend/src/pages/planner/dialogs/SavePlanModal.tsx index a4a2784d..5a605827 100644 --- a/frontend/src/pages/planner/dialogs/SavePlanModal.tsx +++ b/frontend/src/pages/planner/dialogs/SavePlanModal.tsx @@ -1,11 +1,15 @@ -import { memo, useState } from 'react' +import { memo, useEffect, useState } from 'react' import TextInputModal from '../../../components/TextInputModal' -const SavePlanModal = ({ isOpen, onClose, savePlan }: { isOpen: boolean, onClose: Function, savePlan: Function }): JSX.Element => { +const SavePlanModal = ({ isOpen, onClose, savePlan, defaultValue = '' }: { isOpen: boolean, onClose: Function, savePlan: Function, defaultValue?: string }): JSX.Element => { const [planName, setPlanName] = useState('') const isSaveButtonDisabled: boolean = planName === '' + useEffect(() => { + setPlanName(defaultValue) + }, [defaultValue]) + return ( { return err.status !== undefined } @@ -16,8 +19,40 @@ const CurriculumList = (): JSX.Element => { const [loading, setLoading] = useState (true) const [popUpAlert, setPopUpAlert] = useState({ isOpen: false, id: '' }) + const [currentEditedId, setCurrentEditedId] = useState ('') + const [currentEditedName, setCurrentEditedName] = useState ('') + + const { isModalOpen: isSavePlanModalOpen, openModal: openSavePlanModal, closeModal: closeSavePlanModal } = useDummyModal() + + function compare (a: LowDetailPlanView, b: LowDetailPlanView): number { + const timestampA = Date.parse(a.updated_at) + const timestampB = Date.parse(b.updated_at) + if (timestampA < timestampB) { + return -1 + } + if (timestampA > timestampB) { + return 1 + } + return 0 + } + + function arrayMove (arr: any[], oldIndex: number, newIndex: number): void { + if (newIndex >= arr.length) { + let k = newIndex - arr.length + 1 + while ((k--) !== 0) { + arr.push(undefined) + } + } + arr.splice(newIndex, 0, arr.splice(oldIndex, 1)[0]) + }; + const readPlans = async (): Promise => { const response = await DefaultService.readPlans() + response.sort(compare).reverse() + const index = response.findIndex((plan) => plan.is_favorite) + if (index !== -1) { + arrayMove(response, index, 0) + } setPlans(response) setLoading(false) } @@ -32,11 +67,49 @@ const CurriculumList = (): JSX.Element => { }) } }) - }, []) + }) + + async function handleFavourite (id: string, planName: string, fav: boolean): Promise { + try { + await DefaultService.updatePlanMetadata(id, undefined, !fav) + await readPlans() + console.log('plan updated') + } catch (err) { + console.log(err) + if (isApiError(err) && err.status === 401) { + console.log('token invalid or expired, loading re-login page') + toast.error('Token invalido. Redireccionando a pagina de inicio...') + } + } + } + + function openEditModal (id: string, name: string): void { + openSavePlanModal() + setCurrentEditedId(id) + setCurrentEditedName(name) + } + + async function editPlanName (planName: string): Promise { + if (planName === null || planName === '') return + try { + await DefaultService.updatePlanMetadata(currentEditedId, planName, undefined) + await readPlans() + console.log('plan updated') + toast.success('Malla actualizada exitosamente') + } catch (err) { + console.log(err) + if (isApiError(err) && err.status === 401) { + console.log('token invalid or expired, loading re-login page') + toast.error('Token invalido. Redireccionando a pagina de inicio...') + } + } + closeSavePlanModal() + setCurrentEditedId('') + setCurrentEditedName('') + } async function handleDelete (id: string): Promise { try { - console.log('click', id) await DefaultService.deletePlan(id) await readPlans() console.log('plan deleted') @@ -49,6 +122,7 @@ const CurriculumList = (): JSX.Element => { } } } + function handlePopUpAlert (isCanceled: boolean): void { if (!isCanceled) { void handleDelete(popUpAlert.id) @@ -58,6 +132,7 @@ const CurriculumList = (): JSX.Element => { return (
+ {'¿Estás seguro/a de que deseas eliminar esta malla? Esta accion es irreversible'}
@@ -78,7 +153,7 @@ const CurriculumList = (): JSX.Element => { - {/* para favourite */} + @@ -89,7 +164,13 @@ const CurriculumList = (): JSX.Element => { {plans?.map((plan: LowDetailPlanView) => { return ( - { setPopUpAlert({ isOpen: true, id }) }} curriculum={plan}/> + { setPopUpAlert({ isOpen: true, id }) }} + curriculum={plan} + handleFavourite ={handleFavourite} + openPlanNameModal={openEditModal} + /> ) })} diff --git a/frontend/src/pages/user/CurriculumListRow.tsx b/frontend/src/pages/user/CurriculumListRow.tsx index 6fe1c3be..71c7f8a2 100644 --- a/frontend/src/pages/user/CurriculumListRow.tsx +++ b/frontend/src/pages/user/CurriculumListRow.tsx @@ -1,7 +1,18 @@ import { type LowDetailPlanView } from '../../client' import { Link } from '@tanstack/react-router' +import { ReactComponent as StarFull } from '../../assets/starFull.svg' +import { ReactComponent as StarOutline } from '../../assets/starOutline.svg' +import { ReactComponent as EditIcon } from '../../assets/editBlue.svg' -const CurriculumListRow = ({ curriculum, handleDelete, impersonateRut }: { curriculum: LowDetailPlanView, handleDelete?: Function, impersonateRut?: string }): JSX.Element => { +interface RowProps { + curriculum: LowDetailPlanView + handleDelete?: Function + handleFavourite?: Function + openPlanNameModal?: Function + impersonateRut?: string +} + +const CurriculumListRow = ({ curriculum, handleDelete, handleFavourite, openPlanNameModal, impersonateRut }: RowProps): JSX.Element => { function getDateString (date: string): string { const mydate = date.split('T')[0].split('-').reverse().join('-') return mydate @@ -9,6 +20,11 @@ const CurriculumListRow = ({ curriculum, handleDelete, impersonateRut }: { curri return ( + - + ) }
Fav Nombre Fecha Creación Fecha Modificación
+ {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 && + + } +
+