Skip to content

Commit 1afe61e

Browse files
authored
Update Timetable Parsing (#1385)
1 parent 9505c22 commit 1afe61e

File tree

5 files changed

+162
-155
lines changed

5 files changed

+162
-155
lines changed

app/Database/Tables.hs

+47-68
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,16 @@ straightforward.
1919

2020
module Database.Tables where
2121

22-
import Control.Applicative ((<|>))
23-
import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON), Value (..), genericToJSON, withObject,
24-
(.!=), (.:?))
25-
import Data.Aeson.KeyMap (elems)
26-
import Data.Aeson.Types (Options (..), Parser, defaultOptions)
22+
import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON), genericToJSON, withObject,
23+
(.!=), (.:?), (.:))
24+
import Data.Aeson.Types (Options (..), Parser, Value(Object), Value, defaultOptions)
2725
import Data.Char (toLower)
28-
import qualified Data.HashMap.Strict as HM
29-
import Data.Maybe (fromMaybe)
3026
import qualified Data.Text as T
3127
import Data.Time.Clock (UTCTime)
3228
import Database.DataType
3329
import Database.Persist.Sqlite (Key, SqlPersistM, entityVal, selectFirst, (==.))
3430
import Database.Persist.TH
3531
import GHC.Generics
36-
import Text.Read (readMaybe)
37-
import WebParsing.ReqParser (parseReqs)
3832

3933
-- | A two-dimensional point.
4034
type Point = (Double, Double)
@@ -221,27 +215,6 @@ instance ToJSON Location
221215
-- not necessary otherwise.
222216
instance FromJSON SvgJSON
223217

