refactoring files to be located by feature

This commit is contained in:
2025-07-23 09:23:44 -06:00
parent 46e0c36916
commit c95c40f9e7
75 changed files with 279 additions and 303 deletions

View File

@@ -0,0 +1,26 @@
import { z } from "zod";
export enum AssignmentSubmissionType {
ONLINE_TEXT_ENTRY = "online_text_entry",
ONLINE_UPLOAD = "online_upload",
ONLINE_QUIZ = "online_quiz",
DISCUSSION_TOPIC = "discussion_topic",
ONLINE_URL = "online_url",
NONE = "none",
}
export const zodAssignmentSubmissionType = z.enum([
AssignmentSubmissionType.ONLINE_TEXT_ENTRY,
AssignmentSubmissionType.ONLINE_UPLOAD,
AssignmentSubmissionType.ONLINE_QUIZ,
AssignmentSubmissionType.DISCUSSION_TOPIC,
AssignmentSubmissionType.ONLINE_URL,
AssignmentSubmissionType.NONE,
]);
export const AssignmentSubmissionTypeList: AssignmentSubmissionType[] = [
AssignmentSubmissionType.ONLINE_TEXT_ENTRY,
AssignmentSubmissionType.ONLINE_UPLOAD,
AssignmentSubmissionType.ONLINE_QUIZ,
AssignmentSubmissionType.DISCUSSION_TOPIC,
AssignmentSubmissionType.ONLINE_URL,
AssignmentSubmissionType.NONE,
] as const;

View File

@@ -0,0 +1,40 @@
import { IModuleItem } from "../../../../models/local/IModuleItem";
import {
AssignmentSubmissionType,
zodAssignmentSubmissionType,
} from "./assignmentSubmissionType";
import { RubricItem, zodRubricItem } from "./rubricItem";
import { assignmentMarkdownParser } from "./utils/assignmentMarkdownParser";
import { assignmentMarkdownSerializer } from "./utils/assignmentMarkdownSerializer";
import { z } from "zod";
export interface LocalAssignment extends IModuleItem {
name: string;
description: string;
lockAt?: string; // 08/21/2023 23:59:00
dueAt: string; // 08/21/2023 23:59:00
localAssignmentGroupName?: string;
submissionTypes: AssignmentSubmissionType[];
allowedFileUploadExtensions: string[];
rubric: RubricItem[];
githubClassroomAssignmentShareLink?: string;
githubClassroomAssignmentLink?: string;
}
export const zodLocalAssignment = z.object({
name: z.string(),
description: z.string(),
lockAt: z.string().optional(),
dueAt: z.string(),
localAssignmentGroupName: z.string().optional(),
submissionTypes: zodAssignmentSubmissionType.array(),
allowedFileUploadExtensions: z.string().array(),
rubric: zodRubricItem.array(),
githubClassroomAssignmentShareLink: z.string().optional(),
githubClassroomAssignmentLink: z.string().optional(),
});
export const localAssignmentMarkdown = {
parseMarkdown: assignmentMarkdownParser.parseMarkdown,
toMarkdown: assignmentMarkdownSerializer.toMarkdown,
};

View File

@@ -0,0 +1,14 @@
import { z } from "zod";
export interface LocalAssignmentGroup {
canvasId?: number;
id: string;
name: string;
weight: number;
}
export const zodLocalAssignmentGroup = z.object({
canvasId: z.optional(z.number()),
id: z.string(),
name: z.string(),
weight: z.number(),
});

View File

@@ -0,0 +1,16 @@
import { z } from "zod";
export interface RubricItem {
label: string;
points: number;
}
export const zodRubricItem = z.object({
label: z.string(),
points: z.number(),
});
export const rubricItemIsExtraCredit = (item: RubricItem) => {
const extraCredit = "(extra credit)";
return item.label.toLowerCase().includes(extraCredit.toLowerCase());
};

View File

