diff --git a/.env.template b/.env.template index 377825b..fd04e52 100644 --- a/.env.template +++ b/.env.template @@ -7,4 +7,4 @@ LOGIN_ASTRA_PASSWORD= MAZEVO_API_KEY= #Uploader -MONGODB_URI= \ No newline at end of file +MONGODB_URI= diff --git a/go.mod b/go.mod index 1071abf..d5271f1 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/UTDNebula/nebula-api/api v0.0.0-20250222211052-e8c23b26713c github.com/chromedp/cdproto v0.0.0-20250120090109-d38428e4d9c8 github.com/chromedp/chromedp v0.12.1 + github.com/google/go-cmp v0.7.0 github.com/joho/godotenv v1.5.1 github.com/valyala/fastjson v1.6.4 go.mongodb.org/mongo-driver v1.17.2 diff --git a/go.sum b/go.sum index 5ae99cc..ff16643 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/parser/courseParser.go b/parser/courseParser.go index bc7ea33..48c7edc 100644 --- a/parser/courseParser.go +++ b/parser/courseParser.go @@ -12,26 +12,10 @@ import ( "go.mongodb.org/mongo-driver/bson/primitive" ) -var coursePrefixRexp *regexp.Regexp = utils.Regexpf(`^%s`, utils.R_SUBJ_COURSE_CAP) -var contactRegexp *regexp.Regexp = regexp.MustCompile(`\(([0-9]+)-([0-9]+)\)\s+([SUFY]+)`) - -func getCatalogYear(session schema.AcademicSession) string { - sessionYear, err := strconv.Atoi(session.Name[0:2]) - if err != nil { - panic(err) - } - sessionSemester := session.Name[2] - switch sessionSemester { - case 'F': - return strconv.Itoa(sessionYear) - case 'S': - return strconv.Itoa(sessionYear - 1) - case 'U': - return strconv.Itoa(sessionYear - 1) - default: - panic(fmt.Errorf("encountered invalid session semester '%c!'", sessionSemester)) - } -} +var ( + coursePrefixRexp *regexp.Regexp = utils.Regexpf(`^%s`, utils.R_SUBJ_COURSE_CAP) + contactRegexp *regexp.Regexp = regexp.MustCompile(`\(([0-9]+)-([0-9]+)\)\s+([SUFY]+)`) +) func parseCourse(courseNum string, session schema.AcademicSession, rowInfo map[string]*goquery.Selection, classInfo map[string]string) *schema.Course { // Courses are internally keyed by their internal course number and the catalog year they're part of @@ -44,28 +28,36 @@ func parseCourse(courseNum string, session schema.AcademicSession, rowInfo map[s return course } - // Get subject prefix and course number by doing a regexp match on the section id - sectionId := classInfo["Class Section:"] - idMatches := coursePrefixRexp.FindStringSubmatch(sectionId) - - course = &schema.Course{} - - course.Id = primitive.NewObjectID() - course.Course_number = idMatches[2] - course.Subject_prefix = idMatches[1] - course.Title = utils.TrimWhitespace(rowInfo["Course Title:"].Text()) - course.Description = utils.TrimWhitespace(rowInfo["Description:"].Text()) - course.School = utils.TrimWhitespace(rowInfo["College:"].Text()) - course.Credit_hours = classInfo["Semester Credit Hours:"] - course.Class_level = classInfo["Class Level:"] - course.Activity_type = classInfo["Activity Type:"] - course.Grading = classInfo["Grading:"] - course.Internal_course_number = courseNum + course = getCourse(courseNum, session, rowInfo, classInfo) // Get closure for parsing course requisites (god help me) enrollmentReqs, hasEnrollmentReqs := rowInfo["Enrollment Reqs:"] ReqParsers[course.Id] = getReqParser(course, hasEnrollmentReqs, enrollmentReqs) + Courses[courseKey] = course + CourseIDMap[course.Id] = courseKey + return course +} + +// no global state is changed +func getCourse(courseNum string, session schema.AcademicSession, rowInfo map[string]*goquery.Selection, classInfo map[string]string) *schema.Course { + CoursePrefix, CourseNumber := getPrefixAndNumber(classInfo) + + course := schema.Course{ + Id: primitive.NewObjectID(), + Course_number: CourseNumber, + Subject_prefix: CoursePrefix, + Title: utils.TrimWhitespace(rowInfo["Course Title:"].Text()), + Description: utils.TrimWhitespace(rowInfo["Description:"].Text()), + School: utils.TrimWhitespace(rowInfo["College:"].Text()), + Credit_hours: classInfo["Semester Credit Hours:"], + Class_level: classInfo["Class Level:"], + Activity_type: classInfo["Activity Type:"], + Grading: classInfo["Grading:"], + Internal_course_number: courseNum, + Catalog_year: getCatalogYear(session), + } + // Try to get lecture/lab contact hours and offering frequency from course description contactMatches := contactRegexp.FindStringSubmatch(course.Description) // Length of contactMatches should be 4 upon successful match @@ -75,10 +67,34 @@ func parseCourse(courseNum string, session schema.AcademicSession, rowInfo map[s course.Offering_frequency = contactMatches[3] } - // Set the catalog year - course.Catalog_year = catalogYear + return &course +} - Courses[courseKey] = course - CourseIDMap[course.Id] = courseKey - return course +func getCatalogYear(session schema.AcademicSession) string { + sessionYear, err := strconv.Atoi(session.Name[0:2]) + if err != nil { + panic(err) + } + sessionSemester := session.Name[2] + switch sessionSemester { + case 'F': + return strconv.Itoa(sessionYear) + case 'S': + return strconv.Itoa(sessionYear - 1) + case 'U': + return strconv.Itoa(sessionYear - 1) + default: + panic(fmt.Errorf("encountered invalid session semester '%c!'", sessionSemester)) + } +} + +func getPrefixAndNumber(classInfo map[string]string) (string, string) { + if sectionId, ok := classInfo["Class Section:"]; ok { + // Get subject prefix and course number by doing a regexp match on the section id + matches := coursePrefixRexp.FindStringSubmatch(sectionId) + if len(matches) == 3 { + return matches[1], matches[2] + } + } + return "", "" } diff --git a/parser/courseParser_test.go b/parser/courseParser_test.go new file mode 100644 index 0000000..d573b69 --- /dev/null +++ b/parser/courseParser_test.go @@ -0,0 +1,125 @@ +package parser + +import ( + "testing" + + "github.com/UTDNebula/nebula-api/api/schema" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func TestGetCourse(t *testing.T) { + loadTestData(t) + + for name, testCase := range testDataCache { + t.Run(name, func(t *testing.T) { + _, courseNum := getInternalClassAndCourseNum(testCase.ClassInfo) + output := *getCourse(courseNum, testCase.Section.Academic_session, testCase.RowInfo, testCase.ClassInfo) + expected := testCase.Course + + diff := cmp.Diff(expected, output, cmpopts.IgnoreFields(schema.Course{}, "Id")) + + if diff != "" { + t.Errorf("Failed (-expected +got)\n %s", diff) + } + + }) + } +} + +func TestGetCatalogYear(t *testing.T) { + testCases := map[string]struct { + Session schema.AcademicSession + Expected string + }{ + "Case_001": { + Session: schema.AcademicSession{ + Name: "25S", + }, + Expected: "24", + }, "Case_002": { + Session: schema.AcademicSession{ + Name: "25F", + }, + Expected: "25", + }, "Case_003": { + Session: schema.AcademicSession{ + Name: "22U", + }, + Expected: "21", + }, "Case_004": { + Session: schema.AcademicSession{ + Name: "20S", + }, + Expected: "19", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + output := getCatalogYear(tc.Session) + + if output != tc.Expected { + t.Errorf("expected %s got %s", tc.Expected, output) + } + }) + + } +} + +func TestGetPrefixAndCourseNum(t *testing.T) { + testCases := map[string]struct { + classInfo map[string]string + prefix string + number string + }{ + "Case_001": { + classInfo: map[string]string{ + "Class Section:": "ACCT2301.001.25S", + }, + prefix: "ACCT", + number: "2301", + }, + "Case_002": { + classInfo: map[string]string{ + "Class Section:": "ENTP3301.002.24S", + }, + prefix: "ENTP", + number: "3301", + }, + "Case_003": { + classInfo: map[string]string{ + "Class Section:": "Garbage In, Garbage out", + }, + prefix: "", + number: "", + }, + "Case_004": { + classInfo: map[string]string{ + "Class Section:": "ENTP33S", + }, + prefix: "", + number: "", + }, + "Case_005": { + classInfo: map[string]string{ + "Class Section:": "", + }, + prefix: "", + number: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + prefix, number := getPrefixAndNumber(testCase.classInfo) + + if prefix != testCase.prefix { + t.Errorf("expected %s got %s", testCase.prefix, prefix) + } + if number != testCase.number { + t.Errorf("expected %s got %s", testCase.number, number) + } + }) + } +} diff --git a/parser/parser.go b/parser/parser.go index 421f5eb..b1dafb5 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -4,7 +4,6 @@ import ( "fmt" "log" "os" - "strings" "time" "github.com/UTDNebula/api-tools/utils" @@ -110,35 +109,33 @@ func parse(path string) { panic(err) } - // Get the rows of the info table - infoTable := doc.FindMatcher(goquery.Single("table.courseinfo__overviewtable > tbody")) - infoRows := infoTable.ChildrenFiltered("tr") + // Dictionary to hold the row data, keyed by row header + rowInfo := getRowInfo(doc) + // Dictionary to hold the class info, keyed by data label + classInfo := getClassInfo(doc) - var syllabusURI string + // Get the class and course num by splitting classInfo value - // Dictionary to hold the row data, keyed by row header + parseSection(rowInfo, classInfo) + utils.VPrint("Parsed!") +} + +func getRowInfo(doc *goquery.Document) map[string]*goquery.Selection { + infoRows := doc.FindMatcher(goquery.Single("table.courseinfo__overviewtable > tbody")).ChildrenFiltered("tr") rowInfo := make(map[string]*goquery.Selection, len(infoRows.Nodes)) - // Populate rowInfo infoRows.Each(func(_ int, row *goquery.Selection) { rowHeader := utils.TrimWhitespace(row.FindMatcher(goquery.Single("th")).Text()) rowInfo[rowHeader] = row.FindMatcher(goquery.Single("td")) }) + return rowInfo +} - // Get syllabusURI from syllabus row link - if syllabus, ok := rowInfo["syllabus"]; ok { - syllabusURI, _ = syllabus.FindMatcher(goquery.Single("a")).Attr("href") - } - - // Get the rows of the class info subtable - infoSubTable := infoTable.FindMatcher(goquery.Single("table.courseinfo__classsubtable > tbody")) - infoRows = infoSubTable.ChildrenFiltered("tr") - - // Dictionary to hold the class info, keyed by data label - classInfo := make(map[string]string) +func getClassInfo(doc *goquery.Document) map[string]string { + infoRows := doc.FindMatcher(goquery.Single("table.courseinfo__classsubtable > tbody")).ChildrenFiltered("tr") + classInfo := make(map[string]string, len(infoRows.Nodes)) - // Populate classInfo infoRows.Each(func(_ int, row *goquery.Selection) { rowHeaders := row.Find("td.courseinfo__classsubtable__th") rowHeaders.Each(func(_ int, header *goquery.Selection) { @@ -147,17 +144,5 @@ func parse(path string) { classInfo[headerText] = dataText }) }) - - // Get the class and course num by splitting classInfo value - classAndCourseNum := strings.Split(classInfo["Class/Course Number:"], " / ") - classNum := classAndCourseNum[0] - courseNum := utils.TrimWhitespace(classAndCourseNum[1]) - - // Figure out the academic session associated with this specific course/Section - session := getAcademicSession(rowInfo) - - // Try to create the course and section based on collected info - courseRef := parseCourse(courseNum, session, rowInfo, classInfo) - parseSection(courseRef, classNum, syllabusURI, session, rowInfo, classInfo) - utils.VPrint("Parsed!") + return classInfo } diff --git a/parser/sectionParser.go b/parser/sectionParser.go index b056164..1d5dd04 100644 --- a/parser/sectionParser.go +++ b/parser/sectionParser.go @@ -1,6 +1,7 @@ package parser import ( + "encoding/json" "regexp" "strings" "time" @@ -12,73 +13,65 @@ import ( "golang.org/x/net/html/atom" ) -var sectionPrefixRegexp *regexp.Regexp = utils.Regexpf(`^(?i)%s\.(%s)`, utils.R_SUBJ_COURSE, utils.R_SECTION_CODE) -var coreRegexp *regexp.Regexp = regexp.MustCompile(`[0-9]{3}`) -var personRegexp *regexp.Regexp = regexp.MustCompile(`(.+)・(.+)・(.+)`) - -func parseSection(courseRef *schema.Course, classNum string, syllabusURI string, session schema.AcademicSession, rowInfo map[string]*goquery.Selection, classInfo map[string]string) { - // Get subject prefix and course number by doing a regexp match on the section id - sectionId := classInfo["Class Section:"] - idMatches := sectionPrefixRegexp.FindStringSubmatch(sectionId) - - section := &schema.Section{} - - section.Id = primitive.NewObjectID() - section.Section_number = idMatches[1] - section.Course_reference = courseRef.Id - - //TODO: section requisites? - - // Set academic session - section.Academic_session = session - // Add professors - section.Professors = parseProfessors(section.Id, rowInfo, classInfo) - - // Get all TA/RA info - assistantText := utils.TrimWhitespace(rowInfo["TA/RA(s):"].Text()) - assistantMatches := personRegexp.FindAllStringSubmatch(assistantText, -1) - section.Teaching_assistants = make([]schema.Assistant, 0, len(assistantMatches)) - for _, match := range assistantMatches { - assistant := schema.Assistant{} - nameStr := utils.TrimWhitespace(match[1]) - names := strings.Split(nameStr, " ") - assistant.First_name = strings.Join(names[:len(names)-1], " ") - assistant.Last_name = names[len(names)-1] - assistant.Role = utils.TrimWhitespace(match[2]) - assistant.Email = utils.TrimWhitespace(match[3]) - section.Teaching_assistants = append(section.Teaching_assistants, assistant) - } - - section.Internal_class_number = classNum - section.Instruction_mode = classInfo["Instruction Mode:"] - section.Meetings = getMeetings(rowInfo) +const timeLayout = "January 2, 2006" - // Parse core flags (may or may not exist) +var ( + sectionPrefixRegexp *regexp.Regexp = utils.Regexpf(`^(?i)%s\.(%s)`, utils.R_SUBJ_COURSE, utils.R_SECTION_CODE) + coreRegexp *regexp.Regexp = regexp.MustCompile(`[0-9]{3}`) + personRegexp *regexp.Regexp = regexp.MustCompile(`(.+)・(.+)・(.+)`) - if coreText, hasCore := rowInfo["Core:"]; hasCore { - section.Core_flags = coreRegexp.FindAllString(utils.TrimWhitespace(coreText.Text()), -1) - } - - section.Syllabus_uri = syllabusURI + meetingDatesRegexp = regexp.MustCompile(utils.R_DATE_MDY) + meetingDaysRegexp = regexp.MustCompile(utils.R_WEEKDAY) + meetingTimesRegexp = regexp.MustCompile(utils.R_TIME_AM_PM) +) - if semesterGrades, ok := GradeMap[session.Name]; ok { - // We have to trim leading zeroes from the section number in order to match properly, since the grade data does not use leading zeroes - trimmedSectionNumber := strings.TrimLeft(section.Section_number, "0") - // Key into grademap should be uppercased like the grade data - gradeKey := strings.ToUpper(courseRef.Subject_prefix + courseRef.Course_number + trimmedSectionNumber) - sectionGrades, exists := semesterGrades[gradeKey] - if exists { - section.Grade_distribution = sectionGrades - } +// TODO: section requisites? +func parseSection(rowInfo map[string]*goquery.Selection, classInfo map[string]string) { + classNum, courseNum := getInternalClassAndCourseNum(classInfo) + session := getAcademicSession(rowInfo) + courseRef := parseCourse(courseNum, session, rowInfo, classInfo) + + sectionNumber := getSectionNumber(classInfo) + + id := primitive.NewObjectID() + + section := schema.Section{ + Id: id, + Section_number: sectionNumber, + Course_reference: courseRef.Id, + Academic_session: session, + Professors: parseProfessors(id, rowInfo, classInfo), + Teaching_assistants: getTeachingAssistants(rowInfo), + Internal_class_number: classNum, + Instruction_mode: getInstructionMode(classInfo), + Meetings: getMeetings(rowInfo), + Core_flags: getCoreFlags(rowInfo), + Syllabus_uri: getSyllabusUri(rowInfo), + Grade_distribution: getGradeDistribution(session, sectionNumber, courseRef), } + a, _ := json.Marshal(section) + println(string(a)) + a, _ = json.Marshal(*courseRef) + println(string(a)) // Add new section to section map - Sections[section.Id] = section + Sections[section.Id] = §ion // Append new section to course's section listing courseRef.Sections = append(courseRef.Sections, section.Id) } +// todo add logging for failing to get feilds? probably only max verbosity +func getInternalClassAndCourseNum(classInfo map[string]string) (string, string) { + if numbers, ok := classInfo["Class/Course Number:"]; ok { + classAndCourseNum := strings.Split(numbers, " / ") + if len(classAndCourseNum) == 2 { + return classAndCourseNum[0], classAndCourseNum[1] + } + } + return "", "" +} + func getAcademicSession(rowInfo map[string]*goquery.Selection) schema.AcademicSession { session := schema.AcademicSession{} @@ -102,9 +95,40 @@ func getAcademicSession(rowInfo map[string]*goquery.Selection) schema.AcademicSe return session } -var meetingDatesRegexp = regexp.MustCompile(utils.R_DATE_MDY) -var meetingDaysRegexp = regexp.MustCompile(utils.R_WEEKDAY) -var meetingTimesRegexp = regexp.MustCompile(utils.R_TIME_AM_PM) +func getSectionNumber(classInfo map[string]string) string { + if syllabus, ok := classInfo["Class Section:"]; ok { + matches := sectionPrefixRegexp.FindStringSubmatch(syllabus) + if len(matches) == 2 { + return matches[1] + } + } + return "" +} + +func getTeachingAssistants(rowInfo map[string]*goquery.Selection) []schema.Assistant { + assistantMatches := personRegexp.FindAllStringSubmatch(utils.TrimWhitespace(rowInfo["TA/RA(s):"].Text()), -1) + assistants := make([]schema.Assistant, 0, len(assistantMatches)) + + for _, match := range assistantMatches { + names := strings.Split(utils.TrimWhitespace(match[1]), " ") + + assistant := schema.Assistant{ + First_name: strings.Join(names[:len(names)-1], " "), + Last_name: names[len(names)-1], + Role: utils.TrimWhitespace(match[2]), + Email: utils.TrimWhitespace(match[3]), + } + assistants = append(assistants, assistant) + } + return assistants +} + +func getInstructionMode(classInfo map[string]string) string { + if mode, ok := classInfo["Instruction Mode:"]; ok { + return mode + } + return "" +} func getMeetings(rowInfo map[string]*goquery.Selection) []schema.Meeting { meetingItems := rowInfo["Schedule:"].Find("div.courseinfo__meeting-item--multiple") @@ -160,7 +184,40 @@ func getMeetings(rowInfo map[string]*goquery.Selection) []schema.Meeting { return meetings } -const timeLayout = "January 2, 2006" +func getCoreFlags(rowInfo map[string]*goquery.Selection) []string { + if core, ok := rowInfo["Core:"]; ok { + flags := coreRegexp.FindAllString(utils.TrimWhitespace(core.Text()), -1) + + if flags != nil { + return flags + } + } + return []string{} +} + +func getSyllabusUri(rowInfo map[string]*goquery.Selection) string { + if syllabus, ok := rowInfo["Syllabus:"]; ok { + link := syllabus.FindMatcher(goquery.Single("a")) + if link.Length() == 1 { + return link.AttrOr("href", "") + } + } + return "" +} + +func getGradeDistribution(session schema.AcademicSession, sectionNumber string, courseRef *schema.Course) []int { + if semesterGrades, ok := GradeMap[session.Name]; ok { + // We have to trim leading zeroes from the section number in order to match properly, since the grade data does not use leading zeroes + trimmedSectionNumber := strings.TrimLeft(sectionNumber, "0") + // Key into grademap should be uppercased like the grade data + gradeKey := strings.ToUpper(courseRef.Subject_prefix + courseRef.Course_number + trimmedSectionNumber) + sectionGrades, exists := semesterGrades[gradeKey] + if exists { + return sectionGrades + } + } + return []int{} +} func parseTimeOrPanic(value string) time.Time { date, err := time.ParseInLocation(timeLayout, value, timeLocation) diff --git a/parser/sectionParser_test.go b/parser/sectionParser_test.go new file mode 100644 index 0000000..d6d7d25 --- /dev/null +++ b/parser/sectionParser_test.go @@ -0,0 +1,144 @@ +package parser + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestGetInternalClassAndCourseNum(t *testing.T) { + loadTestData(t) + + for name, testCase := range testDataCache { + t.Run(name, func(t *testing.T) { + classNum, courseNum := getInternalClassAndCourseNum(testCase.ClassInfo) + expectedClassNum := testCase.Section.Internal_class_number + expectedCourseNumber := testCase.Course.Internal_course_number + + if classNum != expectedClassNum { + t.Errorf("Class Number: expected %s got %s", expectedClassNum, classNum) + } + + if courseNum != expectedCourseNumber { + t.Errorf("Class Number: expected %s got %s", expectedCourseNumber, courseNum) + } + + }) + } +} + +func TestGetAcademicSession(t *testing.T) { + loadTestData(t) + + for name, testCase := range testDataCache { + t.Run(name, func(t *testing.T) { + output := getAcademicSession(testCase.RowInfo) + expected := testCase.Section.Academic_session + + diff := cmp.Diff(expected, output) + + if diff != "" { + t.Errorf("Failed (-expected +got)\n %s", diff) + } + }) + } +} + +func TestGetSectionNumber(t *testing.T) { + loadTestData(t) + + for name, testCase := range testDataCache { + t.Run(name, func(t *testing.T) { + output := getSectionNumber(testCase.ClassInfo) + expected := testCase.Section.Section_number + + if output != expected { + t.Errorf("expected %s got %s", expected, output) + } + }) + + } +} + +func TestGetTeachingAssistants(t *testing.T) { + loadTestData(t) + + for name, testCase := range testDataCache { + t.Run(name, func(t *testing.T) { + output := getTeachingAssistants(testCase.RowInfo) + expected := testCase.Section.Teaching_assistants + + diff := cmp.Diff(expected, output) + + if diff != "" { + t.Errorf("Failed (-expected +got)\n %s", diff) + } + }) + } +} + +func TestGetInstructionMode(t *testing.T) { + loadTestData(t) + + for name, testCase := range testDataCache { + t.Run(name, func(t *testing.T) { + output := getInstructionMode(testCase.ClassInfo) + expected := testCase.Section.Instruction_mode + + if output != expected { + t.Errorf("expected %s got %s", expected, output) + } + }) + + } +} + +func TestGetMeetings(t *testing.T) { + loadTestData(t) + + for name, testCase := range testDataCache { + t.Run(name, func(t *testing.T) { + output := getMeetings(testCase.RowInfo) + expected := testCase.Section.Meetings + + diff := cmp.Diff(expected, output) + + if diff != "" { + t.Errorf("Failed (-expected +got)\n %s", diff) + } + }) + } +} + +func TestGetCoreFlags(t *testing.T) { + loadTestData(t) + + for name, testCase := range testDataCache { + t.Run(name, func(t *testing.T) { + output := getCoreFlags(testCase.RowInfo) + expected := testCase.Section.Core_flags + + diff := cmp.Diff(expected, output) + + if diff != "" { + t.Errorf("Failed (-expected +got)\n %s", diff) + } + }) + } +} + +func TestGetSyllabusUri(t *testing.T) { + loadTestData(t) + + for name, testCase := range testDataCache { + t.Run(name, func(t *testing.T) { + output := getSyllabusUri(testCase.RowInfo) + expected := testCase.Section.Syllabus_uri + + if output != expected { + t.Errorf("expected %s got %s", expected, output) + } + }) + + } +} diff --git a/parser/test_helper_test.go b/parser/test_helper_test.go new file mode 100644 index 0000000..6fb14e3 --- /dev/null +++ b/parser/test_helper_test.go @@ -0,0 +1,83 @@ +package parser + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "testing" + + "github.com/PuerkitoBio/goquery" + "github.com/UTDNebula/nebula-api/api/schema" +) + +type TestData struct { + RowInfo map[string]*goquery.Selection + ClassInfo map[string]string + Section schema.Section + Course schema.Course +} + +var testDataCache map[string]TestData + +func loadTestData(t *testing.T) { + t.Helper() + if testDataCache != nil { + return + } + + testDataCache = make(map[string]TestData) + dir, err := os.ReadDir("testdata") + if err != nil { + t.Fatalf("Failed to load testdata: %v", err) + } + + for _, file := range dir { + if !file.IsDir() { + continue + } + testCase, err := loadTest(file.Name()) + if err != nil { + t.Fatalf("Failed to load %s: %v", file.Name(), err) + } + testDataCache[file.Name()] = testCase + } +} + +func loadTest(dir string) (TestData, error) { + + htmlBytes, err := os.ReadFile(fmt.Sprintf("testdata/%s/input.html", dir)) + if err != nil { + return TestData{}, err + } + + doc, err := goquery.NewDocumentFromReader(bytes.NewReader(htmlBytes)) + if err != nil { + return TestData{}, err + } + + result := TestData{ + RowInfo: getRowInfo(doc), + ClassInfo: getClassInfo(doc), + } + + jsonBytes, err := os.ReadFile(fmt.Sprintf("testdata/%s/section.json", dir)) + if err != nil { + return result, err + } + err = json.Unmarshal(jsonBytes, &result.Section) + if err != nil { + return TestData{}, err + } + + jsonBytes, err = os.ReadFile(fmt.Sprintf("testdata/%s/course.json", dir)) + if err != nil { + return result, err + } + err = json.Unmarshal(jsonBytes, &result.Course) + if err != nil { + return TestData{}, err + } + + return result, nil +} diff --git a/parser/testdata/case_001/course.json b/parser/testdata/case_001/course.json new file mode 100644 index 0000000..65a0dc3 --- /dev/null +++ b/parser/testdata/case_001/course.json @@ -0,0 +1,23 @@ +{ + "_id": "67bd14d7b35a4cd7c0446f3c", + "subject_prefix": "ACCT", + "course_number": "2301", + "title": "Introductory Financial Accounting", + "description": "ACCT 2301 - Introductory Financial Accounting (3 semester credit hours) An introduction to financial reporting designed to create an awareness of the accounting concepts and principles for preparing the three basic financial statements: the income statement, balance sheet, and statement of cash flows. A minimum grade of C is required to take upper-division ACCT courses. (3-0) S", + "enrollment_reqs": "", + "school": "Naveen Jindal School of Management", + "credit_hours": "3", + "class_level": "Undergraduate", + "activity_type": "Lecture", + "grading": "Graded - Undergraduate", + "internal_course_number": "000061", + "prerequisites": null, + "corequisites": null, + "co_or_pre_requisites": null, + "sections": null, + "lecture_contact_hours": "3", + "laboratory_contact_hours": "0", + "offering_frequency": "S", + "catalog_year": "24", + "attributes": null +} diff --git a/parser/testdata/case_001/input.html b/parser/testdata/case_001/input.html new file mode 100644 index 0000000..29a0d89 --- /dev/null +++ b/parser/testdata/case_001/input.html @@ -0,0 +1,221 @@ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Course Title: + + Introductory Financial Accounting +
+ Class Info: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Class Section: + + ACCT2301.001.25S + + Instruction Mode: + + Face-to-Face +
+ Class Level: + + Undergraduate + + Activity Type: + + Lecture +
+ Semester Credit Hours: + + 3 + + Class/Course Number: + + 26595 / 000061 +
+ Grading: + + Graded - Undergraduate + + Session Type: + + Regular Academic Session +
+ Add Consent: + + No Consent + + Orion Date/Time: + + 2025-02-22 06:30:01 +
+ How often a course is scheduled: + + Once Each Long Summer + + +
+ +
+ Status: + + Enrollment Status: OPEN    Available Seats: 3    Enrolled Total: 64    Waitlist: 0 + +
+ Description: + + ACCT 2301 - Introductory Financial Accounting (3 semester credit hours) An introduction to financial reporting designed to create an awareness of the accounting concepts and principles for preparing the three basic financial statements: the income statement, balance sheet, and statement of cash flows. A minimum grade of C is required to take upper-division ACCT courses. (3-0) S + +
Enrollment Reqs:
  • ACCT 2301 Repeat Restriction
