diff --git a/build.bat b/build.bat index 9a8003a..8d671d1 100644 --- a/build.bat +++ b/build.bat @@ -24,8 +24,8 @@ echo Performing checks... go mod tidy && ^ go vet ./... && ^ staticcheck ./... && ^ -gofmt -w ./.. && ^ -goimports -w ./.. +gofmt -w . && ^ +goimports -w . if ERRORLEVEL 1 exit /b %ERRORLEVEL% :: fail if error occurred echo Checks done! if %skip%==1 exit diff --git a/parser/gradeLoader.go b/parser/gradeLoader.go index 57c7d7a..f92a7f0 100644 --- a/parser/gradeLoader.go +++ b/parser/gradeLoader.go @@ -5,79 +5,64 @@ import ( "fmt" "log" "os" - "path/filepath" + "regexp" "strconv" "strings" -) -var grades = []string{"A+", "A", "A-", "B+", "B", "B-", "C+", "C", "C-", "D+", "D", "D-", "F", "W", "P", "CR", "NC", "I"} + "github.com/UTDNebula/api-tools/utils" +) -func loadGrades(csvDir string) map[string]map[string][]int { +var ( + grades = []string{"A+", "A", "A-", "B+", "B", "B-", "C+", "C", "C-", "D+", "D", "D-", "F", "W", "P", "CR", "NC", "I"} + optionalColumns = []string{"W", "P", "CR", "NC", "I"} + requiredColumns = []string{"Section", "Subject", "Catalog Number", "A+"} + semesterRegex = regexp.MustCompile(`[1-9][0-9][USF]`) +) +func loadGrades(csvDir string) (map[string]map[string][]int, error) { // MAP[SEMESTER] -> MAP[SUBJECT + NUMBER + SECTION] -> GRADE DISTRIBUTION gradeMap := make(map[string]map[string][]int) - if csvDir == "" { - log.Print("No grade data CSV directory specified. Grade data will not be included.") - return gradeMap - } - - dirPtr, err := os.Open(csvDir) - if err != nil { - panic(err) - } - defer dirPtr.Close() + fileNames := utils.GetAllFilesWithExtension(csvDir, ".csv") + for _, name := range fileNames { - csvFiles, err := dirPtr.ReadDir(-1) - if err != nil { - panic(err) - } - - for _, csvEntry := range csvFiles { - - if csvEntry.IsDir() { - continue + semester := semesterRegex.FindString(name) + if semester == "" { + return gradeMap, fmt.Errorf("invalid name %s, must match format {>10}{F,S,U} i.e. 22F", name) } - csvPath := fmt.Sprintf("%s/%s", csvDir, csvEntry.Name()) - - csvFile, err := os.Open(csvPath) + var err error + gradeMap[semester], err = csvToMap(name) if err != nil { - panic(err) - } - defer csvFile.Close() - - // Create logs directory - if _, err := os.Stat("./logs/grades"); err != nil { - os.Mkdir("./logs/grades", os.ModePerm) + return gradeMap, fmt.Errorf("error parsing %s: %v", name, err) } + } - // Create log file [name of csv].log in logs directory - basePath := filepath.Base(csvPath) - csvName := strings.TrimSuffix(basePath, filepath.Ext(basePath)) - logFile, err := os.Create("./logs/grades/" + csvName + ".log") + return gradeMap, nil +} - if err != nil { - log.Panic("Could not create CSV log file.") +func csvToMap(filename string) (map[string][]int, error) { + file, err := os.Open(filename) + if err != nil { + return nil, fmt.Errorf("error opening CSV file '%s': %v", filename, err) + } + defer func(file *os.File) { + if err := file.Close(); err != nil { + log.Printf("failed to close file '%s': %v", filename, err) } - defer logFile.Close() + }(file) - // Put data from csv into map - gradeMap[csvName] = csvToMap(csvFile, logFile) + reader := csv.NewReader(file) + records, err := reader.ReadAll() + if err != nil { + return nil, fmt.Errorf("error parsing %s: %v", filename, err) } - return gradeMap -} - -func csvToMap(csvFile *os.File, logFile *os.File) map[string][]int { - reader := csv.NewReader(csvFile) - records, err := reader.ReadAll() // records is [][]strings - if err != nil { - log.Panicf("Error parsing %s: %s", csvFile.Name(), err.Error()) + if len(records) == 0 { + return nil, fmt.Errorf("empty CSV file '%s'", filename) } indexMap := make(map[string]int) - for j, col := range records[0] { switch col { case "Catalog Number", "Catalog Nbr": @@ -89,18 +74,15 @@ func csvToMap(csvFile *os.File, logFile *os.File) map[string][]int { } } - // required columns - for _, name := range []string{"Section", "Subject", "Catalog Number", "A+"} { + for _, name := range requiredColumns { if _, ok := indexMap[name]; !ok { - fmt.Fprintf(logFile, "could not find %s column", name) - log.Panicf("could not find %s column", name) + return nil, fmt.Errorf("could not find %s column in %s", name, filename) } } - // optional columns - for _, name := range []string{"W", "P", "CR", "NC", "I"} { + for _, name := range optionalColumns { if _, ok := indexMap[name]; !ok { - logFile.WriteString(fmt.Sprintf("could not find %s column\n", name)) + log.Printf("could not find %s column in %s", name, filename) } } @@ -109,7 +91,6 @@ func csvToMap(csvFile *os.File, logFile *os.File) map[string][]int { catalogNumberCol := indexMap["Catalog Number"] distroMap := make(map[string][]int) - for _, record := range records[1:] { // convert grade distribution from string to int intSlice := make([]int, len(grades)) @@ -125,5 +106,6 @@ func csvToMap(csvFile *os.File, logFile *os.File) map[string][]int { distroKey := record[subjectCol] + record[catalogNumberCol] + trimmedSectionNumber distroMap[distroKey] = intSlice[:] } - return distroMap + + return distroMap, nil } diff --git a/parser/gradeLoader_test.go b/parser/gradeLoader_test.go new file mode 100644 index 0000000..efb3e1c --- /dev/null +++ b/parser/gradeLoader_test.go @@ -0,0 +1,134 @@ +package parser + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" +) + +var ( + gradeLoaderTestCases = map[string]struct { + csvContent string + want map[string][]int + fail bool + }{ + "Valid_Data": { + csvContent: `Instructor 1,Instructor 2,Instructor 3,Instructor 4,Instructor 5,Instructor 6,Subject,"Catalog Nbr",Section,A+,A,A-,B+,B,B-,C+,C,C-,D+,D,D-,F,NF,CR,I,NC,P,W +"Curchack, Fred",,,,,,AP,3300,501,6,4,2,2,1,3,1,1,,,,,1,,,,,,0 +"Anjum, Zafar",,,,,,ARAB,1311,001,,26,,,1,,,,,,,,,,,,,,2`, + want: map[string][]int{ + "AP3300501": {6, 4, 2, 2, 1, 3, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0}, + "ARAB13111": {0, 26, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0}, + }, + fail: false, + }, + "Missing_Required_Column_A+": { + csvContent: `Subject,"Catalog Nbr",Section,A,A-,B+ +CS,1337,001,10,5,5`, + fail: true, + }, + "Missing_Required_Column_Subject": { + csvContent: `Instructor,"Catalog Nbr",Section,A+,A +Doe,1337,001,10,5`, + fail: true, + }, + "Empty_File": { + csvContent: ``, + fail: true, + }, + } +) + +func TestLoadGrades(t *testing.T) { + + invalidCSVNames := []string{"22", "2F", "2022F", "20-U", "15Fall"} + + for i, name := range invalidCSVNames { + t.Run( + fmt.Sprintf("Invalid_CSV_Name_%d", i), func(t *testing.T) { + tempDir := t.TempDir() + + temp, err := os.Create(filepath.Join(tempDir, name+".csv")) + if err != nil { + t.Errorf("failed to create temp file: %v", err) + } + defer temp.Close() + + _, err = loadGrades(tempDir) + if err == nil { + t.Errorf("expected error but got none") + } + }, + ) + } + + validCSVNames := []string{"25F", "18U", "26S"} + for i, name := range validCSVNames { + t.Run( + fmt.Sprintf("Valid_CSV_Name_%d", i), func(t *testing.T) { + tempDir := t.TempDir() + + temp, err := os.Create(filepath.Join(tempDir, name+".csv")) + if err != nil { + t.Errorf("failed to create temp file: %v", err) + } + defer temp.Close() + + _, err = temp.WriteString(gradeLoaderTestCases["Valid_Data"].csvContent) + if err != nil { + t.Errorf("failed to write test data: %v", err) + } + + _, err = loadGrades(tempDir) + if err != nil { + t.Errorf("valid .csv failed: %v", err) + } + }, + ) + } + + t.Run("Real_Data", func(t *testing.T) { + _, err := loadGrades("../grade-data/") + if err != nil { + t.Errorf("failed to load grades: %v", err) + } + }) +} + +func TestCSVToMap(t *testing.T) { + tempDir := t.TempDir() + + for name, testCase := range gradeLoaderTestCases { + t.Run(name, func(t *testing.T) { + + temp, err := os.CreateTemp(tempDir, "grades*.csv") + if err != nil { + t.Errorf("failed to create temp file: %v", err) + } + defer temp.Close() + + if _, err = temp.WriteString(testCase.csvContent); err != nil { + t.Errorf("failed to write test data: %v", err) + } + + output, err := csvToMap(temp.Name()) + if err != nil { + if testCase.fail { + return + } + t.Errorf("failed to load csv: %v", err) + } else if testCase.fail { + t.Errorf("expected failure but got none") + } else { + diff := cmp.Diff(testCase.want, output) + if diff != "" { + t.Errorf("Failed (-expected +got)\n %s", diff) + } + } + + }) + } +} diff --git a/parser/parser.go b/parser/parser.go index 2b7049e..1dd2241 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -40,17 +40,25 @@ var ( timeLocation, timeError = time.LoadLocation("America/Chicago") ) +func init() { + if timeError != nil { + log.Fatalf("Failed to initialize timeLocation: %v", timeError) + } +} + // Parse loads scraped course artifacts, applies parsing and validation, and persists structured results. func Parse(inDir string, outDir string, csvPath string, skipValidation bool) { - // Panic if timeLocation didn't load properly - if timeError != nil { - panic(timeError) - } + if csvPath == "" { + log.Print("No grade data CSV directory specified. Grade data will not be included.") + } else { + var err error + GradeMap, err = loadGrades(csvPath) - // Load grade data from csv in advance - GradeMap = loadGrades(csvPath) - if len(GradeMap) != 0 { + if err != nil { + log.Fatalf("Failed to load grade data: %v", err) + return + } log.Printf("Loaded grade distributions for %d semesters.", len(GradeMap)) } diff --git a/parser/parser_test.go b/parser/parser_test.go index cee8873..41e7af4 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -34,7 +34,7 @@ type TestData struct { // testData global dictionary containing the data from /testdata by folder name var testData map[string]TestData -// TestMain loads parser fixtures and handles the -update flag for regenerating expectations. +// TestMain loads parser fixtures and handles the `-update` flag for regenerating expectations. 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 :)") @@ -132,10 +132,13 @@ func updateTestData() error { } defer os.RemoveAll(tempDir) - //Fill temp dir with all the test cases and expected values + GradeMap, err = loadGrades("../grade-data") + if err != nil { + return err + } + //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) @@ -214,20 +217,66 @@ func updateTestData() error { //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) + Parse(tempDir, tempDir, "../grade-data", false) + + targetDir := "testdata" + + err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + relPath, err := filepath.Rel(tempDir, path) + if err != nil { + return err + } + destPath := filepath.Join(targetDir, relPath) + + if info.IsDir() { + return os.MkdirAll(destPath, 0755) + } + + newContent, err := os.ReadFile(path) + if err != nil { + return err + } + + if existingContent, err := os.ReadFile(destPath); err == nil { + if bytes.Equal(newContent, existingContent) { + return nil + } + } + + log.Printf("Updating file: %s", destPath) + return os.WriteFile(destPath, newContent, 0644) + }) - //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 != nil { + return fmt.Errorf("failed to sync test data: %v", err) } - if err := os.CopyFS("testdata", os.DirFS(tempDir)); err != nil { - return fmt.Errorf("failed to copy testdata: %v", err) + err = filepath.Walk(targetDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + relPath, err := filepath.Rel(targetDir, path) + if err != nil { + return err + } + + srcPath := filepath.Join(tempDir, relPath) + if _, err := os.Stat(srcPath); os.IsNotExist(err) { + log.Printf("Removing stale file: %s", path) + return os.RemoveAll(path) + } + return nil + }) + + if err != nil { + return fmt.Errorf("failed to cleanup stale data: %v", err) } - //reset maps to avoid side effects. maybe parser should be an object? clearGlobals() return nil } @@ -244,8 +293,8 @@ func clearGlobals() { // TestParse verifies that parsing input fixtures generates the expected JSON exports. func TestParse(t *testing.T) { tempDir := t.TempDir() - // todo fix grade data, csvPath = ./grade-data panics - Parse("testdata", tempDir, "", false) + + Parse("testdata", tempDir, "../grade-data", false) OutputCourses, err := unmarshallFile[[]schema.Course](filepath.Join(tempDir, "courses.json")) if err != nil { diff --git a/parser/testdata/case_000/course.json b/parser/testdata/case_000/course.json index 5e342be..e050494 100644 --- a/parser/testdata/case_000/course.json +++ b/parser/testdata/case_000/course.json @@ -1,5 +1,5 @@ { - "_id": "67d07ee0c972c18731e23bd7", + "_id": "6972f54d6afb10b361a3e8b1", "subject_prefix": "ACCT", "course_number": "2301", "title": "Introductory Financial Accounting", @@ -15,7 +15,7 @@ "corequisites": null, "co_or_pre_requisites": null, "sections": [ - "67d07ee0c972c18731e23bd8" + "6972f54d6afb10b361a3e8b2" ], "lecture_contact_hours": "3", "laboratory_contact_hours": "0", diff --git a/parser/testdata/case_000/professors.json b/parser/testdata/case_000/professors.json index 207c908..57aec29 100644 --- a/parser/testdata/case_000/professors.json +++ b/parser/testdata/case_000/professors.json @@ -1,6 +1,6 @@ [ { - "_id": "67d07ee0c972c18731e23bd9", + "_id": "6972f54d6afb10b361a3e8b3", "first_name": "Naim Bugra", "last_name": "Ozel", "titles": [ @@ -17,11 +17,11 @@ "image_uri": "", "office_hours": null, "sections": [ - "67d07ee0c972c18731e23bd8" + "6972f54d6afb10b361a3e8b2" ] }, { - "_id": "67d07ee0c972c18731e23bda", + "_id": "6972f54d6afb10b361a3e8b4", "first_name": "Jieying", "last_name": "Zhang", "titles": [ @@ -38,7 +38,7 @@ "image_uri": "", "office_hours": null, "sections": [ - "67d07ee0c972c18731e23bd8" + "6972f54d6afb10b361a3e8b2" ] } ] diff --git a/parser/testdata/case_000/section.json b/parser/testdata/case_000/section.json index a67c0f5..b2faa8f 100644 --- a/parser/testdata/case_000/section.json +++ b/parser/testdata/case_000/section.json @@ -1,7 +1,7 @@ { - "_id": "67d07ee0c972c18731e23bd8", + "_id": "6972f54d6afb10b361a3e8b2", "section_number": "003", - "course_reference": "67d07ee0c972c18731e23bd7", + "course_reference": "6972f54d6afb10b361a3e8b1", "section_corequisites": null, "academic_session": { "name": "25S", @@ -9,8 +9,8 @@ "end_date": "2025-05-16T00:00:00-05:00" }, "professors": [ - "67d07ee0c972c18731e23bd9", - "67d07ee0c972c18731e23bda" + "6972f54d6afb10b361a3e8b3", + "6972f54d6afb10b361a3e8b4" ], "teaching_assistants": [ { @@ -48,6 +48,25 @@ ], "core_flags": [], "syllabus_uri": "https://dox.utdallas.edu/syl152555", - "grade_distribution": [], + "grade_distribution": [ + 9, + 9, + 4, + 6, + 4, + 5, + 12, + 3, + 1, + 3, + 1, + 0, + 4, + 3, + 0, + 0, + 0, + 0 + ], "attributes": null } diff --git a/parser/testdata/case_001/course.json b/parser/testdata/case_001/course.json index 24dcf8b..4a015f2 100644 --- a/parser/testdata/case_001/course.json +++ b/parser/testdata/case_001/course.json @@ -1,5 +1,5 @@ { - "_id": "67d07ee0c972c18731e23bdb", + "_id": "6972f54d6afb10b361a3e8b5", "subject_prefix": "ACCT", "course_number": "2301", "title": "Introductory Financial Accounting", @@ -15,7 +15,7 @@ "corequisites": null, "co_or_pre_requisites": null, "sections": [ - "67d07ee0c972c18731e23bdc" + "6972f54d6afb10b361a3e8b6" ], "lecture_contact_hours": "3", "laboratory_contact_hours": "0", diff --git a/parser/testdata/case_001/professors.json b/parser/testdata/case_001/professors.json index 8bf8c6b..72111e2 100644 --- a/parser/testdata/case_001/professors.json +++ b/parser/testdata/case_001/professors.json @@ -1,6 +1,6 @@ [ { - "_id": "67d07ee0c972c18731e23bdd", + "_id": "6972f54d6afb10b361a3e8b7", "first_name": "Jieying", "last_name": "Zhang", "titles": [ @@ -17,11 +17,11 @@ "image_uri": "", "office_hours": null, "sections": [ - "67d07ee0c972c18731e23bdc" + "6972f54d6afb10b361a3e8b6" ] }, { - "_id": "67d07ee0c972c18731e23bde", + "_id": "6972f54d6afb10b361a3e8b8", "first_name": "Naim Bugra", "last_name": "Ozel", "titles": [ @@ -38,7 +38,7 @@ "image_uri": "", "office_hours": null, "sections": [ - "67d07ee0c972c18731e23bdc" + "6972f54d6afb10b361a3e8b6" ] } ] diff --git a/parser/testdata/case_001/section.json b/parser/testdata/case_001/section.json index eeb9360..4b10fcf 100644 --- a/parser/testdata/case_001/section.json +++ b/parser/testdata/case_001/section.json @@ -1,7 +1,7 @@ { - "_id": "67d07ee0c972c18731e23bdc", + "_id": "6972f54d6afb10b361a3e8b6", "section_number": "001", - "course_reference": "67d07ee0c972c18731e23bdb", + "course_reference": "6972f54d6afb10b361a3e8b5", "section_corequisites": null, "academic_session": { "name": "25S", @@ -9,8 +9,8 @@ "end_date": "2025-05-16T00:00:00-05:00" }, "professors": [ - "67d07ee0c972c18731e23bdd", - "67d07ee0c972c18731e23bde" + "6972f54d6afb10b361a3e8b7", + "6972f54d6afb10b361a3e8b8" ], "teaching_assistants": [ { @@ -48,6 +48,25 @@ ], "core_flags": [], "syllabus_uri": "https://dox.utdallas.edu/syl152552", - "grade_distribution": [], + "grade_distribution": [ + 2, + 7, + 5, + 8, + 8, + 7, + 3, + 6, + 1, + 3, + 3, + 0, + 9, + 1, + 0, + 0, + 0, + 0 + ], "attributes": null } diff --git a/parser/testdata/case_002/course.json b/parser/testdata/case_002/course.json index 141ff6b..1db1746 100644 --- a/parser/testdata/case_002/course.json +++ b/parser/testdata/case_002/course.json @@ -1,5 +1,5 @@ { - "_id": "67d07ee0c972c18731e23bdf", + "_id": "6972f54d6afb10b361a3e8b9", "subject_prefix": "BA", "course_number": "1320", "title": "Business in a Global World", @@ -15,7 +15,7 @@ "corequisites": null, "co_or_pre_requisites": null, "sections": [ - "67d07ee0c972c18731e23be0" + "6972f54d6afb10b361a3e8ba" ], "lecture_contact_hours": "3", "laboratory_contact_hours": "0", diff --git a/parser/testdata/case_002/professors.json b/parser/testdata/case_002/professors.json index c6913f6..1f17b80 100644 --- a/parser/testdata/case_002/professors.json +++ b/parser/testdata/case_002/professors.json @@ -1,6 +1,6 @@ [ { - "_id": "67d07ee0c972c18731e23be1", + "_id": "6972f54d6afb10b361a3e8bb", "first_name": "Peter", "last_name": "Lewin", "titles": [ @@ -17,7 +17,7 @@ "image_uri": "", "office_hours": null, "sections": [ - "67d07ee0c972c18731e23be0" + "6972f54d6afb10b361a3e8ba" ] } ] diff --git a/parser/testdata/case_002/section.json b/parser/testdata/case_002/section.json index 6eb44f5..4ca35d9 100644 --- a/parser/testdata/case_002/section.json +++ b/parser/testdata/case_002/section.json @@ -1,7 +1,7 @@ { - "_id": "67d07ee0c972c18731e23be0", + "_id": "6972f54d6afb10b361a3e8ba", "section_number": "501", - "course_reference": "67d07ee0c972c18731e23bdf", + "course_reference": "6972f54d6afb10b361a3e8b9", "section_corequisites": null, "academic_session": { "name": "25S", @@ -9,7 +9,7 @@ "end_date": "2025-05-16T00:00:00-05:00" }, "professors": [ - "67d07ee0c972c18731e23be1" + "6972f54d6afb10b361a3e8bb" ], "teaching_assistants": [ { @@ -44,6 +44,25 @@ "090" ], "syllabus_uri": "https://dox.utdallas.edu/syl153033", - "grade_distribution": [], + "grade_distribution": [ + 0, + 13, + 23, + 6, + 4, + 7, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0 + ], "attributes": null } diff --git a/parser/testdata/case_003/course.json b/parser/testdata/case_003/course.json index 94219f8..0111554 100644 --- a/parser/testdata/case_003/course.json +++ b/parser/testdata/case_003/course.json @@ -1,5 +1,5 @@ { - "_id": "67d07ee0c972c18731e23be2", + "_id": "6972f54d6afb10b361a3e8bc", "subject_prefix": "BIOL", "course_number": "6111", "title": "Graduate Research Presentation", @@ -15,7 +15,7 @@ "corequisites": null, "co_or_pre_requisites": null, "sections": [ - "67d07ee0c972c18731e23be3" + "6972f54d6afb10b361a3e8bd" ], "lecture_contact_hours": "1", "laboratory_contact_hours": "0", diff --git a/parser/testdata/case_003/professors.json b/parser/testdata/case_003/professors.json index 3cb4a51..e921077 100644 --- a/parser/testdata/case_003/professors.json +++ b/parser/testdata/case_003/professors.json @@ -1,6 +1,6 @@ [ { - "_id": "67d07ee0c972c18731e23be4", + "_id": "6972f54d6afb10b361a3e8be", "first_name": "Tian", "last_name": "Hong", "titles": [ @@ -17,7 +17,7 @@ "image_uri": "", "office_hours": null, "sections": [ - "67d07ee0c972c18731e23be3" + "6972f54d6afb10b361a3e8bd" ] } ] diff --git a/parser/testdata/case_003/section.json b/parser/testdata/case_003/section.json index fff4105..d598a10 100644 --- a/parser/testdata/case_003/section.json +++ b/parser/testdata/case_003/section.json @@ -1,7 +1,7 @@ { - "_id": "67d07ee0c972c18731e23be3", + "_id": "6972f54d6afb10b361a3e8bd", "section_number": "016", - "course_reference": "67d07ee0c972c18731e23be2", + "course_reference": "6972f54d6afb10b361a3e8bc", "section_corequisites": null, "academic_session": { "name": "25S", @@ -9,7 +9,7 @@ "end_date": "2025-05-16T00:00:00-05:00" }, "professors": [ - "67d07ee0c972c18731e23be4" + "6972f54d6afb10b361a3e8be" ], "teaching_assistants": [], "internal_class_number": "29611", diff --git a/parser/testdata/case_004/course.json b/parser/testdata/case_004/course.json index d8c5383..b253341 100644 --- a/parser/testdata/case_004/course.json +++ b/parser/testdata/case_004/course.json @@ -1,5 +1,5 @@ { - "_id": "67d07ee0c972c18731e23be5", + "_id": "6972f54d6afb10b361a3e8bf", "subject_prefix": "AERO", "course_number": "3320", "title": "- Recitation", @@ -15,7 +15,7 @@ "corequisites": null, "co_or_pre_requisites": null, "sections": [ - "67d07ee0c972c18731e23be6" + "6972f54d6afb10b361a3e8c0" ], "lecture_contact_hours": "", "laboratory_contact_hours": "", diff --git a/parser/testdata/case_004/section.json b/parser/testdata/case_004/section.json index 2481524..0608366 100644 --- a/parser/testdata/case_004/section.json +++ b/parser/testdata/case_004/section.json @@ -1,7 +1,7 @@ { - "_id": "67d07ee0c972c18731e23be6", + "_id": "6972f54d6afb10b361a3e8c0", "section_number": "201", - "course_reference": "67d07ee0c972c18731e23be5", + "course_reference": "6972f54d6afb10b361a3e8bf", "section_corequisites": null, "academic_session": { "name": "25S", diff --git a/parser/testdata/case_005/course.json b/parser/testdata/case_005/course.json index 9095afc..209bd70 100644 --- a/parser/testdata/case_005/course.json +++ b/parser/testdata/case_005/course.json @@ -1,5 +1,5 @@ { - "_id": "67d07ee0c972c18731e23be7", + "_id": "6972f54d6afb10b361a3e8c1", "subject_prefix": "AERO", "course_number": "4320", "title": "- Laboratory", @@ -15,7 +15,7 @@ "corequisites": null, "co_or_pre_requisites": null, "sections": [ - "67d07ee0c972c18731e23be8" + "6972f54d6afb10b361a3e8c2" ], "lecture_contact_hours": "", "laboratory_contact_hours": "", diff --git a/parser/testdata/case_005/section.json b/parser/testdata/case_005/section.json index 712c972..247ab4c 100644 --- a/parser/testdata/case_005/section.json +++ b/parser/testdata/case_005/section.json @@ -1,7 +1,7 @@ { - "_id": "67d07ee0c972c18731e23be8", + "_id": "6972f54d6afb10b361a3e8c2", "section_number": "002", - "course_reference": "67d07ee0c972c18731e23be7", + "course_reference": "6972f54d6afb10b361a3e8c1", "section_corequisites": null, "academic_session": { "name": "25S", diff --git a/parser/testdata/case_006/classInfo.json b/parser/testdata/case_006/classInfo.json new file mode 100644 index 0000000..34e725a --- /dev/null +++ b/parser/testdata/case_006/classInfo.json @@ -0,0 +1,14 @@ +{ + "": "", + "Activity Type:": "Lecture", + "Add Consent:": "No Consent", + "Class Level:": "Undergraduate", + "Class Section:": "THEA1310.001.25S", + "Class/Course Number:": "24043 / 003909", + "Grading:": "Graded - Undergraduate", + "How often a course is scheduled:": "Once Each Long Semester", + "Instruction Mode:": "Face-to-Face", + "Orion Date/Time:": "2025-08-28 06:30:01", + "Semester Credit Hours:": "3", + "Session Type:": "Regular Academic Session" +} diff --git a/parser/testdata/case_006/course.json b/parser/testdata/case_006/course.json new file mode 100644 index 0000000..a20be42 --- /dev/null +++ b/parser/testdata/case_006/course.json @@ -0,0 +1,25 @@ +{ + "_id": "6972f54d6afb10b361a3e8c3", + "subject_prefix": "THEA", + "course_number": "1310", + "title": "Understanding Theatre", + "description": "THEA 1310 - Understanding Theatre (3 semester credit hours) Lectures, discussions, and performances designed to explore artistic, philosophical, social, historical, and psychological dimensions of the theatrical experience. Topics may include analysis of scripts, the nature of the theater compared to the other performing arts, and the nature of popular entertainments. (3-0) S", + "enrollment_reqs": "", + "school": "School of Arts, Humanities, and Technology", + "credit_hours": "3", + "class_level": "Undergraduate", + "activity_type": "Lecture", + "grading": "Graded - Undergraduate", + "internal_course_number": "003909", + "prerequisites": null, + "corequisites": null, + "co_or_pre_requisites": null, + "sections": [ + "6972f54d6afb10b361a3e8c4" + ], + "lecture_contact_hours": "3", + "laboratory_contact_hours": "0", + "offering_frequency": "S", + "catalog_year": "24", + "attributes": null +} diff --git a/parser/testdata/case_006/input.html b/parser/testdata/case_006/input.html new file mode 100644 index 0000000..e4efe97 --- /dev/null +++ b/parser/testdata/case_006/input.html @@ -0,0 +1,268 @@ +