running first test

This commit is contained in:
2024-08-21 14:19:30 -06:00
parent c698e33853
commit 2748416e7b
14 changed files with 416 additions and 6 deletions

View File

@@ -6,7 +6,8 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"test": "vitest"
},
"dependencies": {
"@tanstack/react-query": "^5.52.0",

View File

@@ -1,11 +1,12 @@
import { canvasService } from "@/services/canvas/canvasService";
import { useSuspenseQuery } from "@tanstack/react-query";
export const canvasCourseKeys = {
courseDetails: (canavasId: number) => ["canvas course", canavasId] as const,
};
export const useCanvasCourseQuery =(canvasId: number) => useSuspenseQuery({
queryKey: canvasCourseKeys.courseDetails(canvasId),
queryFn: async () => canvasserv
})
export const useCanvasCourseQuery = (canvasId: number) =>
useSuspenseQuery({
queryKey: canvasCourseKeys.courseDetails(canvasId),
queryFn: async () => await canvasService.getCourse(canvasId),
});

View File

@@ -0,0 +1,8 @@
export enum AssignmentSubmissionType {
OnlineTextEntry = "online_text_entry",
OnlineUpload = "online_upload",
OnlineQuiz = "online_quiz",
DiscussionTopic = "discussion_topic",
OnlineUrl = "online_url",
None = "none"
}

View File

@@ -0,0 +1,13 @@
import { AssignmentSubmissionType } from "./assignmentSubmissionType";
import { RubricItem } from "./rubricItem";
export interface LocalAssignment {
name: string;
description: string;
lockAt?: string; // ISO 8601 date string
dueAt: string; // ISO 8601 date string
localAssignmentGroupName?: string;
submissionTypes: AssignmentSubmissionType[];
allowedFileUploadExtensions: string[];
rubric: RubricItem[];
}

View File

@@ -0,0 +1,4 @@
export interface RubricItem {
label: string;
points: number;
}

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 assignmentMarkdownStringifier = {
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

@@ -0,0 +1,138 @@
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

@@ -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[1]) {
return match[1].trim();
}
return "";
};

View File

@@ -0,0 +1,15 @@
import { LocalQuizQuestion } from "./localQuizQuestion";
export interface LocalQuiz {
name: string;
description: string;
password?: string;
lockAt?: string; // ISO 8601 date string
dueAt: string; // ISO 8601 date string
shuffleAnswers: boolean;
showCorrectAnswers: boolean;
oneQuestionAtATime: boolean;
localAssignmentGroupName?: string;
allowedAttempts: number;
questions: LocalQuizQuestion[];
}

View File

@@ -0,0 +1,16 @@
import { LocalQuizQuestionAnswer } from "./localQuizQuestionAnswer";
export interface LocalQuizQuestion {
text: string;
questionType: QuestionType;
points: number;
answers: LocalQuizQuestionAnswer[];
}
export enum QuestionType {
MultipleAnswers = "multiple_answers",
MultipleChoice = "multiple_choice",
Essay = "essay",
ShortAnswer = "short_answer",
Matching = "matching"
}

View File

@@ -0,0 +1,6 @@
export interface LocalQuizQuestionAnswer {
correct: boolean;
text: string;
matchedText?: string;
htmlText: string;
}

View File

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

View File

@@ -16,6 +16,7 @@ const getTerms = async () => {
};
export const canvasService = {
getTerms,
async getCourses(termId: number) {
const url = `courses`;
const coursesResponse =

9
nextjs/vitest.config.ts Normal file
View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
},
})