Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- Remove unused `getTimetableImage` function in `Export/GetImages.hs`
- Refactored various backend text functions and tests to avoid `String` data in favour of `Text` when feasible
- Removed unused files
- Removed `Location` datatype in favour of `Building`

## [0.7.2] - 2025-12-10

Expand Down
21 changes: 8 additions & 13 deletions app/Database/Tables.hs
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ data Time =
Time { weekDay :: Double,
startHour :: Double,
endHour :: Double,
firstRoom :: Maybe Location,
secondRoom :: Maybe Location
firstRoom :: Maybe Building,
secondRoom :: Maybe Building
} deriving (Show, Generic)

data Location =
Expand Down Expand Up @@ -323,8 +323,8 @@ convertTimeVals _ _ _ = (5.0, 25.0, 25.0)
-- | Convert Times into Time
buildTime :: Times -> SqlPersistM Time
buildTime t = do
room1 <- buildLocation (timesFirstRoom t)
room2 <- buildLocation (timesSecondRoom t)
room1 <- getBuilding (timesFirstRoom t)
room2 <- getBuilding (timesSecondRoom t)
return $ Time (timesWeekDay t)
(timesStartHour t)
(timesEndHour t)
Expand All @@ -340,8 +340,9 @@ buildTimes meetingKey t =
(firstRoom' t)
(secondRoom' t)

buildLocation :: Maybe T.Text -> SqlPersistM (Maybe Location)
buildLocation rm = do
-- | Given a building code, get the persistent Building associated with it
getBuilding :: Maybe T.Text -> SqlPersistM (Maybe Building)
getBuilding rm = do
case rm of
Nothing -> return Nothing
Just r -> do
Expand All @@ -350,10 +351,4 @@ buildLocation rm = do
Nothing -> return Nothing
Just entBuilding -> do
let building = entityVal entBuilding
return $ Just $ Location (T.take 2 r) -- Remove room number
(buildingName building)
(buildingCode building)
(buildingAddress building)
(buildingPostalCode building)
(buildingLat building)
(buildingLng building)
return $ Just building
171 changes: 162 additions & 9 deletions backend-test/Controllers/CourseControllerTests.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,22 @@ import Control.Monad (unless)
import Controllers.Course (courseInfo, index, retrieveCourse)
import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import Data.Maybe (fromMaybe, mapMaybe)
import Data.List (nub)
import qualified Data.Text as T
import Database.Persist.Sqlite (SqlPersistM, insert_)
import Database.Tables (Courses (..))
import Database.Persist.Sqlite (SqlPersistM, insert, insert_, insertMany_)
import Database.Tables (Building (..), Courses (..), MeetTime (..), Meeting (..), Time' (..), buildTimes)
import Happstack.Server (rsBody, rsCode)
import Test.Tasty (TestTree)
import Test.Tasty.HUnit (assertEqual, testCase)
import TestHelpers (clearDatabase, mockGetRequest, runServerPart, runServerPartWith, withDatabase)

-- | 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)]

-- | List of test cases as (case description, input course name, course data, list of meeting data, status code, expected JSON output)
retrieveCourseTestCases :: [(String, T.Text, Map.Map T.Text T.Text, [MeetTime], Int, String)]
retrieveCourseTestCases =
[ ("Course exists",
[
("Course exists with meeting times, and other meeting times exist",
"STA238",
Map.fromList [
("name", "STA238H1"),
Expand All @@ -40,28 +43,152 @@ 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")
],
[
MeetTime
Meeting {
meetingCode = "STA238",
meetingSession = "F",
meetingSection = "LEC0101",
meetingCap = 50,
meetingInstructor = "Instructor Name",
meetingEnrol = 15,
meetingWait = 0,
meetingExtra = 0
}
[
Time' {
weekDay' = 0.0,
startHour' = 10.0,
endHour' = 11.0,
firstRoom' = Just "MP",
secondRoom' = Nothing
},
Time' {
weekDay' = 2.0,
startHour' = 13.0,
endHour' = 14.0,
firstRoom' = Just "SS",
secondRoom' = Nothing
}
],
MeetTime
Meeting {
meetingCode = "STA239",
meetingSession = "S",
meetingSection = "LEC0201",
meetingCap = 500,
meetingInstructor = "Instructor Name 2",
meetingEnrol = 150,
meetingWait = 0,
meetingExtra = 0
}
[
Time' {
weekDay' = 1.0,
startHour' = 10.0,
endHour' = 11.0,
firstRoom' = Just "WW",
secondRoom' = Nothing
},
Time' {
weekDay' = 4.0,
startHour' = 13.0,
endHour' = 14.0,
firstRoom' = Just "SS",
secondRoom' = Nothing
}
]
],
200,
"{\"allMeetingTimes\":[{\"meetData\":{\"cap\":50,\"code\":\"STA238\",\"enrol\":15,\"extra\":0,\"instructor\":\"Instructor Name\",\"section\":\"LEC0101\",\"session\":\"F\",\"wait\":0},\"timeData\":[{\"endHour\":11,\"firstRoom\":{\"buildingAddress\":\"N/A\",\"buildingCode\":\"MP\",\"buildingLat\":1,\"buildingLng\":1,\"buildingName\":\"MP\",\"buildingPostalCode\":\"A1A 1A1\"},\"secondRoom\":null,\"startHour\":10,\"weekDay\":0},{\"endHour\":14,\"firstRoom\":{\"buildingAddress\":\"N/A\",\"buildingCode\":\"SS\",\"buildingLat\":1,\"buildingLng\":1,\"buildingName\":\"SS\",\"buildingPostalCode\":\"A1A 1A1\"},\"secondRoom\":null,\"startHour\":13,\"weekDay\":2}]}],\"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 exists with meeting times",
"STA238",
Map.fromList [
("name", "STA238H1"),
("title", "Probability, Statistics and Data Analysis II"),
("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."),
("prereqs", "STA237H1/ STA247H1/ STA257H1/ STAB52H3/ STA256H5"),
("exclusions", "ECO220Y1/ ECO227Y1/ GGR270H1/ PSY201H1/ SOC300H1/ SOC202H1/ SOC252H1/ STA220H1/ STA221H1/ STA255H1/ STA248H1/ STA261H1/ STA288H1/ EEB225H1/ STAB22H3/ STAB27H3/ STAB57H3/ STA220H5/ STA221H5/ STA258H5/ STA260H5/ ECO220Y5/ ECO227Y5"),
("breadth", "The Physical and Mathematical Universes (5)"),
("distribution", "null"),
("prereqString", "STA237H1/ STA247H1/ STA257H1/ STAB52H3/ STA256H5"),
("coreqs", "CSC108H1/ CSC110Y1/ CSC148H1 *Note: the corequisite may be completed either concurrently or in advance."),
("videoUrls", "https://example.com/video1, https://example.com/video2")
],
[
MeetTime
Meeting {
meetingCode = "STA238",
meetingSession = "F",
meetingSection = "LEC0101",
meetingCap = 50,
meetingInstructor = "Instructor Name",
meetingEnrol = 15,
meetingWait = 0,
meetingExtra = 0
}
[
Time' {
weekDay' = 0.0,
startHour' = 10.0,
endHour' = 11.0,
firstRoom' = Just "MP",
secondRoom' = Nothing
},
Time' {
weekDay' = 2.0,
startHour' = 13.0,
endHour' = 14.0,
firstRoom' = Just "SS",
secondRoom' = Nothing
}
]
],
200,
"{\"allMeetingTimes\":[{\"meetData\":{\"cap\":50,\"code\":\"STA238\",\"enrol\":15,\"extra\":0,\"instructor\":\"Instructor Name\",\"section\":\"LEC0101\",\"session\":\"F\",\"wait\":0},\"timeData\":[{\"endHour\":11,\"firstRoom\":{\"buildingAddress\":\"N/A\",\"buildingCode\":\"MP\",\"buildingLat\":1,\"buildingLng\":1,\"buildingName\":\"MP\",\"buildingPostalCode\":\"A1A 1A1\"},\"secondRoom\":null,\"startHour\":10,\"weekDay\":0},{\"endHour\":14,\"firstRoom\":{\"buildingAddress\":\"N/A\",\"buildingCode\":\"SS\",\"buildingLat\":1,\"buildingLng\":1,\"buildingName\":\"SS\",\"buildingPostalCode\":\"A1A 1A1\"},\"secondRoom\":null,\"startHour\":13,\"weekDay\":2}]}],\"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 exists",
"STA238",
Map.fromList [
("name", "STA238H1"),
("title", "Probability, Statistics and Data Analysis II"),
("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."),
("prereqs", "STA237H1/ STA247H1/ STA257H1/ STAB52H3/ STA256H5"),
("exclusions", "ECO220Y1/ ECO227Y1/ GGR270H1/ PSY201H1/ SOC300H1/ SOC202H1/ SOC252H1/ STA220H1/ STA221H1/ STA255H1/ STA248H1/ STA261H1/ STA288H1/ EEB225H1/ STAB22H3/ STAB27H3/ STAB57H3/ STA220H5/ STA221H5/ STA258H5/ STA260H5/ ECO220Y5/ ECO227Y5"),
("breadth", "The Physical and Mathematical Universes (5)"),
("distribution", "null"),
("prereqString", "STA237H1/ STA247H1/ STA257H1/ STAB52H3/ STA256H5"),
("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,
[],
404,
"Course not found"
),

("No course provided",
"",
Map.empty,
[],
404,
"Course not found"
)
]

