diff --git a/course-matrix/backend/__tests__/getMinHourDay.test.ts b/course-matrix/backend/__tests__/getMinHourDay.test.ts
new file mode 100644
index 00000000..5703db76
--- /dev/null
+++ b/course-matrix/backend/__tests__/getMinHourDay.test.ts
@@ -0,0 +1,193 @@
+import { describe, expect, it, test } from "@jest/globals";
+
+import { Offering } from "../src/types/generatorTypes";
+import {
+ createOffering,
+ getMinHour,
+ getMinHourDay,
+} from "../src/utils/generatorHelpers";
+
+describe("getMinHourDay function", () => {
+ it("Back to back to back courses", async () => {
+ const offering1: Offering = createOffering({
+ id: 1,
+ course_id: 101,
+ day: "MO",
+ start: "09:00:00",
+ end: "10:00:00",
+ });
+ const offering2: Offering = createOffering({
+ id: 2,
+ course_id: 102,
+ day: "MO",
+ start: "10:00:00",
+ end: "11:00:00",
+ });
+ const offering3: Offering = createOffering({
+ id: 3,
+ course_id: 103,
+ day: "MO",
+ start: "11:00:00",
+ end: "12:00:00",
+ });
+ const schedule: Offering[] = [offering1, offering2, offering3];
+
+ const result = getMinHourDay(schedule, 0);
+
+ expect(result).toBe(true);
+ });
+
+ it("courses that has a max gap of 4 hours", async () => {
+ const offering1: Offering = createOffering({
+ id: 1,
+ course_id: 101,
+ day: "MO",
+ start: "09:00:00",
+ end: "10:00:00",
+ });
+ const offering2: Offering = createOffering({
+ id: 2,
+ course_id: 102,
+ day: "MO",
+ start: "10:00:00",
+ end: "11:00:00",
+ });
+ const offering3: Offering = createOffering({
+ id: 3,
+ course_id: 103,
+ day: "MO",
+ start: "15:00:00",
+ end: "16:00:00",
+ });
+ const schedule: Offering[] = [offering3, offering2, offering1];
+
+ const result = getMinHourDay(schedule, 3);
+
+ expect(result).toBe(false);
+ });
+
+ it("only 1 offering in list, return 0", async () => {
+ const offering1: Offering = createOffering({
+ id: 1,
+ course_id: 101,
+ day: "MO",
+ start: "09:00:00",
+ end: "10:00:00",
+ });
+ const schedule: Offering[] = [offering1];
+
+ const result = getMinHourDay(schedule, 23);
+
+ expect(result).toBe(true);
+ });
+
+ it("getMinHour test", async () => {
+ const arr_day = [
+ "MO",
+ "MO",
+ "TU",
+ "TH",
+ "FR",
+ "MO",
+ "TU",
+ "TH",
+ "MO",
+ "MO",
+ ];
+ const arr_start = [
+ "09:00:00",
+ "10:00:00",
+ "09:00:00",
+ "12:00:00",
+ "13:00:00",
+ "12:00:00",
+ "14:00:00",
+ "16:00:00",
+ "13:00:00",
+ "15:00:00",
+ ];
+ const arr_end = [
+ "10:00:00",
+ "11:00:00",
+ "10:00:00",
+ "15:00:00",
+ "16:00:00",
+ "13:00:00",
+ "19:00:00",
+ "18:00:00",
+ "14:00:00",
+ "18:00:00",
+ ];
+ const schedule: Offering[] = [];
+ for (let i = 0; i < 10; i++) {
+ schedule.push(
+ createOffering({
+ id: i,
+ course_id: 100 + i,
+ day: arr_day[i],
+ start: arr_start[i],
+ end: arr_end[i],
+ }),
+ );
+ }
+
+ const result = getMinHour(schedule, 4);
+
+ expect(result).toEqual(true);
+ });
+
+ it("getMinHour test 2", async () => {
+ const arr_day = [
+ "MO",
+ "MO",
+ "TU",
+ "TH",
+ "FR",
+ "MO",
+ "TU",
+ "TH",
+ "MO",
+ "MO",
+ ];
+ const arr_start = [
+ "09:00:00",
+ "10:00:00",
+ "09:00:00",
+ "12:00:00",
+ "13:00:00",
+ "12:00:00",
+ "14:00:00",
+ "16:00:00",
+ "13:00:00",
+ "15:00:00",
+ ];
+ const arr_end = [
+ "10:00:00",
+ "11:00:00",
+ "10:00:00",
+ "15:00:00",
+ "16:00:00",
+ "13:00:00",
+ "19:00:00",
+ "18:00:00",
+ "14:00:00",
+ "18:00:00",
+ ];
+ const schedule: Offering[] = [];
+ for (let i = 0; i < 10; i++) {
+ schedule.push(
+ createOffering({
+ id: i,
+ course_id: 100 + i,
+ day: arr_day[i],
+ start: arr_start[i],
+ end: arr_end[i],
+ }),
+ );
+ }
+
+ const result = getMinHour(schedule, 3);
+
+ expect(result).toEqual(false);
+ });
+});
diff --git a/course-matrix/backend/__tests__/isValidOffering.test.ts b/course-matrix/backend/__tests__/isValidOffering.test.ts
index 70a28cb8..f994bba6 100644
--- a/course-matrix/backend/__tests__/isValidOffering.test.ts
+++ b/course-matrix/backend/__tests__/isValidOffering.test.ts
@@ -1,11 +1,11 @@
import { describe, expect, it, test } from "@jest/globals";
-import { createOffering, isValidOffering } from "../src/utils/generatorHelpers";
import {
Offering,
Restriction,
RestrictionType,
} from "../src/types/generatorTypes";
+import { createOffering, isValidOffering } from "../src/utils/generatorHelpers";
describe("isValidOffering", () => {
const sampleOffering: Offering = createOffering({
@@ -29,6 +29,7 @@ describe("isValidOffering", () => {
endTime: "09:00:00",
disabled: true,
numDays: 0,
+ maxGap: 24,
},
];
expect(isValidOffering(sampleOffering, restrictions)).toBe(true);
@@ -43,6 +44,7 @@ describe("isValidOffering", () => {
endTime: "11:00:00",
disabled: false,
numDays: 0,
+ maxGap: 24,
},
];
expect(isValidOffering(sampleOffering, restrictions)).toBe(false);
@@ -57,6 +59,7 @@ describe("isValidOffering", () => {
endTime: "",
disabled: false,
numDays: 0,
+ maxGap: 24,
},
];
expect(isValidOffering(sampleOffering, restrictions)).toBe(false);
@@ -71,6 +74,7 @@ describe("isValidOffering", () => {
endTime: "12:00:00",
disabled: false,
numDays: 0,
+ maxGap: 24,
},
];
expect(isValidOffering(sampleOffering, restrictions)).toBe(false);
@@ -85,6 +89,7 @@ describe("isValidOffering", () => {
endTime: "",
disabled: false,
numDays: 0,
+ maxGap: 24,
},
];
expect(isValidOffering(sampleOffering, restrictions)).toBe(false);
@@ -99,6 +104,7 @@ describe("isValidOffering", () => {
endTime: "",
disabled: false,
numDays: 0,
+ maxGap: 24,
},
];
expect(isValidOffering(sampleOffering, restrictions)).toBe(true);
diff --git a/course-matrix/backend/src/constants/availableFunctions.ts b/course-matrix/backend/src/constants/availableFunctions.ts
index 0d540011..5ac1f2c4 100644
--- a/course-matrix/backend/src/constants/availableFunctions.ts
+++ b/course-matrix/backend/src/constants/availableFunctions.ts
@@ -15,9 +15,11 @@ import {
categorizeValidOfferings,
getFreq,
getMaxDays,
+ getMaxHour,
getValidOfferings,
groupOfferings,
trim,
+ shuffle,
} from "../utils/generatorHelpers";
// Add all possible function names here
@@ -199,6 +201,7 @@ export const availableFunctions: AvailableFunctions = {
const courseOfferingsList: OfferingList[] = [];
const validCourseOfferingsList: GroupedOfferingList[] = [];
const maxdays = getMaxDays(restrictions);
+ const maxhours = getMaxHour(restrictions);
const validSchedules: Offering[][] = [];
// Fetch offerings for each course
for (const course of courses) {
@@ -244,19 +247,22 @@ export const availableFunctions: AvailableFunctions = {
validCourseOfferingsList.push(groupedOfferings);
}
- const categorizedOfferings = categorizeValidOfferings(
+ let categorizedOfferings = categorizeValidOfferings(
validCourseOfferingsList,
);
+ // console.log(JSON.stringify(categorizedOfferings));
// Generate valid schedules for the given courses and restrictions
- await getValidSchedules(
+ categorizedOfferings = shuffle(categorizedOfferings);
+ getValidSchedules(
validSchedules,
categorizedOfferings,
[],
0,
categorizedOfferings.length,
maxdays,
+ maxhours,
+ false,
);
-
// Return error if no valid schedules are found
if (validSchedules.length === 0) {
return { status: 404, error: "No valid schedules found." };
@@ -445,6 +451,7 @@ ${offeringData.meeting_section} `;
disabled: restriction?.disabled,
num_days: restriction?.numDays,
calendar_id: timetableData?.id,
+ max_gap: restriction?.maxGap,
},
])
.select();
diff --git a/course-matrix/backend/src/controllers/aiController.ts b/course-matrix/backend/src/controllers/aiController.ts
index 3b5c6fe4..92fe16bb 100644
--- a/course-matrix/backend/src/controllers/aiController.ts
+++ b/course-matrix/backend/src/controllers/aiController.ts
@@ -1,7 +1,9 @@
-import asyncHandler from "../middleware/asyncHandler";
import "openai/shims/node";
-import { Request, Response } from "express";
+
import { createOpenAI } from "@ai-sdk/openai";
+import { OpenAIEmbeddings } from "@langchain/openai";
+import { PineconeStore } from "@langchain/pinecone";
+import { Index, Pinecone, RecordMetadata } from "@pinecone-database/pinecone";
import {
CoreMessage,
generateObject,
@@ -11,37 +13,37 @@ import {
tool,
ToolExecutionError,
} from "ai";
-import { Index, Pinecone, RecordMetadata } from "@pinecone-database/pinecone";
-import { PineconeStore } from "@langchain/pinecone";
-import { OpenAIEmbeddings } from "@langchain/openai";
+import { Request, Response } from "express";
import { Document } from "langchain/document";
+import OpenAI from "openai";
+import { z } from "zod";
+
import {
- NAMESPACE_KEYWORDS,
- GENERAL_ACADEMIC_TERMS,
- DEPARTMENT_CODES,
- ASSISTANT_TERMS,
- USEFUL_INFO,
- BREADTH_REQUIREMENT_KEYWORDS,
- YEAR_LEVEL_KEYWORDS,
-} from "../constants/promptKeywords";
+ availableFunctions,
+ FunctionNames,
+} from "../constants/availableFunctions";
import {
CHATBOT_MEMORY_THRESHOLD,
CHATBOT_TIMETABLE_CMD,
CHATBOT_TOOL_CALL_MAX_STEPS,
+ namespaceToMinResults,
} from "../constants/constants";
-import { namespaceToMinResults } from "../constants/constants";
-import OpenAI from "openai";
-import { convertBreadthRequirement } from "../utils/convert-breadth-requirement";
-import { convertYearLevel } from "../utils/convert-year-level";
import {
- availableFunctions,
- FunctionNames,
-} from "../constants/availableFunctions";
-import { z } from "zod";
-import { analyzeQuery } from "../utils/analyzeQuery";
-import { includeFilters } from "../utils/includeFilters";
+ ASSISTANT_TERMS,
+ BREADTH_REQUIREMENT_KEYWORDS,
+ DEPARTMENT_CODES,
+ GENERAL_ACADEMIC_TERMS,
+ NAMESPACE_KEYWORDS,
+ USEFUL_INFO,
+ YEAR_LEVEL_KEYWORDS,
+} from "../constants/promptKeywords";
+import asyncHandler from "../middleware/asyncHandler";
import { TimetableFormSchema } from "../models/timetable-form";
import { CreateTimetableArgs } from "../models/timetable-generate";
+import { analyzeQuery } from "../utils/analyzeQuery";
+import { convertBreadthRequirement } from "../utils/convert-breadth-requirement";
+import { convertYearLevel } from "../utils/convert-year-level";
+import { includeFilters } from "../utils/includeFilters";
const openai = createOpenAI({
baseURL: process.env.OPENAI_BASE_URL,
@@ -84,7 +86,8 @@ export async function searchSelectedNamespaces(
});
try {
- // Search results count given by the min result count for a given namespace (or k if k is greater)
+ // Search results count given by the min result count for a given
+ // namespace (or k if k is greater)
const results = await namespaceStore.similaritySearch(
query,
Math.max(k, namespaceToMinResults.get(namespace)),
@@ -106,7 +109,8 @@ export async function searchSelectedNamespaces(
return allResults;
}
-// Reformulate user query to make more concise query to database, taking into consideration context
+// Reformulate user query to make more concise query to database, taking into
+// consideration context
export async function reformulateQuery(
latestQuery: string,
conversationHistory: any[],
@@ -197,15 +201,21 @@ export async function reformulateQuery(
}
/**
- * @description Handles user queries and generates responses using GPT-4o, with optional knowledge retrieval.
+ * @description Handles user queries and generates responses using GPT-4o, with
+ * optional knowledge retrieval.
*
* @param {Request} req - The Express request object, containing:
- * @param {Object[]} req.body.messages - Array of message objects representing the conversation history.
- * @param {string} req.body.messages[].role - The role of the message sender (e.g., "user", "assistant").
- * @param {Object[]} req.body.messages[].content - An array containing message content objects.
- * @param {string} req.body.messages[].content[].text - The actual text of the message.
+ * @param {Object[]} req.body.messages - Array of message objects representing
+ * the conversation history.
+ * @param {string} req.body.messages[].role - The role of the message sender
+ * (e.g., "user", "assistant").
+ * @param {Object[]} req.body.messages[].content - An array containing message
+ * content objects.
+ * @param {string} req.body.messages[].content[].text - The actual text of the
+ * message.
*
- * @param {Response} res - The Express response object used to stream the generated response.
+ * @param {Response} res - The Express response object used to stream the
+ * generated response.
*
* @returns {void} Responds with a streamed text response of the AI output
*
@@ -251,7 +261,9 @@ export const chat = asyncHandler(async (req: Request, res: Response) => {
## Tool call guidelines
- Include the timetable ID in all getTimetables tool call responses
- - Link: For every tool call, for each timetable that it gets/deletes/modifies/creates, include a link with it displayed as "View Timetable" to ${process.env.CLIENT_APP_URL}/dashboard/timetable?edit=[[TIMETABLE_ID]] , where TIMETABLE_ID is the id of the respective timetable.
+ - Link: For every tool call, for each timetable that it gets/deletes/modifies/creates, include a link with it displayed as "View Timetable" to ${
+ process.env.CLIENT_APP_URL
+ }/dashboard/timetable?edit=[[TIMETABLE_ID]] , where TIMETABLE_ID is the id of the respective timetable.
- If the user provides a course code of length 6 like CSCA08, then assume they mean CSCA08H3 (H3 appended)
- If the user wants to create a timetable, first call getCourses to get course information on the requested courses, then call generateTimetable.
- Do not make up fake courses or offerings.
@@ -299,7 +311,6 @@ export const chat = asyncHandler(async (req: Request, res: Response) => {
"Return a list of possible timetables based on provided courses and restrictions.",
parameters: TimetableFormSchema,
execute: async (args) => {
- console.log("Args for generate: ", args);
console.log("restrictions :", JSON.stringify(args.restrictions));
const data = await availableFunctions.generateTimetable(
args,
@@ -319,7 +330,9 @@ export const chat = asyncHandler(async (req: Request, res: Response) => {
},
}),
},
- maxSteps: CHATBOT_TOOL_CALL_MAX_STEPS, // Controls how many back and forths the model can take with user or calling multiple tools
+ maxSteps: CHATBOT_TOOL_CALL_MAX_STEPS, // Controls how many back and forths
+ // the model can take with user or
+ // calling multiple tools
experimental_repairToolCall: async ({
toolCall,
tools,
@@ -379,7 +392,8 @@ export const chat = asyncHandler(async (req: Request, res: Response) => {
console.log(">>>> Original query:", latestMessage);
console.log(">>>> Reformulated query:", reformulatedQuery);
- // Analyze the query to determine if search is needed and which namespaces to search
+ // Analyze the query to determine if search is needed and which namespaces
+ // to search
const { requiresSearch, relevantNamespaces } =
analyzeQuery(reformulatedQuery);
@@ -469,7 +483,8 @@ export const testSimilaritySearch = asyncHandler(
async (req: Request, res: Response) => {
const { message } = req.body;
- // Analyze the query to determine if search is needed and which namespaces to search
+ // Analyze the query to determine if search is needed and which namespaces
+ // to search
const { requiresSearch, relevantNamespaces } = analyzeQuery(message);
let context = "[No context provided]";
diff --git a/course-matrix/backend/src/controllers/generatorController.ts b/course-matrix/backend/src/controllers/generatorController.ts
index c88b17f9..b998b93b 100644
--- a/course-matrix/backend/src/controllers/generatorController.ts
+++ b/course-matrix/backend/src/controllers/generatorController.ts
@@ -13,9 +13,11 @@ import {
categorizeValidOfferings,
getFreq,
getMaxDays,
+ getMaxHour,
getValidOfferings,
groupOfferings,
trim,
+ shuffle,
} from "../utils/generatorHelpers";
// Express route handler to generate timetables based on user input
@@ -28,6 +30,7 @@ export default {
const courseOfferingsList: OfferingList[] = [];
const validCourseOfferingsList: GroupedOfferingList[] = [];
const maxdays = getMaxDays(restrictions);
+ const maxhours = getMaxHour(restrictions);
const validSchedules: Offering[][] = [];
// Fetch offerings for each course
for (const course of courses) {
@@ -72,10 +75,12 @@ export default {
validCourseOfferingsList.push(groupedOfferings);
}
- const categorizedOfferings = categorizeValidOfferings(
+ let categorizedOfferings = categorizeValidOfferings(
validCourseOfferingsList,
);
// Generate valid schedules for the given courses and restrictions
+ categorizedOfferings = shuffle(categorizedOfferings);
+
await getValidSchedules(
validSchedules,
categorizedOfferings,
@@ -83,6 +88,8 @@ export default {
0,
categorizedOfferings.length,
maxdays,
+ maxhours,
+ false,
);
// Return error if no valid schedules are found
@@ -90,11 +97,11 @@ export default {
return res.status(404).json({ error: "No valid schedules found." });
}
// Return the valid schedules
+ console.log("Total timetables generated", validSchedules.length);
const returnVal = {
amount: validSchedules.length,
schedules: trim(validSchedules),
};
- console.log(returnVal);
return res.status(200).json(returnVal);
} catch (error) {
// Catch any error and return the error message
diff --git a/course-matrix/backend/src/controllers/restrictionsController.ts b/course-matrix/backend/src/controllers/restrictionsController.ts
index 892ee655..1404dab0 100644
--- a/course-matrix/backend/src/controllers/restrictionsController.ts
+++ b/course-matrix/backend/src/controllers/restrictionsController.ts
@@ -1,8 +1,9 @@
import { Request, Response } from "express";
-import asyncHandler from "../middleware/asyncHandler";
-import { supabase } from "../db/setupDb";
import { start } from "repl";
+import { supabase } from "../db/setupDb";
+import asyncHandler from "../middleware/asyncHandler";
+
export default {
/**
* Create, Read, Update and Delete restrictions
@@ -23,18 +24,19 @@ export default {
disabled,
num_days,
calendar_id,
+ max_gap,
} = req.body;
const user_id = (req as any).user.id;
- //Check for calendar_id
+ // Check for calendar_id
if (!calendar_id) {
return res.status(400).json({ error: "calendar id is required" });
}
// Function to construct date in local time
if (
- !["Restrict Day", "Days Off"].includes(type) &&
+ !["Restrict Day", "Days Off", "Max Gap"].includes(type) &&
!start_time &&
!end_time
) {
@@ -43,7 +45,7 @@ export default {
.json({ error: "Start time or end time must be provided" });
}
- //Retrieve users allowed to access the timetable
+ // Retrieve users allowed to access the timetable
const { data: timetableData, error: timetableError } = await supabase
.schema("timetable")
.from("timetables")
@@ -55,14 +57,14 @@ export default {
if (timetableError)
return res.status(400).json({ error: timetableError.message });
- //Validate timetable validity:
+ // Validate timetable validity:
if (!timetableData || timetableData.length === 0) {
return res.status(404).json({ error: "Calendar id not found" });
}
const timetable_user_id = timetableData.user_id;
- //Validate user access
+ // Validate user access
if (user_id !== timetable_user_id) {
return res
.status(401)
@@ -95,6 +97,7 @@ export default {
disabled,
num_days,
calendar_id,
+ max_gap,
},
])
.select();
@@ -119,12 +122,12 @@ export default {
const { calendar_id } = req.params;
const user_id = (req as any).user.id;
- //Check for calendar_id
+ // Check for calendar_id
if (!calendar_id) {
return res.status(400).json({ error: "calendar id is required" });
}
- //Retrieve users allowed to access the timetable
+ // Retrieve users allowed to access the timetable
const { data: timetableData, error: timetableError } = await supabase
.schema("timetable")
.from("timetables")
@@ -136,14 +139,14 @@ export default {
if (timetableError)
return res.status(400).json({ error: timetableError.message });
- //Validate timetable validity:
+ // Validate timetable validity:
if (!timetableData || timetableData.length === 0) {
return res.status(404).json({ error: "Calendar id not found" });
}
const timetable_user_id = timetableData.user_id;
- //Validate user access
+ // Validate user access
if (user_id !== timetable_user_id) {
return res
.status(401)
@@ -179,12 +182,12 @@ export default {
const updateData = req.body;
const user_id = (req as any).user.id;
- //Check for calendar_id
+ // Check for calendar_id
if (!calendar_id) {
return res.status(400).json({ error: "calendar id is required" });
}
- //Check restriction id
+ // Check restriction id
const { data: restrictionCurrData, error: restrictionCurrError } =
await supabase
.schema("timetable")
@@ -202,7 +205,7 @@ export default {
return res.status(404).json({ error: "Restriction id does not exist" });
}
- //Ensure start_time and end_time only contain time value
+ // Ensure start_time and end_time only contain time value
if (updateData.start_time) {
const restriction_start_time = new Date(updateData.start_time);
updateData.start_time = restriction_start_time
@@ -215,7 +218,7 @@ export default {
updateData.end_time = restriction_end_time.toISOString().split("T")[1];
}
- //Retrieve users allowed to access the timetable
+ // Retrieve users allowed to access the timetable
const { data: timetableData, error: timetableError } = await supabase
.schema("timetable")
.from("timetables")
@@ -227,21 +230,21 @@ export default {
if (timetableError)
return res.status(400).json({ error: timetableError.message });
- //Validate timetable validity:
+ // Validate timetable validity:
if (!timetableData || timetableData.length === 0) {
return res.status(404).json({ error: "Calendar id not found" });
}
const timetable_user_id = timetableData.user_id;
- //Validate user access
+ // Validate user access
if (user_id !== timetable_user_id) {
return res
.status(401)
.json({ error: "Unauthorized access to timetable restriction" });
}
- //Validate mapping restriction id to calendar id
+ // Validate mapping restriction id to calendar id
if (restrictionCurrData.calendar_id !== timetableData.id) {
return res.status(400).json({
error: "Restriction id does not belong to the provided calendar id",
@@ -287,12 +290,12 @@ export default {
const { calendar_id } = req.query;
const user_id = (req as any).user.id;
- //Check for calendar_id
+ // Check for calendar_id
if (!calendar_id) {
return res.status(400).json({ error: "calendar id is required" });
}
- //Check restriction id
+ // Check restriction id
const { data: restrictionCurrData, error: restrictionCurrError } =
await supabase
.schema("timetable")
@@ -311,7 +314,7 @@ export default {
return res.status(404).json({ error: "Restriction id does not exist" });
}
- //Retrieve users allowed to access the timetable
+ // Retrieve users allowed to access the timetable
const { data: timetableData, error: timetableError } = await supabase
.schema("timetable")
.from("timetables")
@@ -324,12 +327,12 @@ export default {
return res.status(400).json({ error: timetableError.message });
}
- //Validate timetable validity:
+ // Validate timetable validity:
if (!timetableData || timetableData.length === 0) {
return res.status(404).json({ error: "Calendar id not found" });
}
- //Validate mapping restriction id to calendar id
+ // Validate mapping restriction id to calendar id
if (restrictionCurrData.calendar_id !== timetableData.id) {
return res.status(400).json({
error: "Restriction id does not belong to the provided calendar id",
@@ -338,7 +341,7 @@ export default {
const timetable_user_id = timetableData.user_id;
- //Validate user access
+ // Validate user access
if (user_id !== timetable_user_id) {
return res
.status(401)
diff --git a/course-matrix/backend/src/models/timetable-form.ts b/course-matrix/backend/src/models/timetable-form.ts
index e9810afe..dc8f4b24 100644
--- a/course-matrix/backend/src/models/timetable-form.ts
+++ b/course-matrix/backend/src/models/timetable-form.ts
@@ -5,11 +5,7 @@ export type TimetableForm = {
date: Date;
semester: string;
search: string;
- courses: {
- id: number;
- code: string;
- name: string;
- }[];
+ courses: { id: number; code: string; name: string }[];
restrictions: RestrictionForm[];
};
@@ -17,6 +13,7 @@ export type RestrictionForm = {
type: string;
days?: string[];
numDays?: number;
+ maxGap?: number;
startTime?: string;
endTime?: string;
disabled?: boolean;
@@ -69,6 +66,7 @@ export const RestrictionSchema = z.object({
"Restrict Between",
"Restrict Day",
"Days Off",
+ "Max Gap",
])
.describe(
"The type of restriction being applied. Restrict before restricts all times before 'endTime', Restrict Before restricts all times after 'startTime', Restrict Between restricts all times between 'startTime' and 'endTime', Restrict Day restricts the entirety of each day in field 'days', and Days Off enforces as least 'numDays' days off per week.",
@@ -85,6 +83,14 @@ export const RestrictionSchema = z.object({
.describe(
"If type is Days Off, then this field is used and describes min number of days off per week. For example, if set to 2, and 'type' is Days Off, then this means we want at least 2 days off per week.",
),
+ maxGap: z
+ .number()
+ .positive()
+ .max(23, "Cannot have a gap of an entire day for between courses")
+ .optional()
+ .describe(
+ "If type is Max Gap, then this field is used to describe the maximum gap between courses, in hours. For example, if set to 3, then 2 entires must not be further than 3 hours apart. ",
+ ),
startTime: z
.string()
.optional()
diff --git a/course-matrix/backend/src/services/getValidSchedules.ts b/course-matrix/backend/src/services/getValidSchedules.ts
index 982fd14e..466a879e 100644
--- a/course-matrix/backend/src/services/getValidSchedules.ts
+++ b/course-matrix/backend/src/services/getValidSchedules.ts
@@ -1,23 +1,33 @@
import { CategorizedOfferingList, Offering } from "../types/generatorTypes";
-import { canInsertList, getFrequencyTable } from "../utils/generatorHelpers";
+import {
+ canInsertList,
+ getFrequencyTable,
+ getMinHour,
+} from "../utils/generatorHelpers";
// Function to generate all valid schedules based on offerings and restrictions
-export async function getValidSchedules(
+export function getValidSchedules(
validSchedules: Offering[][],
courseOfferingsList: CategorizedOfferingList[],
curList: Offering[],
cur: number,
len: number,
maxdays: number,
+ maxhours: number,
+ exit: boolean,
) {
// Base case: if all courses have been considered
if (cur == len) {
- const freq: Map
{restric.type}: At least{" "} {restric.numDays} days off
+ ) : ( ++ {restric.type}: {restric.maxGap}{" "} + hours +
)}