224-
-- JSON encoding/decoding
225-
instance FromJSON Courses where
226-
parseJSON = withObject "Expected Object for Courses" $ \o -> do
227-
newCode <- o .:? "code" .!= "CSC???"
228-
newTitle <- o .:? "courseTitle"
229-
newDescription <- o .:? "courseDescription"
230-
newPrereqString <- o .:? "prerequisite"
231-
let newPrereqs = fmap (T.pack . show . parseReqs . T.unpack) newPrereqString
232-
newExclusions <- o .:? "exclusion"
233-
newCoreqs <- o .:? "corequisite"
234-
return $ Courses newCode
235-
newTitle
236-
newDescription
237-
newPrereqs
238-
newExclusions
239-
Nothing -- breadth
240-
Nothing -- distribution
241-
Nothing -- (Just prereqString)
242-
newCoreqs
243-
[]
244-
245218
instance ToJSON Meeting where
246219
toJSON = genericToJSON defaultOptions {
247220
fieldLabelModifier =
@@ -251,23 +224,16 @@ instance ToJSON Meeting where
251224

252225
instance FromJSON Meeting where
253226
parseJSON = withObject "Expected Object for Lecture, Tutorial or Practical" $ \o -> do
254-
teachingMethod :: T.Text <- o .:? "teachingMethod" .!= ""
227+
teachingMethod :: T.Text <- o .:? "teachMethod" .!= ""
255228
sectionNumber :: T.Text <- o .:? "sectionNumber" .!= ""
256229
let sectionId = T.concat [teachingMethod, sectionNumber]
257230

258-
capStr <- o .:? "enrollmentCapacity" .!= "-1"
259-
enrolStr <- o .:? "actualEnrolment" .!= "0"
260-
waitStr <- o .:? "actualWaitlist" .!= "0"
261-
let cap = fromMaybe (-1) $ readMaybe capStr
262-
enrol = fromMaybe 0 $ readMaybe enrolStr
263-
wait = fromMaybe 0 $ readMaybe waitStr
264-
instrMap2 :: Value <- o .:? "instructors" .!= Null
265-
let instrList =
266-
case instrMap2 of
267-
Object obj -> elems obj
268-
_ -> []
269-
231+
cap <- o .:? "maxEnrolment" .!= (-1)
232+
enrol <- o .:? "currentEnrolment" .!= 0
233+
wait <- o .:? "currentWaitlist" .!= 0
234+
instrList <- o .:? "instructors" .!= []
270235
instrs <- mapM parseInstr instrList
236+
271237
let extra = 0
272238
let instructor = T.intercalate "; " $ filter (not . T.null) instrs
273239
if teachingMethod == "LEC" || teachingMethod == "TUT" || teachingMethod == "PRA"
@@ -278,19 +244,26 @@ instance FromJSON Meeting where
278244

279245
instance FromJSON Time' where
280246
parseJSON = withObject "Expected Object for Times" $ \o -> do
281-
meetingDayStr <- o .:? "meetingDay"
282-
meetingStartTimeStr <- o .:? "meetingStartTime"
283-
meetingEndTimeStr <- o .:? "meetingEndTime"
284-
meetingRoom1 <- o .:? "assignedRoom1" .!= Nothing
247+
startObject <- o .: "start"
248+
endObject <- o .: "end"
249+
meetingDay :: Maybe Int <- startObject .:? "day" .!= Nothing
250+
meetingStartTime :: Maybe Int <- startObject .:? "millisofday" .!= Nothing
251+
meetingEndTime :: Maybe Int <- endObject .:? "millisofday" .!= Nothing
252+
253+
building <- o .: "building"
254+
buildingCode <- building .: "buildingCode"
255+
buildingRoomNumber <- building .: "buildingRoomNumber"
256+
let meetingRoom1 = Just (T.concat [buildingCode, buildingRoomNumber])
285257
meetingRoom2 <- o .:? "assignedRoom2" .!= Nothing
286-
let (meetingDay, meetingStartTime, meetingEndTime) = getTimeVals meetingDayStr meetingStartTimeStr meetingEndTimeStr
287-
return $ Time' meetingDay meetingStartTime meetingEndTime meetingRoom1 meetingRoom2
258+
259+
let (adjustedDay, adjustedStartTime, adjustedEndTime) = convertTimeVals meetingDay meetingStartTime meetingEndTime
260+
return $ Time' adjustedDay adjustedStartTime adjustedEndTime meetingRoom1 meetingRoom2
288261

289262
instance FromJSON MeetTime where
290263
parseJSON (Object o) = do
291264
meeting <- parseJSON (Object o)
292-
timeMap :: HM.HashMap T.Text Time' <- o .:? "schedule" .!= HM.empty <|> return HM.empty
293-
return $ MeetTime meeting (HM.elems timeMap)
265+
timesList :: [Time'] <- o .:? "meetingTimes" .!= []
266+
return $ MeetTime meeting timesList
294267
parseJSON _ = fail "Invalid meeting"
295268

296269
-- | Helpers for parsing JSON
@@ -301,30 +274,36 @@ parseInstr (Object io) = do
301274
return (T.concat [firstName, ". ", lastName])
302275
parseInstr _ = return ""
303276

304-
-- | Converts 24-hour time into a double
277+
-- | Converts the miliseconds time into hourly time
305278
-- | Assumes times are rounded to the nearest hour
306-
getHourVal :: String -> Double
307-
getHourVal time = (read $ take 2 time :: Double) + (/) (read $ drop 3 time :: Double) 60
308-
309-
-- | Converts a weekday into a double
310-
-- | Monday to Friday becomes 0.0 to 4.0
311-
getDayVal :: String -> Double
312-
getDayVal "MO" = 0.0
313-
getDayVal "TU" = 1.0
314-
getDayVal "WE" = 2.0
315-
getDayVal "TH" = 3.0
316-
getDayVal "FR" = 4.0
317-
getDayVal _ = 4.0
279+
getHourVal :: Int -> Double
280+
getHourVal millis =
281+
let
282+
seconds = fromIntegral millis / 1000.0
283+
minutes = seconds / 60
284+
hours = minutes / 60
285+
in
286+
hours
287+
288+
-- | Converts a the given day into a double representation for the database
289+
-- | Monday (1) to Friday (5) becomes 0.0 to 4.0
290+
getDayVal :: Int -> Double
291+
getDayVal 1 = 0.0
292+
getDayVal 2 = 1.0
293+
getDayVal 3 = 2.0
294+
getDayVal 4 = 3.0
295+
getDayVal 5 = 4.0
296+
getDayVal _ = 4.0
318297

319298
-- | Convert the given day, start time and end time to a tuple of Doubles. If nothing is given,
320299
-- the place holder is 5 and 25, indicating the day and times are invalid.
321-
getTimeVals :: Maybe String -> Maybe String -> Maybe String -> (Double, Double, Double)
322-
getTimeVals (Just day) (Just start) (Just end) = do
300+
convertTimeVals :: Maybe Int -> Maybe Int -> Maybe Int -> (Double, Double, Double)
301+
convertTimeVals (Just day) (Just start) (Just end) =
323302
let dayDbl = getDayVal day
324303
startDbl = getHourVal start
325304
endDbl = getHourVal end
326-
(dayDbl, startDbl, endDbl)
327-
getTimeVals _ _ _ = (5.0, 25.0, 25.0)
305+
in (dayDbl, startDbl, endDbl)
306+
convertTimeVals _ _ _ = (5.0, 25.0, 25.0)
328307

329308
-- | Convert Times into Time
330309
buildTime :: Times -> SqlPersistM Time

app/DevelopmentConfig.hs

+45-15
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ module Config (
1414
genCssPath,
1515
timetableUrl,
1616
timetableApiUrl,
17-
orgApiUrl,
1817
fasCalendarUrl,
1918
programsUrl,
19+
createReqBody,
20+
reqHeaders,
2021
fallStartDate,
2122
fallEndDate,
2223
winterStartDate,
@@ -25,10 +26,13 @@ module Config (
2526
holidays
2627
) where
2728

29+
import Data.Aeson
2830
import Data.Text (Text)
31+
import qualified Data.Text as T
2932
import Data.Time (Day, fromGregorian)
3033
import Happstack.Server (Conf (..), LogAccess, nullConf)
3134
import System.Log.Logger (Priority (INFO), logM)
35+
import Network.HTTP.Types.Header (RequestHeaders)
3236

3337
-- SERVER CONFIGURATION
3438

@@ -76,15 +80,11 @@ genCssPath = "./public/style/"
7680

7781
-- | The URL for U of T's official timetable.
7882
timetableUrl :: String
79-
timetableUrl = "https://timetable.iit.artsci.utoronto.ca/"
83+
timetableUrl = "https://ttb.utoronto.ca/"
8084

8185
-- | The Faculty of Arts and Science API for course timetables (by unit).
8286
timetableApiUrl :: Text
83-
timetableApiUrl = "https://timetable.iit.artsci.utoronto.ca/api/20229/courses?org="
84-
85-
-- | The Faculty of Arts and Science API for a list of all units.
86-
orgApiUrl :: String
87-
orgApiUrl = "https://timetable.iit.artsci.utoronto.ca/api/orgs"
87+
timetableApiUrl = "https://api.easi.utoronto.ca/ttb/getPageableCourses"
8888

8989
-- | The URLs of the Faculty of Arts & Science calendar.
9090
fasCalendarUrl :: String
@@ -93,31 +93,61 @@ fasCalendarUrl = "https://artsci.calendar.utoronto.ca/"
9393
programsUrl :: String
9494
programsUrl = "https://artsci.calendar.utoronto.ca/listing-program-subject-areas"
9595

96+
-- HTTP REQUEST STRINGS
97+
98+
-- | Create the body for the HTTP request based on the org
99+
createReqBody :: Text -> Value
100+
createReqBody org = object [ "campuses" .= ([] :: [T.Text]),
101+
"courseCodeAndTitleProps" .= object
102+
[ "courseCode" .= ("" :: T.Text),
103+
"courseSectionCode" .= ("" :: T.Text),
104+
"courseTitle" .= org,
105+
"searchCourseDescription" .= True
106+
],
107+
"courseLevels" .= ([] :: [T.Text]),
108+
"creditWeights" .= ([] :: [T.Text]),
109+
"dayPreferences" .= ([] :: [T.Text]),
110+
"deliveryModes" .= ([] :: [T.Text]),
111+
"departmentProps" .= ([] :: [T.Text]),
112+
"direction" .= ("asc" :: T.Text),
113+
"divisions" .= [T.pack "ARTSC"],
114+
"instructor" .= ("" :: T.Text),
115+
"page" .= (1 :: Int),
116+
"pageSize" .= (200 :: Int),
117+
"requirementProps" .= ([] :: [T.Text]),
118+
"sessions" .= [T.pack "20239", T.pack "20241", T.pack "20239-20241"],
119+
"timePreferences" .= ([] :: [T.Text])
120+
]
121+
122+
-- | The headers for the HTTP request
123+
reqHeaders :: RequestHeaders
124+
reqHeaders = [("Content-Type", "application/json"), ("Accept", "application/json")]
125+
96126
-- CALENDAR RESPONSE DATES
97127

98128
-- | First day of classes for the fall term.
99129
fallStartDate :: Day
100-
fallStartDate = fromGregorian 2022 09 08
130+
fallStartDate = fromGregorian 2023 09 07
101131

102132
-- | Last day of classes for the fall term.
103133
fallEndDate :: Day
104-
fallEndDate = fromGregorian 2022 12 07
134+
fallEndDate = fromGregorian 2023 12 06
105135

106136
-- | First day of classes for the winter term.
107137
winterStartDate :: Day
108-
winterStartDate = fromGregorian 2023 01 09
138+
winterStartDate = fromGregorian 2024 01 08
109139

110140
-- | Last day of classes for the winter term.
111141
winterEndDate :: Day
112-
winterEndDate = fromGregorian 2023 04 06
142+
winterEndDate = fromGregorian 2024 04 05
113143

114144
-- | Out of date day. Used to control forbidden inputs for days.
115145
outDay :: Day
116146
outDay = fromGregorian 2024 01 01
117147

118148
-- Holidays for the fall and winter term.
119149
holidays :: [String]
120-
holidays = ["20221010T", "20221107T", "20221108T",
121-
"20221109T", "20221110T", "20221111T",
122-
"20230220T", "20230221T", "20230222T",
123-
"20200223T", "20230224T"]
150+
holidays = ["20231009T", "20231106T", "20231107T",
151+
"20231108T", "20231109T", "20231110T",
152+
"20240219T", "20240220T", "20230221T",
153+
"20240222T", "20240223T"]

0 commit comments

Comments
 (0)