+ Instructor(s): + +
+
Jieying Zhang ・ Primary Instructor (50%) ・ jxz146230@utdallas.edu +
+
+
Naim Bugra Ozel ・ Primary Instructor (50%) ・ nbo150030@utdallas.edu +
+ +
TA/RA(s):
+
Galymzhan Tazhibayev ・ Teaching Assistant ・ gxt230023@utdallas.edu +
+
+
Dipta Banik ・ Teaching Assistant ・ dxb220047@utdallas.edu +
+
+ Schedule: + +
+

Class Location and Times

+

Term: 25S
Type: Regular Academic Session
Starts: January 21, 2025
Ends: May 16, 2025

+ +
+

+ January 21, 2025-May 9, 2025
+ Tuesday, Thursday
+ 8:30am-9:45am
+ JSOM 2.717 + +

+
SOM Building
Floor 2 - Room 2.717
+
+ +
+ +
+ College: + + Naveen Jindal School of Management + +
+ Syllabus: + + Syllabus for Introductory Financial Accounting (ACCT2301.001.25S) + +
+ Evaluation: + + An evaluation report for Introductory Financial Accounting (ACCT2301.001.25S) hasn't been posted. + +
+
+
The direct link to this class is: https://go.utdallas.edu/acct2301.001.25s
+ Register for this class on Orion: https://orion.utdallas.edu +
+
+ +
\ No newline at end of file diff --git a/parser/testdata/case_001/section.json b/parser/testdata/case_001/section.json new file mode 100644 index 0000000..ea2917c --- /dev/null +++ b/parser/testdata/case_001/section.json @@ -0,0 +1,53 @@ +{ + "_id": "67bbc02f368eb371bd4712ea", + "section_number": "001", + "course_reference": "67bbc02f368eb371bd4712e9", + "section_corequisites": null, + "academic_session": { + "name": "25S", + "start_date": "2025-01-21T00:00:00-06:00", + "end_date": "2025-05-16T00:00:00-05:00" + }, + "professors": [ + "67bbc02f368eb371bd4712eb", + "67bbc02f368eb371bd4712ec" + ], + "teaching_assistants": [ + { + "first_name": "Galymzhan", + "last_name": "Tazhibayev", + "role": "Teaching Assistant", + "email": "gxt230023@utdallas.edu" + }, + { + "first_name": "Dipta", + "last_name": "Banik", + "role": "Teaching Assistant", + "email": "dxb220047@utdallas.edu" + } + ], + "internal_class_number": "26595", + "instruction_mode": "Face-to-Face", + "meetings": [ + { + "start_date": "2025-01-21T00:00:00-06:00", + "end_date": "2025-05-09T00:00:00-05:00", + "meeting_days": [ + "Tuesday", + "Thursday" + ], + "start_time": "8:30am", + "end_time": "9:45am", + "modality": "", + "location": { + "building": "JSOM", + "room": "2.717", + "map_uri": "https://locator.utdallas.edu/SOM_2.717" + } + } + ], + "core_flags": [], + "syllabus_uri": "https://dox.utdallas.edu/syl152552", + "grade_distribution": [], + "attributes": null +} \ No newline at end of file diff --git a/parser/testdata/case_002/course.json b/parser/testdata/case_002/course.json new file mode 100644 index 0000000..7c5f048 --- /dev/null +++ b/parser/testdata/case_002/course.json @@ -0,0 +1,23 @@ +{ + "_id": "67bd25abd55e2ff564c3b9f0", + "subject_prefix": "BA", + "course_number": "1320", + "title": "Business in a Global World", + "description": "BA 1320 - Business in a Global World (3 semester credit hours) This course provides a primer on the impacts of globalization on business. We equip students with the basic facts of globalization and examine the business underpinnings and the institutions that shape globalization. We discuss major trends and the future of international management. The aim is an ability to think strategically and critically about global business issues. (3-0) S", + "enrollment_reqs": "", + "school": "Naveen Jindal School of Management", + "credit_hours": "3", + "class_level": "Undergraduate", + "activity_type": "Lecture", + "grading": "Graded - Undergraduate", + "internal_course_number": "015444", + "prerequisites": null, + "corequisites": null, + "co_or_pre_requisites": null, + "sections": null, + "lecture_contact_hours": "3", + "laboratory_contact_hours": "0", + "offering_frequency": "S", + "catalog_year": "24", + "attributes": null +} diff --git a/parser/testdata/case_002/input.html b/parser/testdata/case_002/input.html new file mode 100644 index 0000000..27b81ba --- /dev/null +++ b/parser/testdata/case_002/input.html @@ -0,0 +1,215 @@ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Course Title: + + Business in a Global World +
+ Class Info: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Class Section: + + BA1320.501.25S + + Instruction Mode: + + Face-to-Face +
+ Class Level: + + Undergraduate + + Activity Type: + + Lecture +
+ Semester Credit Hours: + + 3 + + Class/Course Number: + + 27195 / 015444 +
+ Grading: + + Graded - Undergraduate + + Session Type: + + Regular Academic Session +
+ Add Consent: + + No Consent + + Orion Date/Time: + + 2025-02-24 06:30:01 +
+ How often a course is scheduled: + + Once Each Long Summer + + +
+ +
+ Status: + + Enrollment Status: CLOSED/FULL    Available Seats: 0    Enrolled Total: 56    Waitlist: 0 + +
+ Description: + + BA 1320 - Business in a Global World (3 semester credit hours) This course provides a primer on the impacts of globalization on business. We equip students with the basic facts of globalization and examine the business underpinnings and the institutions that shape globalization. We discuss major trends and the future of international management. The aim is an ability to think strategically and critically about global business issues. (3-0) S + +
Enrollment Reqs:
  • BA 1320 Repeat Restriction