-- | 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 =
runRetrieveCourseTest :: String -> T.Text -> Map.Map T.Text T.Text -> [MeetTime] -> Int -> String -> TestTree
runRetrieveCourseTest label courseName courseData meetingTimes expectedCode expectedBody =
testCase label $ do
let currCourseName = fromMaybe "" $ Map.lookup "name" courseData

Expand All @@ -83,10 +210,16 @@ runRetrieveCourseTest label courseName courseData expectedCode expectedBody =
, coursesVideoUrls = videoUrls
}

let buildingCodes = getUniqueBuildings meetingTimes

runDb $ do
clearDatabase
unless (T.null currCourseName) $
insert_ courseToInsert
unless (null buildingCodes) $
insertBuildings buildingCodes
unless (null meetingTimes) $
insertMeetingTimes meetingTimes

response <- runServerPartWith Controllers.Course.retrieveCourse $ mockGetRequest "/course" [("name", T.unpack courseName)] ""
let statusCode = rsCode response
Expand All @@ -97,14 +230,34 @@ runRetrieveCourseTest label courseName courseData expectedCode expectedBody =

-- | Run all the retrieveCourse test cases
runRetrieveCourseTests :: [TestTree]
runRetrieveCourseTests = map (\(label, courseName, courseData, expectedCode, expectedBody) -> runRetrieveCourseTest label courseName courseData expectedCode expectedBody) retrieveCourseTestCases
runRetrieveCourseTests = map (\(label, courseName, courseData, meetingTimes, expectedCode, expectedBody) -> runRetrieveCourseTest label courseName courseData meetingTimes expectedCode expectedBody) retrieveCourseTestCases

