diff --git a/CHANGELOG.md b/CHANGELOG.md index a652e3fb2..95c94201d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### 🚨 Breaking changes +- Added 404 status code return to `retrieveCourse` in `Controllers/Course` + ### ✨ New features/enhancements ### 🐛 Bug fixes @@ -14,6 +16,7 @@ - Refactor GraphDropdown component from being a child of Graph to being a child of NavBar - Added test cases for the saveGraphJSON function in `Controllers/Graph` +- Remove unused variable from `Graph.js` ## [0.7.2] - 2025-12-10 diff --git a/app/Controllers/Course.hs b/app/Controllers/Course.hs index 5c118bb54..6921be3b1 100644 --- a/app/Controllers/Course.hs +++ b/app/Controllers/Course.hs @@ -7,7 +7,7 @@ import qualified Data.Text as T (Text, unlines) import Database.Persist (Entity) import Database.Persist.Sqlite (SqlPersistM, entityVal, selectList) import Database.Tables as Tables (Courses, coursesCode) -import Happstack.Server (Response, ServerPart, lookText', toResponse) +import Happstack.Server (Response, ServerPart, lookText', notFound, ok, toResponse) import Models.Course (getDeptCourses, returnCourse) import Util.Happstack (createJSONResponse) @@ -17,7 +17,9 @@ retrieveCourse :: ServerPart Response retrieveCourse = do name <- lookText' "name" courses <- liftIO $ returnCourse name - return $ createJSONResponse courses + case courses of + Just x -> ok $ createJSONResponse x + Nothing -> notFound $ toResponse ("Course not found" :: String) -- | Builds a list of all course codes in the database. index :: ServerPart Response diff --git a/backend-test/Controllers/CourseControllerTests.hs b/backend-test/Controllers/CourseControllerTests.hs index 1a49657ad..0aa2610fb 100644 --- a/backend-test/Controllers/CourseControllerTests.hs +++ b/backend-test/Controllers/CourseControllerTests.hs @@ -18,13 +18,13 @@ import Data.Maybe (fromMaybe) import qualified Data.Text as T import Database.Persist.Sqlite (SqlPersistM, insert_) import Database.Tables (Courses (..)) -import Happstack.Server (rsBody) +import Happstack.Server (rsBody, rsCode) import Test.Tasty (TestTree) import Test.Tasty.HUnit (assertEqual, testCase) -import TestHelpers (mockGetRequest, clearDatabase, runServerPart, runServerPartWith, withDatabase) +import TestHelpers (clearDatabase, mockGetRequest, runServerPart, runServerPartWith, withDatabase) --- | List of test cases as (input course name, course data, expected JSON output) -retrieveCourseTestCases :: [(String, T.Text, Map.Map T.Text T.Text, String)] +-- | List of test cases as (input course name, course data, status code, expected JSON output) +retrieveCourseTestCases :: [(String, T.Text, Map.Map T.Text T.Text, Int, String)] retrieveCourseTestCases = [ ("Course exists", "STA238", @@ -40,25 +40,28 @@ retrieveCourseTestCases = ("coreqs", "CSC108H1/ CSC110Y1/ CSC148H1 *Note: the corequisite may be completed either concurrently or in advance."), ("videoUrls", "https://example.com/video1, https://example.com/video2") ], + 200, "{\"allMeetingTimes\":[],\"breadth\":null,\"coreqs\":\"CSC108H1/ CSC110Y1/ CSC148H1 *Note: the corequisite may be completed either concurrently or in advance.\",\"description\":\"An introduction to statistical inference and practice. Statistical models and parameters, estimators of parameters and their statistical properties, methods of estimation, confidence intervals, hypothesis testing, likelihood function, the linear model. Use of statistical computation for data analysis and simulation.\",\"distribution\":null,\"exclusions\":\"ECO220Y1/ ECO227Y1/ GGR270H1/ PSY201H1/ SOC300H1/ SOC202H1/ SOC252H1/ STA220H1/ STA221H1/ STA255H1/ STA248H1/ STA261H1/ STA288H1/ EEB225H1/ STAB22H3/ STAB27H3/ STAB57H3/ STA220H5/ STA221H5/ STA258H5/ STA260H5/ ECO220Y5/ ECO227Y5\",\"name\":\"STA238H1\",\"prereqString\":\"STA237H1/ STA247H1/ STA257H1/ STAB52H3/ STA256H5\",\"title\":\"Probability, Statistics and Data Analysis II\",\"videoUrls\":[\"https://example.com/video1\",\"https://example.com/video2\"]}" ), ("Course does not exist", "STA238", Map.empty, - "null" + 404, + "Course not found" ), ("No course provided", "", Map.empty, - "null" + 404, + "Course not found" ) ] --- | Run a test case (case, input, expected output) on the retrieveCourse function. -runRetrieveCourseTest :: String -> T.Text -> Map.Map T.Text T.Text -> String -> TestTree -runRetrieveCourseTest label courseName courseData expected = +-- | Run a test case (case, input, expected status code, expected output) on the retrieveCourse function. +runRetrieveCourseTest :: String -> T.Text -> Map.Map T.Text T.Text -> Int -> String -> TestTree +runRetrieveCourseTest label courseName courseData expectedCode expectedBody = testCase label $ do let currCourseName = fromMaybe "" $ Map.lookup "name" courseData @@ -86,12 +89,15 @@ runRetrieveCourseTest label courseName courseData expected = insert_ courseToInsert response <- runServerPartWith Controllers.Course.retrieveCourse $ mockGetRequest "/course" [("name", T.unpack courseName)] "" - let actual = BL.unpack $ rsBody response - assertEqual ("Unexpected response body for " ++ label) expected actual + let statusCode = rsCode response + assertEqual ("Unexpected status code for " ++ label) expectedCode statusCode + + let actualBody = BL.unpack $ rsBody response + assertEqual ("Unexpected response body for " ++ label) expectedBody actualBody -- | Run all the retrieveCourse test cases runRetrieveCourseTests :: [TestTree] -runRetrieveCourseTests = map (\(label, courseName, courseData, expected) -> runRetrieveCourseTest label courseName courseData expected) retrieveCourseTestCases +runRetrieveCourseTests = map (\(label, courseName, courseData, expectedCode, expectedBody) -> runRetrieveCourseTest label courseName courseData expectedCode expectedBody) retrieveCourseTestCases -- | Helper function to insert courses into the database insertCourses :: [T.Text] -> SqlPersistM () diff --git a/js/components/common/react_modal.js.jsx b/js/components/common/react_modal.js.jsx index b6e21d17e..750689d40 100644 --- a/js/components/common/react_modal.js.jsx +++ b/js/components/common/react_modal.js.jsx @@ -101,6 +101,16 @@ class CourseModal extends React.Component { }) } else if (prevState.courseId !== this.state.courseId) { getCourse(this.state.courseId).then(course => { + if (!course) { + this.setState({ + course: {}, + sessions: {}, + courseTitle: "Course Not Found", + }) + console.error(`Course with code ${this.state.courseId} not found`) + return + } + const newCourse = { ...course, description: this.convertToLink(course.description), diff --git a/js/components/common/utils.js b/js/components/common/utils.js index 3e3f6dd19..01b641545 100644 --- a/js/components/common/utils.js +++ b/js/components/common/utils.js @@ -1,13 +1,24 @@ /** * Retrieves a course from file. * @param {string} courseName The course code. This + '.txt' is the name of the file. - * @returns {Promise} Promise object representing the JSON object containing course information. + * @returns {Promise} Promise object representing the JSON object containing course information + * or null if not found. */ export function getCourse(courseName) { "use strict" return fetch("course?name=" + courseName) - .then(response => response.json()) + .then(response => { + if (response.status === 404) { + return null + } + + if (!response.ok) { + throw new Error(`Failed to fetch course with name ${courseName}`) + } + + return response.json() + }) .catch(error => { throw error }) diff --git a/js/components/graph/Graph.js b/js/components/graph/Graph.js index b3a76b8ec..b85d56e0f 100644 --- a/js/components/graph/Graph.js +++ b/js/components/graph/Graph.js @@ -22,9 +22,6 @@ const ZOOM_ENUM = { ZOOM_OUT: -1, ZOOM_IN: 1, } -const TIMEOUT_NAMES_ENUM = { - INFOBOX: 0 -} export class Graph extends React.Component { constructor(props) { diff --git a/js/components/grid/course_panel.js.jsx b/js/components/grid/course_panel.js.jsx index 9616057e5..1b3b1f9b6 100644 --- a/js/components/grid/course_panel.js.jsx +++ b/js/components/grid/course_panel.js.jsx @@ -157,6 +157,13 @@ function Course(props) { S: [], Y: [], } + + if (!data) { + setCourseInfo(course) + console.error(`Course with code ${props.courseCode} not found`) + return + } + course.courseCode = data.name const parsedLectures = parseLectures(data.allMeetingTimes)