+ Instructor(s): + +
+
Peter Lewin ・ Primary Instructor ・ plewin@utdallas.edu +
+ +
TA/RA(s):
+
Ravi Kiran Reddy Konda ・ Teaching Assistant ・ rxk230080@utdallas.edu +
+
+ Schedule: + +
+

Class Location and Times

+

Term: 25S
Type: Regular Academic Session
Starts: January 21, 2025
Ends: May 16, 2025

+ +
+

+ January 21, 2025-May 9, 2025
+ Tuesday, Thursday
+ 5:30pm-6:45pm
+ JSOM 12.218 + +

+
SOM Building
Floor 12 - Room 12.218
+
+ +
+ +
Core:Texas Core Areas 080+090 - Social and Behavioral Science + CAO
+ College: + + Naveen Jindal School of Management + +
+ Syllabus: + + Syllabus for Business in a Global World (BA1320.501.25S) + +
+ Evaluation: + + An evaluation report for Business in a Global World (BA1320.501.25S) hasn't been posted. + +
+
+
The direct link to this class is: https://go.utdallas.edu/ba1320.501.25s
+ Register for this class on Orion: https://orion.utdallas.edu +
+
+ +
diff --git a/parser/testdata/case_002/section.json b/parser/testdata/case_002/section.json new file mode 100644 index 0000000..a37267c --- /dev/null +++ b/parser/testdata/case_002/section.json @@ -0,0 +1,49 @@ +{ + "_id": "67bd25abd55e2ff564c3b9f1", + "section_number": "501", + "course_reference": "67bd25abd55e2ff564c3b9f0", + "section_corequisites": null, + "academic_session": { + "name": "25S", + "start_date": "2025-01-21T00:00:00-06:00", + "end_date": "2025-05-16T00:00:00-05:00" + }, + "professors": [ + "67bd25abd55e2ff564c3b9f2" + ], + "teaching_assistants": [ + { + "first_name": "Ravi Kiran Reddy", + "last_name": "Konda", + "role": "Teaching Assistant", + "email": "rxk230080@utdallas.edu" + } + ], + "internal_class_number": "27195", + "instruction_mode": "Face-to-Face", + "meetings": [ + { + "start_date": "2025-01-21T00:00:00-06:00", + "end_date": "2025-05-09T00:00:00-05:00", + "meeting_days": [ + "Tuesday", + "Thursday" + ], + "start_time": "5:30pm", + "end_time": "6:45pm", + "modality": "", + "location": { + "building": "JSOM", + "room": "12.218", + "map_uri": "https://locator.utdallas.edu/SOM_12.218" + } + } + ], + "core_flags": [ + "080", + "090" + ], + "syllabus_uri": "https://dox.utdallas.edu/syl153033", + "grade_distribution": [], + "attributes": null +} \ No newline at end of file diff --git a/parser/testdata/case_003/course.json b/parser/testdata/case_003/course.json new file mode 100644 index 0000000..ab4317c --- /dev/null +++ b/parser/testdata/case_003/course.json @@ -0,0 +1,23 @@ +{ + "_id": "67bd25abd55e2ff564c3b9f3", + "subject_prefix": "BIOL", + "course_number": "6111", + "title": "Graduate Research Presentation", + "description": "BIOL 6111 - Graduate Research Presentation (1 semester credit hour) This course will train graduate students (MS and PhD) in hypothesis building and testing, designing, and conducting experiments, and presenting scientific findings in an efficient and clear manner. During the class, graduate students will discuss and present their graduate research work-in-progress. Significant time outside of class will also be required to analyze data, assemble, and practice presentations. May be repeated for credit as topics vary (2 semester credit hours maximum). Department consent required. (1-0) S", + "enrollment_reqs": "", + "school": "School of Natural Sciences and Mathematics", + "credit_hours": "1", + "class_level": "Graduate", + "activity_type": "Lecture", + "grading": "Graded - Graduate", + "internal_course_number": "016577", + "prerequisites": null, + "corequisites": null, + "co_or_pre_requisites": null, + "sections": null, + "lecture_contact_hours": "1", + "laboratory_contact_hours": "0", + "offering_frequency": "S", + "catalog_year": "24", + "attributes": null +} diff --git a/parser/testdata/case_003/input.html b/parser/testdata/case_003/input.html new file mode 100644 index 0000000..025fc66 --- /dev/null +++ b/parser/testdata/case_003/input.html @@ -0,0 +1,211 @@ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Course Title: + + Graduate Research Presentation +
+ Class Info: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Class Section: + + BIOL6111.016.25S + + Instruction Mode: + + Face-to-Face +
+ Class Level: + + Graduate + + Activity Type: + + Lecture +
+ Semester Credit Hours: + + 1 + + Class/Course Number: + + 29611 / 016577 +
+ Grading: + + Graded - Graduate + + Session Type: + + Regular Academic Session +
+ Add Consent: + + Department Consent Required + + Orion Date/Time: + + 2025-02-13 06:30:01 +
+ How often a course is scheduled: + + Once Each Long Summer + + +
+ +
+ Status: + + Enrollment Status: OPEN    Available Seats: 9    Enrolled Total: 1    Waitlist: 0 + +
+ Description: + + BIOL 6111 - Graduate Research Presentation (1 semester credit hour) This course will train graduate students (MS and PhD) in hypothesis building and testing, designing, and conducting experiments, and presenting scientific findings in an efficient and clear manner. During the class, graduate students will discuss and present their graduate research work-in-progress. Significant time outside of class will also be required to analyze data, assemble, and practice presentations. May be repeated for credit as topics vary (2 semester credit hours maximum). Department consent required. (1-0) S + +
Class Attributes:
  • Course Allows Subtitles
