mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
running first test
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
13
nextjs/src/models/local/assignmnet/localAssignment.ts
Normal file
13
nextjs/src/models/local/assignmnet/localAssignment.ts
Normal 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[];
|
||||
}
|
||||
4
nextjs/src/models/local/assignmnet/rubricItem.ts
Normal file
4
nextjs/src/models/local/assignmnet/rubricItem.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface RubricItem {
|
||||
label: string;
|
||||
points: number;
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
10
nextjs/src/models/local/assignmnet/utils/markdownUtils.ts
Normal file
10
nextjs/src/models/local/assignmnet/utils/markdownUtils.ts
Normal 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 "";
|
||||
};
|
||||
15
nextjs/src/models/local/quiz/localQuiz.ts
Normal file
15
nextjs/src/models/local/quiz/localQuiz.ts
Normal 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[];
|
||||
}
|
||||
16
nextjs/src/models/local/quiz/localQuizQuestion.ts
Normal file
16
nextjs/src/models/local/quiz/localQuizQuestion.ts
Normal 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"
|
||||
}
|
||||
6
nextjs/src/models/local/quiz/localQuizQuestionAnswer.ts
Normal file
6
nextjs/src/models/local/quiz/localQuizQuestionAnswer.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface LocalQuizQuestionAnswer {
|
||||
correct: boolean;
|
||||
text: string;
|
||||
matchedText?: string;
|
||||
htmlText: string;
|
||||
}
|
||||
@@ -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);
|
||||
// });
|
||||
});
|
||||
@@ -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
9
nextjs/vitest.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user