assignment markdown tests pass

This commit is contained in:
2024-08-21 14:48:46 -06:00
parent 2748416e7b
commit aa85f80a4c
5 changed files with 315 additions and 289 deletions

View File

@@ -1,8 +1,8 @@
export enum AssignmentSubmissionType { export enum AssignmentSubmissionType {
OnlineTextEntry = "online_text_entry", ONLINE_TEXT_ENTRY = "online_text_entry",
OnlineUpload = "online_upload", ONLINE_UPLOAD = "online_upload",
OnlineQuiz = "online_quiz", ONLINE_QUIZ = "online_quiz",
DiscussionTopic = "discussion_topic", DISCUSSION_TOPIC = "discussion_topic",
OnlineUrl = "online_url", ONLINE_URL = "online_url",
None = "none" NONE = "none",
} }

View File

@@ -1,48 +1,145 @@
import { AssignmentSubmissionType } from "../assignmentSubmissionType"; import { AssignmentSubmissionType } from "../assignmentSubmissionType";
import { LocalAssignment } from "../localAssignment"; import { LocalAssignment } from "../localAssignment";
import { RubricItem } from "../rubricItem"; import { RubricItem } from "../rubricItem";
import { extractLabelValue } from "./markdownUtils";
const assignmentRubricToMarkdown = (assignment: LocalAssignment) => { const parseFileUploadExtensions = (input: string) => {
return assignment.rubric const allowedFileUploadExtensions: string[] = [];
.map((item: RubricItem) => { const regex = /- (.+)/;
const pointLabel = item.points > 1 ? "pts" : "pt";
return `- ${item.points}${pointLabel}: ${item.label}`; const words = input.split("AllowedFileUploadExtensions:");
}) if (words.length < 2) return allowedFileUploadExtensions;
.join("\n");
const inputAfterSubmissionTypes = words[1];
const lines = inputAfterSubmissionTypes
.split("\n")
.map((line) => line.trim());
for (const line of lines) {
const match = regex.exec(line);
if (!match) {
if (line === "") continue;
else break;
}
allowedFileUploadExtensions.push(match[1].trim());
}
return allowedFileUploadExtensions;
}; };
const settingsToMarkdown = (assignment: LocalAssignment) => { const parseIndividualRubricItemMarkdown = (rawMarkdown: string) => {
const printableDueDate = assignment.dueAt.toString().replace("\u202F", " "); const pointsPattern = /\s*-\s*(-?\d+(?:\.\d+)?)\s*pt(s)?:/;
const printableLockAt = const match = pointsPattern.exec(rawMarkdown);
assignment.lockAt?.toString().replace("\u202F", " ") || ""; if (!match) {
throw new Error(`Points not found: ${rawMarkdown}`);
}
const submissionTypesMarkdown = assignment.submissionTypes const points = parseFloat(match[1]);
.map((submissionType: AssignmentSubmissionType) => `- ${submissionType}`) const label = rawMarkdown.split(": ").slice(1).join(": ");
.join("\n");
const allowedFileUploadExtensionsMarkdown = const item: RubricItem = { points, label };
assignment.allowedFileUploadExtensions return item;
.map((fileExtension: string) => `- ${fileExtension}`)
.join("\n");
const settingsMarkdown = [
`Name: ${assignment.name}`,
`LockAt: ${printableLockAt}`,
`DueAt: ${printableDueDate}`,
`AssignmentGroupName: ${assignment.localAssignmentGroupName}`,
`SubmissionTypes:\n${submissionTypesMarkdown}`,
`AllowedFileUploadExtensions:\n${allowedFileUploadExtensionsMarkdown}`,
].join("\n");
return settingsMarkdown;
}; };
export const assignmentMarkdownStringifier = { const parseSettings = (input: string) => {
toMarkdown(assignment: LocalAssignment): string { const name = extractLabelValue(input, "Name");
const settingsMarkdown = settingsToMarkdown(assignment); const rawLockAt = extractLabelValue(input, "LockAt");
const rubricMarkdown = assignmentRubricToMarkdown(assignment); const rawDueAt = extractLabelValue(input, "DueAt");
const assignmentMarkdown = `${settingsMarkdown}\n---\n\n${assignment.description}\n\n## Rubric\n\n${rubricMarkdown}`; const assignmentGroupName = extractLabelValue(input, "AssignmentGroupName");
const submissionTypes = parseSubmissionTypes(input);
const fileUploadExtensions = parseFileUploadExtensions(input);
return assignmentMarkdown; const lockAt = (rawLockAt ? new Date(rawLockAt) : undefined)?.toISOString();
const dueAt = new Date(rawDueAt).toISOString();
if (isNaN(new Date(dueAt).getTime())) {
throw new Error(`Error with DueAt: ${rawDueAt}`);
}
return {
name,
assignmentGroupName,
submissionTypes,
fileUploadExtensions,
dueAt,
lockAt,
};
};
const parseSubmissionTypes = (input: string): AssignmentSubmissionType[] => {
const submissionTypes: AssignmentSubmissionType[] = [];
const regex = /- (.+)/;
const words = input.split("SubmissionTypes:");
if (words.length < 2) return submissionTypes;
const inputAfterSubmissionTypes = words[1]; // doesn't consider other settings that follow...
const lines = inputAfterSubmissionTypes
.split("\n")
.map((line) => line.trim());
for (const line of lines) {
const match = regex.exec(line);
if (!match) {
if (line === "") continue;
else break;
}
const typeString = match[1].trim();
const type = Object.values(AssignmentSubmissionType).find(
(t) => t === typeString
);
if (type) {
submissionTypes.push(type);
} else {
console.warn(`Unknown submission type: ${typeString}`);
}
}
return submissionTypes;
};
const parseRubricMarkdown = (rawMarkdown: string) => {
if (!rawMarkdown.trim()) return [];
const lines = rawMarkdown.trim().split("\n");
return lines.map(parseIndividualRubricItemMarkdown);
};
export const assignmentMarkdownParser = {
parseMarkdown(input: string): LocalAssignment {
const settingsString = input.split("---")[0];
const {
name,
assignmentGroupName,
submissionTypes,
fileUploadExtensions,
dueAt,
lockAt,
} = parseSettings(settingsString);
const description = input
.split("---\n")
.slice(1)
.join("---\n")
.split("## Rubric")[0]
.trim();
const rubricString = input.split("## Rubric\n")[1];
const rubric = parseRubricMarkdown(rubricString);
const assignment: LocalAssignment = {
name: name.trim(),
localAssignmentGroupName: assignmentGroupName.trim(),
submissionTypes: submissionTypes,
allowedFileUploadExtensions: fileUploadExtensions,
dueAt: dueAt,
lockAt: lockAt,
rubric: rubric,
description: description,
};
return assignment;
}, },
}; };