+ Instructor(s): + +
+
Tian Hong ・ Primary Instructor ・ txh240018@utdallas.edu +
+ +
TA/RA(s):(none)
+ Schedule: + +
+

Class Location and Times

+

Term: 25S
Type: Regular Academic Session
Starts: January 21, 2025
Ends: May 16, 2025

+ +
+

+ January 21, 2025-May 9, 2025
+ Thursday
+ 9:30am-10:20am
+ +

+
See instructor for room assignment
+
+ +
+ +
+ College: + + School of Natural Sciences and Mathematics + +
+ Syllabus: + + Syllabus for Graduate Research Presentation (BIOL6111.016.25S) + +
+ Evaluation: + + An evaluation report for Graduate Research Presentation (BIOL6111.016.25S) hasn't been posted. + +
+
+
The direct link to this class is: https://go.utdallas.edu/biol6111.016.25s
+ Register for this class on Orion: https://orion.utdallas.edu +
+
+ +
diff --git a/parser/testdata/case_003/section.json b/parser/testdata/case_003/section.json new file mode 100644 index 0000000..4ccfc51 --- /dev/null +++ b/parser/testdata/case_003/section.json @@ -0,0 +1,38 @@ +{ + "_id": "67bd25abd55e2ff564c3b9f4", + "section_number": "016", + "course_reference": "67bd25abd55e2ff564c3b9f3", + "section_corequisites": null, + "academic_session": { + "name": "25S", + "start_date": "2025-01-21T00:00:00-06:00", + "end_date": "2025-05-16T00:00:00-05:00" + }, + "professors": [ + "67bd25abd55e2ff564c3b9f5" + ], + "teaching_assistants": [], + "internal_class_number": "29611", + "instruction_mode": "Face-to-Face", + "meetings": [ + { + "start_date": "2025-01-21T00:00:00-06:00", + "end_date": "2025-05-09T00:00:00-05:00", + "meeting_days": [ + "Thursday" + ], + "start_time": "9:30am", + "end_time": "10:20am", + "modality": "", + "location": { + "building": "", + "room": "", + "map_uri": "" + } + } + ], + "core_flags": [], + "syllabus_uri": "https://dox.utdallas.edu/syl153921", + "grade_distribution": [], + "attributes": null +} \ No newline at end of file diff --git a/parser/testdata/case_004/course.json b/parser/testdata/case_004/course.json new file mode 100644 index 0000000..bf75d76 --- /dev/null +++ b/parser/testdata/case_004/course.json @@ -0,0 +1,23 @@ +{ + "_id": "67bd26ea53d4e338e52a4e34", + "subject_prefix": "AERO", + "course_number": "3320", + "title": "- Recitation", + "description": "- ()", + "enrollment_reqs": "", + "school": "Undergraduate Studies", + "credit_hours": "Non-Enroll", + "class_level": "Undergraduate", + "activity_type": "Laboratory - No Lab Fee", + "grading": "Graded - Undergraduate", + "internal_course_number": "000243", + "prerequisites": null, + "corequisites": null, + "co_or_pre_requisites": null, + "sections": null, + "lecture_contact_hours": "", + "laboratory_contact_hours": "", + "offering_frequency": "", + "catalog_year": "24", + "attributes": null +} \ No newline at end of file diff --git a/parser/testdata/case_004/input.html b/parser/testdata/case_004/input.html new file mode 100644 index 0000000..c3fae5c --- /dev/null +++ b/parser/testdata/case_004/input.html @@ -0,0 +1,209 @@ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Course Title: + + - Recitation +
+ Class Info: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Class Section: + + AERO3320.201.25S + + Instruction Mode: + + Face-to-Face +
+ Class Level: + + Undergraduate + + Activity Type: + + Laboratory - No Lab Fee +
+ Semester Credit Hours: + + Non-Enroll + + Class/Course Number: + + 28551 / 000243 +
+ Grading: + + Graded - Undergraduate + + Session Type: + + Regular Academic Session +
+ Add Consent: + + Instructor Consent Required + + Orion Date/Time: + + 2025-01-25 06:30:01 +
+ How often a course is scheduled: + + ∅ + + +
+ +
+ Status: + + Enrollment Status: OPEN    Available Seats: 16    Enrolled Total: 3    Waitlist: 0 + +
+ Description: + + - () + +
Class Notes:
  • Must register in lecture. Course taught at UNT. The course will have multiple deliveries. The instructors will make an announcement to students for the course expectations for attendance/delivery.
