Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add itcr parser #968

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions courses/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def get_regexed_courses(self, course):
"umich": r"([A-Z]{2,8}\s\d{3})",
"chapman": r"([A-Z]{2,4}\s\d{3})",
"salisbury": r"([A-Z]{3,4} \d{2,3})",
"itcr": r"([A-Z]{2}\d{4})",
}
course_code_to_name = {}
if self.context["school"] in school_to_course_regex:
Expand Down
1 change: 1 addition & 0 deletions parsing/schools/active
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ umd
umich
uoft
vandy
itcr
15 changes: 15 additions & 0 deletions parsing/schools/itcr/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright (C) 2023 Semester.ly Technologies, LLC
#
# Semester.ly is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Semester.ly is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.

from parsing.schools import load_school_logger

load_school_logger("itcr")
36 changes: 36 additions & 0 deletions parsing/schools/itcr/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"school": {
"code": "itcr",
"name": "Instituto Tecnológico de Costa Rica"
},
"course_code_regex": "([A-Z]{2}\\d{4})$",
"terms": [
"1",
"2",
"V"
],
"granularity": 5,
"ampm": false,
"full_academic_year_registration": false,
"single_access": false,
"active_semesters": {
"2023": [
"1"
],
"2022": [
"V",
"2",
"1"
],
"2021": [
"V",
"2",
"1"
],
"2020": [
"2",
"1"
]
},
"registrar": false
}
207 changes: 207 additions & 0 deletions parsing/schools/itcr/courses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
# Copyright (C) 2023 Semester.ly Technologies, LLC
#
# Semester.ly is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Semester.ly is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.


import json


from parsing.library.base_parser import BaseParser
from parsing.library.utils import dict_filter_by_dict
from datetime import datetime


import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class Parser(BaseParser):
"""TEC course parser.

Attributes:
API_URL (str): Description
DAY_MAP (TYPE): Description
last_course (dict): Description
schools (list): Description
semester (TYPE): Description
verbosity (TYPE): Description
"""

API_URL = "https://tec-appsext.itcr.ac.cr/guiahorarios/escuela.aspx/"
DAY_MAP = {
"LUNES": "M",
"MARTES": "T",
"MIERCOLES": "W",
"JUEVES": "R",
"VIERNES": "F",
"SABADO": "S",
"DOMINGO": "U",
}

def __new__(cls, *args, **kwargs):
"""Set static variables within closure.

Returns:
Parser
"""
return object.__new__(cls)

def __init__(self, **kwargs):
"""Construct itcr parser object."""
self.schools = []
self.last_course = {}
super(Parser, self).__init__("itcr", **kwargs)

def _get_schools(self):
headers = {"Content-Type": "application/json"}
request = self.requester.post(
f"{Parser.API_URL}cargaEscuelas",
data="{}",
headers=headers,
verify=False,
)
self.schools = json.loads(request["d"])

def _get_courses(self, school):
headers = {"Content-Type": "application/json"}
payload = json.dumps({"escuela": school["IDE_DEPTO"], "ano": self.year})
request = self.requester.post(
f"{Parser.API_URL}getdatosEscuelaAno",
data=payload,
headers=headers,
verify=False,
)
try:
data = json.loads(request["d"])
return data
except Exception:
return []

def _parse_schools(self):
for school in self.schools:
self._parse_school(school)

def _parse_school(self, school):
courses = self._get_courses(school)
if self.term.isdigit():
courses = [
course
for course in courses
if (
course["IDE_MODALIDAD"] == "S"
and course["IDE_PER_MOD"] == int(self.term)
)
]
elif self.term == "V":
courses = [
course
for course in courses
if (course["IDE_MODALIDAD"] == "V" and course["IDE_PER_MOD"] == 1)
]
else:
courses = []

sections = self._parse_sections(courses)
for courseCode in sections:
course = sections[courseCode]
self._load_ingestor(course[0], course)

def _parse_sections(self, courses):
res = {}
for course in courses:
section_code = course["IDE_MATERIA"] + str(course["IDE_GRUPO"])
if res.get(section_code, None) is None:
res[section_code] = []
res[section_code].append(course)