View File

@@ -0,0 +1,48 @@
import { AssignmentSubmissionType } from "../assignmentSubmissionType";
import { LocalAssignment } from "../localAssignment";
import { RubricItem } from "../rubricItem";
const assignmentRubricToMarkdown = (assignment: LocalAssignment) => {
return assignment.rubric
.map((item: RubricItem) => {
const pointLabel = item.points > 1 ? "pts" : "pt";
return `- ${item.points}${pointLabel}: ${item.label}`;
})
.join("\n");
};
const settingsToMarkdown = (assignment: LocalAssignment) => {
const printableDueDate = assignment.dueAt.toString().replace("\u202F", " ");
const printableLockAt =
assignment.lockAt?.toString().replace("\u202F", " ") || "";
const submissionTypesMarkdown = assignment.submissionTypes
.map((submissionType: AssignmentSubmissionType) => `- ${submissionType}`)
.join("\n");
const allowedFileUploadExtensionsMarkdown =
assignment.allowedFileUploadExtensions
.map((fileExtension: string) => `- ${fileExtension}`)
.join("\n");
const settingsMarkdown = [
`Name: ${assignment.name}`,
`LockAt: ${printableLockAt}`,
`DueAt: ${printableDueDate}`,
`AssignmentGroupName: ${assignment.localAssignmentGroupName}`,
`SubmissionTypes:\n${submissionTypesMarkdown}`,
`AllowedFileUploadExtensions:\n${allowedFileUploadExtensionsMarkdown}`,
].join("\n");
return settingsMarkdown;
};
export const assignmentMarkdownSerializer = {
toMarkdown(assignment: LocalAssignment): string {
const settingsMarkdown = settingsToMarkdown(assignment);
const rubricMarkdown = assignmentRubricToMarkdown(assignment);
const assignmentMarkdown = `${settingsMarkdown}\n---\n\n${assignment.description}\n\n## Rubric\n\n${rubricMarkdown}`;
return assignmentMarkdown;
},
};

View File