+ Instructor(s): + + -Staff- +
TA/RA(s):(none)
+ Schedule: + +
+

Class Location and Times

+

Term: 25S
Type: Regular Academic Session
Starts: January 21, 2025
Ends: May 16, 2025

+ +
+

+ January 21, 2025-May 9, 2025
+ Thursday
+ 2:00pm-3:00pm
+ + +

+
+
+ +
+ +
+ College: + + Undergraduate Studies + +
+ Syllabus: + + A Syllabus for - Recitation (AERO3320.201.25S) Has Not Been Posted + +
+ Evaluation: + + An evaluation report for (AERO3320.201.25S) hasn't been posted. + +
+
+
The direct link to this class is: https://go.utdallas.edu/aero3320.201.25s
+ Register for this class on Orion: https://orion.utdallas.edu +
+
+ +
diff --git a/parser/testdata/case_004/section.json b/parser/testdata/case_004/section.json new file mode 100644 index 0000000..d84dab2 --- /dev/null +++ b/parser/testdata/case_004/section.json @@ -0,0 +1,36 @@ +{ + "_id": "67bd26ea53d4e338e52a4e35", + "section_number": "201", + "course_reference": "67bd26ea53d4e338e52a4e34", + "section_corequisites": null, + "academic_session": { + "name": "25S", + "start_date": "2025-01-21T00:00:00-06:00", + "end_date": "2025-05-16T00:00:00-05:00" + }, + "professors": [], + "teaching_assistants": [], + "internal_class_number": "28551", + "instruction_mode": "Face-to-Face", + "meetings": [ + { + "start_date": "2025-01-21T00:00:00-06:00", + "end_date": "2025-05-09T00:00:00-05:00", + "meeting_days": [ + "Thursday" + ], + "start_time": "2:00pm", + "end_time": "3:00pm", + "modality": "", + "location": { + "building": "", + "room": "", + "map_uri": "" + } + } + ], + "core_flags": [], + "syllabus_uri": "", + "grade_distribution": [], + "attributes": null +} \ No newline at end of file diff --git a/parser/testdata/case_005/course.json b/parser/testdata/case_005/course.json new file mode 100644 index 0000000..da300c8 --- /dev/null +++ b/parser/testdata/case_005/course.json @@ -0,0 +1,23 @@ +{ + "_id": "67bd26ea53d4e338e52a4e36", + "subject_prefix": "AERO", + "course_number": "4320", + "title": "- Laboratory", + "description": "- ()", + "enrollment_reqs": "", + "school": "Undergraduate Studies", + "credit_hours": "4", + "class_level": "Undergraduate", + "activity_type": "Lecture", + "grading": "Graded - Undergraduate", + "internal_course_number": "000255", + "prerequisites": null, + "corequisites": null, + "co_or_pre_requisites": null, + "sections": null, + "lecture_contact_hours": "", + "laboratory_contact_hours": "", + "offering_frequency": "", + "catalog_year": "24", + "attributes": null +} \ No newline at end of file diff --git a/parser/testdata/case_005/input.html b/parser/testdata/case_005/input.html new file mode 100644 index 0000000..e8140e0 --- /dev/null +++ b/parser/testdata/case_005/input.html @@ -0,0 +1,219 @@ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Course Title: + + - Laboratory +
+ Class Info: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Class Section: + + AERO4320.002.25S + + Instruction Mode: + + Face-to-Face +
+ Class Level: + + Undergraduate + + Activity Type: + + Lecture +
+ Semester Credit Hours: + + 4 + + Class/Course Number: + + 27906 / 000255 +
+ Grading: + + Graded - Undergraduate + + Session Type: + + Regular Academic Session +
+ Add Consent: + + Instructor Consent Required + + Orion Date/Time: + + 2025-01-25 06:30:01 +
+ How often a course is scheduled: + + ∅ + + +
+ +
+ Status: + + Enrollment Status: OPEN    Available Seats: 17    Enrolled Total: 2    Waitlist: 0 + +
+ Description: + + - () + +
Class Notes:
  • Lecture and Lab combined. Must register in recitation. Course taught at UNT. The course will have multiple deliveries. The instructors will make an announcement to students for the course expectations for attendance/delivery.
