diff --git a/Management/Models/Local/Quiz/LocalQuizQuestion.cs b/Management/Models/Local/Quiz/LocalQuizQuestion.cs index ef7b7b1..cdf1631 100644 --- a/Management/Models/Local/Quiz/LocalQuizQuestion.cs +++ b/Management/Models/Local/Quiz/LocalQuizQuestion.cs @@ -1,4 +1,3 @@ -using System.Security.Cryptography; using System.Text.RegularExpressions; using Akka.Util.Internal; diff --git a/nextjs/src/models/local/assignmnet/localAssignmentGroup.ts b/nextjs/src/models/local/assignmnet/localAssignmentGroup.ts new file mode 100644 index 0000000..f83201c --- /dev/null +++ b/nextjs/src/models/local/assignmnet/localAssignmentGroup.ts @@ -0,0 +1,6 @@ +export interface LocalAssignmentGroup { + canvasId?: number; + id: string; + name: string; + weight: number; +} \ No newline at end of file diff --git a/nextjs/src/models/local/assignmnet/utils/assignmentMarkdownParser.ts b/nextjs/src/models/local/assignmnet/utils/assignmentMarkdownParser.ts index 0def561..4b037c4 100644 --- a/nextjs/src/models/local/assignmnet/utils/assignmentMarkdownParser.ts +++ b/nextjs/src/models/local/assignmnet/utils/assignmentMarkdownParser.ts @@ -51,8 +51,8 @@ const parseSettings = (input: string) => { const submissionTypes = parseSubmissionTypes(input); const fileUploadExtensions = parseFileUploadExtensions(input); - const dueAt = timeUtils.parseDateOrThrow(rawDueAt, "DueAt"); - const lockAt = timeUtils.parseDateOrUndefined(rawLockAt); + const dueAt = timeUtils.verifyDateOrThrow(rawDueAt, "DueAt"); + const lockAt = timeUtils.verifyDateStringOrUndefined(rawLockAt); return { name, diff --git a/nextjs/src/models/local/localCourse.ts b/nextjs/src/models/local/localCourse.ts new file mode 100644 index 0000000..dc7dbd7 --- /dev/null +++ b/nextjs/src/models/local/localCourse.ts @@ -0,0 +1,45 @@ +import { LocalAssignmentGroup } from "./assignmnet/localAssignmentGroup"; +import { LocalModule } from "./localModules"; + +export interface LocalCourse { + modules: LocalModule[]; + settings: LocalCourseSettings; +} + +export interface SimpleTimeOnly { + hour: number; + minute: number; +} + + + +export interface LocalCourseSettings { + assignmentGroups: LocalAssignmentGroup[]; + daysOfWeek: DayOfWeek[]; + canvasId?: number; + startDate: string; + endDate: string; + defaultDueTime: SimpleTimeOnly; +} + +export enum DayOfWeek { + Sunday = "Sunday", + Monday = "Monday", + Tuesday = "Tuesday", + Wednesday = "Wednesday", + Thursday = "Thursday", + Friday = "Friday", + Saturday = "Saturday", +} + + +// export const LocalCourseSettingsUtils = { +// toYaml(settings: LocalCourseSettings): string { +// return dump(settings, { noRefs: true }); +// }, + +// parseYaml(rawText: string): LocalCourseSettings { +// const settings = load(rawText) as LocalCourseSettings; +// return createLocalCourseSettings(settings); +// }, +// }; \ No newline at end of file diff --git a/nextjs/src/models/local/localModules.ts b/nextjs/src/models/local/localModules.ts new file mode 100644 index 0000000..206b0c0 --- /dev/null +++ b/nextjs/src/models/local/localModules.ts @@ -0,0 +1,75 @@ +import { LocalAssignment } from "./assignmnet/localAssignment"; +import { IModuleItem } from "./IModuleItem"; +import { LocalCoursePage } from "./page/localCoursePage"; +import { LocalQuiz } from "./quiz/localQuiz"; +import { getDateFromString } from "./timeUtils"; + +export interface LocalModule { + name: string; + assignments: LocalAssignment[]; + quizzes: LocalQuiz[]; + pages: LocalCoursePage[]; +} + +export const LocalModuleUtils = { + getSortedModuleItems(module: LocalModule): IModuleItem[] { + return [...module.assignments, ...module.quizzes, ...module.pages].sort( + (a, b) => + (getDateFromString(a.dueAt)?.getTime() ?? 0) - + (getDateFromString(b.dueAt)?.getTime() ?? 0) + ); + }, + + equals(module1: LocalModule, module2: LocalModule): boolean { + return ( + module1.name.toLowerCase() === module2.name.toLowerCase() && + LocalModuleUtils.compareCollections( + module1.assignments.sort((a, b) => a.name.localeCompare(b.name)), + module2.assignments.sort((a, b) => a.name.localeCompare(b.name)) + ) && + LocalModuleUtils.compareCollections( + module1.quizzes.sort((a, b) => a.name.localeCompare(b.name)), + module2.quizzes.sort((a, b) => a.name.localeCompare(b.name)) + ) && + LocalModuleUtils.compareCollections( + module1.pages.sort((a, b) => a.name.localeCompare(b.name)), + module2.pages.sort((a, b) => a.name.localeCompare(b.name)) + ) + ); + }, + + compareCollections(first: T[], second: T[]): boolean { + if (first.length !== second.length) return false; + + for (let i = 0; i < first.length; i++) { + if (JSON.stringify(first[i]) !== JSON.stringify(second[i])) return false; + } + + return true; + }, + + getHashCode(module: LocalModule): number { + const hash = new Map(); + hash.set(module.name.toLowerCase(), 1); + LocalModuleUtils.addRangeToHash( + hash, + module.assignments.sort((a, b) => a.name.localeCompare(b.name)) + ); + LocalModuleUtils.addRangeToHash( + hash, + module.quizzes.sort((a, b) => a.name.localeCompare(b.name)) + ); + LocalModuleUtils.addRangeToHash( + hash, + module.pages.sort((a, b) => a.name.localeCompare(b.name)) + ); + + return Array.from(hash.values()).reduce((acc, val) => acc + val, 0); + }, + + addRangeToHash(hash: Map, items: T[]): void { + for (const item of items) { + hash.set(JSON.stringify(item), 1); + } + }, +}; diff --git a/nextjs/src/models/local/quiz/utils/quizMarkdownUtils.ts b/nextjs/src/models/local/quiz/utils/quizMarkdownUtils.ts index 51ed193..76cc00c 100644 --- a/nextjs/src/models/local/quiz/utils/quizMarkdownUtils.ts +++ b/nextjs/src/models/local/quiz/utils/quizMarkdownUtils.ts @@ -1,4 +1,4 @@ -import { timeUtils } from "../../timeUtils"; +import { verifyDateOrThrow, verifyDateStringOrUndefined } from "../../timeUtils"; import { LocalQuiz } from "../localQuiz"; import { quizQuestionMarkdownUtils } from "./quizQuestionMarkdownUtils"; @@ -74,10 +74,10 @@ const getQuizWithOnlySettings = (settings: string): LocalQuiz => { ); const rawDueAt = extractLabelValue(settings, "DueAt"); - const dueAt = timeUtils.parseDateOrThrow(rawDueAt, "DueAt"); + const dueAt = verifyDateOrThrow(rawDueAt, "DueAt"); const rawLockAt = extractLabelValue(settings, "LockAt"); - const lockAt = timeUtils.parseDateOrUndefined(rawLockAt); + const lockAt = verifyDateStringOrUndefined(rawLockAt); const description = extractDescription(settings); const localAssignmentGroupName = extractLabelValue( diff --git a/nextjs/src/models/local/tests/markdown/assignmentMarkdown.test.ts b/nextjs/src/models/local/tests/assignmentMarkdown.test.ts similarity index 93% rename from nextjs/src/models/local/tests/markdown/assignmentMarkdown.test.ts rename to nextjs/src/models/local/tests/assignmentMarkdown.test.ts index 01efdc6..ea6afd8 100644 --- a/nextjs/src/models/local/tests/markdown/assignmentMarkdown.test.ts +++ b/nextjs/src/models/local/tests/assignmentMarkdown.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect } from "vitest"; -import { LocalAssignment } from "../../assignmnet/localAssignment"; -import { AssignmentSubmissionType } from "../../assignmnet/assignmentSubmissionType"; -import { assignmentMarkdownSerializer } from "../../assignmnet/utils/assignmentMarkdownSerializer"; -import { assignmentMarkdownParser } from "../../assignmnet/utils/assignmentMarkdownParser"; +import { LocalAssignment } from "../assignmnet/localAssignment"; +import { AssignmentSubmissionType } from "../assignmnet/assignmentSubmissionType"; +import { assignmentMarkdownSerializer } from "../assignmnet/utils/assignmentMarkdownSerializer"; +import { assignmentMarkdownParser } from "../assignmnet/utils/assignmentMarkdownParser"; describe("AssignmentMarkdownTests", () => { it("can parse assignment settings", () => { diff --git a/nextjs/src/models/local/tests/markdown/pageMarkdown.test.ts b/nextjs/src/models/local/tests/pageMarkdown.test.ts similarity index 77% rename from nextjs/src/models/local/tests/markdown/pageMarkdown.test.ts rename to nextjs/src/models/local/tests/pageMarkdown.test.ts index 6a63ab0..bc5da5c 100644 --- a/nextjs/src/models/local/tests/markdown/pageMarkdown.test.ts +++ b/nextjs/src/models/local/tests/pageMarkdown.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; -import { LocalCoursePage } from "../../page/localCoursePage"; -import { pageMarkdownUtils } from "../../page/pageMarkdownUtils"; +import { LocalCoursePage } from "../page/localCoursePage"; +import { pageMarkdownUtils } from "../page/pageMarkdownUtils"; describe("PageMarkdownTests", () => { it("can parse page", () => { diff --git a/nextjs/src/models/local/tests/markdown/quiz/matchingAnswers.test.ts b/nextjs/src/models/local/tests/quizMarkdown/matchingAnswers.test.ts similarity index 97% rename from nextjs/src/models/local/tests/markdown/quiz/matchingAnswers.test.ts rename to nextjs/src/models/local/tests/quizMarkdown/matchingAnswers.test.ts index 7325537..d8c0351 100644 --- a/nextjs/src/models/local/tests/markdown/quiz/matchingAnswers.test.ts +++ b/nextjs/src/models/local/tests/quizMarkdown/matchingAnswers.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { QuestionType } from "../../../../../models/local/quiz/localQuizQuestion"; +import { QuestionType } from "../../quiz/localQuizQuestion"; import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils"; import { quizQuestionMarkdownUtils } from "@/models/local/quiz/utils/quizQuestionMarkdownUtils"; diff --git a/nextjs/src/models/local/tests/markdown/quiz/multipleAnswers.test.ts b/nextjs/src/models/local/tests/quizMarkdown/multipleAnswers.test.ts similarity index 94% rename from nextjs/src/models/local/tests/markdown/quiz/multipleAnswers.test.ts rename to nextjs/src/models/local/tests/quizMarkdown/multipleAnswers.test.ts index ffaf836..23acdc6 100644 --- a/nextjs/src/models/local/tests/markdown/quiz/multipleAnswers.test.ts +++ b/nextjs/src/models/local/tests/quizMarkdown/multipleAnswers.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; -import { LocalQuiz } from "../../../../../models/local/quiz/localQuiz"; -import { QuestionType } from "../../../../../models/local/quiz/localQuizQuestion"; +import { LocalQuiz } from "../../quiz/localQuiz"; +import { QuestionType } from "../../quiz/localQuizQuestion"; import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils"; import { quizQuestionMarkdownUtils } from "@/models/local/quiz/utils/quizQuestionMarkdownUtils"; @@ -26,7 +26,7 @@ describe("MultipleAnswersTests", () => { { correct: true, text: "false" }, { correct: false, text: "neither" }, ], - matchDistractors: [] + matchDistractors: [], }, ], }; diff --git a/nextjs/src/models/local/tests/markdown/quiz/multipleChoice.test.ts b/nextjs/src/models/local/tests/quizMarkdown/multipleChoice.test.ts similarity index 83% rename from nextjs/src/models/local/tests/markdown/quiz/multipleChoice.test.ts rename to nextjs/src/models/local/tests/quizMarkdown/multipleChoice.test.ts index 1ebe12f..8bb6584 100644 --- a/nextjs/src/models/local/tests/markdown/quiz/multipleChoice.test.ts +++ b/nextjs/src/models/local/tests/quizMarkdown/multipleChoice.test.ts @@ -1,10 +1,7 @@ import { describe, it, expect } from "vitest"; -import { LocalQuiz } from "../../../../../models/local/quiz/localQuiz"; -import { - LocalQuizQuestion, - QuestionType, -} from "../../../../../models/local/quiz/localQuizQuestion"; -import { LocalQuizQuestionAnswer } from "../../../../../models/local/quiz/localQuizQuestionAnswer"; +import { LocalQuiz } from "../../quiz/localQuiz"; +import { LocalQuizQuestion, QuestionType } from "../../quiz/localQuizQuestion"; +import { LocalQuizQuestionAnswer } from "../../quiz/localQuizQuestionAnswer"; import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils"; import { quizQuestionMarkdownUtils } from "@/models/local/quiz/utils/quizQuestionMarkdownUtils"; @@ -37,7 +34,7 @@ lines { correct: true, text: "true" }, { correct: false, text: "false\n\nendline" }, ], - matchDistractors: [] + matchDistractors: [], }, ], }; diff --git a/nextjs/src/models/local/tests/markdown/quiz/quizDeterministicChecks.test.ts b/nextjs/src/models/local/tests/quizMarkdown/quizDeterministicChecks.test.ts similarity index 96% rename from nextjs/src/models/local/tests/markdown/quiz/quizDeterministicChecks.test.ts rename to nextjs/src/models/local/tests/quizMarkdown/quizDeterministicChecks.test.ts index 479a8b2..eab3500 100644 --- a/nextjs/src/models/local/tests/markdown/quiz/quizDeterministicChecks.test.ts +++ b/nextjs/src/models/local/tests/quizMarkdown/quizDeterministicChecks.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; -import { LocalQuiz } from "../../../../../models/local/quiz/localQuiz"; -import { quizMarkdownUtils } from "../../../../../models/local/quiz/utils/quizMarkdownUtils"; +import { LocalQuiz } from "../../quiz/localQuiz"; +import { quizMarkdownUtils } from "../../quiz/utils/quizMarkdownUtils"; import { QuestionType } from "@/models/local/quiz/localQuizQuestion"; // Test suite for deterministic checks on LocalQuiz @@ -88,7 +88,7 @@ describe("QuizDeterministicChecks", () => { questionType: QuestionType.ESSAY, points: 1, matchDistractors: [], - answers: [] + answers: [], }, ], allowedAttempts: -1, diff --git a/nextjs/src/models/local/tests/markdown/quiz/quizMarkdown.test.ts b/nextjs/src/models/local/tests/quizMarkdown/quizMarkdown.test.ts similarity index 97% rename from nextjs/src/models/local/tests/markdown/quiz/quizMarkdown.test.ts rename to nextjs/src/models/local/tests/quizMarkdown/quizMarkdown.test.ts index 834bce8..44afcb4 100644 --- a/nextjs/src/models/local/tests/markdown/quiz/quizMarkdown.test.ts +++ b/nextjs/src/models/local/tests/quizMarkdown/quizMarkdown.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; -import { LocalQuiz } from "../../../../../models/local/quiz/localQuiz"; -import { quizMarkdownUtils } from "../../../../../models/local/quiz/utils/quizMarkdownUtils"; +import { LocalQuiz } from "../../quiz/localQuiz"; +import { quizMarkdownUtils } from "../../quiz/utils/quizMarkdownUtils"; import { QuestionType } from "@/models/local/quiz/localQuizQuestion"; import { quizQuestionMarkdownUtils } from "@/models/local/quiz/utils/quizQuestionMarkdownUtils"; diff --git a/nextjs/src/models/local/tests/markdown/quiz/testAnswer.test.ts b/nextjs/src/models/local/tests/quizMarkdown/testAnswer.test.ts similarity index 74% rename from nextjs/src/models/local/tests/markdown/quiz/testAnswer.test.ts rename to nextjs/src/models/local/tests/quizMarkdown/testAnswer.test.ts index a97b709..b9b37de 100644 --- a/nextjs/src/models/local/tests/markdown/quiz/testAnswer.test.ts +++ b/nextjs/src/models/local/tests/quizMarkdown/testAnswer.test.ts @@ -1,10 +1,10 @@ -import { QuestionType } from '../../../../../models/local/quiz/localQuizQuestion'; -import { quizMarkdownUtils } from '../../../../../models/local/quiz/utils/quizMarkdownUtils'; -import { quizQuestionMarkdownUtils } from '../../../../../models/local/quiz/utils/quizQuestionMarkdownUtils'; -import { describe, it, expect } from 'vitest'; +import { QuestionType } from "../../quiz/localQuizQuestion"; +import { quizMarkdownUtils } from "../../quiz/utils/quizMarkdownUtils"; +import { quizQuestionMarkdownUtils } from "../../quiz/utils/quizQuestionMarkdownUtils"; +import { describe, it, expect } from "vitest"; -describe('TextAnswerTests', () => { - it('can parse essay', () => { +describe("TextAnswerTests", () => { + it("can parse essay", () => { const rawMarkdownQuiz = ` Name: Test Quiz ShuffleAnswers: true @@ -26,10 +26,10 @@ essay expect(firstQuestion.points).toBe(1); expect(firstQuestion.questionType).toBe(QuestionType.ESSAY); - expect(firstQuestion.text).not.toContain('essay'); + expect(firstQuestion.text).not.toContain("essay"); }); - it('can parse short answer', () => { + it("can parse short answer", () => { const rawMarkdownQuiz = ` Name: Test Quiz ShuffleAnswers: true @@ -51,10 +51,10 @@ short answer expect(firstQuestion.points).toBe(1); expect(firstQuestion.questionType).toBe(QuestionType.SHORT_ANSWER); - expect(firstQuestion.text).not.toContain('short answer'); + expect(firstQuestion.text).not.toContain("short answer"); }); - it('short answer to markdown is correct', () => { + it("short answer to markdown is correct", () => { const rawMarkdownQuiz = ` Name: Test Quiz ShuffleAnswers: true @@ -74,14 +74,15 @@ short answer const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz); const firstQuestion = quiz.questions[0]; - const questionMarkdown = quizQuestionMarkdownUtils.toMarkdown(firstQuestion); + const questionMarkdown = + quizQuestionMarkdownUtils.toMarkdown(firstQuestion); const expectedMarkdown = `Points: 1 Which events are triggered when the user clicks on an input field? short_answer`; expect(questionMarkdown).toContain(expectedMarkdown); }); - it('essay question to markdown is correct', () => { + it("essay question to markdown is correct", () => { const rawMarkdownQuiz = ` Name: Test Quiz ShuffleAnswers: true @@ -101,7 +102,8 @@ essay const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz); const firstQuestion = quiz.questions[0]; - const questionMarkdown = quizQuestionMarkdownUtils.toMarkdown(firstQuestion); + const questionMarkdown = + quizQuestionMarkdownUtils.toMarkdown(firstQuestion); const expectedMarkdown = `Points: 1 Which events are triggered when the user clicks on an input field? essay`; diff --git a/nextjs/src/models/local/tests/markdown/rubricMarkdown.test.ts b/nextjs/src/models/local/tests/rubricMarkdown.test.ts similarity index 93% rename from nextjs/src/models/local/tests/markdown/rubricMarkdown.test.ts rename to nextjs/src/models/local/tests/rubricMarkdown.test.ts index 84d8fe9..8fc6543 100644 --- a/nextjs/src/models/local/tests/markdown/rubricMarkdown.test.ts +++ b/nextjs/src/models/local/tests/rubricMarkdown.test.ts @@ -1,9 +1,6 @@ import { describe, it, expect } from "vitest"; -import { - RubricItem, - rubricItemIsExtraCredit, -} from "../../assignmnet/rubricItem"; -import { assignmentMarkdownParser } from "../../assignmnet/utils/assignmentMarkdownParser"; +import { RubricItem, rubricItemIsExtraCredit } from "../assignmnet/rubricItem"; +import { assignmentMarkdownParser } from "../assignmnet/utils/assignmentMarkdownParser"; describe("RubricMarkdownTests", () => { it("can parse one item", () => { diff --git a/nextjs/src/models/local/timeUtils.ts b/nextjs/src/models/local/timeUtils.ts index 941774a..ec17d00 100644 --- a/nextjs/src/models/local/timeUtils.ts +++ b/nextjs/src/models/local/timeUtils.ts @@ -1,12 +1,13 @@ -const parseDateOrUndefined = (value: string): string | undefined => { + +export const getDateFromString = (value: string) => { // may need to check for other formats - const validDateRegex = /([1-9][1-9]|[0-2])\/(0[1-9]|[1-2][0-9]|3[01])\/\d{4} (0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])/; + const validDateRegex = + /([1-9][1-9]|[0-2])\/(0[1-9]|[1-2][0-9]|3[01])\/\d{4} (0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])/; if (!validDateRegex.test(value)) { return undefined; } - const [datePart, timePart] = value.split(" "); const [day, month, year] = datePart.split("/").map(Number); const [hours, minutes, seconds] = timePart.split(":").map(Number); @@ -15,6 +16,26 @@ const parseDateOrUndefined = (value: string): string | undefined => { if (isNaN(date.getTime())) { return undefined; } + return date; +}; + +export const verifyDateStringOrUndefined = ( + value: string +): string | undefined => { + const date = getDateFromString(value); + return date ? dateToMarkdownString(date) : undefined; +}; + +export const verifyDateOrThrow = ( + value: string, + labelForError: string +): string => { + const myDate = verifyDateStringOrUndefined(value); + if (!myDate) throw new Error(`Invalid format for ${labelForError}: ${value}`); + return myDate; +}; + +export const dateToMarkdownString = (date: Date) => { const stringDay = String(date.getDate()).padStart(2, "0"); const stringMonth = String(date.getMonth() + 1).padStart(2, "0"); // Months are zero-based const stringYear = date.getFullYear(); @@ -24,12 +45,3 @@ const parseDateOrUndefined = (value: string): string | undefined => { return `${stringDay}/${stringMonth}/${stringYear} ${stringHours}:${stringMinutes}:${stringSeconds}`; }; - -export const timeUtils = { - parseDateOrUndefined, - parseDateOrThrow: (value: string, labelForError: string): string => { - const myDate = parseDateOrUndefined(value); - if (!myDate) throw new Error(`Invalid format for ${labelForError}: ${value}`); - return myDate; - }, -}; diff --git a/nextjs/src/services/fileStorage/courseDifferences.ts b/nextjs/src/services/fileStorage/courseDifferences.ts new file mode 100644 index 0000000..b572975 --- /dev/null +++ b/nextjs/src/services/fileStorage/courseDifferences.ts @@ -0,0 +1,152 @@ +import { LocalCourse, LocalCourseSettings } from "@/models/local/localCourse"; +import { LocalModule } from "@/models/local/localModules"; + +export const CourseDifferences = { + getDeletedChanges( + newCourse: LocalCourse, + oldCourse: LocalCourse + ): DeleteCourseChanges { + if (newCourse === oldCourse) { + const emptyDeletes: DeleteCourseChanges = { + namesOfModulesToDeleteCompletely: [], + deleteContentsOfModule: [], + }; + return emptyDeletes; + } + + const moduleNamesNoLongerReferenced = oldCourse.modules + .filter( + (oldModule) => + !newCourse.modules.some( + (newModule) => newModule.name === oldModule.name + ) + ) + .map((oldModule) => oldModule.name); + + const modulesWithDeletions = oldCourse.modules + .filter( + (oldModule) => + !newCourse.modules.some( + (newModule) => + JSON.stringify(newModule) === JSON.stringify(oldModule) + ) + ) + .map((oldModule) => { + const newModule = newCourse.modules.find( + (m) => m.name === oldModule.name + ); + if (!newModule) return oldModule; + + const unreferencedAssignments = oldModule.assignments.filter( + (oldAssignment) => + !newModule.assignments.some( + (newAssignment) => newAssignment.name === oldAssignment.name + ) + ); + const unreferencedQuizzes = oldModule.quizzes.filter( + (oldQuiz) => + !newModule.quizzes.some((newQuiz) => newQuiz.name === oldQuiz.name) + ); + const unreferencedPages = oldModule.pages.filter( + (oldPage) => + !newModule.pages.some((newPage) => newPage.name === oldPage.name) + ); + + return { + ...oldModule, + assignments: unreferencedAssignments, + quizzes: unreferencedQuizzes, + pages: unreferencedPages, + }; + }); + + return { + namesOfModulesToDeleteCompletely: moduleNamesNoLongerReferenced, + deleteContentsOfModule: modulesWithDeletions, + }; + }, + + getNewChanges( + newCourse: LocalCourse, + oldCourse: LocalCourse + ): NewCourseChanges { + if (newCourse === oldCourse) { + const emptyChanges: NewCourseChanges = { + modules: [], + }; + return emptyChanges; + } + + const differentModules = newCourse.modules + .filter( + (newModule) => + !oldCourse.modules.some( + (oldModule) => + JSON.stringify(oldModule) === JSON.stringify(newModule) + ) + ) + .map((newModule) => { + const oldModule = oldCourse.modules.find( + (m) => m.name === newModule.name + ); + if (!oldModule) return newModule; + + const newAssignments = newModule.assignments.filter( + (newAssignment) => + !oldModule.assignments.some( + (oldAssignment) => + JSON.stringify(newAssignment) === JSON.stringify(oldAssignment) + ) + ); + const newQuizzes = newModule.quizzes.filter( + (newQuiz) => + !oldModule.quizzes.some( + (oldQuiz) => JSON.stringify(newQuiz) === JSON.stringify(oldQuiz) + ) + ); + const newPages = newModule.pages.filter( + (newPage) => + !oldModule.pages.some( + (oldPage) => JSON.stringify(newPage) === JSON.stringify(oldPage) + ) + ); + + return { + ...newModule, + assignments: newAssignments, + quizzes: newQuizzes, + pages: newPages, + }; + }); + + return { + settings: newCourse.settings, + modules: differentModules, + }; + }, +}; + +export interface DeleteCourseChanges { + namesOfModulesToDeleteCompletely: string[]; + deleteContentsOfModule: LocalModule[]; +} + +export interface NewCourseChanges { + modules: LocalModule[]; + settings?: LocalCourseSettings; +} + +// Default values for DeleteCourseChanges and NewCourseChanges +// export const createDeleteCourseChanges = ( +// init?: Partial +// ): DeleteCourseChanges => ({ +// namesOfModulesToDeleteCompletely: init?.namesOfModulesToDeleteCompletely ?? [], +// deleteContentsOfModule: init?.deleteContentsOfModule ?? [], +// }); + +// export const createNewCourseChanges = ( +// init?: Partial +// ): NewCourseChanges => ({ +// modules: init?.modules ?? [], +// settings: init?.settings, +// }); diff --git a/nextjs/src/services/tests/courseDifferencesDeletions.test.ts b/nextjs/src/services/tests/courseDifferencesDeletions.test.ts new file mode 100644 index 0000000..6645804 --- /dev/null +++ b/nextjs/src/services/tests/courseDifferencesDeletions.test.ts @@ -0,0 +1,423 @@ +import { describe, it, expect } from "vitest"; +import { LocalCourse } from "@/models/local/localCourse"; +import { CourseDifferences } from "../fileStorage/courseDifferences"; + +describe("CourseDifferencesDeletionsTests", () => { + it("same module does not get deleted", () => { + const oldCourse: LocalCourse = { + settings: { + assignmentGroups: [], + daysOfWeek: [], + startDate: "09/07/2024 23:59:00", + endDate: "09/07/2024 23:59:00", + defaultDueTime: { + hour: 23, + minute: 59, + }, + }, + modules: [ + { + name: "test module", + assignments: [], + quizzes: [], + pages: [], + }, + ], + }; + const newCourse: LocalCourse = { + ...oldCourse, + modules: [ + { + name: "test module", + assignments: [], + quizzes: [], + pages: [], + }, + ], + }; + + const differences = CourseDifferences.getDeletedChanges(newCourse, oldCourse); + + expect(differences.namesOfModulesToDeleteCompletely).toHaveLength(0); + }); + + it("changed module - old one gets deleted", () => { + const oldCourse: LocalCourse = { + settings: { + assignmentGroups: [], + daysOfWeek: [], + startDate: "09/07/2024 23:59:00", + endDate: "09/07/2024 23:59:00", + defaultDueTime: { + hour: 23, + minute: 59, + }, + }, + modules: [ + { + name: "test module", + assignments: [], + quizzes: [], + pages: [], + }, + ], + }; + const newCourse: LocalCourse = { + ...oldCourse, + modules: [ + { + name: "test module 2", + assignments: [], + quizzes: [], + pages: [], + }, + ], + }; + + const differences = CourseDifferences.getDeletedChanges(newCourse, oldCourse); + + expect(differences.namesOfModulesToDeleteCompletely).toHaveLength(1); + expect(differences.namesOfModulesToDeleteCompletely[0]).toBe("test module"); + }); + + it("new assignment name gets deleted", () => { + const oldCourse: LocalCourse = { + settings: { + assignmentGroups: [], + daysOfWeek: [], + startDate: "09/07/2024 23:59:00", + endDate: "09/07/2024 23:59:00", + defaultDueTime: { + hour: 23, + minute: 59, + }, + }, + modules: [ + { + name: "test module", + assignments: [ + { + name: "test assignment", + description: "test description", + dueAt: "09/07/2024 23:59:00", + submissionTypes: [], + allowedFileUploadExtensions: [], + rubric: [] + }, + ], + quizzes: [], + pages: [], + }, + ], + }; + const newCourse: LocalCourse = { + ...oldCourse, + modules: [ + { + name: "test module", + assignments: [ + { + name: "test assignment changed name", + description: "test description", + dueAt: "09/07/2024 23:59:00", + submissionTypes: [], + allowedFileUploadExtensions: [], + rubric: [] + }, + ], + quizzes: [], + pages: [], + }, + ], + }; + + const differences = CourseDifferences.getDeletedChanges(newCourse, oldCourse); + + expect(differences.namesOfModulesToDeleteCompletely).toHaveLength(0); + expect(differences.deleteContentsOfModule).toHaveLength(1); + expect(differences.deleteContentsOfModule[0].assignments).toHaveLength(1); + expect(differences.deleteContentsOfModule[0].assignments[0].name).toBe( + "test assignment" + ); + }); + + it("assignments with changed descriptions do not get deleted", () => { + const oldCourse: LocalCourse = { + settings: { + assignmentGroups: [], + daysOfWeek: [], + startDate: "09/07/2024 23:59:00", + endDate: "09/07/2024 23:59:00", + defaultDueTime: { + hour: 23, + minute: 59, + }, + }, + modules: [ + { + name: "test module", + assignments: [ + { + name: "test assignment", + description: "test description", + dueAt: "09/07/2024 23:59:00", + submissionTypes: [], + allowedFileUploadExtensions: [], + rubric: [] + }, + ], + quizzes: [], + pages: [], + }, + ], + }; + const newCourse: LocalCourse = { + ...oldCourse, + modules: [ + { + name: "test module", + assignments: [ + { + name: "test assignment", + description: "test description", + dueAt: "09/07/2024 23:59:00", + submissionTypes: [], + allowedFileUploadExtensions: [], + rubric: [] + }, + ], + quizzes: [], + pages: [], + }, + ], + }; + + const differences = CourseDifferences.getDeletedChanges(newCourse, oldCourse); + + expect(differences.deleteContentsOfModule).toHaveLength(0); + }); + + it("can detect changed and unchanged assignments", () => { + const oldCourse: LocalCourse = { + settings: { + assignmentGroups: [], + daysOfWeek: [], + startDate: "09/07/2024 23:59:00", + endDate: "09/07/2024 23:59:00", + defaultDueTime: { + hour: 23, + minute: 59, + }, + }, + modules: [ + { + name: "test module", + assignments: [ + { + name: "test assignment", + description: "test description", + dueAt: "09/07/2024 23:59:00", + submissionTypes: [], + allowedFileUploadExtensions: [], + rubric: [] + }, + { + name: "test assignment 2", + description: "test description", + dueAt: "09/07/2024 23:59:00", + submissionTypes: [], + allowedFileUploadExtensions: [], + rubric: [] + }, + ], + quizzes: [], + pages: [], + }, + ], + }; + const newCourse: LocalCourse = { + ...oldCourse, + modules: [ + { + name: "test module", + assignments: [ + { + name: "test assignment", + description: "test description", + dueAt: "09/07/2024 23:59:00", + submissionTypes: [], + allowedFileUploadExtensions: [], + rubric: [] + }, + { + name: "test assignment 2 changed", + description: "test description", + dueAt: "09/07/2024 23:59:00", + submissionTypes: [], + allowedFileUploadExtensions: [], + rubric: [] + }, + ], + quizzes: [], + pages: [], + }, + ], + }; + + const differences = CourseDifferences.getDeletedChanges(newCourse, oldCourse); + + expect(differences.deleteContentsOfModule).toHaveLength(1); + expect(differences.deleteContentsOfModule[0].assignments).toHaveLength(1); + expect(differences.deleteContentsOfModule[0].assignments[0].name).toBe( + "test assignment 2" + ); + }); + + it("changed quizzes get deleted", () => { + const oldCourse: LocalCourse = { + settings: { + assignmentGroups: [], + daysOfWeek: [], + startDate: "09/07/2024 23:59:00", + endDate: "09/07/2024 23:59:00", + defaultDueTime: { + hour: 23, + minute: 59, + }, + }, + modules: [ + { + name: "test module", + assignments: [], + quizzes: [ + { + name: "Test Quiz", + description: "test description", + dueAt: "09/07/2024 23:59:00", + shuffleAnswers: false, + showCorrectAnswers: false, + oneQuestionAtATime: false, + allowedAttempts: 0, + questions: [] + }, + { + name: "Test Quiz 2", + description: "test description", + dueAt: "09/07/2024 23:59:00", + shuffleAnswers: false, + showCorrectAnswers: false, + oneQuestionAtATime: false, + allowedAttempts: 0, + questions: [] + }, + ], + pages: [], + }, + ], + }; + const newCourse: LocalCourse = { + ...oldCourse, + modules: [ + { + name: "test module", + assignments: [], + quizzes: [ + { + name: "Test Quiz", + description: "test description", + dueAt: "09/07/2024 23:59:00", + shuffleAnswers: false, + showCorrectAnswers: false, + oneQuestionAtATime: false, + allowedAttempts: 0, + questions: [] + }, + { + name: "Test Quiz 3", + description: "test description", + dueAt: "09/07/2024 23:59:00", + shuffleAnswers: false, + showCorrectAnswers: false, + oneQuestionAtATime: false, + allowedAttempts: 0, + questions: [] + }, + ], + pages: [], + }, + ], + }; + + const differences = CourseDifferences.getDeletedChanges(newCourse, oldCourse); + + expect(differences.deleteContentsOfModule).toHaveLength(1); + expect(differences.deleteContentsOfModule[0].quizzes).toHaveLength(1); + expect(differences.deleteContentsOfModule[0].quizzes[0].name).toBe( + "Test Quiz 2" + ); + }); + + it("changed pages get deleted", () => { + const oldCourse: LocalCourse = { + settings: { + assignmentGroups: [], + daysOfWeek: [], + startDate: "09/07/2024 23:59:00", + endDate: "09/07/2024 23:59:00", + defaultDueTime: { + hour: 23, + minute: 59, + }, + }, + modules: [ + { + name: "test module", + assignments: [], + quizzes: [], + pages: [ + { + name: "Test Page", + text: "test contents", + dueAt: "09/07/2024 23:59:00", + }, + { + name: "Test Page 2", + text: "test contents", + dueAt: "09/07/2024 23:59:00" + }, + ], + }, + ], + }; + const newCourse: LocalCourse = { + ...oldCourse, + modules: [ + { + name: "test module", + assignments: [], + quizzes: [], + pages: [ + { + name: "Test Page", + text: "test contents", + dueAt: "09/07/2024 23:59:00" + }, + { + name: "Test Page 3", + text: "test contents", + dueAt: "09/07/2024 23:59:00" + }, + ], + }, + ], + }; + + const differences = CourseDifferences.getDeletedChanges(newCourse, oldCourse); + + expect(differences.deleteContentsOfModule).toHaveLength(1); + expect(differences.deleteContentsOfModule[0].pages).toHaveLength(1); + expect(differences.deleteContentsOfModule[0].pages[0].name).toBe( + "Test Page 2" + ); + }); +});