@@ -0,0 +1,162 @@
import {
verifyDateOrThrow,
verifyDateStringOrUndefined,
} from "../../../../../models/local/utils/timeUtils";
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) {
if (line === "") continue;
else 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 rawLockAt = extractLabelValue(input, "LockAt");
const rawDueAt = extractLabelValue(input, "DueAt");
const assignmentGroupName = extractLabelValue(input, "AssignmentGroupName");
const submissionTypes = parseSubmissionTypes(input);
const fileUploadExtensions = parseFileUploadExtensions(input);
const githubClassroomAssignmentShareLink = extractLabelValue(
input,
"GithubClassroomAssignmentShareLink"
);
const githubClassroomAssignmentLink = extractLabelValue(
input,
"GithubClassroomAssignmentLink"
);
const dueAt = verifyDateOrThrow(rawDueAt, "DueAt");
const lockAt = verifyDateStringOrUndefined(rawLockAt);
return {
assignmentGroupName,
submissionTypes,
fileUploadExtensions,
dueAt,
lockAt,
githubClassroomAssignmentShareLink,
githubClassroomAssignmentLink,
};
};
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 = {
parseRubricMarkdown,
parseMarkdown(input: string, name: string): LocalAssignment {
const settingsString = input.split("---")[0];
const {
assignmentGroupName,
submissionTypes,
fileUploadExtensions,
dueAt,
lockAt,
githubClassroomAssignmentShareLink,
githubClassroomAssignmentLink,
} = 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,
localAssignmentGroupName: assignmentGroupName.trim(),
submissionTypes: submissionTypes,
allowedFileUploadExtensions: fileUploadExtensions,
dueAt: dueAt,
lockAt: lockAt,
rubric: rubric,
description: description,
};
if (githubClassroomAssignmentShareLink) {
assignment.githubClassroomAssignmentShareLink =
githubClassroomAssignmentShareLink;
}
if (githubClassroomAssignmentLink) {
assignment.githubClassroomAssignmentLink = githubClassroomAssignmentLink;
}
return assignment;
},
};

View File

@@ -0,0 +1,54 @@
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 settingsMarkdownArr = [
`LockAt: ${printableLockAt}`,
`DueAt: ${printableDueDate}`,
`AssignmentGroupName: ${assignment.localAssignmentGroupName}`,
`GithubClassroomAssignmentLink: ${assignment.githubClassroomAssignmentLink ?? ""}`,
`GithubClassroomAssignmentShareLink: ${assignment.githubClassroomAssignmentShareLink ?? ""}`,
`SubmissionTypes:\n${submissionTypesMarkdown}`,
`AllowedFileUploadExtensions:\n${allowedFileUploadExtensionsMarkdown}`,
];
return settingsMarkdownArr.join("\n");
};
export const assignmentMarkdownSerializer = {
toMarkdown(assignment: LocalAssignment): string {
try {
const settingsMarkdown = settingsToMarkdown(assignment);
const rubricMarkdown = assignmentRubricToMarkdown(assignment);
const assignmentMarkdown = `${settingsMarkdown}\n---\n\n${assignment.description}\n\n## Rubric\n\n${rubricMarkdown}`;
return assignmentMarkdown;
} catch (e) {
console.log(assignment);
console.log("Error converting assignment to markdown");
throw e;
}
},
};

View File

@@ -0,0 +1,12 @@
import { RubricItem } from "../rubricItem";
export const assignmentPoints = (rubric: RubricItem[]) => {
const basePoints = rubric
.map((r) => {
if (r.label.toLowerCase().includes("(extra credit)")) return 0;
if (r.points < 0) return 0; // don't count negative points towards the point totals
return r.points;
})
.reduce((acc, current) => (current > 0 ? acc + current : acc), 0);
return basePoints;
};

View File

@@ -0,0 +1,10 @@
export const extractLabelValue = (input: string, label: string) => {
const pattern = new RegExp(`${label}: (.*?)\n`);
const match = pattern.exec(input);
if (match && match.length > 1 && match[1]) {
return match[1].trim();
}
return "";
};