@@ -1,138 +0,0 @@
import { AssignmentSubmissionType } from "../assignmentSubmissionType";
import { LocalAssignment } from "../localAssignment";
import { RubricItem } from "../rubricItem";
import { extractLabelValue } from "./markdownUtils";
const parseFileUploadExtensions = (input: string) => {
const allowedFileUploadExtensions: string[] = [];
const regex = /- (.+)/;
const words = input.split("AllowedFileUploadExtensions:");
if (words.length < 2) return allowedFileUploadExtensions;
const inputAfterSubmissionTypes = words[1];
const lines = inputAfterSubmissionTypes
.split("\n")
.map((line) => line.trim());
for (const line of lines) {
const match = regex.exec(line);
if (!match) break;
allowedFileUploadExtensions.push(match[1].trim());
}
return allowedFileUploadExtensions;
};
const parseIndividualRubricItemMarkdown = (rawMarkdown: string) => {
const pointsPattern = /\s*-\s*(-?\d+(?:\.\d+)?)\s*pt(s)?:/;
const match = pointsPattern.exec(rawMarkdown);
if (!match) {
throw new Error(`Points not found: ${rawMarkdown}`);
}
const points = parseFloat(match[1]);
const label = rawMarkdown.split(": ").slice(1).join(": ");
const item: RubricItem = { points, label };
return item;
};
const parseSettings = (input: string) => {
const name = extractLabelValue(input, "Name");
const rawLockAt = extractLabelValue(input, "LockAt");
const rawDueAt = extractLabelValue(input, "DueAt");
const assignmentGroupName = extractLabelValue(input, "AssignmentGroupName");
const submissionTypes = parseSubmissionTypes(input);
const fileUploadExtensions = parseFileUploadExtensions(input);
const lockAt = (rawLockAt ? new Date(rawLockAt) : undefined)?.toISOString();
const dueAt = new Date(rawDueAt).toISOString();
if (isNaN(new Date(dueAt).getTime())) {
throw new Error(`Error with DueAt: ${rawDueAt}`);
}
return {
name,
assignmentGroupName,
submissionTypes,
fileUploadExtensions,
dueAt,
lockAt,
};
};
const parseSubmissionTypes = (input: string): AssignmentSubmissionType[] => {
const submissionTypes: AssignmentSubmissionType[] = [];
const regex = /- (.+)/;
const words = input.split("SubmissionTypes:");
if (words.length < 2) return submissionTypes;
const inputAfterSubmissionTypes = words[1];
const lines = inputAfterSubmissionTypes
.split("\n")
.map((line) => line.trim());
for (const line of lines) {
const match = regex.exec(line);
if (!match) break;
const typeString = match[1].trim();
const type = Object.values(AssignmentSubmissionType).find(
(t) => t === typeString
);
if (type) {
submissionTypes.push(type);
} else {
console.warn(`Unknown submission type: ${typeString}`);
}
}
return submissionTypes;
};
const parseRubricMarkdown = (rawMarkdown: string) => {
if (!rawMarkdown.trim()) return [];
const lines = rawMarkdown.trim().split("\n");
return lines.map(parseIndividualRubricItemMarkdown);
};
export const assignmentMarkdownParser = {
parseMarkdown(input: string): LocalAssignment {
const settingsString = input.split("---")[0];
const {
name,
assignmentGroupName,
submissionTypes,
fileUploadExtensions,
dueAt,
lockAt,
} = parseSettings(settingsString);
const description = input
.split("---\n")
.slice(1)
.join("---\n")
.split("## Rubric")[0]
.trim();
const rubricString = input.split("## Rubric\n")[1];
const rubric = parseRubricMarkdown(rubricString);
const assignment: LocalAssignment = {
name: name.trim(),
localAssignmentGroupName: assignmentGroupName.trim(),
submissionTypes: submissionTypes,
allowedFileUploadExtensions: fileUploadExtensions,
dueAt: dueAt,
lockAt: lockAt,
rubric: rubric,
description: description,
};
return assignment;
},
};

View File

