Skip to content

Commit b3588fd

Browse files
committed
Merge branch 'develop' into ax/scrum-144-add-integration-tests
2 parents 5e860e6 + e93d766 commit b3588fd

20 files changed

+1659
-364
lines changed

course-matrix/backend/src/controllers/coursesController.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,65 @@ export default {
101101
return res.status(500).send({ err });
102102
}
103103
}),
104+
105+
/**
106+
* Gets the total number of sections for a list of courses.
107+
*
108+
* @param {Request} req - The request object containing query parameters.
109+
* @param {Response} res - The response object to send the total number of sections.
110+
* @returns {Promise<Response>} - The response object with the total number of sections.
111+
*
112+
*/
113+
getNumberOfSections: asyncHandler(async (req: Request, res: Response) => {
114+
try {
115+
const { course_ids, semester } = req.query;
116+
117+
if (!semester) {
118+
return res.status(400).send({ error: "Semester is required" });
119+
}
120+
121+
if (!course_ids) {
122+
return res.status(200).send({ totalNumberOfCourseSections: 0 });
123+
}
124+
125+
const course_ids_array = (course_ids as string).split(",");
126+
127+
let totalNumberOfCourseSections = 0;
128+
const promises = course_ids_array.map(async (course_id) => {
129+
const { data: courseOfferingsData, error: courseOfferingsError } =
130+
await supabase
131+
.schema("course")
132+
.from("offerings")
133+
.select()
134+
.eq("course_id", course_id)
135+
.eq("offering", semester);
136+
137+
const offerings = courseOfferingsData || [];
138+
139+
const hasLectures = offerings.some((offering) =>
140+
offering.meeting_section.startsWith("LEC"),
141+
);
142+
const hasTutorials = offerings.some((offering) =>
143+
offering.meeting_section.startsWith("TUT"),
144+
);
145+
const hasPracticals = offerings.some((offering) =>
146+
offering.meeting_section.startsWith("PRA"),
147+
);
148+
if (hasLectures) {
149+
totalNumberOfCourseSections += 1;
150+
}
151+
if (hasTutorials) {
152+
totalNumberOfCourseSections += 1;
153+
}
154+
if (hasPracticals) {
155+
totalNumberOfCourseSections += 1;
156+
}
157+
});
158+
159+
await Promise.all(promises);
160+
return res.status(200).send({ totalNumberOfCourseSections });
161+
} catch (err) {
162+
return res.status(500).send({ err });
163+
}
164+
}),
104165
};

course-matrix/backend/src/controllers/eventsController.ts

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { start } from "repl";
1616
* @returns An array of event objects ready to be inserted.
1717
*/
1818