return res

def _load_ingestor(self, course, section):
try:
num_credits = float(course["CAN_CREDITOS"])
except Exception:
num_credits = 0

# Load core course fields
self.ingestor["name"] = course["DSC_MATERIA"]
self.ingestor["description"] = ""
self.ingestor["code"] = course["IDE_MATERIA"]
self.ingestor["num_credits"] = num_credits
self.ingestor["department_name"] = course["DSC_DEPTO"]
self.ingestor["campus"] = course["DSC_SEDE"]

created_course = self.ingestor.ingest_course()

if (
self.last_course
and created_course["code"] == course["IDE_MATERIA"]
and created_course["name"] != course["DSC_MATERIA"]
):
self.ingestor["section_name"] = course["IDE_MATERIA"]
self.last_course = created_course

for meeting in section:
# Load core section fields
self.ingestor["section_code"] = str(meeting["IDE_GRUPO"])
self.ingestor["instrs"] = meeting["NOM_PROFESOR"]

self.ingestor["section_type"] = meeting["TIPO_CURSO"]

# We have no data on the capacity
self.ingestor["size"] = 1
self.ingestor["enrollment"] = 0
self.ingestor["waitlist"] = 0

created_section = self.ingestor.ingest_section(created_course)

# Theres no real way to get this data from the current api
# so for now just filling with the current date
self.ingestor["date_start"] = datetime.now()
self.ingestor["date_end"] = datetime.now()

self.ingestor["time_start"] = meeting["HINICIO"]
self.ingestor["time_end"] = meeting["HFIN"]
self.ingestor["days"] = [Parser.DAY_MAP.get(meeting["NOM_DIA"], "")]
course_campus = f'{meeting["DSC_SEDE"]} ({meeting["TIPO_CURSO"]})'
self.ingestor["location"] = {
"campus": course_campus,
"building": course_campus,
"room": "",
}
self.ingestor.ingest_meeting(created_section)

def start(
self,
verbosity=3,
textbooks=False,
departments_filter=None,
years_and_terms_filter=None,
):
"""Start parse."""
self.verbosity = verbosity

# Default to hardcoded current year.
years = {"2023", "2022", "2021", "2020"}
terms = {"1", "2", "V"}

years_and_terms = dict_filter_by_dict(
{year: list(terms) for year in years}, years_and_terms_filter
)

for year, terms in list(years_and_terms.items()):
self.ingestor["year"] = year
self.year = year
for term in terms:
self.ingestor["term"] = term
self.term = term
self._get_schools()
self._parse_schools()
Empty file.
Empty file.
17 changes: 17 additions & 0 deletions static/js/redux/constants/schools.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const VALID_SCHOOLS = [
"umich",
"chapman",
"salisbury",
"itcr",
];

export const getSchoolSpecificInfo = (school) => {
Expand Down Expand Up @@ -148,6 +149,22 @@ export const getSchoolSpecificInfo = (school) => {
1: "",
},
};
case "itcr":
return {
primaryDisplay: "name",
areasName: "Areas",
departmentsName: "Deparments",
levelsName: "Levels",
timesName: "Times",
courseRegex: "([A-Z]{2}\\d{4})",
campuses: {
"CAMPUS TECNOLOGICO CENTRAL CARTAGO": "CAMPUS TECNOLOGICO CENTRAL CARTAGO",
"CENTRO ACADEMICO DE LIMON": "CENTRO ACADEMICO DE LIMON",
"CENTRO ACADEMICO DE ALAJUELA": "CENTRO ACADEMICO DE ALAJUELA",
"CAMPUS TECNOLOGICO LOCAL SAN JOSE": "CAMPUS TECNOLOGICO LOCAL SAN JOSE",
"CAMPUS TECNOLOGICO LOCAL SAN CARLOS": "CAMPUS TECNOLOGICO LOCAL SAN CARLOS",
},
};
default:
return {
primaryDisplay: "code",
Expand Down