@@ -1,140 +1,159 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from "vitest";
import { LocalAssignment } from '../../assignmnet/localAssignment'; import { LocalAssignment } from "../../assignmnet/localAssignment";
import { AssignmentSubmissionType } from '../../assignmnet/assignmentSubmissionType'; import { AssignmentSubmissionType } from "../../assignmnet/assignmentSubmissionType";
import { assignmentMarkdownStringifier } from '../../assignmnet/utils/assignmentMarkdownParser'; import { assignmentMarkdownSerializer } from "../../assignmnet/utils/assignmentMarkdownSerializer";
import { assignmentMarkdownParser } from '../../assignmnet/utils/assignmentMarkdownStringifier'; import { assignmentMarkdownParser } from "../../assignmnet/utils/assignmentMarkdownParser";
describe('AssignmentMarkdownTests', () => { describe("AssignmentMarkdownTests", () => {
it('can parse assignment settings', () => { it("can parse assignment settings", () => {
const assignment: LocalAssignment ={ const assignment: LocalAssignment = {
name: 'test assignment', name: "test assignment",
description: 'here is the description', description: "here is the description",
dueAt: new Date().toISOString(), dueAt: new Date().toISOString(),
lockAt: new Date().toISOString(), lockAt: new Date().toISOString(),
submissionTypes: [AssignmentSubmissionType.OnlineUpload], submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
localAssignmentGroupName: 'Final Project', localAssignmentGroupName: "Final Project",
rubric: [ rubric: [
{ points: 4, label: 'do task 1' }, { points: 4, label: "do task 1" },
{ points: 2, label: 'do task 2' }, { points: 2, label: "do task 2" },
], ],
allowedFileUploadExtensions: [], allowedFileUploadExtensions: [],
}; };
const assignmentMarkdown = assignmentMarkdownStringifier.toMarkdown(assignment); const assignmentMarkdown =
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(assignmentMarkdown); assignmentMarkdownSerializer.toMarkdown(assignment);
const parsedAssignment =
assignmentMarkdownParser.parseMarkdown(assignmentMarkdown);
expect(parsedAssignment).toEqual(assignment); expect(parsedAssignment).toEqual(assignment);
}); });
// it('assignment with empty rubric can be parsed', () => { it("assignment with empty rubric can be parsed", () => {
// const assignment = new LocalAssignment({ const assignment: LocalAssignment = {
// name: 'test assignment', name: "test assignment",
// description: 'here is the description', description: "here is the description",
// dueAt: new Date(), dueAt: new Date().toISOString(),
// lockAt: new Date(), lockAt: new Date().toISOString(),
// submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD], submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
// localAssignmentGroupName: 'Final Project', localAssignmentGroupName: "Final Project",
// rubric: [], rubric: [],
// }); allowedFileUploadExtensions: [],
};
// const assignmentMarkdown = assignment.toMarkdown(); const assignmentMarkdown =
// const parsedAssignment = LocalAssignment.parseMarkdown(assignmentMarkdown); assignmentMarkdownSerializer.toMarkdown(assignment);
const parsedAssignment =
assignmentMarkdownParser.parseMarkdown(assignmentMarkdown);
// expect(parsedAssignment).toEqual(assignment); expect(parsedAssignment).toEqual(assignment);
// }); });
// it('assignment with empty submission types can be parsed', () => { it("assignment with empty submission types can be parsed", () => {
// const assignment = new LocalAssignment({ const assignment: LocalAssignment = {
// name: 'test assignment', name: "test assignment",
// description: 'here is the description', description: "here is the description",
// dueAt: new Date(), dueAt: new Date().toISOString(),
// lockAt: new Date(), lockAt: new Date().toISOString(),
// submissionTypes: [], submissionTypes: [],
// localAssignmentGroupName: 'Final Project', localAssignmentGroupName: "Final Project",
// rubric: [ rubric: [
// new RubricItem({ points: 4, label: 'do task 1' }), { points: 4, label: "do task 1" },
// new RubricItem({ points: 2, label: 'do task 2' }), { points: 2, label: "do task 2" },
// ], ],
// }); allowedFileUploadExtensions: [],
};
// const assignmentMarkdown = assignment.toMarkdown(); const assignmentMarkdown =
// const parsedAssignment = LocalAssignment.parseMarkdown(assignmentMarkdown); assignmentMarkdownSerializer.toMarkdown(assignment);
const parsedAssignment =
assignmentMarkdownParser.parseMarkdown(assignmentMarkdown);
// expect(parsedAssignment).toEqual(assignment); expect(parsedAssignment).toEqual(assignment);
// }); });
// it('assignment without lockAt date can be parsed', () => { it("assignment without lockAt date can be parsed", () => {
// const assignment = new LocalAssignment({ const assignment: LocalAssignment = {
// name: 'test assignment', name: "test assignment",
// description: 'here is the description', description: "here is the description",
// dueAt: new Date(), dueAt: new Date().toISOString(),
// lockAt: null, lockAt: undefined,
// submissionTypes: [], submissionTypes: [],
// localAssignmentGroupName: 'Final Project', localAssignmentGroupName: "Final Project",
// rubric: [ rubric: [
// new RubricItem({ points: 4, label: 'do task 1' }), { points: 4, label: "do task 1" },
// new RubricItem({ points: 2, label: 'do task 2' }), { points: 2, label: "do task 2" },
// ], ],
// }); allowedFileUploadExtensions: [],
};
// const assignmentMarkdown = assignment.toMarkdown(); const assignmentMarkdown =
// const parsedAssignment = LocalAssignment.parseMarkdown(assignmentMarkdown); assignmentMarkdownSerializer.toMarkdown(assignment);
const parsedAssignment =
assignmentMarkdownParser.parseMarkdown(assignmentMarkdown);
// expect(parsedAssignment).toEqual(assignment); expect(parsedAssignment).toEqual(assignment);
// }); });
// it('assignment without description can be parsed', () => { it("assignment without description can be parsed", () => {
// const assignment = new LocalAssignment({ const assignment: LocalAssignment = {
// name: 'test assignment', name: "test assignment",
// description: '', description: "",
// dueAt: new Date(), dueAt: new Date().toISOString(),
// lockAt: new Date(), lockAt: new Date().toISOString(),
// submissionTypes: [], submissionTypes: [],
// localAssignmentGroupName: 'Final Project', localAssignmentGroupName: "Final Project",
// rubric: [ rubric: [
// new RubricItem({ points: 4, label: 'do task 1' }), { points: 4, label: "do task 1" },
// new RubricItem({ points: 2, label: 'do task 2' }), { points: 2, label: "do task 2" },
// ], ],
// }); allowedFileUploadExtensions: [],
};
// const assignmentMarkdown = assignment.toMarkdown(); const assignmentMarkdown =
// const parsedAssignment = LocalAssignment.parseMarkdown(assignmentMarkdown); assignmentMarkdownSerializer.toMarkdown(assignment);
const parsedAssignment =
assignmentMarkdownParser.parseMarkdown(assignmentMarkdown);
// expect(parsedAssignment).toEqual(assignment); expect(parsedAssignment).toEqual(assignment);
// }); });
// it('assignments can have three dashes', () => { it("assignments can have three dashes", () => {
// const assignment = new LocalAssignment({ const assignment: LocalAssignment = {
// name: 'test assignment', name: "test assignment",
// description: 'test assignment\n---\nsomestuff', description: "test assignment\n---\nsomestuff",
// dueAt: new Date(), dueAt: new Date().toISOString(),
// lockAt: new Date(), lockAt: new Date().toISOString(),
// submissionTypes: [], submissionTypes: [],
// localAssignmentGroupName: 'Final Project', localAssignmentGroupName: "Final Project",
// rubric: [], rubric: [],
// }); allowedFileUploadExtensions: [],
};
// const assignmentMarkdown = assignment.toMarkdown(); const assignmentMarkdown =
// const parsedAssignment = LocalAssignment.parseMarkdown(assignmentMarkdown); assignmentMarkdownSerializer.toMarkdown(assignment);
const parsedAssignment =
assignmentMarkdownParser.parseMarkdown(assignmentMarkdown);
// expect(parsedAssignment).toEqual(assignment); expect(parsedAssignment).toEqual(assignment);
// }); });
// it('assignments can restrict upload types', () => { it("assignments can restrict upload types", () => {
// const assignment = new LocalAssignment({ const assignment: LocalAssignment = {
// name: 'test assignment', name: "test assignment",
// description: 'here is the description', description: "here is the description",
// dueAt: new Date(), dueAt: new Date().toISOString(),
// lockAt: new Date(), lockAt: new Date().toISOString(),
// submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD], submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
// allowedFileUploadExtensions: ['pdf', 'txt'], allowedFileUploadExtensions: ["pdf", "txt"],
// localAssignmentGroupName: 'Final Project', localAssignmentGroupName: "Final Project",
// rubric: [], rubric: [],
// }); };
// const assignmentMarkdown = assignment.toMarkdown(); const assignmentMarkdown =
// const parsedAssignment = LocalAssignment.parseMarkdown(assignmentMarkdown); assignmentMarkdownSerializer.toMarkdown(assignment);
const parsedAssignment =
assignmentMarkdownParser.parseMarkdown(assignmentMarkdown);
// expect(parsedAssignment).toEqual(assignment); expect(parsedAssignment).toEqual(assignment);
// }); });
}); });