19-
function getNextWeekDayOccurance(targetDay: string): string {
19+
export function getNextWeekDayOccurance(targetDay: string): string {
2020
//Map weekday code to JS day number
2121
const weekdayMap: { [key: string]: number } = {
2222
SU: 0,
@@ -45,7 +45,7 @@ function getNextWeekDayOccurance(targetDay: string): string {
4545
return today.toISOString().split("T")[0];
4646
}
4747

48-
function generateWeeklyCourseEvents(
48+
export function generateWeeklyCourseEvents(
4949
user_id: string,
5050
courseEventName: string,
5151
courseDay: string,
@@ -482,20 +482,23 @@ export default {
482482
});
483483
}
484484

485-
const { data: courseEventData, error: courseEventError } =
486-
await supabase
487-
.schema("timetable")
488-
.from("course_events")
489-
.select("*")
490-
.eq("id", id)
491-
.eq("user_id", user_id)
492-
.eq("calendar_id", calendar_id)
493-
.maybeSingle();
494-
495-
if (courseEventData.calendar_id !== timetableData.id) {
496-
return res.status(400).json({
497-
error: "Restriction id does not belong to the provided calendar id",
498-
});
485+
if (!old_offering_id && !new_offering_id) {
486+
const { data: courseEventData, error: courseEventError } =
487+
await supabase
488+
.schema("timetable")
489+
.from("course_events")
490+
.select("*")
491+
.eq("id", id)
492+
.eq("user_id", user_id)
493+
.eq("calendar_id", calendar_id)
494+
.maybeSingle();
495+
496+
if (courseEventData.calendar_id !== timetableData.id) {
497+
return res.status(400).json({
498+
error:
499+
"Restriction id does not belong to the provided calendar id",
500+
});
501+
}
499502
}
500503

501504
const courseEventName = `${newofferingData.code} - ${newofferingData.meeting_section}`;
@@ -714,16 +717,19 @@ export default {
714717
if (courseEventError)
715718
return res.status(400).json({ error: courseEventError.message });
716719

717-
if (!courseEventData || courseEventData.length === 0) {
718-
return res
719-
.status(400)
720-
.json({ error: "Provided note ID is invalid or does not exist" });
721-
}
720+
if (!offering_id) {
721+
if (!courseEventData || courseEventData.length === 0) {
722+
return res
723+
.status(400)
724+
.json({ error: "Provided note ID is invalid or does not exist" });
725+
}
722726

723-
if (courseEventData.calendar_id !== timetableData.id) {
724-
return res.status(400).json({
725-
error: "Restriction id does not belong to the provided calendar id",
726-
});
727+
if (courseEventData.calendar_id !== timetableData.id) {
728+
return res.status(400).json({
729+
error:
730+
"Restriction id does not belong to the provided calendar id",
731+
});
732+
}
727733
}
728734

729735
//Build the delete query
@@ -741,14 +747,13 @@ export default {
741747
deleteQuery = deleteQuery.eq("id", id);
742748
}
743749

744-
const { error: deleteError } = await deleteQuery
750+
const { data: deleteData, error: deleteError } = await deleteQuery
745751
.eq("calendar_id", calendar_id)
746752
.eq("user_id", user_id)
747753
.select("*");
748754
if (deleteError)
749755
return res.status(400).json({ error: deleteError.message });
750-
751-
return res.status(200).send("Event successfully deleted");
756+
return res.status(200).json("Event successfully deleted");
752757
} else if (event_type === "user") {
753758
//Validate note availability
754759
const { data: userEventData, error: userEventError } = await supabase

course-matrix/backend/src/controllers/offeringsController.ts

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,95 @@
11
import { Request, Response } from "express";
22
import asyncHandler from "../middleware/asyncHandler";
33
import { supabase } from "../db/setupDb";
4+
import { generateWeeklyCourseEvents } from "./eventsController";
45

56
export default {
7+
/**
8+
* Get a list of offering events based on offering ids, semester start date, and semester end date.
9+
*
10+
* @param {Request} req - The request object containing query parameters.
11+
* @param {Response} res - The response object to send the offering events data.
12+
* @returns {Promise<Response>} - The response object with the offering events data.
13+
*/
14+
getOfferingEvents: asyncHandler(async (req: Request, res: Response) => {
15+
const { offering_ids, semester_start_date, semester_end_date } = req.query;
16+
17+
// Retrieve the authenticated user
18+
const user_id = (req as any).user.id;
19+
20+
// Check if semester start and end dates are provided
21+
if (!semester_start_date || !semester_end_date) {
22+
return res.status(400).json({
23+
error: "Semester start and end dates are required.",
24+
});
25+
}
26+
27+
if (!offering_ids) {
28+
return res.status(200).json([]); // Return an empty array if no offering_ids are provided
29+
}
30+
31+
const offering_ids_array = (offering_ids as string).split(",");
32+
let eventsToInsert: any[] = [];
33+
34+
const promises = offering_ids_array.map(async (offering_id) => {
35+
// Get the offering data
36+
const { data: offeringData, error: offeringError } = await supabase
37+
.schema("course")
38+
.from("offerings")
39+
.select("*")
40+
.eq("id", offering_id)
41+
.maybeSingle();
42+
43+
if (offeringError) {
44+
return res.status(400).json({ error: offeringError.message });
45+
}
46+
47+
if (!offeringData || offeringData.length === 0) {
48+
return res.status(400).json({
49+
error: "Invalid offering_id or course offering not found.",
50+
});
51+
}
52+
53+
// Generate event details
54+
const courseEventName = ` ${offeringData.code} - ${offeringData.meeting_section} `;
55+
let courseDay = offeringData.day;
56+
let courseStartTime = offeringData.start;
57+
let courseEndTime = offeringData.end;
58+
59+
// Some offerings do not have a day, start time, or end time in the database, so we set default values
60+
if (!courseDay || !courseStartTime || !courseEndTime) {
61+
courseDay = "MO";
62+
courseStartTime = "08:00:00";
63+
courseEndTime = "09:00:00";
64+
}
65+
66+
const mockCalendarId = "1";
67+
const events = generateWeeklyCourseEvents(
68+
user_id,
69+
courseEventName,
70+
courseDay,
71+
courseStartTime,
72+
courseEndTime,
73+
mockCalendarId,
74+
offering_id as string,
75+
semester_start_date as string,
76+
semester_end_date as string,
77+
);
78+
eventsToInsert = [...eventsToInsert, ...events];
79+
});
80+
81+
await Promise.all(promises);
82+
83+
if (eventsToInsert.length === 0) {
84+
return res.status(400).json({
85+
error: "Failed to generate course events",
86+
});
87+
}
88+
89+
// Return the generated events
90+
return res.status(200).json(eventsToInsert);
91+
}),
92+
693
/**
794
* Get a list of offerings based on course code and semester.
895
*
@@ -14,7 +101,21 @@ export default {
14101
try {
15102
const { course_code, semester } = req.query;
16103

17-
let offeringsQuery = supabase
104+
let offeringsQuery;
105+
106+
// If course code or semester is not provided, return all offerings
107+
if (!course_code || !semester) {
108+
offeringsQuery = supabase.schema("course").from("offerings").select();
109+
110+
const { data: offeringsData, error: offeringsError } =
111+
await offeringsQuery;
112+
113+
const offerings = offeringsData || [];
114+
115+
return res.status(200).json(offerings);
116+
}
117+
118+
offeringsQuery = supabase
18119
.schema("course")
19120
.from("offerings")
20121
.select()

course-matrix/backend/src/controllers/restrictionsController.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ export default {
3333
}
3434

3535
// Function to construct date in local time
36-
if (!start_time && !end_time) {
36+
if (
37+
!["Restrict Day", "Days Off"].includes(type) &&
38+
!start_time &&
39+
!end_time
40+
) {
3741
return res
3842
.status(400)
3943
.json({ error: "Start time or end time must be provided" });
@@ -78,12 +82,6 @@ export default {
7882
endTime = restriction_end_time.toISOString().split("T")[1];
7983
}
8084

81-
if (!start_time && !end_time) {
82-
return res
83-
.status(400)
84-
.json({ error: "Start time or end time must be provided" });
85-
}
86-
8785
const { data: restrictionData, error: restrictionError } = await supabase
8886
.schema("timetable")
8987
.from("restriction")
@@ -359,7 +357,9 @@ export default {
359357
return res.status(400).json({ error: restrictionError.message });
360358
}
361359

362-
return res.status(200).send("Restriction successfully deleted");
360+
return res
361+
.status(200)
362+
.json({ message: "Restriction succesfully deleted" });
363363
} catch (error) {
364364
return res.status(500).send({ error });
365365
}

course-matrix/backend/src/routes/courseRouter.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,35 @@ export const offeringsRouter = express.Router();
1616
*/
1717
coursesRouter.get("/", authHandler, coursesController.getCourses);
1818

19+
/**
20+
* Route to get the total number of sections from a list of courses.
21+
* @route GET /total-courses
22+
* @middleware authHandler - Middleware to check if the user is authenticated.
23+
*/
24+
coursesRouter.get(
25+
"/total-sections",
26+
authHandler,
27+
coursesController.getNumberOfSections,
28+
);
29+
1930
/**
2031
* Route to get a list of departments.
2132
* @route GET /
2233
* @middleware authHandler - Middleware to check if the user is authenticated.
2334
*/
2435
departmentsRouter.get("/", authHandler, departmentsController.getDepartments);
2536

37+
/**
38+
* Route to get a list of events for an offering.
39+
* @route GET /events/:offering_id
40+
* @middleware authHandler - Middleware to check if the user is authenticated.
41+
*/
42+
offeringsRouter.get(
43+
"/events",
44+
authHandler,
45+
offeringsController.getOfferingEvents,
46+
);
47+
2648
/**
2749
* Route to get a list of offerings.
2850
* @route GET /

course-matrix/frontend/src/api/baseApiSlice.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ const baseQuery = fetchBaseQuery({ baseUrl: BASE_URL });
55

66
export const apiSlice = createApi({
77
baseQuery,
8-
tagTypes: ["Auth", "Course", "Department", "Offering", "Timetable", "Event"],
8+
tagTypes: [
9+
"Auth",
10+
"Course",
11+
"Department",
12+
"Offering",
13+
"Timetable",
14+
"Event",
15+
"Restrictions",
16+
],
917
endpoints: () => ({}),
1018
});

course-matrix/frontend/src/api/coursesApiSlice.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,21 @@ export const coursesApiSlice = apiSlice.injectEndpoints({
1717
credentials: "include",
1818
}),
1919
}),
20+
getNumberOfCourseSections: builder.query({
21+
query: (params) => ({
22+
url: `${COURSES_URL}/total-sections`,
23+
method: "GET",
24+
params: params,
25+
headers: {
26+
"Content-Type": "application/json",
27+
Accept: "application/json, text/plain, */*",
28+
},
29+
providesTags: ["Course"],
30+
credentials: "include",
31+
}),
32+
}),
2033
}),
2134
});
2235

23-
export const { useGetCoursesQuery } = coursesApiSlice;
36+
export const { useGetCoursesQuery, useGetNumberOfCourseSectionsQuery } =
37+
coursesApiSlice;

0 commit comments

Comments
 (0)