-- | Helper function to insert courses into the database
insertCourses :: [T.Text] -> SqlPersistM ()
insertCourses = mapM_ insertCourse
where
insertCourse code = insert_ (Courses code Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing [])

-- | Helper function to insert MeetTimes into the database
insertMeetingTimes :: [MeetTime] -> SqlPersistM ()
insertMeetingTimes = mapM_ insertMeeting
where
insertMeeting (MeetTime meetingData meetingTime) = do
meetingKey <- insert meetingData
insertMany_ $ map (buildTimes meetingKey) meetingTime

-- | Helper function to insert dummy buildings from a list of codes
insertBuildings :: [T.Text] -> SqlPersistM ()
insertBuildings = mapM_ insertBuilding
where
insertBuilding code = insert_ Building {buildingCode = code, buildingName = code, buildingAddress = "N/A", buildingPostalCode = "A1A 1A1", buildingLat = 1.0, buildingLng = 1.0}

-- | Helper function to get a list of unique firstRoom' and secondRoom' values involved in a MeetTime
getUniqueBuildings :: [MeetTime] -> [T.Text]
getUniqueBuildings = nub . concatMap getMeetBuildings
where
getMeetBuildings (MeetTime _ times') = mapMaybe firstRoom' times' ++ mapMaybe secondRoom' times'

-- | List of test cases as (label, input courses, expected output)
indexTestCases :: [(String, [T.Text], String)]
indexTestCases =
Expand Down
2 changes: 1 addition & 1 deletion backend-test/TestHelpers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ clearDatabase :: SqlPersistM ()
clearDatabase = do
deleteWhere ([] :: [Filter Department])
deleteWhere ([] :: [Filter Courses])
deleteWhere ([] :: [Filter Meeting])
deleteWhere ([] :: [Filter Times])
deleteWhere ([] :: [Filter Meeting])
deleteWhere ([] :: [Filter Breadth])
deleteWhere ([] :: [Filter Distribution])
deleteWhere ([] :: [Filter Database.Tables.Text])
Expand Down
Loading