diff --git a/.gitignore b/.gitignore index 7774531..9471423 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ deploy_log.sh .vscode/ .firebase/ /api-tools +/qodana.yaml # output data and logs data/ diff --git a/parser/courseParser.go b/parser/courseParser.go index 48c7edc..7d59baf 100644 --- a/parser/courseParser.go +++ b/parser/courseParser.go @@ -13,14 +13,21 @@ import ( ) var ( - coursePrefixRexp *regexp.Regexp = utils.Regexpf(`^%s`, utils.R_SUBJ_COURSE_CAP) - contactRegexp *regexp.Regexp = regexp.MustCompile(`\(([0-9]+)-([0-9]+)\)\s+([SUFY]+)`) + // coursePrefixRegexp matches the course prefix and number (e.g., "CS 1337"). + coursePrefixRegexp = utils.Regexpf(`^%s`, utils.R_SUBJ_COURSE_CAP) + + // contactRegexp matches the contact hours and offering frequency from the course description + // (e.g. "(12-34) SUS") + contactRegexp = 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 { +// parseCourse returns a pointer to the course specified by the +// provided information. If the associated course is not found in +// Courses, it will run getCourse and add the result to Courses. +func parseCourse(internalCourseNumber 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 catalogYear := getCatalogYear(session) - courseKey := courseNum + catalogYear + courseKey := internalCourseNumber + catalogYear // Don't recreate the course if it already exists course, courseExists := Courses[courseKey] @@ -28,7 +35,7 @@ func parseCourse(courseNum string, session schema.AcademicSession, rowInfo map[s return course } - course = getCourse(courseNum, session, rowInfo, classInfo) + course = getCourse(internalCourseNumber, session, rowInfo, classInfo) // Get closure for parsing course requisites (god help me) enrollmentReqs, hasEnrollmentReqs := rowInfo["Enrollment Reqs:"] @@ -39,8 +46,10 @@ func parseCourse(courseNum string, session schema.AcademicSession, rowInfo map[s 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 { +// getCourse extracts course details from the provided information and creates a schema.Course object. +// This function does not modify any global state. +// Returns a pointer to the newly created schema.Course object. +func getCourse(internalCourseNumber string, session schema.AcademicSession, rowInfo map[string]*goquery.Selection, classInfo map[string]string) *schema.Course { CoursePrefix, CourseNumber := getPrefixAndNumber(classInfo) course := schema.Course{ @@ -54,7 +63,7 @@ func getCourse(courseNum string, session schema.AcademicSession, rowInfo map[str Class_level: classInfo["Class Level:"], Activity_type: classInfo["Activity Type:"], Grading: classInfo["Grading:"], - Internal_course_number: courseNum, + Internal_course_number: internalCourseNumber, Catalog_year: getCatalogYear(session), } @@ -70,6 +79,10 @@ func getCourse(courseNum string, session schema.AcademicSession, rowInfo map[str return &course } +// getCatalogYear determines the catalog year from the academic session information. +// It assumes the session name starts with a 2-digit year and a semester character ('F', 'S', 'U'). +// Fall (S) and Summer U sessions are associated with the previous calendar year. +// (e.g, 20F = 20, 20S = 19) func getCatalogYear(session schema.AcademicSession) string { sessionYear, err := strconv.Atoi(session.Name[0:2]) if err != nil { @@ -79,22 +92,24 @@ func getCatalogYear(session schema.AcademicSession) string { switch sessionSemester { case 'F': return strconv.Itoa(sessionYear) - case 'S': - return strconv.Itoa(sessionYear - 1) - case 'U': + case 'S', 'U': return strconv.Itoa(sessionYear - 1) default: panic(fmt.Errorf("encountered invalid session semester '%c!'", sessionSemester)) } } +// getPrefixAndNumber returns the 2nd and 3rd matched values from a coursePrefixRegexp on +// `ClassInfo["Class Section:"]`. It expects ClassInfo to contain "Class Section:" key. +// If there are no matches, empty strings are returned. 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) + matches := coursePrefixRegexp.FindStringSubmatch(sectionId) if len(matches) == 3 { return matches[1], matches[2] } + panic("failed to course prefix and number") } - return "", "" + panic("could not find 'Class Section:' in ClassInfo") } diff --git a/parser/courseParser_test.go b/parser/courseParser_test.go index d573b69..4ecf2e1 100644 --- a/parser/courseParser_test.go +++ b/parser/courseParser_test.go @@ -1,23 +1,23 @@ package parser import ( + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "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) + t.Parallel() - for name, testCase := range testDataCache { + for name, testCase := range testData { 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")) + diff := cmp.Diff(expected, output, cmpopts.IgnoreFields(schema.Course{}, "Id", "Sections", "Enrollment_reqs", "Prerequisites")) if diff != "" { t.Errorf("Failed (-expected +got)\n %s", diff) @@ -28,97 +28,131 @@ func TestGetCourse(t *testing.T) { } func TestGetCatalogYear(t *testing.T) { + t.Parallel() + testCases := map[string]struct { Session schema.AcademicSession Expected string + Panic bool }{ "Case_001": { - Session: schema.AcademicSession{ - Name: "25S", - }, + Session: schema.AcademicSession{Name: "25S"}, Expected: "24", - }, "Case_002": { - Session: schema.AcademicSession{ - Name: "25F", - }, + }, + "Case_002": { + Session: schema.AcademicSession{Name: "25F"}, Expected: "25", - }, "Case_003": { - Session: schema.AcademicSession{ - Name: "22U", - }, + }, + "Case_003": { + Session: schema.AcademicSession{Name: "22U"}, Expected: "21", - }, "Case_004": { - Session: schema.AcademicSession{ - Name: "20S", - }, + }, + "Case_004": { + Session: schema.AcademicSession{Name: "20S"}, Expected: "19", }, + "Case_005": { + Session: schema.AcademicSession{Name: "Garbage"}, + Panic: true, + }, + "Case_006": { + Session: schema.AcademicSession{Name: "20P"}, + Panic: true, + }, } - for name, tc := range testCases { + for name, testCase := range testCases { t.Run(name, func(t *testing.T) { - output := getCatalogYear(tc.Session) + t.Parallel() + + defer func() { + // Test fails if we panic when we didn't want to or didn't when we did + if rec := recover(); rec != nil { + if !testCase.Panic { + t.Errorf("unexpected panic for session %q: %v", testCase.Session.Name, rec) + } + } else { + if testCase.Panic { + t.Errorf("expected panic for session %q but got none", testCase.Session.Name) + } + } + }() - if output != tc.Expected { - t.Errorf("expected %s got %s", tc.Expected, output) + // only call if we *expect* it to succeed + output := getCatalogYear(testCase.Session) + if !testCase.Panic && output != testCase.Expected { + t.Errorf("expected %q, got %q", testCase.Expected, output) } }) - } } func TestGetPrefixAndCourseNum(t *testing.T) { + t.Parallel() + testCases := map[string]struct { - classInfo map[string]string - prefix string - number string + ClassInfo map[string]string + Prefix string + Number string + Panic bool }{ "Case_001": { - classInfo: map[string]string{ + ClassInfo: map[string]string{ "Class Section:": "ACCT2301.001.25S", }, - prefix: "ACCT", - number: "2301", + Prefix: "ACCT", + Number: "2301", }, "Case_002": { - classInfo: map[string]string{ + ClassInfo: map[string]string{ "Class Section:": "ENTP3301.002.24S", }, - prefix: "ENTP", - number: "3301", + Prefix: "ENTP", + Number: "3301", }, "Case_003": { - classInfo: map[string]string{ + ClassInfo: map[string]string{ "Class Section:": "Garbage In, Garbage out", }, - prefix: "", - number: "", + Panic: true, }, "Case_004": { - classInfo: map[string]string{ + ClassInfo: map[string]string{ "Class Section:": "ENTP33S", }, - prefix: "", - number: "", + Panic: true, }, "Case_005": { - classInfo: map[string]string{ + ClassInfo: map[string]string{ "Class Section:": "", }, - prefix: "", - number: "", + Panic: true, }, } for name, testCase := range testCases { t.Run(name, func(t *testing.T) { - prefix, number := getPrefixAndNumber(testCase.classInfo) + defer func() { + if r := recover(); r != nil { + if !testCase.Panic { + t.Errorf("unexpected panic for input %q: %v", name, r) + } + } else { + if testCase.Panic { + t.Errorf("expected panic for input %q but none occurred", name) + } + } + }() - 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) + prefix, number := getPrefixAndNumber(testCase.ClassInfo) + + if !testCase.Panic { + if prefix != testCase.Prefix { + t.Errorf("expected %q got %q", testCase.Prefix, prefix) + } + if number != testCase.Number { + t.Errorf("expected %q got %q", testCase.Number, number) + } } }) } diff --git a/parser/parser.go b/parser/parser.go index b1dafb5..82b1d08 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -13,25 +13,33 @@ import ( "github.com/UTDNebula/nebula-api/api/schema" ) -// Main dictionaries for mapping unique keys to the actual data -var Sections = make(map[primitive.ObjectID]*schema.Section) -var Courses = make(map[string]*schema.Course) -var Professors = make(map[string]*schema.Professor) +var ( + // Sections dictionary for mapping UUIDs to a *schema.Section + Sections = make(map[primitive.ObjectID]*schema.Section) -// Auxilliary dictionaries for mapping the generated ObjectIDs to the keys used in the above maps, used for validation purposes -var CourseIDMap = make(map[primitive.ObjectID]string) -var ProfessorIDMap = make(map[primitive.ObjectID]string) + // Courses dictionary for keys (Internal_course_number + Catalog_year) to a *schema.Course + Courses = make(map[string]*schema.Course) -// Requisite parser closures associated with courses -var ReqParsers = make(map[primitive.ObjectID]func()) + // Professors dictionary for keys (First_name + Last_name) to a *schema.Professor + Professors = make(map[string]*schema.Professor) -// Grade mappings for section grade distributions, mapping is MAP[SEMESTER] -> MAP[SUBJECT + NUMBER + SECTION] -> GRADE DISTRIBUTION -var GradeMap map[string]map[string][]int + //CourseIDMap auxiliary dictionary for mapping UUIDs to a *schema.Course + CourseIDMap = make(map[primitive.ObjectID]string) -// Time location for dates (uses America/Chicago tz database zone for CDT which accounts for daylight saving) -var timeLocation, timeError = time.LoadLocation("America/Chicago") + //ProfessorIDMap auxiliary dictionary for mapping UUIDs to a *schema.Professor + ProfessorIDMap = make(map[primitive.ObjectID]string) -// Externally exposed parse function + // ReqParsers dictionary mapping course UUIDs to the func() that parsers its Reqs + ReqParsers = make(map[primitive.ObjectID]func()) + + // GradeMap mappings for section grade distributions, mapping is MAP[SEMESTER] -> MAP[SUBJECT + NUMBER + SECTION] -> GRADE DISTRIBUTION + GradeMap map[string]map[string][]int + + // timeLocation Time location for dates (uses America/Chicago tz database zone for CDT which accounts for daylight saving) + timeLocation, timeError = time.LoadLocation("America/Chicago") +) + +// Parse Externally exposed parse function func Parse(inDir string, outDir string, csvPath string, skipValidation bool) { // Panic if timeLocation didn't load properly @@ -91,7 +99,9 @@ func Parse(inDir string, outDir string, csvPath string, skipValidation bool) { utils.WriteJSON(fmt.Sprintf("%s/professors.json", outDir), utils.GetMapValues(Professors)) } -// Internal parse function +// parse is an internal helper function that parses a single HTML file. +// It opens the file, creates a goquery document, and calls parseSection to +// extract section data. func parse(path string) { utils.VPrintf("Parsing %s...", path) @@ -109,14 +119,8 @@ func parse(path string) { panic(err) } - // 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) - - // Get the class and course num by splitting classInfo value + parseSection(getRowInfo(doc), getClassInfo(doc)) - parseSection(rowInfo, classInfo) utils.VPrint("Parsed!") } diff --git a/parser/parser_test.go b/parser/parser_test.go new file mode 100644 index 0000000..6a79fd1 --- /dev/null +++ b/parser/parser_test.go @@ -0,0 +1,589 @@ +package parser + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "flag" + "fmt" + "log" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/PuerkitoBio/goquery" + "github.com/UTDNebula/api-tools/utils" + "github.com/UTDNebula/nebula-api/api/schema" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type TestData struct { + Input string + RowInfo map[string]*goquery.Selection + ClassInfo map[string]string + Section schema.Section + Course schema.Course + Professors []schema.Professor +} + +// testData global dictionary containing the data from /testdata by folder name +var testData map[string]TestData + +// TestMain entry point for all tests in the parser package. +// The function will load `./testdata` into memory before running +// the tests so that test can run in parallel. +// +// You can optionally provide the flag `update`, which will run +// updateTestData. Example usage +// +// `go test -v ./parser -args -update` +func TestMain(m *testing.M) { + update := flag.Bool("update", false, "Regenerates the expected output for the provided test inputs. Should only be used when you are 100% sure your code is correct! It will make all test pass :)") + + if !flag.Parsed() { + flag.Parse() + } + + if *update { + if err := updateTestData(); err != nil { + log.Fatalf("Error updating test data: %v", err) + } + log.Println("Successfully updated test data") + os.Exit(0) + } + + testData = make(map[string]TestData) + dir, err := os.ReadDir("testdata") + if err != nil { + log.Fatalf("Failed to load testdata: %v", err) + } + + for _, file := range dir { + if !file.IsDir() { + continue + } + if testData[file.Name()], err = loadTest(file.Name()); err != nil { + log.Fatalf("Failed to load %s: %v", file.Name(), err) + } + } + + os.Exit(m.Run()) +} + +func loadTest(dir string) (result TestData, err error) { + htmlBytes, err := os.ReadFile(fmt.Sprintf("testdata/%s/input.html", dir)) + if err != nil { + return + } + + doc, err := goquery.NewDocumentFromReader(bytes.NewReader(htmlBytes)) + if err != nil { + return + } + result.Input = string(htmlBytes) + result.RowInfo = getRowInfo(doc) + result.Section, err = unmarshallFile[schema.Section](fmt.Sprintf("testdata/%s/section.json", dir)) + if err != nil { + return + } + result.Course, err = unmarshallFile[schema.Course](fmt.Sprintf("testdata/%s/course.json", dir)) + if err != nil { + return + } + result.Professors, err = unmarshallFile[[]schema.Professor](fmt.Sprintf("testdata/%s/professors.json", dir)) + if err != nil { + return + } + result.ClassInfo, err = unmarshallFile[map[string]string](fmt.Sprintf("testdata/%s/classinfo.json", dir)) + if err != nil { + return + } + + return +} + +// updateTestData regenerates /testdata by parsing all `.html` files under it +// (recursively via utils.GetAllFilesWithExtension) and saving the current +// output as the new expected output. +// +// The expected format for each test case is: +// +// /case_XXX/ +// - input.html +// - classinfo.json +// - course.json +// - section.json +// - professors.json +// +// It also regenerates the cumulative JSONs (e.g., Courses.json) by running +// Parse on the /testdata. +// +// The function creates the new testdata in a temp dir, then replaces the +// existing one atomically to avoid corruption. Duplicate inputs (based on +// SHA-256) are skipped. +// +// Errors may still occur while copying or deleting the testdata directory. +func updateTestData() error { + log.Printf("Updating test data for the given inputs") + //doesn't do anything since there is no profile data + loadProfiles("") + + tempDir, err := os.MkdirTemp("", "testdata-*") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(tempDir) + + //Fill temp dir with all the test cases and expected values + + duplicates := make(map[string]bool) + + for i, input := range utils.GetAllFilesWithExtension("testdata", ".html") { + parse(input) + + for _, course := range Courses { + ReqParsers[course.Id]() + } + + htmlBytes, err := os.ReadFile(input) + if err != nil { + return fmt.Errorf("failed to load test data: %v", err) + } + + //ensure no duplicate inputs + hash := sha256.Sum256(htmlBytes) + hashStr := hex.EncodeToString(hash[:]) + if duplicate := duplicates[hashStr]; duplicate { + log.Printf("Duplicate test found %s, slipping\n", input) + continue + } else { + duplicates[hashStr] = true + } + + //This is gross, the parseXYZ() functions don't return so the only way to access the results is from the maps + var course schema.Course + for _, c := range Courses { + course = *c + break + } + + var section schema.Section + for _, s := range Sections { + section = *s + break + } + + professors := make([]schema.Professor, 0, len(Professors)) + for _, prof := range Professors { + professors = append(professors, *prof) + } + + doc, err := goquery.NewDocumentFromReader(bytes.NewReader(htmlBytes)) + if err != nil { + return fmt.Errorf("failed to parse HTML: %v", err) + } + classInfo := getClassInfo(doc) + + caseDir := filepath.Join(tempDir, fmt.Sprintf("case_%03d", i)) + if err = os.Mkdir(caseDir, 0777); err != nil { + return fmt.Errorf("failed to create directory: %v", err) + } + + //copy current input.html to individual folder + if err = os.WriteFile(filepath.Join(caseDir, "input.html"), htmlBytes, 0777); err != nil { + return fmt.Errorf("failed to write test data: %v", err) + } + + if err = utils.WriteJSON(filepath.Join(caseDir, "course.json"), course); err != nil { + return fmt.Errorf("failed to write course %v: %v", course.Id, err) + } + + if err = utils.WriteJSON(filepath.Join(caseDir, "ClassInfo.json"), classInfo); err != nil { + return fmt.Errorf("failed to write class info %v", err) + } + + if err = utils.WriteJSON(filepath.Join(caseDir, "section.json"), section); err != nil { + return fmt.Errorf("failed to write section %v: %v", section.Id, err) + } + + if err = utils.WriteJSON(filepath.Join(caseDir, "professors.json"), professors); err != nil { + return fmt.Errorf("failed to write professors %v", err) + } + + //reset all the maps, this is important since we are depending on them to only contain the current set + clearGlobals() + } + + //rerun parser to get Courses.json, Sections.json, Professors.json + + //Parse(tempDir, tempDir, "../grade-data", false) + //Grade data isn't work with tests currently + Parse(tempDir, tempDir, "", false) + + //overwrite the current test data with the new data + if err := os.RemoveAll("testdata"); err != nil { + return fmt.Errorf("failed to remove testdata: %v", err) + } + + if err := os.CopyFS("testdata", os.DirFS(tempDir)); err != nil { + return fmt.Errorf("failed to copy testdata: %v", err) + } + + //reset maps to avoid side effects. maybe parser should be an object? + clearGlobals() + return nil +} + +func clearGlobals() { + Sections = make(map[primitive.ObjectID]*schema.Section) + Courses = make(map[string]*schema.Course) + Professors = make(map[string]*schema.Professor) + CourseIDMap = make(map[primitive.ObjectID]string) + ProfessorIDMap = make(map[primitive.ObjectID]string) + ReqParsers = make(map[primitive.ObjectID]func()) +} + +func TestParse(t *testing.T) { + tempDir := t.TempDir() + // todo fix grade data, csvPath = ./grade-data panics + Parse("testdata", tempDir, "", false) + + OutputCourses, err := unmarshallFile[[]schema.Course](filepath.Join(tempDir, "courses.json")) + if err != nil { + t.Errorf("failded to load output courses.json %v", err) + } + + OutputProfessors, err := unmarshallFile[[]schema.Professor](filepath.Join(tempDir, "professors.json")) + if err != nil { + t.Errorf("failded to load output professors.json %v", err) + } + + OutputSections, err := unmarshallFile[[]schema.Section](filepath.Join(tempDir, "sections.json")) + if err != nil { + t.Errorf("failded to load output sections.json %v", err) + } + + ExpectedCourses, err := unmarshallFile[[]schema.Course](filepath.Join("testdata", "courses.json")) + if err != nil { + t.Errorf("failded to load expected courses.json %v", err) + } + + ExpectedProfessors, err := unmarshallFile[[]schema.Professor](filepath.Join("testdata", "professors.json")) + if err != nil { + t.Errorf("failded to load expected professors.json %v", err) + } + + ExpectedSections, err := unmarshallFile[[]schema.Section](filepath.Join("testdata", "sections.json")) + if err != nil { + t.Errorf("failded to load expected sections.json %v", err) + } + + //Build the ValueByID maps, this is used to for comparing because we cant directly compare ids + CoursesById := make(map[primitive.ObjectID]schema.Course) + for _, course := range OutputCourses { + CoursesById[course.Id] = course + } + for _, course := range ExpectedCourses { + CoursesById[course.Id] = course + } + + ProfessorsByID := make(map[primitive.ObjectID]schema.Professor) + for _, prof := range OutputProfessors { + ProfessorsByID[prof.Id] = prof + } + for _, prof := range ExpectedProfessors { + ProfessorsByID[prof.Id] = prof + } + + SectionsByID := make(map[primitive.ObjectID]schema.Section) + for _, section := range OutputSections { + SectionsByID[section.Id] = section + } + for _, section := range ExpectedSections { + SectionsByID[section.Id] = section + } + + // check courses + CoursesByKey := make(map[string]schema.Course) + //output in to map + for _, course := range OutputCourses { + //same key as used in courseParser.go + key := course.Course_number + course.Catalog_year + CoursesByKey[key] = course + } + + for _, expectedCourse := range ExpectedCourses { + key := expectedCourse.Course_number + expectedCourse.Catalog_year + t.Run(key, func(t *testing.T) { + if outputCourse, ok := CoursesByKey[key]; ok { + diff := cmp.Diff(expectedCourse, outputCourse, + cmpopts.IgnoreFields(schema.Course{}, "Id"), + cmp.Transformer("Sections", func(sections []primitive.ObjectID) []string { + result := make([]string, 0, len(sections)) + for _, id := range sections { + if section, ok := SectionsByID[id]; ok { + //We don't need to check sections for correctness, just check that the reference is correct + result = append(result, section.Section_number) + } else { + result = append(result, "") + } + } + return result + }), + ) + + if diff != "" { + t.Errorf("Failed (-expected +got)\n %s", diff) + } + + //remove found course from map, this will allow us to see if there are extra courses in output + delete(CoursesByKey, key) + } else { + t.Errorf("Expected course %s not found in output", key) + } + }) + + } + + if len(CoursesByKey) > 0 { + var builder strings.Builder + + builder.WriteString(fmt.Sprintf("Found %d extra Course(s)\n", len(CoursesByKey))) + + for _, course := range CoursesByKey { + courseText, _ := json.MarshalIndent(course, "", "\t") + builder.WriteString(string(courseText)) + builder.WriteString("\n") + } + t.Error(builder.String()) + } + + //check professors + ProfessorsByKey := make(map[string]schema.Professor) + + for _, professor := range OutputProfessors { + //same key as used in professorParser.go + key := professor.First_name + professor.Last_name + ProfessorsByKey[key] = professor + } + + for _, expectedProfessor := range ExpectedProfessors { + key := expectedProfessor.First_name + expectedProfessor.Last_name + t.Run(key, func(t *testing.T) { + + if outputProfessor, ok := ProfessorsByKey[key]; ok { + + diff := cmp.Diff(expectedProfessor, outputProfessor, + cmpopts.IgnoreFields(schema.Professor{}, "Id"), + cmp.Transformer("Sections", func(sections []primitive.ObjectID) []string { + result := make([]string, 0, len(sections)) + for _, id := range sections { + if section, ok := SectionsByID[id]; ok { + //We don't need to check sections for correctness, just check that the reference is correct + result = append(result, section.Section_number) + } else { + result = append(result, "") + } + } + return result + }), + ) + + if diff != "" { + t.Errorf("Failed (-expected +got)\n %s", diff) + } + delete(ProfessorsByKey, key) + + } else { + t.Errorf("Expected professor %s not found in output", key) + } + }) + } + + if len(ProfessorsByKey) > 0 { + var builder strings.Builder + + builder.WriteString(fmt.Sprintf("Found %d extra Professor(s)\n", len(ProfessorsByKey))) + + for _, course := range ProfessorsByKey { + courseText, _ := json.MarshalIndent(course, "", "\t") + builder.WriteString(string(courseText)) + builder.WriteString("\n") + } + t.Error(builder.String()) + } + + //check sections + SectionsByKey := make(map[string]schema.Section) + + for _, section := range OutputSections { + //the ok shouldn't fail since this is after we checked all the courses + course := CoursesById[section.Course_reference] + key := course.Course_number + course.Catalog_year + section.Section_number + SectionsByKey[key] = section + } + + for _, expectedSection := range ExpectedSections { + + course := CoursesById[expectedSection.Course_reference] + key := course.Course_number + course.Catalog_year + expectedSection.Section_number + t.Run(key, func(t *testing.T) { + if outputSection, ok := SectionsByKey[key]; ok { + + diff := cmp.Diff(expectedSection, outputSection, + cmpopts.IgnoreFields(schema.Section{}, "Id"), + cmp.Transformer("Course_reference", func(id primitive.ObjectID) string { + if c, ok := CoursesById[id]; ok { + return c.Course_number + c.Catalog_year + } + return "" + }), + cmp.Transformer("Professors", func(profIds []primitive.ObjectID) []string { + result := make([]string, 0, len(profIds)) + for _, id := range profIds { + if professor, ok := ProfessorsByID[id]; ok { + //We don't need to check sections for correctness, just check that the reference is correct + result = append(result, professor.First_name+professor.Last_name) + } else { + result = append(result, "") + } + } + return result + }), + ) + + if diff != "" { + t.Errorf("Failed (-expected +got)\n %s", diff) + } + + delete(SectionsByKey, key) + } else { + t.Errorf("Expected Section %s not found in output", key) + } + }) + } + + if len(SectionsByKey) > 0 { + var builder strings.Builder + + builder.WriteString(fmt.Sprintf("Found %d extra Sections(s) : \n", len(SectionsByKey))) + + for _, section := range SectionsByKey { + courseText, _ := json.MarshalIndent(section, "", "\t") + builder.WriteString(string(courseText)) + builder.WriteString("\n") + } + t.Error(builder.String()) + } +} + +// unmarshallFile reads a JSON file from the given path and unmarshals it into type T. +func unmarshallFile[T any](path string) (T, error) { + var result T + + file, err := os.ReadFile(path) + if err != nil { + return result, fmt.Errorf("error reading file '%s': %w", path, err) // Wrap original error + } + if err = json.Unmarshal(file, &result); err != nil { + return result, fmt.Errorf("error unmarshalling JSON from file '%s': %w", path, err) // Wrap original error + } + + return result, nil +} + +func TestGetClassInfo(t *testing.T) { + t.Parallel() + + for name, testCase := range testData { + t.Run(name, func(t *testing.T) { + t.Parallel() + + doc, err := goquery.NewDocumentFromReader(strings.NewReader(testCase.Input)) + if err != nil { + return + } + output := getClassInfo(doc) + expected := testCase.ClassInfo + + diff := cmp.Diff(expected, output) + if diff != "" { + t.Errorf("Failed (-expected +got)\n %s", diff) + } + }) + + } +} + +func TestGetRowInfo(t *testing.T) { + t.Parallel() + // don't include any weird characters in the content, it's not a bug with getRowInfo but + // goquery will modify content when encoding/decoding html so the result will not match content. + testCases := map[string]struct { + Title string + Content string + }{ + "case_001": { + Title: "Course Title:", + Content: "Introductory Financial Accounting", + }, + "case_002": { + Title: "Evaluation:", + Content: "An evaluation report for Introductory Financial Accounting (ACCT2301.003.25S) has not been posted.", + }, + "case_003": { + Title: "Schedule:", + Content: "Syllabus for Introductory Financial Accounting (ACCT2301.003.25S)", + }, + "case_004": { + Title: "Class Info:", + Content: "
", + }, + "case_005": { + Title: "", + Content: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + html := fmt.Sprintf(` +
+ + + + + + + +
%s%s
+
`, testCase.Title, testCase.Content) + + doc, err := goquery.NewDocumentFromReader(strings.NewReader(html)) + if err != nil { + t.Fatalf("failed to create document: %v", err) + } + + rowInfo := getRowInfo(doc) + + if row, ok := rowInfo[utils.TrimWhitespace(testCase.Title)]; ok { + content, err := row.Html() + if err != nil { + t.Fatalf("failed to get row content: %v", err) + } + + if diff := cmp.Diff(testCase.Content, content); diff != "" { + t.Errorf("Failed (-expected +got)\n %s", diff) + } + } else { + t.Errorf("Failed to find row in infoRows") + } + }) + } +} diff --git a/parser/sectionParser.go b/parser/sectionParser.go index b777e3d..6f8cadc 100644 --- a/parser/sectionParser.go +++ b/parser/sectionParser.go @@ -12,19 +12,32 @@ import ( "golang.org/x/net/html/atom" ) +// timeLayout is the layout string used for parsing dates in "Month Day, Year" format. const timeLayout = "January 2, 2006" 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(`(.+)・(.+)・(.+)`) + // parser.sectionPrefixRegexp matches "SUBJ.101" (e.g., "HIST.101", case-insensitive). + sectionPrefixRegexp = utils.Regexpf(`^(?i)%s\.(%s)`, utils.R_SUBJ_COURSE, utils.R_SECTION_CODE) + // coreRegexp matches any 3-digit number, used for core curriculum codes (e.g., "090"). + coreRegexp = regexp.MustCompile(`[0-9]{3}`) + + // personRegexp matches any 3 strings (no spaces) seperated by '・', (e.g, Name・Role・Email) + personRegexp = regexp.MustCompile(`(.+)・(.+)・(.+)`) + + // meetingDatesRegexp matches a full date in "Month Day, Year" format (e.g., "January 5, 2022") meetingDatesRegexp = regexp.MustCompile(utils.R_DATE_MDY) - meetingDaysRegexp = regexp.MustCompile(utils.R_WEEKDAY) + + // meetingDaysRegexp matches any day of the week (e.g., Monday, Tuesday, etc.) + meetingDaysRegexp = regexp.MustCompile(utils.R_WEEKDAY) + + // meetingTimesRegexp matches a time in 12-hour AM/PM format (e.g., "5:00 pm", "11:30am") meetingTimesRegexp = regexp.MustCompile(utils.R_TIME_AM_PM) ) -// TODO: section requisites? +// parseSection creates a schema.Section from rowInfo and ClassInfo, +// adds it to Sections, and updates the associated Course and Professors. +// Internally calls parseCourse and parseProfessors, which modify global maps. func parseSection(rowInfo map[string]*goquery.Selection, classInfo map[string]string) { classNum, courseNum := getInternalClassAndCourseNum(classInfo) session := getAcademicSession(rowInfo) @@ -56,24 +69,31 @@ func parseSection(rowInfo map[string]*goquery.Selection, classInfo map[string]st courseRef.Sections = append(courseRef.Sections, section.Id) } -// todo add logging for failing to get feilds? probably only max verbosity +// getInternalClassAndCourseNum returns a sections internal course and class number, +// both 0-padded, 5-digit numbers as strings. +// It expects ClassInfo to contain "Class/Course Number:" key. +// If the key is not found or the value is not in the expected "classNum / courseNum" format, +// it returns empty strings. 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] } + panic("failed to parse internal class number and course number") } - return "", "" + panic("could not find 'Class/Course Number:' in ClassInfo") } +// getAcademicSession returns the schema.AcademicSession parsed from the provided rowInfo. +// It extracts academic session details from the "Schedule:" section in rowInfo. func getAcademicSession(rowInfo map[string]*goquery.Selection) schema.AcademicSession { session := schema.AcademicSession{} infoNodes := rowInfo["Schedule:"].FindMatcher(goquery.Single("p.courseinfo__sectionterm")).Contents().Nodes for _, node := range infoNodes { if node.DataAtom == atom.B { - //since the key is not a TextElement, the Text is stored in it's first child, a TextElement + //since the key is not a TextElement, the Text is stored in its first child, a TextElement key := utils.TrimWhitespace(node.FirstChild.Data) value := utils.TrimWhitespace(node.NextSibling.Data) @@ -87,19 +107,32 @@ func getAcademicSession(rowInfo map[string]*goquery.Selection) schema.AcademicSe } } } + + if session.Name == "" { + panic("failed to find academic session, session name can not be empty") + } + return session } +// getSectionNumber returns the matched value from a sectionPrefixRegexp on +// `ClassInfo["Class Section:"]`. It expects ClassInfo to contain "Class Section:" key. +// If there is no matches, getSectionNumber will panic as sectionNumber is a required +// field. 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] } + panic("failed to parse section number") } - return "" + panic("could not find 'Class Section:' in ClassInfo") } +// getTeachingAssistants parses TA/RA information from rowInfo and returns a list of schema.Assistant. +// Assistants are found by matching personRegexp, and therefore are expected to have Name, Role, and Email. +// If no "TA/RA(s):" row is found in rowInfo or no assistants are parsed, an empty slice is returned. func getTeachingAssistants(rowInfo map[string]*goquery.Selection) []schema.Assistant { taRow, ok := rowInfo["TA/RA(s):"] if !ok { @@ -122,6 +155,9 @@ func getTeachingAssistants(rowInfo map[string]*goquery.Selection) []schema.Assis return assistants } +// getInstructionMode returns the instruction mode (e.g., in-person, online) from ClassInfo. +// It expects ClassInfo to contain "Instruction Mode:" key. +// If the key is not present, it returns an empty string. func getInstructionMode(classInfo map[string]string) string { if mode, ok := classInfo["Instruction Mode:"]; ok { return mode @@ -129,9 +165,33 @@ func getInstructionMode(classInfo map[string]string) string { return "" } +// getMeetings parses meeting schedule information from the row information map. +// +// The function does not guarantee any number of meetings nor any fields of +// each meeting. Therefore, both an empty slice or a slice containing a meeting +// where all its values are empty are perfectly valid. +// +// Each meeting is parsed as following: +// +// Start and End Date +// - Accepts 0, 1 or 2 dates matched using meetingDatesRegexp. +// - If only 1 date is specified, it is used for both dates. +// +// Start and End Time +// - Accepts 0, 1 or 2 times matched using meetingTimesRegexp. +// - If only 1 time is specified, it is used for both times. +// - Times are only parsed into strings to save memory +// +// Meeting days +// - Captures all strings that match meetingDaysRegexp +// - If there are no matches an empty slice will be used +// +// Location +// - Skips locations that don't have a valid locator link +// - Skips locations whose text don't match format func getMeetings(rowInfo map[string]*goquery.Selection) []schema.Meeting { meetingItems := rowInfo["Schedule:"].Find("div.courseinfo__meeting-item--multiple") - var meetings []schema.Meeting = make([]schema.Meeting, 0, meetingItems.Length()) + var meetings = make([]schema.Meeting, 0, meetingItems.Length()) meetingItems.Each(func(i int, s *goquery.Selection) { meeting := schema.Meeting{} @@ -183,6 +243,10 @@ func getMeetings(rowInfo map[string]*goquery.Selection) []schema.Meeting { return meetings } +// getCoreFlags extracts any matching core curriculum flags from rowInfo. +// It expects rowInfo to contain "Core:" key. +// Core curriculum flags are expected to be 3-digit numbers. +// Returns an empty slice if no "Core:" row is found or no flags are found. func getCoreFlags(rowInfo map[string]*goquery.Selection) []string { if core, ok := rowInfo["Core:"]; ok { flags := coreRegexp.FindAllString(utils.TrimWhitespace(core.Text()), -1) @@ -194,6 +258,9 @@ func getCoreFlags(rowInfo map[string]*goquery.Selection) []string { return []string{} } +// getSyllabusUri extracts and returns the syllabus URL from rowInfo, if present. +// It expects rowInfo to contain "Syllabus:" key, and the syllabus URL to be within an tag. +// Returns an empty string if no "Syllabus:" row or link is found. func getSyllabusUri(rowInfo map[string]*goquery.Selection) string { if syllabus, ok := rowInfo["Syllabus:"]; ok { link := syllabus.FindMatcher(goquery.Single("a")) @@ -204,6 +271,14 @@ func getSyllabusUri(rowInfo map[string]*goquery.Selection) string { return "" } +// getGradeDistribution returns the grade distribution for the given section. +// It retrieves grade distribution from the global `GradeMap`. +// +// If GradeMap contains the resulting key it will return the specified slice, +// otherwise it will return an empty slice, `[]int{}`. +// The key is generated using the following formula: +// key = SubjectPrefix + InternalCourseNumber + InternalSectionNumber. +// Note that the InternalSectionNumber is trimmed of leading '0's 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 @@ -218,6 +293,9 @@ func getGradeDistribution(session schema.AcademicSession, sectionNumber string, return []int{} } +// parseTimeOrPanic is a simplified version time.ParseInLocation. The layout and +// location are constants, timeLayout and timeLocation respectively. If time.ParseInLocation +// returns an error, parseTimeOrPanic will panic regardless of the error type. func parseTimeOrPanic(value string) time.Time { date, err := time.ParseInLocation(timeLayout, value, timeLocation) if err != nil { diff --git a/parser/sectionParser_test.go b/parser/sectionParser_test.go index d6d7d25..100b431 100644 --- a/parser/sectionParser_test.go +++ b/parser/sectionParser_test.go @@ -1,16 +1,20 @@ package parser import ( + "fmt" "testing" + "time" "github.com/google/go-cmp/cmp" ) func TestGetInternalClassAndCourseNum(t *testing.T) { - loadTestData(t) + t.Parallel() - for name, testCase := range testDataCache { + for name, testCase := range testData { t.Run(name, func(t *testing.T) { + t.Parallel() + classNum, courseNum := getInternalClassAndCourseNum(testCase.ClassInfo) expectedClassNum := testCase.Section.Internal_class_number expectedCourseNumber := testCase.Course.Internal_course_number @@ -25,13 +29,43 @@ func TestGetInternalClassAndCourseNum(t *testing.T) { }) } + + fails := []map[string]string{ + { + "Class Section:": "ENTP33S", + }, + { + "Class Section:": "", + }, + { + "Class Section:": "Garbage In, Garbage out", + }, + } + + for i, fail := range fails { + name := fmt.Sprintf("case_%03d", i+len(testData)) + + t.Run(name, func(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r == nil { + t.Errorf("expected panic for input %s but none occurred", fail) + } + }() + getInternalClassAndCourseNum(fail) + + }) + } } func TestGetAcademicSession(t *testing.T) { - loadTestData(t) + t.Parallel() - for name, testCase := range testDataCache { + for name, testCase := range testData { t.Run(name, func(t *testing.T) { + t.Parallel() + output := getAcademicSession(testCase.RowInfo) expected := testCase.Section.Academic_session @@ -45,10 +79,12 @@ func TestGetAcademicSession(t *testing.T) { } func TestGetSectionNumber(t *testing.T) { - loadTestData(t) + t.Parallel() - for name, testCase := range testDataCache { + for name, testCase := range testData { t.Run(name, func(t *testing.T) { + t.Parallel() + output := getSectionNumber(testCase.ClassInfo) expected := testCase.Section.Section_number @@ -56,15 +92,45 @@ func TestGetSectionNumber(t *testing.T) { t.Errorf("expected %s got %s", expected, output) } }) + } + + fails := []map[string]string{ + { + "Class Section:": "ENTP33S", + }, + { + "Class Section:": "", + }, + { + "Class Section:": "Garbage In, Garbage out", + }, + } + + for i, fail := range fails { + name := fmt.Sprintf("case_%03d", i+len(testData)) + + t.Run(name, func(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r == nil { + t.Errorf("expected panic for input %s but none occurred", fail) + } + }() + getSectionNumber(fail) + + }) } } func TestGetTeachingAssistants(t *testing.T) { - loadTestData(t) + t.Parallel() - for name, testCase := range testDataCache { + for name, testCase := range testData { t.Run(name, func(t *testing.T) { + t.Parallel() + output := getTeachingAssistants(testCase.RowInfo) expected := testCase.Section.Teaching_assistants @@ -78,10 +144,12 @@ func TestGetTeachingAssistants(t *testing.T) { } func TestGetInstructionMode(t *testing.T) { - loadTestData(t) + t.Parallel() - for name, testCase := range testDataCache { + for name, testCase := range testData { t.Run(name, func(t *testing.T) { + t.Parallel() + output := getInstructionMode(testCase.ClassInfo) expected := testCase.Section.Instruction_mode @@ -94,10 +162,12 @@ func TestGetInstructionMode(t *testing.T) { } func TestGetMeetings(t *testing.T) { - loadTestData(t) + t.Parallel() - for name, testCase := range testDataCache { + for name, testCase := range testData { t.Run(name, func(t *testing.T) { + t.Parallel() + output := getMeetings(testCase.RowInfo) expected := testCase.Section.Meetings @@ -111,10 +181,12 @@ func TestGetMeetings(t *testing.T) { } func TestGetCoreFlags(t *testing.T) { - loadTestData(t) + t.Parallel() - for name, testCase := range testDataCache { + for name, testCase := range testData { t.Run(name, func(t *testing.T) { + t.Parallel() + output := getCoreFlags(testCase.RowInfo) expected := testCase.Section.Core_flags @@ -128,10 +200,11 @@ func TestGetCoreFlags(t *testing.T) { } func TestGetSyllabusUri(t *testing.T) { - loadTestData(t) + t.Parallel() - for name, testCase := range testDataCache { + for name, testCase := range testData { t.Run(name, func(t *testing.T) { + t.Parallel() output := getSyllabusUri(testCase.RowInfo) expected := testCase.Section.Syllabus_uri @@ -142,3 +215,59 @@ func TestGetSyllabusUri(t *testing.T) { } } + +func TestParseTimeOrPanic(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + Input string + Expected time.Time + Panic bool + }{ + "Case_001": { + Input: "January 2, 2006", + Expected: time.Date(2006, time.January, 2, 0, 0, 0, 0, timeLocation), + Panic: false, + }, + "Case_002": { + Input: "March 15, 2020", + Expected: time.Date(2020, time.March, 15, 0, 0, 0, 0, timeLocation), + Panic: false, + }, + "Case_003": { + Input: "15 March, 2020", // wrong format + Panic: true, + }, + "Case_004": { + Input: "Not a date", // clearly wrong + Panic: true, + }, + "Case_005": { + Input: "", // empty input + Panic: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r != nil { + if !testCase.Panic { + t.Errorf("unexpected panic for input %q: %v", testCase.Input, r) + } + } else { + if testCase.Panic { + t.Errorf("expected panic for input %q but none occurred", testCase.Input) + } + } + }() + + got := parseTimeOrPanic(testCase.Input) + if !testCase.Panic && !got.Equal(testCase.Expected) { + t.Errorf("expected %v, got %v", testCase.Expected, got) + } + }) + } +} diff --git a/parser/testHelper.go b/parser/testHelper.go deleted file mode 100644 index 6fb14e3..0000000 --- a/parser/testHelper.go +++ /dev/null @@ -1,83 +0,0 @@ -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_000/classInfo.json b/parser/testdata/case_000/classInfo.json new file mode 100644 index 0000000..d548b8a --- /dev/null +++ b/parser/testdata/case_000/classInfo.json @@ -0,0 +1,14 @@ +{ + "": "", + "Activity Type:": "Lecture", + "Add Consent:": "No Consent", + "Class Level:": "Undergraduate", + "Class Section:": "ACCT2301.003.25S", + "Class/Course Number:": "27706 / 000061", + "Grading:": "Graded - Undergraduate", + "How often a course is scheduled:": "Once Each Long Summer", + "Instruction Mode:": "Face-to-Face", + "Orion Date/Time:": "2025-03-07 15:30:01", + "Semester Credit Hours:": "3", + "Session Type:": "Regular Academic Session" +} diff --git a/parser/testdata/case_000/course.json b/parser/testdata/case_000/course.json new file mode 100644 index 0000000..5e342be --- /dev/null +++ b/parser/testdata/case_000/course.json @@ -0,0 +1,25 @@ +{ + "_id": "67d07ee0c972c18731e23bd7", + "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": "ACCT 2301 Repeat Restriction", + "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": [ + "67d07ee0c972c18731e23bd8" + ], + "lecture_contact_hours": "3", + "laboratory_contact_hours": "0", + "offering_frequency": "S", + "catalog_year": "24", + "attributes": null +} diff --git a/parser/testdata/case_000/input.html b/parser/testdata/case_000/input.html new file mode 100644 index 0000000..5f241a1 --- /dev/null +++ b/parser/testdata/case_000/input.html @@ -0,0 +1,221 @@ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Course Title: + + Introductory Financial Accounting +
+ Class Info: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Class Section: + + ACCT2301.003.25S + + Instruction Mode: + + Face-to-Face +
+ Class Level: + + Undergraduate + + Activity Type: + + Lecture +
+ Semester Credit Hours: + + 3 + + Class/Course Number: + + 27706 / 000061 +
+ Grading: + + Graded - Undergraduate + + Session Type: + + Regular Academic Session +
+ Add Consent: + + No Consent + + Orion Date/Time: + + 2025-03-07 15: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): + +
+
Naim Bugra Ozel ・ Primary Instructor (50%) ・ nbo150030@utdallas.edu +
+
+
Jieying Zhang ・ Primary Instructor (50%) ・ jxz146230@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
+ 10:00am-11:15am
+ JSOM 2.717 + +

+
SOM Building
Floor 2 - Room 2.717
+
+ +
+ +
+ College: + + Naveen Jindal School of Management + +
+ Syllabus: + + Syllabus for Introductory Financial Accounting (ACCT2301.003.25S) + +
+ Evaluation: + + An evaluation report for Introductory Financial Accounting (ACCT2301.003.25S) hasn't been posted. + +
+
+
+ +
diff --git a/parser/testdata/case_000/professors.json b/parser/testdata/case_000/professors.json new file mode 100644 index 0000000..207c908 --- /dev/null +++ b/parser/testdata/case_000/professors.json @@ -0,0 +1,44 @@ +[ + { + "_id": "67d07ee0c972c18731e23bd9", + "first_name": "Naim Bugra", + "last_name": "Ozel", + "titles": [ + "Primary Instructor (50%)" + ], + "email": "nbo150030@utdallas.edu", + "phone_number": "", + "office": { + "building": "", + "room": "", + "map_uri": "" + }, + "profile_uri": "", + "image_uri": "", + "office_hours": null, + "sections": [ + "67d07ee0c972c18731e23bd8" + ] + }, + { + "_id": "67d07ee0c972c18731e23bda", + "first_name": "Jieying", + "last_name": "Zhang", + "titles": [ + "Primary Instructor (50%)" + ], + "email": "jxz146230@utdallas.edu", + "phone_number": "", + "office": { + "building": "", + "room": "", + "map_uri": "" + }, + "profile_uri": "", + "image_uri": "", + "office_hours": null, + "sections": [ + "67d07ee0c972c18731e23bd8" + ] + } +] diff --git a/parser/testdata/case_000/section.json b/parser/testdata/case_000/section.json new file mode 100644 index 0000000..a67c0f5 --- /dev/null +++ b/parser/testdata/case_000/section.json @@ -0,0 +1,53 @@ +{ + "_id": "67d07ee0c972c18731e23bd8", + "section_number": "003", + "course_reference": "67d07ee0c972c18731e23bd7", + "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": [ + "67d07ee0c972c18731e23bd9", + "67d07ee0c972c18731e23bda" + ], + "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": "27706", + "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": "10:00am", + "end_time": "11:15am", + "modality": "", + "location": { + "building": "JSOM", + "room": "2.717", + "map_uri": "https://locator.utdallas.edu/SOM_2.717" + } + } + ], + "core_flags": [], + "syllabus_uri": "https://dox.utdallas.edu/syl152555", + "grade_distribution": [], + "attributes": null +} diff --git a/parser/testdata/case_001/classInfo.json b/parser/testdata/case_001/classInfo.json new file mode 100644 index 0000000..dc9d380 --- /dev/null +++ b/parser/testdata/case_001/classInfo.json @@ -0,0 +1,14 @@ +{ + "": "", + "Activity Type:": "Lecture", + "Add Consent:": "No Consent", + "Class Level:": "Undergraduate", + "Class Section:": "ACCT2301.001.25S", + "Class/Course Number:": "26595 / 000061", + "Grading:": "Graded - Undergraduate", + "How often a course is scheduled:": "Once Each Long Summer", + "Instruction Mode:": "Face-to-Face", + "Orion Date/Time:": "2025-02-22 06:30:01", + "Semester Credit Hours:": "3", + "Session Type:": "Regular Academic Session" +} diff --git a/parser/testdata/case_001/course.json b/parser/testdata/case_001/course.json index 65a0dc3..24dcf8b 100644 --- a/parser/testdata/case_001/course.json +++ b/parser/testdata/case_001/course.json @@ -1,23 +1,25 @@ { - "_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 + "_id": "67d07ee0c972c18731e23bdb", + "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": "ACCT 2301 Repeat Restriction", + "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": [ + "67d07ee0c972c18731e23bdc" + ], + "lecture_contact_hours": "3", + "laboratory_contact_hours": "0", + "offering_frequency": "S", + "catalog_year": "24", + "attributes": null } diff --git a/parser/testdata/case_001/professors.json b/parser/testdata/case_001/professors.json new file mode 100644 index 0000000..8bf8c6b --- /dev/null +++ b/parser/testdata/case_001/professors.json @@ -0,0 +1,44 @@ +[ + { + "_id": "67d07ee0c972c18731e23bdd", + "first_name": "Jieying", + "last_name": "Zhang", + "titles": [ + "Primary Instructor (50%)" + ], + "email": "jxz146230@utdallas.edu", + "phone_number": "", + "office": { + "building": "", + "room": "", + "map_uri": "" + }, + "profile_uri": "", + "image_uri": "", + "office_hours": null, + "sections": [ + "67d07ee0c972c18731e23bdc" + ] + }, + { + "_id": "67d07ee0c972c18731e23bde", + "first_name": "Naim Bugra", + "last_name": "Ozel", + "titles": [ + "Primary Instructor (50%)" + ], + "email": "nbo150030@utdallas.edu", + "phone_number": "", + "office": { + "building": "", + "room": "", + "map_uri": "" + }, + "profile_uri": "", + "image_uri": "", + "office_hours": null, + "sections": [ + "67d07ee0c972c18731e23bdc" + ] + } +] diff --git a/parser/testdata/case_001/section.json b/parser/testdata/case_001/section.json index ea2917c..eeb9360 100644 --- a/parser/testdata/case_001/section.json +++ b/parser/testdata/case_001/section.json @@ -1,53 +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 + "_id": "67d07ee0c972c18731e23bdc", + "section_number": "001", + "course_reference": "67d07ee0c972c18731e23bdb", + "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": [ + "67d07ee0c972c18731e23bdd", + "67d07ee0c972c18731e23bde" + ], + "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 +} diff --git a/parser/testdata/case_002/classInfo.json b/parser/testdata/case_002/classInfo.json new file mode 100644 index 0000000..e8c2d48 --- /dev/null +++ b/parser/testdata/case_002/classInfo.json @@ -0,0 +1,14 @@ +{ + "": "", + "Activity Type:": "Lecture", + "Add Consent:": "No Consent", + "Class Level:": "Undergraduate", + "Class Section:": "BA1320.501.25S", + "Class/Course Number:": "27195 / 015444", + "Grading:": "Graded - Undergraduate", + "How often a course is scheduled:": "Once Each Long Summer", + "Instruction Mode:": "Face-to-Face", + "Orion Date/Time:": "2025-02-24 06:30:01", + "Semester Credit Hours:": "3", + "Session Type:": "Regular Academic Session" +} diff --git a/parser/testdata/case_002/course.json b/parser/testdata/case_002/course.json index 7c5f048..141ff6b 100644 --- a/parser/testdata/case_002/course.json +++ b/parser/testdata/case_002/course.json @@ -1,23 +1,25 @@ { - "_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 + "_id": "67d07ee0c972c18731e23bdf", + "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": "BA 1320 Repeat Restriction", + "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": [ + "67d07ee0c972c18731e23be0" + ], + "lecture_contact_hours": "3", + "laboratory_contact_hours": "0", + "offering_frequency": "S", + "catalog_year": "24", + "attributes": null } diff --git a/parser/testdata/case_002/professors.json b/parser/testdata/case_002/professors.json new file mode 100644 index 0000000..c6913f6 --- /dev/null +++ b/parser/testdata/case_002/professors.json @@ -0,0 +1,23 @@ +[ + { + "_id": "67d07ee0c972c18731e23be1", + "first_name": "Peter", + "last_name": "Lewin", + "titles": [ + "Primary Instructor" + ], + "email": "plewin@utdallas.edu", + "phone_number": "", + "office": { + "building": "", + "room": "", + "map_uri": "" + }, + "profile_uri": "", + "image_uri": "", + "office_hours": null, + "sections": [ + "67d07ee0c972c18731e23be0" + ] + } +] diff --git a/parser/testdata/case_002/section.json b/parser/testdata/case_002/section.json index a37267c..6eb44f5 100644 --- a/parser/testdata/case_002/section.json +++ b/parser/testdata/case_002/section.json @@ -1,49 +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 + "_id": "67d07ee0c972c18731e23be0", + "section_number": "501", + "course_reference": "67d07ee0c972c18731e23bdf", + "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": [ + "67d07ee0c972c18731e23be1" + ], + "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 +} diff --git a/parser/testdata/case_003/classInfo.json b/parser/testdata/case_003/classInfo.json new file mode 100644 index 0000000..cf2fdd6 --- /dev/null +++ b/parser/testdata/case_003/classInfo.json @@ -0,0 +1,14 @@ +{ + "": "", + "Activity Type:": "Lecture", + "Add Consent:": "Department Consent Required", + "Class Level:": "Graduate", + "Class Section:": "BIOL6111.016.25S", + "Class/Course Number:": "29611 / 016577", + "Grading:": "Graded - Graduate", + "How often a course is scheduled:": "Once Each Long Summer", + "Instruction Mode:": "Face-to-Face", + "Orion Date/Time:": "2025-02-13 06:30:01", + "Semester Credit Hours:": "1", + "Session Type:": "Regular Academic Session" +} diff --git a/parser/testdata/case_003/course.json b/parser/testdata/case_003/course.json index ab4317c..94219f8 100644 --- a/parser/testdata/case_003/course.json +++ b/parser/testdata/case_003/course.json @@ -1,23 +1,25 @@ { - "_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 + "_id": "67d07ee0c972c18731e23be2", + "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": [ + "67d07ee0c972c18731e23be3" + ], + "lecture_contact_hours": "1", + "laboratory_contact_hours": "0", + "offering_frequency": "S", + "catalog_year": "24", + "attributes": null } diff --git a/parser/testdata/case_003/professors.json b/parser/testdata/case_003/professors.json new file mode 100644 index 0000000..3cb4a51 --- /dev/null +++ b/parser/testdata/case_003/professors.json @@ -0,0 +1,23 @@ +[ + { + "_id": "67d07ee0c972c18731e23be4", + "first_name": "Tian", + "last_name": "Hong", + "titles": [ + "Primary Instructor" + ], + "email": "txh240018@utdallas.edu", + "phone_number": "", + "office": { + "building": "", + "room": "", + "map_uri": "" + }, + "profile_uri": "", + "image_uri": "", + "office_hours": null, + "sections": [ + "67d07ee0c972c18731e23be3" + ] + } +] diff --git a/parser/testdata/case_003/section.json b/parser/testdata/case_003/section.json index 4ccfc51..fff4105 100644 --- a/parser/testdata/case_003/section.json +++ b/parser/testdata/case_003/section.json @@ -1,38 +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 + "_id": "67d07ee0c972c18731e23be3", + "section_number": "016", + "course_reference": "67d07ee0c972c18731e23be2", + "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": [ + "67d07ee0c972c18731e23be4" + ], + "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 +} diff --git a/parser/testdata/case_004/classInfo.json b/parser/testdata/case_004/classInfo.json new file mode 100644 index 0000000..732d841 --- /dev/null +++ b/parser/testdata/case_004/classInfo.json @@ -0,0 +1,14 @@ +{ + "": "", + "Activity Type:": "Laboratory - No Lab Fee", + "Add Consent:": "Instructor Consent Required", + "Class Level:": "Undergraduate", + "Class Section:": "AERO3320.201.25S", + "Class/Course Number:": "28551 / 000243", + "Grading:": "Graded - Undergraduate", + "How often a course is scheduled:": "∅", + "Instruction Mode:": "Face-to-Face", + "Orion Date/Time:": "2025-01-25 06:30:01", + "Semester Credit Hours:": "Non-Enroll", + "Session Type:": "Regular Academic Session" +} diff --git a/parser/testdata/case_004/course.json b/parser/testdata/case_004/course.json index bf75d76..d8c5383 100644 --- a/parser/testdata/case_004/course.json +++ b/parser/testdata/case_004/course.json @@ -1,23 +1,25 @@ { - "_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 + "_id": "67d07ee0c972c18731e23be5", + "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": [ + "67d07ee0c972c18731e23be6" + ], + "lecture_contact_hours": "", + "laboratory_contact_hours": "", + "offering_frequency": "", + "catalog_year": "24", + "attributes": null +} diff --git a/parser/testdata/case_004/professors.json b/parser/testdata/case_004/professors.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/parser/testdata/case_004/professors.json @@ -0,0 +1 @@ +[] diff --git a/parser/testdata/case_004/section.json b/parser/testdata/case_004/section.json index d84dab2..2481524 100644 --- a/parser/testdata/case_004/section.json +++ b/parser/testdata/case_004/section.json @@ -1,36 +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 + "_id": "67d07ee0c972c18731e23be6", + "section_number": "201", + "course_reference": "67d07ee0c972c18731e23be5", + "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 +} diff --git a/parser/testdata/case_005/classInfo.json b/parser/testdata/case_005/classInfo.json new file mode 100644 index 0000000..5ee1236 --- /dev/null +++ b/parser/testdata/case_005/classInfo.json @@ -0,0 +1,14 @@ +{ + "": "", + "Activity Type:": "Lecture", + "Add Consent:": "Instructor Consent Required", + "Class Level:": "Undergraduate", + "Class Section:": "AERO4320.002.25S", + "Class/Course Number:": "27906 / 000255", + "Grading:": "Graded - Undergraduate", + "How often a course is scheduled:": "∅", + "Instruction Mode:": "Face-to-Face", + "Orion Date/Time:": "2025-01-25 06:30:01", + "Semester Credit Hours:": "4", + "Session Type:": "Regular Academic Session" +} diff --git a/parser/testdata/case_005/course.json b/parser/testdata/case_005/course.json index da300c8..9095afc 100644 --- a/parser/testdata/case_005/course.json +++ b/parser/testdata/case_005/course.json @@ -1,23 +1,25 @@ { - "_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 + "_id": "67d07ee0c972c18731e23be7", + "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": [ + "67d07ee0c972c18731e23be8" + ], + "lecture_contact_hours": "", + "laboratory_contact_hours": "", + "offering_frequency": "", + "catalog_year": "24", + "attributes": null +} diff --git a/parser/testdata/case_005/professors.json b/parser/testdata/case_005/professors.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/parser/testdata/case_005/professors.json @@ -0,0 +1 @@ +[] diff --git a/parser/testdata/case_005/section.json b/parser/testdata/case_005/section.json index cfc0abb..712c972 100644 --- a/parser/testdata/case_005/section.json +++ b/parser/testdata/case_005/section.json @@ -1,51 +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 + "_id": "67d07ee0c972c18731e23be8", + "section_number": "002", + "course_reference": "67d07ee0c972c18731e23be7", + "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 +} diff --git a/parser/testdata/courses.json b/parser/testdata/courses.json new file mode 100644 index 0000000..78ae4d4 --- /dev/null +++ b/parser/testdata/courses.json @@ -0,0 +1,128 @@ +[ + { + "_id": "67d07ee0c972c18731e23bee", + "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": "BA 1320 Repeat Restriction", + "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": [ + "67d07ee0c972c18731e23bef" + ], + "lecture_contact_hours": "3", + "laboratory_contact_hours": "0", + "offering_frequency": "S", + "catalog_year": "24", + "attributes": null + }, + { + "_id": "67d07ee0c972c18731e23bf1", + "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": [ + "67d07ee0c972c18731e23bf2" + ], + "lecture_contact_hours": "1", + "laboratory_contact_hours": "0", + "offering_frequency": "S", + "catalog_year": "24", + "attributes": null + }, + { + "_id": "67d07ee0c972c18731e23bf4", + "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": [ + "67d07ee0c972c18731e23bf5" + ], + "lecture_contact_hours": "", + "laboratory_contact_hours": "", + "offering_frequency": "", + "catalog_year": "24", + "attributes": null + }, + { + "_id": "67d07ee0c972c18731e23bf6", + "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": [ + "67d07ee0c972c18731e23bf7" + ], + "lecture_contact_hours": "", + "laboratory_contact_hours": "", + "offering_frequency": "", + "catalog_year": "24", + "attributes": null + }, + { + "_id": "67d07ee0c972c18731e23be9", + "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": "ACCT 2301 Repeat Restriction", + "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": [ + "67d07ee0c972c18731e23bea", + "67d07ee0c972c18731e23bed" + ], + "lecture_contact_hours": "3", + "laboratory_contact_hours": "0", + "offering_frequency": "S", + "catalog_year": "24", + "attributes": null + } +] diff --git a/parser/testdata/professors.json b/parser/testdata/professors.json new file mode 100644 index 0000000..2a931c4 --- /dev/null +++ b/parser/testdata/professors.json @@ -0,0 +1,88 @@ +[ + { + "_id": "67d07ee0c972c18731e23beb", + "first_name": "Naim Bugra", + "last_name": "Ozel", + "titles": [ + "Primary Instructor (50%)" + ], + "email": "nbo150030@utdallas.edu", + "phone_number": "", + "office": { + "building": "", + "room": "", + "map_uri": "" + }, + "profile_uri": "", + "image_uri": "", + "office_hours": null, + "sections": [ + "67d07ee0c972c18731e23bea", + "67d07ee0c972c18731e23bed" + ] + }, + { + "_id": "67d07ee0c972c18731e23bec", + "first_name": "Jieying", + "last_name": "Zhang", + "titles": [ + "Primary Instructor (50%)" + ], + "email": "jxz146230@utdallas.edu", + "phone_number": "", + "office": { + "building": "", + "room": "", + "map_uri": "" + }, + "profile_uri": "", + "image_uri": "", + "office_hours": null, + "sections": [ + "67d07ee0c972c18731e23bea", + "67d07ee0c972c18731e23bed" + ] + }, + { + "_id": "67d07ee0c972c18731e23bf0", + "first_name": "Peter", + "last_name": "Lewin", + "titles": [ + "Primary Instructor" + ], + "email": "plewin@utdallas.edu", + "phone_number": "", + "office": { + "building": "", + "room": "", + "map_uri": "" + }, + "profile_uri": "", + "image_uri": "", + "office_hours": null, + "sections": [ + "67d07ee0c972c18731e23bef" + ] + }, + { + "_id": "67d07ee0c972c18731e23bf3", + "first_name": "Tian", + "last_name": "Hong", + "titles": [ + "Primary Instructor" + ], + "email": "txh240018@utdallas.edu", + "phone_number": "", + "office": { + "building": "", + "room": "", + "map_uri": "" + }, + "profile_uri": "", + "image_uri": "", + "office_hours": null, + "sections": [ + "67d07ee0c972c18731e23bf2" + ] + } +] diff --git a/parser/testdata/sections.json b/parser/testdata/sections.json new file mode 100644 index 0000000..92319de --- /dev/null +++ b/parser/testdata/sections.json @@ -0,0 +1,282 @@ +[ + { + "_id": "67d07ee0c972c18731e23bef", + "section_number": "501", + "course_reference": "67d07ee0c972c18731e23bee", + "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": [ + "67d07ee0c972c18731e23bf0" + ], + "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 + }, + { + "_id": "67d07ee0c972c18731e23bf2", + "section_number": "016", + "course_reference": "67d07ee0c972c18731e23bf1", + "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": [ + "67d07ee0c972c18731e23bf3" + ], + "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 + }, + { + "_id": "67d07ee0c972c18731e23bf5", + "section_number": "201", + "course_reference": "67d07ee0c972c18731e23bf4", + "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 + }, + { + "_id": "67d07ee0c972c18731e23bf7", + "section_number": "002", + "course_reference": "67d07ee0c972c18731e23bf6", + "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 + }, + { + "_id": "67d07ee0c972c18731e23bea", + "section_number": "003", + "course_reference": "67d07ee0c972c18731e23be9", + "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": [ + "67d07ee0c972c18731e23beb", + "67d07ee0c972c18731e23bec" + ], + "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": "27706", + "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": "10:00am", + "end_time": "11:15am", + "modality": "", + "location": { + "building": "JSOM", + "room": "2.717", + "map_uri": "https://locator.utdallas.edu/SOM_2.717" + } + } + ], + "core_flags": [], + "syllabus_uri": "https://dox.utdallas.edu/syl152555", + "grade_distribution": [], + "attributes": null + }, + { + "_id": "67d07ee0c972c18731e23bed", + "section_number": "001", + "course_reference": "67d07ee0c972c18731e23be9", + "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": [ + "67d07ee0c972c18731e23bec", + "67d07ee0c972c18731e23beb" + ], + "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 + } +]