+ Instructor(s): + + -Staff- +
TA/RA(s):(none)
+ Schedule: + +
+

Class Location and Times

+

Term: 25S
Type: Regular Academic Session
Starts: January 21, 2025
Ends: May 16, 2025

+ +
+

+ January 21, 2025-May 9, 2025
+ Tuesday
+ 1:00pm-3:45pm
+ + +

+
+
+
+

+ January 21, 2025-May 9, 2025
+ Thursday
+ 3:30pm-5:20pm
+ + +

+
+
+ +
+ +
+ College: + + Undergraduate Studies + +
+ Syllabus: + + A Syllabus for - Laboratory (AERO4320.002.25S) Has Not Been Posted + +
+ Evaluation: + + An evaluation report for (AERO4320.002.25S) hasn't been posted. + +
+
+
The direct link to this class is: https://go.utdallas.edu/aero4320.002.25s
+ Register for this class on Orion: https://orion.utdallas.edu +
+
+ +
diff --git a/parser/testdata/case_005/section.json b/parser/testdata/case_005/section.json new file mode 100644 index 0000000..cfc0abb --- /dev/null +++ b/parser/testdata/case_005/section.json @@ -0,0 +1,51 @@ +{ + "_id": "67bd26ea53d4e338e52a4e37", + "section_number": "002", + "course_reference": "67bd26ea53d4e338e52a4e36", + "section_corequisites": null, + "academic_session": { + "name": "25S", + "start_date": "2025-01-21T00:00:00-06:00", + "end_date": "2025-05-16T00:00:00-05:00" + }, + "professors": [], + "teaching_assistants": [], + "internal_class_number": "27906", + "instruction_mode": "Face-to-Face", + "meetings": [ + { + "start_date": "2025-01-21T00:00:00-06:00", + "end_date": "2025-05-09T00:00:00-05:00", + "meeting_days": [ + "Tuesday" + ], + "start_time": "1:00pm", + "end_time": "3:45pm", + "modality": "", + "location": { + "building": "", + "room": "", + "map_uri": "" + } + }, + { + "start_date": "2025-01-21T00:00:00-06:00", + "end_date": "2025-05-09T00:00:00-05:00", + "meeting_days": [ + "Thursday" + ], + "start_time": "3:30pm", + "end_time": "5:20pm", + "modality": "", + "location": { + "building": "", + "room": "", + "map_uri": "" + } + } + ], + "core_flags": [], + "syllabus_uri": "", + "grade_distribution": [], + "attributes": null +} \ No newline at end of file