mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
moving v2 to top level
This commit is contained in:
82
src/models/canvas/assignments/canvasAssignment.ts
Normal file
82
src/models/canvas/assignments/canvasAssignment.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { CanvasDiscussionTopicModel } from "../discussions/canvasDiscussionModelTopic";
|
||||
import { CanvasSubmissionModel } from "../submissions/canvasSubmissionModel";
|
||||
import { CanvasAssignmentDate } from "./canvasAssignmentDate";
|
||||
import { CanvasAssignmentOverride } from "./canvasAssignmentOverride";
|
||||
import { CanvasExternalToolTagAttributes } from "./canvasExternalToolTagAttributes";
|
||||
import { CanvasLockInfo } from "./canvasLockInfo";
|
||||
import { CanvasRubricCriteria } from "./canvasRubricCriteria";
|
||||
import { CanvasTurnitinSettings } from "./canvasTurnitinSettings";
|
||||
|
||||
export interface CanvasAssignment {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
created_at: string; // ISO 8601 date string
|
||||
has_overrides: boolean;
|
||||
course_id: number;
|
||||
html_url: string;
|
||||
submissions_download_url: string;
|
||||
assignment_group_id: number;
|
||||
due_date_required: boolean;
|
||||
max_name_length: number;
|
||||
peer_reviews: boolean;
|
||||
automatic_peer_reviews: boolean;
|
||||
position: number;
|
||||
grading_type: string;
|
||||
published: boolean;
|
||||
unpublishable: boolean;
|
||||
only_visible_to_overrides: boolean;
|
||||
locked_for_user: boolean;
|
||||
moderated_grading: boolean;
|
||||
grader_count: number;
|
||||
allowed_attempts: number;
|
||||
is_quiz_assignment: boolean;
|
||||
submission_types: string[];
|
||||
updated_at?: string; // ISO 8601 date string
|
||||
due_at?: string; // ISO 8601 date string
|
||||
lock_at?: string; // ISO 8601 date string
|
||||
unlock_at?: string; // ISO 8601 date string
|
||||
all_dates?: CanvasAssignmentDate[];
|
||||
allowed_extensions?: string[];
|
||||
turnitin_enabled?: boolean;
|
||||
vericite_enabled?: boolean;
|
||||
turnitin_settings?: CanvasTurnitinSettings;
|
||||
grade_group_students_individually?: boolean;
|
||||
external_tool_tag_attributes?: CanvasExternalToolTagAttributes;
|
||||
peer_review_count?: number;
|
||||
peer_reviews_assign_at?: string; // ISO 8601 date string
|
||||
intra_group_peer_reviews?: boolean;
|
||||
group_category_id?: number;
|
||||
needs_grading_count?: number;
|
||||
needs_grading_count_by_section?: {
|
||||
section_id: string;
|
||||
needs_grading_count: number;
|
||||
}[];
|
||||
post_to_sis?: boolean;
|
||||
integration_id?: string;
|
||||
integration_data?: any;
|
||||
muted?: boolean;
|
||||
points_possible?: number;
|
||||
has_submitted_submissions?: boolean;
|
||||
grading_standard_id?: number;
|
||||
lock_info?: CanvasLockInfo;
|
||||
lock_explanation?: string;
|
||||
quiz_id?: number;
|
||||
anonymous_submissions?: boolean;
|
||||
discussion_topic?: CanvasDiscussionTopicModel;
|
||||
freeze_on_copy?: boolean;
|
||||
frozen?: boolean;
|
||||
frozen_attributes?: string[];
|
||||
submission?: CanvasSubmissionModel;
|
||||
use_rubric_for_grading?: boolean;
|
||||
rubric_settings?: any;
|
||||
rubric?: CanvasRubricCriteria[];
|
||||
assignment_visibility?: number[];
|
||||
overrides?: CanvasAssignmentOverride[];
|
||||
omit_from_final_grade?: boolean;
|
||||
final_grader_id?: number;
|
||||
grader_comments_visible_to_graders?: boolean;
|
||||
graders_anonymous_to_graders?: boolean;
|
||||
grader_names_visible_to_final_grader?: boolean;
|
||||
anonymous_grading?: boolean;
|
||||
}
|
||||
8
src/models/canvas/assignments/canvasAssignmentDate.ts
Normal file
8
src/models/canvas/assignments/canvasAssignmentDate.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface CanvasAssignmentDate {
|
||||
title: string;
|
||||
id?: number;
|
||||
base?: boolean;
|
||||
due_at?: string; // ISO 8601 date string
|
||||
unlock_at?: string; // ISO 8601 date string
|
||||
lock_at?: string; // ISO 8601 date string
|
||||
}
|
||||
10
src/models/canvas/assignments/canvasAssignmentGroup.ts
Normal file
10
src/models/canvas/assignments/canvasAssignmentGroup.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface CanvasAssignmentGroup {
|
||||
id: number; // TypeScript doesn't have `ulong`, so using `number` for large integers.
|
||||
name: string;
|
||||
position: number;
|
||||
group_weight: number;
|
||||
// sis_source_id?: string; // Uncomment if needed.
|
||||
// integration_data?: Record<string, string>; // Uncomment if needed.
|
||||
// assignments?: CanvasAssignment[]; // Uncomment if needed, assuming CanvasAssignment is defined.
|
||||
// rules?: any; // Assuming 'rules' is of unknown type, so using 'any' here.
|
||||
}
|
||||
13
src/models/canvas/assignments/canvasAssignmentOverride.ts
Normal file
13
src/models/canvas/assignments/canvasAssignmentOverride.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export interface CanvasAssignmentOverride {
|
||||
id: number;
|
||||
assignment_id: number;
|
||||
course_section_id: number;
|
||||
title: string;
|
||||
student_ids?: number[];
|
||||
group_id?: number;
|
||||
due_at?: string; // ISO 8601 date string
|
||||
all_day?: boolean;
|
||||
all_day_date?: string; // ISO 8601 date string
|
||||
unlock_at?: string; // ISO 8601 date string
|
||||
lock_at?: string; // ISO 8601 date string
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface CanvasExternalToolTagAttributes {
|
||||
url: string;
|
||||
resource_link_id: string;
|
||||
new_tab?: boolean;
|
||||
}
|
||||
7
src/models/canvas/assignments/canvasLockInfo.ts
Normal file
7
src/models/canvas/assignments/canvasLockInfo.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface CanvasLockInfo {
|
||||
asset_string: string;
|
||||
unlock_at?: string; // ISO 8601 date string
|
||||
lock_at?: string; // ISO 8601 date string
|
||||
context_module?: any;
|
||||
manually_locked?: boolean;
|
||||
}
|
||||
13
src/models/canvas/assignments/canvasRubric.ts
Normal file
13
src/models/canvas/assignments/canvasRubric.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export interface CanvasRubric {
|
||||
id?: number;
|
||||
title: string;
|
||||
context_id: number;
|
||||
context_type: string;
|
||||
points_possible: number;
|
||||
reusable: boolean;
|
||||
read_only: boolean;
|
||||
hide_score_total?: boolean;
|
||||
// Uncomment and define if needed
|
||||
// data: CanvasRubricCriteria[];
|
||||
// free_form_criterion_comments?: boolean;
|
||||
}
|
||||
12
src/models/canvas/assignments/canvasRubricAssociation.ts
Normal file
12
src/models/canvas/assignments/canvasRubricAssociation.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface CanvasRubricAssociation {
|
||||
id: number;
|
||||
rubric_id: number;
|
||||
association_id: number;
|
||||
association_type: string;
|
||||
use_for_grading: boolean;
|
||||
summary_data?: string;
|
||||
purpose: string;
|
||||
hide_score_total?: boolean;
|
||||
hide_points: boolean;
|
||||
hide_outcome_results: boolean;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { CanvasRubric } from "./canvasRubric";
|
||||
import { CanvasRubricAssociation } from "./canvasRubricAssociation";
|
||||
|
||||
export interface CanvasRubricCreationResponse {
|
||||
rubric: CanvasRubric;
|
||||
rubric_association: CanvasRubricAssociation;
|
||||
}
|
||||
16
src/models/canvas/assignments/canvasRubricCriteria.ts
Normal file
16
src/models/canvas/assignments/canvasRubricCriteria.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface CanvasRubricCriteria {
|
||||
id: string;
|
||||
description: string;
|
||||
long_description: string;
|
||||
points?: number;
|
||||
learning_outcome_id?: string;
|
||||
vendor_guid?: string;
|
||||
criterion_use_range?: boolean;
|
||||
ratings?: {
|
||||
points: number;
|
||||
id: string;
|
||||
description: string;
|
||||
long_description: string;
|
||||
}[];
|
||||
ignore_for_scoring?: boolean;
|
||||
}
|
||||
10
src/models/canvas/assignments/canvasTurnitinSettings.ts
Normal file
10
src/models/canvas/assignments/canvasTurnitinSettings.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface CanvasTurnitinSettings {
|
||||
originality_report_visibility: string;
|
||||
s_paper_check: boolean;
|
||||
internet_check: boolean;
|
||||
journal_check: boolean;
|
||||
exclude_biblio: boolean;
|
||||
exclude_quoted: boolean;
|
||||
exclude_small_matches_type?: boolean;
|
||||
exclude_small_matches_value?: number;
|
||||
}
|
||||
3
src/models/canvas/courses/canvasCalendarLinkModel.ts
Normal file
3
src/models/canvas/courses/canvasCalendarLinkModel.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface CanvasCalendarLinkModel {
|
||||
ics: string;
|
||||
}
|
||||
56
src/models/canvas/courses/canvasCourseModel.ts
Normal file
56
src/models/canvas/courses/canvasCourseModel.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { CanvasEnrollmentModel } from "../enrollments/canvasEnrollmentModel";
|
||||
import { CanvasCalendarLinkModel } from "./canvasCalendarLinkModel";
|
||||
import { CanvasCourseProgressModel } from "./canvasCourseProgressModel";
|
||||
import { CanvasTermModel } from "./canvasTermModel";
|
||||
|
||||
export interface CanvasCourseModel {
|
||||
id: number;
|
||||
sis_course_id: string;
|
||||
uuid: string;
|
||||
integration_id: string;
|
||||
name: string;
|
||||
course_code: string;
|
||||
workflow_state: string;
|
||||
account_id: number;
|
||||
root_account_id: number;
|
||||
enrollment_term_id: number;
|
||||
created_at: string; // ISO 8601 date string
|
||||
locale: string;
|
||||
calendar: CanvasCalendarLinkModel;
|
||||
default_view: string;
|
||||
syllabus_body: string;
|
||||
permissions: { [key: string]: boolean };
|
||||
storage_quota_mb: number;
|
||||
storage_quota_used_mb: number;
|
||||
license: string;
|
||||
course_format: string;
|
||||
time_zone: string;
|
||||
sis_import_id?: number;
|
||||
grading_standard_id?: number;
|
||||
start_at?: string; // ISO 8601 date string
|
||||
end_at?: string; // ISO 8601 date string
|
||||
enrollments?: CanvasEnrollmentModel[];
|
||||
total_students?: number;
|
||||
needs_grading_count?: number;
|
||||
term?: CanvasTermModel;
|
||||
course_progress?: CanvasCourseProgressModel;
|
||||
apply_assignment_group_weights?: boolean;
|
||||
is_public?: boolean;
|
||||
is_public_to_auth_users?: boolean;
|
||||
public_syllabus?: boolean;
|
||||
public_syllabus_to_auth?: boolean;
|
||||
public_description?: string;
|
||||
hide_final_grades?: boolean;
|
||||
allow_student_assignment_edits?: boolean;
|
||||
allow_wiki_comments?: boolean;
|
||||
allow_student_forum_attachments?: boolean;
|
||||
open_enrollment?: boolean;
|
||||
self_enrollment?: boolean;
|
||||
restrict_enrollments_to_course_dates?: boolean;
|
||||
access_restricted_by_date?: boolean;
|
||||
blueprint?: boolean;
|
||||
blueprint_restrictions?: { [key: string]: boolean };
|
||||
blueprint_restrictions_by_object_type?: {
|
||||
[key: string]: { [key: string]: boolean };
|
||||
};
|
||||
}
|
||||
6
src/models/canvas/courses/canvasCourseProgressModel.ts
Normal file
6
src/models/canvas/courses/canvasCourseProgressModel.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface CanvasCourseProgressModel {
|
||||
requirement_count?: number;
|
||||
requirement_completed_count?: number;
|
||||
next_requirement_url?: string;
|
||||
completed_at?: string; // ISO 8601 date string
|
||||
}
|
||||
16
src/models/canvas/courses/canvasCourseSettingsModel.ts
Normal file
16
src/models/canvas/courses/canvasCourseSettingsModel.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface CanvasCourseSettingsModel {
|
||||
allow_final_grade_override: boolean;
|
||||
allow_student_discussion_topics: boolean;
|
||||
allow_student_forum_attachments: boolean;
|
||||
allow_student_discussion_editing: boolean;
|
||||
grading_standard_enabled: boolean;
|
||||
allow_student_organized_groups: boolean;
|
||||
hide_final_grades: boolean;
|
||||
hide_distribution_graphs: boolean;
|
||||
lock_all_announcements: boolean;
|
||||
restrict_student_past_view: boolean;
|
||||
restrict_student_future_view: boolean;
|
||||
show_announcements_on_home_page: boolean;
|
||||
home_page_announcement_limit: number;
|
||||
grading_standard_id?: number;
|
||||
}
|
||||
6
src/models/canvas/courses/canvasTermModel.ts
Normal file
6
src/models/canvas/courses/canvasTermModel.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface CanvasTermModel {
|
||||
id: number;
|
||||
name: string;
|
||||
start_at?: string; // ISO 8601 date string
|
||||
end_at?: string; // ISO 8601 date string
|
||||
}
|
||||
40
src/models/canvas/discussions/canvasDiscussionModelTopic.ts
Normal file
40
src/models/canvas/discussions/canvasDiscussionModelTopic.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { CanvasUserDisplayModel } from "../users/userDisplayModel";
|
||||
import { CanvasFileAttachmentModel } from "./canvasFileAttachmentModel";
|
||||
|
||||
export interface CanvasDiscussionTopicModel {
|
||||
id: number;
|
||||
title: string;
|
||||
message: string;
|
||||
html_url: string;
|
||||
read_state: string;
|
||||
subscription_hold: string;
|
||||
assignment_id: number;
|
||||
lock_explanation: string;
|
||||
user_name: string;
|
||||
topic_children: number[];
|
||||
podcast_url: string;
|
||||
discussion_type: string;
|
||||
attachments: CanvasFileAttachmentModel[];
|
||||
permissions: { [key: string]: boolean };
|
||||
author: CanvasUserDisplayModel;
|
||||
unread_count?: number;
|
||||
subscribed?: boolean;
|
||||
posted_at?: string; // ISO 8601 date string
|
||||
last_reply_at?: string; // ISO 8601 date string
|
||||
require_initial_post?: boolean;
|
||||
user_can_see_posts?: boolean;
|
||||
discussion_subentry_count?: number;
|
||||
delayed_post_at?: string; // ISO 8601 date string
|
||||
published?: boolean;
|
||||
lock_at?: string; // ISO 8601 date string
|
||||
locked?: boolean;
|
||||
pinned?: boolean;
|
||||
locked_for_user?: boolean;
|
||||
lock_info?: any;
|
||||
group_topic_children?: any;
|
||||
root_topic_id?: number;
|
||||
group_category_id?: number;
|
||||
allow_rating?: boolean;
|
||||
only_graders_can_rate?: boolean;
|
||||
sort_by_rating?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface CanvasFileAttachmentModel {
|
||||
content_type: string;
|
||||
url: string;
|
||||
filename: string;
|
||||
display_name: string;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export interface CanvasEnrollmentTermModel {
|
||||
id: number;
|
||||
name: string;
|
||||
sis_term_id?: string;
|
||||
sis_import_id?: number;
|
||||
start_at?: string; // ISO 8601 date string
|
||||
end_at?: string; // ISO 8601 date string
|
||||
grading_period_group_id?: number;
|
||||
workflow_state?: string;
|
||||
overrides?: {
|
||||
[key: string]: {
|
||||
start_at?: string; // ISO 8601 date string
|
||||
end_at?: string; // ISO 8601 date string
|
||||
};
|
||||
};
|
||||
}
|
||||
48
src/models/canvas/enrollments/canvasEnrollmentModel.ts
Normal file
48
src/models/canvas/enrollments/canvasEnrollmentModel.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { CanvasUserDisplayModel } from "../users/userDisplayModel";
|
||||
import { CanvasGradeModel } from "./canvasGradeModel";
|
||||
|
||||
export interface CanvasEnrollmentModel {
|
||||
id: number;
|
||||
course_id: number;
|
||||
enrollment_state: string;
|
||||
type: string;
|
||||
user_id: number;
|
||||
role: string;
|
||||
role_id: number;
|
||||
html_url: string;
|
||||
grades: CanvasGradeModel;
|
||||
user: CanvasUserDisplayModel;
|
||||
override_grade: string;
|
||||
sis_course_id?: string;
|
||||
course_integration_id?: string;
|
||||
course_section_id?: number;
|
||||
section_integration_id?: string;
|
||||
sis_account_id?: string;
|
||||
sis_section_id?: string;
|
||||
sis_user_id?: string;
|
||||
limit_privileges_to_course_section?: boolean;
|
||||
sis_import_id?: number;
|
||||
root_account_id?: number;
|
||||
associated_user_id?: number;
|
||||
created_at?: string; // ISO 8601 date string
|
||||
updated_at?: string; // ISO 8601 date string
|
||||
start_at?: string; // ISO 8601 date string
|
||||
end_at?: string; // ISO 8601 date string
|
||||
last_activity_at?: string; // ISO 8601 date string
|
||||
last_attended_at?: string; // ISO 8601 date string
|
||||
total_activity_time?: number;
|
||||
override_score?: number;
|
||||
unposted_current_grade?: string;
|
||||
unposted_final_grade?: string;
|
||||
unposted_current_score?: string;
|
||||
unposted_final_score?: string;
|
||||
has_grading_periods?: boolean;
|
||||
totals_for_all_grading_periods_option?: boolean;
|
||||
current_grading_period_title?: string;
|
||||
current_grading_period_id?: number;
|
||||
current_period_override_grade?: string;
|
||||
current_period_override_score?: number;
|
||||
current_period_unposted_final_score?: number;
|
||||
current_period_unposted_current_grade?: string;
|
||||
current_period_unposted_final_grade?: string;
|
||||
}
|
||||
11
src/models/canvas/enrollments/canvasGradeModel.ts
Normal file
11
src/models/canvas/enrollments/canvasGradeModel.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface CanvasGradeModel {
|
||||
html_url?: string;
|
||||
current_grade?: number;
|
||||
final_grade?: number;
|
||||
current_score?: number;
|
||||
final_score?: number;
|
||||
unposted_current_grade?: number;
|
||||
unposted_final_grade?: number;
|
||||
unposted_current_score?: number;
|
||||
unposted_final_score?: number;
|
||||
}
|
||||
18
src/models/canvas/modules/canvasModule.ts
Normal file
18
src/models/canvas/modules/canvasModule.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { CanvasModuleItem } from "./canvasModuleItems";
|
||||
|
||||
export interface CanvasModule {
|
||||
id: number;
|
||||
workflow_state: string;
|
||||
position: number;
|
||||
name: string;
|
||||
unlock_at?: string; // ISO 8601 date string
|
||||
require_sequential_progress?: boolean;
|
||||
prerequisite_module_ids?: number[];
|
||||
items_count: number;
|
||||
items_url: string;
|
||||
items?: CanvasModuleItem[];
|
||||
state?: string;
|
||||
completed_at?: string; // ISO 8601 date string
|
||||
publish_final_grade?: boolean;
|
||||
published?: boolean;
|
||||
}
|
||||
26
src/models/canvas/modules/canvasModuleItems.ts
Normal file
26
src/models/canvas/modules/canvasModuleItems.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export interface CanvasModuleItem {
|
||||
id: number;
|
||||
module_id: number;
|
||||
position: number;
|
||||
title: string;
|
||||
indent?: number;
|
||||
type: string;
|
||||
content_id?: number;
|
||||
html_url: string;
|
||||
url?: string;
|
||||
page_url?: string;
|
||||
external_url?: string;
|
||||
new_tab: boolean;
|
||||
completion_requirement?: {
|
||||
type: string;
|
||||
min_score?: number;
|
||||
completed?: boolean;
|
||||
};
|
||||
published?: boolean;
|
||||
content_details?: {
|
||||
due_at?: string; // ISO 8601 date string
|
||||
lock_at?: string; // ISO 8601 date string
|
||||
points_possible: number;
|
||||
locked_for_user: boolean;
|
||||
};
|
||||
}
|
||||
16
src/models/canvas/pages/canvasPageModel.ts
Normal file
16
src/models/canvas/pages/canvasPageModel.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface CanvasPage {
|
||||
page_id: number;
|
||||
url: string;
|
||||
title: string;
|
||||
published: boolean;
|
||||
front_page: boolean;
|
||||
body?: string;
|
||||
// Uncomment and define if needed
|
||||
// created_at: string; // ISO 8601 date string
|
||||
// updated_at: string; // ISO 8601 date string
|
||||
// editing_roles: string;
|
||||
// last_edited_by: UserDisplayModel;
|
||||
// locked_for_user: boolean;
|
||||
// lock_info?: LockInfoModel;
|
||||
// lock_explanation?: string;
|
||||
}
|
||||
19
src/models/canvas/quizzes/canvasQuizAnswerModel.ts
Normal file
19
src/models/canvas/quizzes/canvasQuizAnswerModel.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface CanvasQuizAnswer {
|
||||
id: number;
|
||||
text: string;
|
||||
html?: string;
|
||||
weight: number;
|
||||
// comments?: string;
|
||||
// text_after_answers?: string;
|
||||
// answer_match_left?: string;
|
||||
// answer_match_right?: string;
|
||||
// matching_answer_incorrect_matches?: string;
|
||||
// numerical_answer_type?: string;
|
||||
// exact?: number;
|
||||
// margin?: number;
|
||||
// approximate?: number;
|
||||
// precision?: number;
|
||||
// start?: number;
|
||||
// end?: number;
|
||||
// blank_id?: number;
|
||||
}
|
||||
44
src/models/canvas/quizzes/canvasQuizModel.ts
Normal file
44
src/models/canvas/quizzes/canvasQuizModel.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { CanvasLockInfo } from "../assignments/canvasLockInfo";
|
||||
import { CanvasQuizPermissions } from "./canvasQuizPermission";
|
||||
|
||||
export interface CanvasQuiz {
|
||||
id: number;
|
||||
title: string;
|
||||
html_url: string;
|
||||
mobile_url: string;
|
||||
preview_url?: string;
|
||||
description: string;
|
||||
quiz_type: string;
|
||||
assignment_group_id?: number;
|
||||
time_limit?: number;
|
||||
shuffle_answers?: boolean;
|
||||
hide_results?: string;
|
||||
show_correct_answers?: boolean;
|
||||
show_correct_answers_last_attempt?: boolean;
|
||||
show_correct_answers_at?: string; // ISO 8601 date string
|
||||
hide_correct_answers_at?: string; // ISO 8601 date string
|
||||
one_time_results?: boolean;
|
||||
scoring_policy?: string;
|
||||
allowed_attempts: number;
|
||||
one_question_at_a_time?: boolean;
|
||||
question_count?: number;
|
||||
points_possible?: number;
|
||||
cant_go_back?: boolean;
|
||||
access_code?: string;
|
||||
ip_filter?: string;
|
||||
due_at?: string; // ISO 8601 date string
|
||||
lock_at?: string; // ISO 8601 date string
|
||||
unlock_at?: string; // ISO 8601 date string
|
||||
published?: boolean;
|
||||
unpublishable?: boolean;
|
||||
locked_for_user?: boolean;
|
||||
lock_info?: CanvasLockInfo;
|
||||
lock_explanation?: string;
|
||||
speedgrader_url?: string;
|
||||
quiz_extensions_url?: string;
|
||||
permissions: CanvasQuizPermissions;
|
||||
all_dates?: any; // Depending on the structure of the dates, this could be further specified
|
||||
version_number?: number;
|
||||
question_types?: string[];
|
||||
anonymous_submissions?: boolean;
|
||||
}
|
||||
9
src/models/canvas/quizzes/canvasQuizPermission.ts
Normal file
9
src/models/canvas/quizzes/canvasQuizPermission.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface CanvasQuizPermissions {
|
||||
read: boolean;
|
||||
submit: boolean;
|
||||
create: boolean;
|
||||
manage: boolean;
|
||||
read_statistics: boolean;
|
||||
review_grades: boolean;
|
||||
update: boolean;
|
||||
}
|
||||
14
src/models/canvas/quizzes/canvasQuizQuestionModel.ts
Normal file
14
src/models/canvas/quizzes/canvasQuizQuestionModel.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { CanvasQuizAnswer } from "./canvasQuizAnswerModel";
|
||||
|
||||
export interface CanvasQuizQuestion {
|
||||
id: number;
|
||||
quiz_id: number;
|
||||
position?: number;
|
||||
question_name: string;
|
||||
question_type: string;
|
||||
question_text: string;
|
||||
correct_comments: string;
|
||||
incorrect_comments: string;
|
||||
neutral_comments: string;
|
||||
answers?: CanvasQuizAnswer[];
|
||||
}
|
||||
50
src/models/canvas/submissions/canvasSubmissionModel.ts
Normal file
50
src/models/canvas/submissions/canvasSubmissionModel.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { CanvasAssignment } from "../assignments/canvasAssignment";
|
||||
import { CanvasCourseModel } from "../courses/canvasCourseModel";
|
||||
import { CanvasUserModel } from "../users/canvasUserModel";
|
||||
import { CanvasUserDisplayModel } from "../users/userDisplayModel";
|
||||
|
||||
export interface CanvasSubmissionModel {
|
||||
assignment_id: number;
|
||||
grade: string;
|
||||
html_url: string;
|
||||
preview_url: string;
|
||||
submission_type: string;
|
||||
user_id: number;
|
||||
user: CanvasUserModel;
|
||||
workflow_state: string;
|
||||
late_policy_status: string;
|
||||
assignment?: CanvasAssignment;
|
||||
course?: CanvasCourseModel;
|
||||
attempt?: number;
|
||||
body?: string;
|
||||
grade_matches_current_submission?: boolean;
|
||||
score?: number;
|
||||
submission_comments?: {
|
||||
id: number;
|
||||
author_id: number;
|
||||
author_name: string;
|
||||
author: CanvasUserDisplayModel;
|
||||
comment: string;
|
||||
created_at: string; // ISO 8601 date string
|
||||
edited_at?: string; // ISO 8601 date string
|
||||
media_comment?: {
|
||||
content_type: string;
|
||||
display_name: string;
|
||||
media_id: string;
|
||||
media_type: string;
|
||||
url: string;
|
||||
};
|
||||
}[];
|
||||
submitted_at?: string; // ISO 8601 date string
|
||||
url?: string;
|
||||
grader_id?: number;
|
||||
graded_at?: string; // ISO 8601 date string
|
||||
late?: boolean;
|
||||
assignment_visible?: boolean;
|
||||
excused?: boolean;
|
||||
missing?: boolean;
|
||||
points_deducted?: number;
|
||||
seconds_late?: number;
|
||||
extra_attempts?: number;
|
||||
anonymous_id?: string;
|
||||
}
|
||||
21
src/models/canvas/users/canvasUserModel.ts
Normal file
21
src/models/canvas/users/canvasUserModel.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { CanvasEnrollmentModel } from "../enrollments/canvasEnrollmentModel";
|
||||
|
||||
export interface CanvasUserModel {
|
||||
id: number;
|
||||
name: string;
|
||||
sortable_name: string;
|
||||
short_name: string;
|
||||
sis_user_id: string;
|
||||
integration_id: string;
|
||||
login_id: string;
|
||||
avatar_url: string;
|
||||
enrollments: CanvasEnrollmentModel[];
|
||||
email: string;
|
||||
locale: string;
|
||||
effective_locale: string;
|
||||
time_zone: string;
|
||||
bio: string;
|
||||
permissions: { [key: string]: boolean };
|
||||
sis_import_id?: number;
|
||||
last_login?: string; // ISO 8601 date string
|
||||
}
|
||||
9
src/models/canvas/users/userDisplayModel.ts
Normal file
9
src/models/canvas/users/userDisplayModel.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface CanvasUserDisplayModel {
|
||||
avatar_image_url: string;
|
||||
html_url: string;
|
||||
anonymous_id: string;
|
||||
id?: number;
|
||||
short_name?: string;
|
||||
display_name?: string;
|
||||
pronouns?: string;
|
||||
}
|
||||
4
src/models/local/IModuleItem.ts
Normal file
4
src/models/local/IModuleItem.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface IModuleItem {
|
||||
name: string;
|
||||
dueAt: string;
|
||||
}
|
||||
26
src/models/local/assignment/assignmentSubmissionType.ts
Normal file
26
src/models/local/assignment/assignmentSubmissionType.ts
Normal 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;
|
||||
36
src/models/local/assignment/localAssignment.ts
Normal file
36
src/models/local/assignment/localAssignment.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { IModuleItem } from "../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[];
|
||||
}
|
||||
|
||||
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(),
|
||||
});
|
||||
|
||||
export const localAssignmentMarkdown = {
|
||||
parseMarkdown: assignmentMarkdownParser.parseMarkdown,
|
||||
toMarkdown: assignmentMarkdownSerializer.toMarkdown,
|
||||
};
|
||||
14
src/models/local/assignment/localAssignmentGroup.ts
Normal file
14
src/models/local/assignment/localAssignmentGroup.ts
Normal 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(),
|
||||
});
|
||||
16
src/models/local/assignment/rubricItem.ts
Normal file
16
src/models/local/assignment/rubricItem.ts
Normal 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());
|
||||
};
|
||||
146
src/models/local/assignment/utils/assignmentMarkdownParser.ts
Normal file
146
src/models/local/assignment/utils/assignmentMarkdownParser.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
verifyDateOrThrow,
|
||||
verifyDateStringOrUndefined,
|
||||
} from "../../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 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 dueAt = verifyDateOrThrow(rawDueAt, "DueAt");
|
||||
const lockAt = verifyDateStringOrUndefined(rawLockAt);
|
||||
|
||||
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 = {
|
||||
parseRubricMarkdown,
|
||||
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;
|
||||
},
|
||||
};
|
||||
@@ -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 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 {
|
||||
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;
|
||||
}
|
||||
},
|
||||
};
|
||||
12
src/models/local/assignment/utils/assignmentPointsUtils.ts
Normal file
12
src/models/local/assignment/utils/assignmentPointsUtils.ts
Normal 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;
|
||||
};
|
||||
10
src/models/local/assignment/utils/markdownUtils.ts
Normal file
10
src/models/local/assignment/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.length > 1 && match[1]) {
|
||||
return match[1].trim();
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
17
src/models/local/courseItemTypes.ts
Normal file
17
src/models/local/courseItemTypes.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { LocalAssignment } from "./assignment/localAssignment";
|
||||
import { LocalCoursePage } from "./page/localCoursePage";
|
||||
import { LocalQuiz } from "./quiz/localQuiz";
|
||||
|
||||
export type CourseItemType = "Assignment" | "Quiz" | "Page";
|
||||
export type CourseItemReturnType<T extends CourseItemType> = T extends "Assignment"
|
||||
? LocalAssignment
|
||||
: T extends "Quiz"
|
||||
? LocalQuiz
|
||||
: LocalCoursePage;
|
||||
|
||||
|
||||
export const typeToFolder = {
|
||||
Assignment: "assignments",
|
||||
Quiz: "quizzes",
|
||||
Page: "pages",
|
||||
} as const;
|
||||
13
src/models/local/lecture.ts
Normal file
13
src/models/local/lecture.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export interface Lecture {
|
||||
name: string
|
||||
date: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export const zodLecture = z.object({
|
||||
name: z.string(),
|
||||
date: z.string(),
|
||||
content: z.string(),
|
||||
});
|
||||
125
src/models/local/localCourseSettings.ts
Normal file
125
src/models/local/localCourseSettings.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
AssignmentSubmissionType,
|
||||
zodAssignmentSubmissionType,
|
||||
} from "./assignment/assignmentSubmissionType";
|
||||
import {
|
||||
LocalAssignmentGroup,
|
||||
zodLocalAssignmentGroup,
|
||||
} from "./assignment/localAssignmentGroup";
|
||||
import { parse, stringify } from "yaml";
|
||||
|
||||
// export interface LocalCourse {
|
||||
// modules: LocalModule[];
|
||||
// settings: LocalCourseSettings;
|
||||
// }
|
||||
|
||||
export interface SimpleTimeOnly {
|
||||
hour: number;
|
||||
minute: number;
|
||||
}
|
||||
export const zodSimpleTimeOnly = z.object({
|
||||
hour: z.number().int().min(0).max(23), // hour should be an integer between 0 and 23
|
||||
minute: z.number().int().min(0).max(59), // minute should be an integer between 0 and 59
|
||||
});
|
||||
|
||||
export enum DayOfWeek {
|
||||
Sunday = "Sunday",
|
||||
Monday = "Monday",
|
||||
Tuesday = "Tuesday",
|
||||
Wednesday = "Wednesday",
|
||||
Thursday = "Thursday",
|
||||
Friday = "Friday",
|
||||
Saturday = "Saturday",
|
||||
}
|
||||
|
||||
export const zodDayOfWeek = z.enum([
|
||||
DayOfWeek.Sunday,
|
||||
DayOfWeek.Monday,
|
||||
DayOfWeek.Tuesday,
|
||||
DayOfWeek.Wednesday,
|
||||
DayOfWeek.Thursday,
|
||||
DayOfWeek.Friday,
|
||||
DayOfWeek.Saturday,
|
||||
]);
|
||||
|
||||
export interface LocalCourseSettings {
|
||||
name: string;
|
||||
assignmentGroups: LocalAssignmentGroup[];
|
||||
daysOfWeek: DayOfWeek[];
|
||||
canvasId: number;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
defaultDueTime: SimpleTimeOnly;
|
||||
defaultLockHoursOffset?: number;
|
||||
defaultAssignmentSubmissionTypes: AssignmentSubmissionType[];
|
||||
defaultFileUploadTypes: string[];
|
||||
holidays: {
|
||||
name: string;
|
||||
days: string[];
|
||||
}[];
|
||||
assets: {
|
||||
sourceUrl: string;
|
||||
canvasUrl: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const zodLocalCourseSettings = z.object({
|
||||
name: z.string(),
|
||||
assignmentGroups: zodLocalAssignmentGroup.array(),
|
||||
daysOfWeek: zodDayOfWeek.array(),
|
||||
canvasId: z.number(),
|
||||
startDate: z.string(),
|
||||
endDate: z.string(),
|
||||
defaultDueTime: zodSimpleTimeOnly,
|
||||
defaultLockHoursOffset: z.number().int().optional(),
|
||||
defaultAssignmentSubmissionTypes: zodAssignmentSubmissionType.array(),
|
||||
defaultFileUploadTypes: z.string().array(),
|
||||
holidays: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
days: z.string().array(),
|
||||
})
|
||||
.array(),
|
||||
assets: z
|
||||
.object({
|
||||
sourceUrl: z.string(),
|
||||
canvasUrl: z.string(),
|
||||
})
|
||||
.array(),
|
||||
});
|
||||
|
||||
export function getDayOfWeek(date: Date): DayOfWeek {
|
||||
const dayIndex = date.getDay(); // Returns 0 for Sunday, 1 for Monday, etc.
|
||||
return DayOfWeek[Object.keys(DayOfWeek)[dayIndex] as keyof typeof DayOfWeek];
|
||||
}
|
||||
|
||||
export const localCourseYamlUtils = {
|
||||
parseSettingYaml: (settingsString: string): LocalCourseSettings => {
|
||||
const settings = parse(settingsString, {});
|
||||
return lowercaseFirstLetter(settings);
|
||||
},
|
||||
settingsToYaml: (settings: Omit<LocalCourseSettings, "name">) => {
|
||||
return stringify(settings);
|
||||
},
|
||||
};
|
||||
|
||||
function lowercaseFirstLetter<T>(obj: T): T {
|
||||
if (obj === null || typeof obj !== "object") return obj as T;
|
||||
|
||||
if (Array.isArray(obj)) return obj.map(lowercaseFirstLetter) as unknown as T;
|
||||
|
||||
const result: Record<string, any> = {};
|
||||
Object.keys(obj).forEach((key) => {
|
||||
const value = (obj as Record<string, any>)[key];
|
||||
const newKey = key.charAt(0).toLowerCase() + key.slice(1);
|
||||
|
||||
if (value && typeof value === "object") {
|
||||
result[newKey] = lowercaseFirstLetter(value);
|
||||
} else {
|
||||
result[newKey] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return result as T;
|
||||
}
|
||||
75
src/models/local/localModules.ts
Normal file
75
src/models/local/localModules.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { LocalAssignment } from "./assignment/localAssignment";
|
||||
import { IModuleItem } from "./IModuleItem";
|
||||
import { LocalCoursePage } from "./page/localCoursePage";
|
||||
import { LocalQuiz } from "./quiz/localQuiz";
|
||||
import { getDateFromString } from "./utils/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<T>(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<string, number>();
|
||||
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<T>(hash: Map<string, number>, items: T[]): void {
|
||||
for (const item of items) {
|
||||
hash.set(JSON.stringify(item), 1);
|
||||
}
|
||||
},
|
||||
};
|
||||
43
src/models/local/page/localCoursePage.ts
Normal file
43
src/models/local/page/localCoursePage.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { z } from "zod";
|
||||
import { extractLabelValue } from "../assignment/utils/markdownUtils";
|
||||
import { IModuleItem } from "../IModuleItem";
|
||||
import { verifyDateOrThrow } from "../utils/timeUtils";
|
||||
|
||||
export interface LocalCoursePage extends IModuleItem {
|
||||
name: string;
|
||||
text: string;
|
||||
dueAt: string;
|
||||
}
|
||||
|
||||
export const zodLocalCoursePage = z.object({
|
||||
name: z.string(),
|
||||
text: z.string(),
|
||||
dueAt: z.string(), // ISO 8601 date string
|
||||
});
|
||||
|
||||
export const localPageMarkdownUtils = {
|
||||
toMarkdown: (page: LocalCoursePage) => {
|
||||
const printableDueDate = verifyDateOrThrow(
|
||||
page.dueAt,
|
||||
"page DueDateForOrdering"
|
||||
);
|
||||
const settingsMarkdown = `Name: ${page.name}\nDueDateForOrdering: ${printableDueDate}\n---\n`;
|
||||
return settingsMarkdown + page.text;
|
||||
},
|
||||
|
||||
parseMarkdown: (pageMarkdown: string) => {
|
||||
const rawSettings = pageMarkdown.split("---")[0];
|
||||
const name = extractLabelValue(rawSettings, "Name");
|
||||
const rawDate = extractLabelValue(rawSettings, "DueDateForOrdering");
|
||||
const dueAt = verifyDateOrThrow(rawDate, "page DueDateForOrdering");
|
||||
|
||||
const text = pageMarkdown.split("---\n")[1];
|
||||
|
||||
const page: LocalCoursePage = {
|
||||
name,
|
||||
dueAt,
|
||||
text,
|
||||
};
|
||||
return page;
|
||||
},
|
||||
};
|
||||
37
src/models/local/quiz/localQuiz.ts
Normal file
37
src/models/local/quiz/localQuiz.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { z } from "zod";
|
||||
import { IModuleItem } from "../IModuleItem";
|
||||
import { LocalQuizQuestion, zodLocalQuizQuestion } from "./localQuizQuestion";
|
||||
import { quizMarkdownUtils } from "./utils/quizMarkdownUtils";
|
||||
|
||||
export interface LocalQuiz extends IModuleItem {
|
||||
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[];
|
||||
}
|
||||
|
||||
export const zodLocalQuiz = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
password: z.string().optional(),
|
||||
lockAt: z.string().optional(),
|
||||
dueAt: z.string(),
|
||||
shuffleAnswers: z.boolean(),
|
||||
showCorrectAnswers: z.boolean(),
|
||||
oneQuestionAtATime: z.boolean(),
|
||||
localAssignmentGroupName: z.string().optional(),
|
||||
allowedAttempts: z.number(),
|
||||
questions: zodLocalQuizQuestion.array(),
|
||||
});
|
||||
|
||||
export const localQuizMarkdownUtils = {
|
||||
parseMarkdown: quizMarkdownUtils.parseMarkdown,
|
||||
toMarkdown: quizMarkdownUtils.toMarkdown,
|
||||
};
|
||||
38
src/models/local/quiz/localQuizQuestion.ts
Normal file
38
src/models/local/quiz/localQuizQuestion.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
LocalQuizQuestionAnswer,
|
||||
zodLocalQuizQuestionAnswer,
|
||||
} from "./localQuizQuestionAnswer";
|
||||
|
||||
export enum QuestionType {
|
||||
MULTIPLE_ANSWERS = "multiple_answers",
|
||||
MULTIPLE_CHOICE = "multiple_choice",
|
||||
ESSAY = "essay",
|
||||
SHORT_ANSWER = "short_answer",
|
||||
MATCHING = "matching",
|
||||
NONE = "",
|
||||
}
|
||||
|
||||
export const zodQuestionType = z.enum([
|
||||
QuestionType.MULTIPLE_ANSWERS,
|
||||
QuestionType.MULTIPLE_CHOICE,
|
||||
QuestionType.ESSAY,
|
||||
QuestionType.SHORT_ANSWER,
|
||||
QuestionType.MATCHING,
|
||||
QuestionType.NONE,
|
||||
]);
|
||||
|
||||
export interface LocalQuizQuestion {
|
||||
text: string;
|
||||
questionType: QuestionType;
|
||||
points: number;
|
||||
answers: LocalQuizQuestionAnswer[];
|
||||
matchDistractors: string[];
|
||||
}
|
||||
export const zodLocalQuizQuestion = z.object({
|
||||
text: z.string(),
|
||||
questionType: zodQuestionType,
|
||||
points: z.number(),
|
||||
answers: zodLocalQuizQuestionAnswer.array(),
|
||||
matchDistractors: z.array(z.string()),
|
||||
});
|
||||
13
src/models/local/quiz/localQuizQuestionAnswer.ts
Normal file
13
src/models/local/quiz/localQuizQuestionAnswer.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export interface LocalQuizQuestionAnswer {
|
||||
correct: boolean;
|
||||
text: string;
|
||||
matchedText?: string;
|
||||
}
|
||||
|
||||
export const zodLocalQuizQuestionAnswer = z.object({
|
||||
correct: z.boolean(),
|
||||
text: z.string(),
|
||||
matchedText: z.string().optional(),
|
||||
});
|
||||
154
src/models/local/quiz/utils/quizMarkdownUtils.ts
Normal file
154
src/models/local/quiz/utils/quizMarkdownUtils.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
verifyDateOrThrow,
|
||||
verifyDateStringOrUndefined,
|
||||
} from "../../utils/timeUtils";
|
||||
import { LocalQuiz } from "../localQuiz";
|
||||
import { quizQuestionMarkdownUtils } from "./quizQuestionMarkdownUtils";
|
||||
|
||||
const extractLabelValue = (input: string, label: string): string => {
|
||||
const pattern = new RegExp(`${label}: (.*?)\n`);
|
||||
const match = pattern.exec(input);
|
||||
return match ? match[1].trim() : "";
|
||||
};
|
||||
|
||||
const extractDescription = (input: string): string => {
|
||||
const pattern = new RegExp("Description: (.*?)$", "s");
|
||||
const match = pattern.exec(input);
|
||||
return match ? match[1].trim() : "";
|
||||
};
|
||||
|
||||
const parseBooleanOrThrow = (value: string, label: string): boolean => {
|
||||
if (value.toLowerCase() === "true") return true;
|
||||
if (value.toLowerCase() === "false") return false;
|
||||
throw new Error(`Error with ${label}: ${value}`);
|
||||
};
|
||||
|
||||
const parseBooleanOrDefault = (
|
||||
value: string,
|
||||
label: string,
|
||||
defaultValue: boolean
|
||||
): boolean => {
|
||||
if (value.toLowerCase() === "true") return true;
|
||||
if (value.toLowerCase() === "false") return false;
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
const parseNumberOrThrow = (value: string, label: string): number => {
|
||||
const parsed = parseInt(value, 10);
|
||||
if (isNaN(parsed)) {
|
||||
throw new Error(`Error with ${label}: ${value}`);
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
const getQuizWithOnlySettings = (settings: string): LocalQuiz => {
|
||||
const name = extractLabelValue(settings, "Name");
|
||||
|
||||
const rawShuffleAnswers = extractLabelValue(settings, "ShuffleAnswers");
|
||||
const shuffleAnswers = parseBooleanOrThrow(
|
||||
rawShuffleAnswers,
|
||||
"ShuffleAnswers"
|
||||
);
|
||||
|
||||
const password = extractLabelValue(settings, "Password") || undefined;
|
||||
|
||||
const rawShowCorrectAnswers = extractLabelValue(
|
||||
settings,
|
||||
"ShowCorrectAnswers"
|
||||
);
|
||||
const showCorrectAnswers = parseBooleanOrDefault(
|
||||
rawShowCorrectAnswers,
|
||||
"ShowCorrectAnswers",
|
||||
true
|
||||
);
|
||||
|
||||
const rawOneQuestionAtATime = extractLabelValue(
|
||||
settings,
|
||||
"OneQuestionAtATime"
|
||||
);
|
||||
const oneQuestionAtATime = parseBooleanOrThrow(
|
||||
rawOneQuestionAtATime,
|
||||
"OneQuestionAtATime"
|
||||
);
|
||||
|
||||
const rawAllowedAttempts = extractLabelValue(settings, "AllowedAttempts");
|
||||
const allowedAttempts = parseNumberOrThrow(
|
||||
rawAllowedAttempts,
|
||||
"AllowedAttempts"
|
||||
);
|
||||
|
||||
const rawDueAt = extractLabelValue(settings, "DueAt");
|
||||
const dueAt = verifyDateOrThrow(rawDueAt, "DueAt");
|
||||
|
||||
const rawLockAt = extractLabelValue(settings, "LockAt");
|
||||
const lockAt = verifyDateStringOrUndefined(rawLockAt);
|
||||
|
||||
const description = extractDescription(settings);
|
||||
const localAssignmentGroupName = extractLabelValue(
|
||||
settings,
|
||||
"AssignmentGroup"
|
||||
);
|
||||
|
||||
const quiz: LocalQuiz = {
|
||||
name,
|
||||
description,
|
||||
password,
|
||||
lockAt,
|
||||
dueAt,
|
||||
shuffleAnswers,
|
||||
showCorrectAnswers,
|
||||
oneQuestionAtATime,
|
||||
localAssignmentGroupName,
|
||||
allowedAttempts,
|
||||
questions: [],
|
||||
};
|
||||
return quiz;
|
||||
};
|
||||
|
||||
export const quizMarkdownUtils = {
|
||||
toMarkdown(quiz: LocalQuiz): string {
|
||||
if (!quiz) {
|
||||
throw Error(`quiz was undefined, cannot parse markdown`);
|
||||
}
|
||||
if (
|
||||
typeof quiz.questions === "undefined" ||
|
||||
typeof quiz.oneQuestionAtATime === "undefined"
|
||||
) {
|
||||
console.log("quiz is probably not a quiz", quiz);
|
||||
throw Error(`quiz ${quiz.name} is probably not a quiz`);
|
||||
}
|
||||
const questionMarkdownArray = quiz.questions.map((q) =>
|
||||
quizQuestionMarkdownUtils.toMarkdown(q)
|
||||
);
|
||||
const questionDelimiter = "\n\n---\n\n";
|
||||
const questionMarkdown = questionMarkdownArray.join(questionDelimiter);
|
||||
|
||||
return `Name: ${quiz.name}
|
||||
LockAt: ${quiz.lockAt ?? ""}
|
||||
DueAt: ${quiz.dueAt}
|
||||
Password: ${quiz.password ?? ""}
|
||||
ShuffleAnswers: ${quiz.shuffleAnswers.toString().toLowerCase()}
|
||||
ShowCorrectAnswers: ${quiz.showCorrectAnswers.toString().toLowerCase()}
|
||||
OneQuestionAtATime: ${quiz.oneQuestionAtATime.toString().toLowerCase()}
|
||||
AssignmentGroup: ${quiz.localAssignmentGroupName}
|
||||
AllowedAttempts: ${quiz.allowedAttempts}
|
||||
Description: ${quiz.description}
|
||||
---
|
||||
${questionMarkdown}`;
|
||||
},
|
||||
|
||||
parseMarkdown(input: string): LocalQuiz {
|
||||
const splitInput = input.split("---\n");
|
||||
const settings = splitInput[0];
|
||||
const quizWithoutQuestions = getQuizWithOnlySettings(settings);
|
||||
|
||||
const rawQuestions = splitInput.slice(1);
|
||||
const questions = rawQuestions
|
||||
.filter((str) => str.trim().length > 0)
|
||||
.map((q, i) => quizQuestionMarkdownUtils.parseMarkdown(q, i));
|
||||
|
||||
return {
|
||||
...quizWithoutQuestions,
|
||||
questions,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { QuestionType } from "../localQuizQuestion";
|
||||
import { LocalQuizQuestionAnswer } from "../localQuizQuestionAnswer";
|
||||
|
||||
export const quizQuestionAnswerMarkdownUtils = {
|
||||
// getHtmlText(): string {
|
||||
// return MarkdownService.render(this.text);
|
||||
// }
|
||||
|
||||
parseMarkdown(input: string, questionType: string): LocalQuizQuestionAnswer {
|
||||
const isCorrect = input.startsWith("*") || input[1] === "*";
|
||||
|
||||
if (questionType === QuestionType.MATCHING) {
|
||||
const matchingPattern = /^\^ ?/;
|
||||
const textWithoutMatchDelimiter = input
|
||||
.replace(matchingPattern, "")
|
||||
.trim();
|
||||
const [text, ...matchedParts] = textWithoutMatchDelimiter.split("-");
|
||||
const answer: LocalQuizQuestionAnswer = {
|
||||
correct: true,
|
||||
text: text.trim(),
|
||||
matchedText: matchedParts.join("-").trim(),
|
||||
};
|
||||
return answer;
|
||||
}
|
||||
|
||||
const startingQuestionPattern = /^(\*?[a-z]?\))|\[\s*\]|\[\*\]|\^ /;
|
||||
|
||||
let replaceCount = 0;
|
||||
const text = input
|
||||
.replace(startingQuestionPattern, (m) => (replaceCount++ === 0 ? "" : m))
|
||||
.trim();
|
||||
|
||||
const answer: LocalQuizQuestionAnswer = {
|
||||
correct: isCorrect,
|
||||
text: text,
|
||||
};
|
||||
return answer;
|
||||
},
|
||||
};
|
||||
241
src/models/local/quiz/utils/quizQuestionMarkdownUtils.ts
Normal file
241
src/models/local/quiz/utils/quizQuestionMarkdownUtils.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { LocalQuiz } from "../localQuiz";
|
||||
import { LocalQuizQuestion, QuestionType } from "../localQuizQuestion";
|
||||
import { LocalQuizQuestionAnswer } from "../localQuizQuestionAnswer";
|
||||
import { quizQuestionAnswerMarkdownUtils } from "./quizQuestionAnswerMarkdownUtils";
|
||||
|
||||
const _validFirstAnswerDelimiters = [
|
||||
"*a)",
|
||||
"a)",
|
||||
"*)",
|
||||
")",
|
||||
"[ ]",
|
||||
"[]",
|
||||
"[*]",
|
||||
"^",
|
||||
];
|
||||
const _multipleChoicePrefix = ["a)", "*a)", "*)", ")"];
|
||||
const _multipleAnswerPrefix = ["[ ]", "[*]", "[]"];
|
||||
|
||||
const getAnswerStringsWithMultilineSupport = (
|
||||
linesWithoutPoints: string[],
|
||||
questionIndex: number
|
||||
) => {
|
||||
const indexOfAnswerStart = linesWithoutPoints.findIndex((l) =>
|
||||
_validFirstAnswerDelimiters.some((prefix) =>
|
||||
l.trimStart().startsWith(prefix)
|
||||
)
|
||||
);
|
||||
if (indexOfAnswerStart === -1) {
|
||||
const debugLine = linesWithoutPoints.find((l) => l.trim().length > 0);
|
||||
throw Error(
|
||||
`question ${
|
||||
questionIndex + 1
|
||||
}: no answers when detecting question type on ${debugLine}`
|
||||
);
|
||||
}
|
||||
|
||||
const answerLinesRaw = linesWithoutPoints.slice(indexOfAnswerStart);
|
||||
|
||||
const answerStartPattern = /^(\*?[a-z]?\))|(?<!\S)\[\s*\]|\[\*\]|\^/;
|
||||
const answerLines = answerLinesRaw.reduce((acc: string[], line: string) => {
|
||||
const isNewAnswer = answerStartPattern.test(line);
|
||||
if (isNewAnswer) {
|
||||
acc.push(line);
|
||||
} else if (acc.length !== 0) {
|
||||
acc[acc.length - 1] += "\n" + line;
|
||||
} else {
|
||||
acc.push(line);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
return answerLines;
|
||||
};
|
||||
const getQuestionType = (
|
||||
linesWithoutPoints: string[],
|
||||
questionIndex: number
|
||||
): QuestionType => {
|
||||
if (linesWithoutPoints.length === 0) return QuestionType.NONE;
|
||||
if (
|
||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase() === "essay"
|
||||
)
|
||||
return QuestionType.ESSAY;
|
||||
if (
|
||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase() ===
|
||||
"short answer"
|
||||
)
|
||||
return QuestionType.SHORT_ANSWER;
|
||||
if (
|
||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase() ===
|
||||
"short_answer"
|
||||
)
|
||||
return QuestionType.SHORT_ANSWER;
|
||||
|
||||
const answerLines = getAnswerStringsWithMultilineSupport(
|
||||
linesWithoutPoints,
|
||||
questionIndex
|
||||
);
|
||||
const firstAnswerLine = answerLines[0];
|
||||
const isMultipleChoice = _multipleChoicePrefix.some((prefix) =>
|
||||
firstAnswerLine.startsWith(prefix)
|
||||
);
|
||||
|
||||
if (isMultipleChoice) return QuestionType.MULTIPLE_CHOICE;
|
||||
|
||||
const isMultipleAnswer = _multipleAnswerPrefix.some((prefix) =>
|
||||
firstAnswerLine.startsWith(prefix)
|
||||
);
|
||||
if (isMultipleAnswer) return QuestionType.MULTIPLE_ANSWERS;
|
||||
|
||||
const isMatching = firstAnswerLine.startsWith("^");
|
||||
if (isMatching) return QuestionType.MATCHING;
|
||||
|
||||
return QuestionType.NONE;
|
||||
};
|
||||
|
||||
const getAnswers = (
|
||||
linesWithoutPoints: string[],
|
||||
questionIndex: number,
|
||||
questionType: string
|
||||
): LocalQuizQuestionAnswer[] => {
|
||||
const answerLines = getAnswerStringsWithMultilineSupport(
|
||||
linesWithoutPoints,
|
||||
questionIndex
|
||||
);
|
||||
|
||||
const answers = answerLines.map((a, i) =>
|
||||
quizQuestionAnswerMarkdownUtils.parseMarkdown(a, questionType)
|
||||
);
|
||||
return answers;
|
||||
};
|
||||
|
||||
const getAnswerMarkdown = (
|
||||
question: LocalQuizQuestion,
|
||||
answer: LocalQuizQuestionAnswer,
|
||||
index: number
|
||||
): string => {
|
||||
const multilineMarkdownCompatibleText = answer.text.startsWith("```")
|
||||
? "\n" + answer.text
|
||||
: answer.text;
|
||||
|
||||
if (question.questionType === "multiple_answers") {
|
||||
const correctIndicator = answer.correct ? "*" : " ";
|
||||
const questionTypeIndicator = `[${correctIndicator}] `;
|
||||
|
||||
return `${questionTypeIndicator}${multilineMarkdownCompatibleText}`;
|
||||
} else if (question.questionType === "matching") {
|
||||
return `^ ${answer.text} - ${answer.matchedText}`;
|
||||
} else {
|
||||
const questionLetter = String.fromCharCode(97 + index);
|
||||
const correctIndicator = answer.correct ? "*" : "";
|
||||
const questionTypeIndicator = `${correctIndicator}${questionLetter}) `;
|
||||
|
||||
return `${questionTypeIndicator}${multilineMarkdownCompatibleText}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const quizQuestionMarkdownUtils = {
|
||||
toMarkdown(question: LocalQuizQuestion): string {
|
||||
const answerArray = question.answers.map((a, i) =>
|
||||
getAnswerMarkdown(question, a, i)
|
||||
);
|
||||
|
||||
const distractorText =
|
||||
question.questionType === QuestionType.MATCHING
|
||||
? question.matchDistractors?.map((d) => `\n^ - ${d}`).join("") ?? ""
|
||||
: "";
|
||||
|
||||
const answersText = answerArray.join("\n");
|
||||
const questionTypeIndicator =
|
||||
question.questionType === "essay" ||
|
||||
question.questionType === "short_answer"
|
||||
? question.questionType
|
||||
: "";
|
||||
|
||||
return `Points: ${question.points}\n${question.text}\n${answersText}${distractorText}${questionTypeIndicator}`;
|
||||
},
|
||||
|
||||
parseMarkdown(input: string, questionIndex: number): LocalQuizQuestion {
|
||||
const lines = input.trim().split("\n");
|
||||
const firstLineIsPoints = lines[0].toLowerCase().includes("points: ");
|
||||
|
||||
const textHasPoints =
|
||||
lines.length > 0 &&
|
||||
lines[0].includes(": ") &&
|
||||
lines[0].split(": ").length > 1 &&
|
||||
!isNaN(parseFloat(lines[0].split(": ")[1]));
|
||||
|
||||
const points =
|
||||
firstLineIsPoints && textHasPoints
|
||||
? parseFloat(lines[0].split(": ")[1])
|
||||
: 1;
|
||||
|
||||
const linesWithoutPoints = firstLineIsPoints ? lines.slice(1) : lines;
|
||||
|
||||
const { linesWithoutAnswers } = linesWithoutPoints.reduce(
|
||||
({ linesWithoutAnswers, taking }, currentLine) => {
|
||||
if (!taking)
|
||||
return { linesWithoutAnswers: linesWithoutAnswers, taking: false };
|
||||
|
||||
const lineIsAnswer = _validFirstAnswerDelimiters.some((prefix) =>
|
||||
currentLine.trimStart().startsWith(prefix)
|
||||
);
|
||||
if (lineIsAnswer)
|
||||
return { linesWithoutAnswers: linesWithoutAnswers, taking: false };
|
||||
|
||||
return {
|
||||
linesWithoutAnswers: [...linesWithoutAnswers, currentLine],
|
||||
taking: true,
|
||||
};
|
||||
},
|
||||
{ linesWithoutAnswers: [] as string[], taking: true }
|
||||
);
|
||||
const questionType = getQuestionType(linesWithoutPoints, questionIndex);
|
||||
|
||||
const questionTypesWithoutAnswers = [
|
||||
"essay",
|
||||
"short answer",
|
||||
"short_answer",
|
||||
];
|
||||
|
||||
const descriptionLines = questionTypesWithoutAnswers.includes(
|
||||
questionType.toLowerCase()
|
||||
)
|
||||
? linesWithoutAnswers
|
||||
.slice(0, linesWithoutPoints.length)
|
||||
.filter(
|
||||
(line, index) =>
|
||||
!questionTypesWithoutAnswers.includes(line.toLowerCase())
|
||||
)
|
||||
: linesWithoutAnswers;
|
||||
|
||||
const description = descriptionLines.join("\n");
|
||||
|
||||
const typesWithAnswers = [
|
||||
"multiple_choice",
|
||||
"multiple_answers",
|
||||
"matching",
|
||||
];
|
||||
const answers = typesWithAnswers.includes(questionType)
|
||||
? getAnswers(linesWithoutPoints, questionIndex, questionType)
|
||||
: [];
|
||||
|
||||
const answersWithoutDistractors =
|
||||
questionType === QuestionType.MATCHING
|
||||
? answers.filter((a) => a.text)
|
||||
: answers;
|
||||
|
||||
const distractors =
|
||||
questionType === QuestionType.MATCHING
|
||||
? answers.filter((a) => !a.text).map((a) => a.matchedText ?? "")
|
||||
: [];
|
||||
|
||||
const question: LocalQuizQuestion = {
|
||||
text: description,
|
||||
questionType,
|
||||
points,
|
||||
answers: answersWithoutDistractors,
|
||||
matchDistractors: distractors,
|
||||
};
|
||||
return question;
|
||||
},
|
||||
};
|
||||
159
src/models/local/tests/assignmentMarkdown.test.ts
Normal file
159
src/models/local/tests/assignmentMarkdown.test.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { LocalAssignment } from "../assignment/localAssignment";
|
||||
import { AssignmentSubmissionType } from "../assignment/assignmentSubmissionType";
|
||||
import { assignmentMarkdownSerializer } from "../assignment/utils/assignmentMarkdownSerializer";
|
||||
import { assignmentMarkdownParser } from "../assignment/utils/assignmentMarkdownParser";
|
||||
|
||||
describe("AssignmentMarkdownTests", () => {
|
||||
it("can parse assignment settings", () => {
|
||||
const assignment: LocalAssignment = {
|
||||
name: "test assignment",
|
||||
description: "here is the description",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [
|
||||
{ points: 4, label: "do task 1" },
|
||||
{ points: 2, label: "do task 2" },
|
||||
],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
|
||||
const assignmentMarkdown =
|
||||
assignmentMarkdownSerializer.toMarkdown(assignment);
|
||||
const parsedAssignment =
|
||||
assignmentMarkdownParser.parseMarkdown(assignmentMarkdown);
|
||||
|
||||
expect(parsedAssignment).toEqual(assignment);
|
||||
});
|
||||
|
||||
it("assignment with empty rubric can be parsed", () => {
|
||||
const assignment: LocalAssignment = {
|
||||
name: "test assignment",
|
||||
description: "here is the description",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
|
||||
const assignmentMarkdown =
|
||||
assignmentMarkdownSerializer.toMarkdown(assignment);
|
||||
const parsedAssignment =
|
||||
assignmentMarkdownParser.parseMarkdown(assignmentMarkdown);
|
||||
|
||||
expect(parsedAssignment).toEqual(assignment);
|
||||
});
|
||||
|
||||
it("assignment with empty submission types can be parsed", () => {
|
||||
const assignment: LocalAssignment = {
|
||||
name: "test assignment",
|
||||
description: "here is the description",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [
|
||||
{ points: 4, label: "do task 1" },
|
||||
{ points: 2, label: "do task 2" },
|
||||
],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
|
||||
const assignmentMarkdown =
|
||||
assignmentMarkdownSerializer.toMarkdown(assignment);
|
||||
const parsedAssignment =
|
||||
assignmentMarkdownParser.parseMarkdown(assignmentMarkdown);
|
||||
|
||||
expect(parsedAssignment).toEqual(assignment);
|
||||
});
|
||||
|
||||
it("assignment without lockAt date can be parsed", () => {
|
||||
const assignment: LocalAssignment = {
|
||||
name: "test assignment",
|
||||
description: "here is the description",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: undefined,
|
||||
submissionTypes: [],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [
|
||||
{ points: 4, label: "do task 1" },
|
||||
{ points: 2, label: "do task 2" },
|
||||
],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
|
||||
const assignmentMarkdown =
|
||||
assignmentMarkdownSerializer.toMarkdown(assignment);
|
||||
const parsedAssignment =
|
||||
assignmentMarkdownParser.parseMarkdown(assignmentMarkdown);
|
||||
|
||||
expect(parsedAssignment).toEqual(assignment);
|
||||
});
|
||||
|
||||
it("assignment without description can be parsed", () => {
|
||||
const assignment: LocalAssignment = {
|
||||
name: "test assignment",
|
||||
description: "",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [
|
||||
{ points: 4, label: "do task 1" },
|
||||
{ points: 2, label: "do task 2" },
|
||||
],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
|
||||
const assignmentMarkdown =
|
||||
assignmentMarkdownSerializer.toMarkdown(assignment);
|
||||
const parsedAssignment =
|
||||
assignmentMarkdownParser.parseMarkdown(assignmentMarkdown);
|
||||
|
||||
expect(parsedAssignment).toEqual(assignment);
|
||||
});
|
||||
|
||||
it("assignments can have three dashes", () => {
|
||||
const assignment: LocalAssignment = {
|
||||
name: "test assignment",
|
||||
description: "test assignment\n---\nsomestuff",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
|
||||
const assignmentMarkdown =
|
||||
assignmentMarkdownSerializer.toMarkdown(assignment);
|
||||
const parsedAssignment =
|
||||
assignmentMarkdownParser.parseMarkdown(assignmentMarkdown);
|
||||
|
||||
expect(parsedAssignment).toEqual(assignment);
|
||||
});
|
||||
|
||||
it("assignments can restrict upload types", () => {
|
||||
const assignment: LocalAssignment = {
|
||||
name: "test assignment",
|
||||
description: "here is the description",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
|
||||
allowedFileUploadExtensions: ["pdf", "txt"],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [],
|
||||
};
|
||||
|
||||
const assignmentMarkdown =
|
||||
assignmentMarkdownSerializer.toMarkdown(assignment);
|
||||
const parsedAssignment =
|
||||
assignmentMarkdownParser.parseMarkdown(assignmentMarkdown);
|
||||
|
||||
expect(parsedAssignment).toEqual(assignment);
|
||||
});
|
||||
});
|
||||
18
src/models/local/tests/pageMarkdown.test.ts
Normal file
18
src/models/local/tests/pageMarkdown.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { LocalCoursePage, localPageMarkdownUtils } from "../page/localCoursePage";
|
||||
|
||||
describe("PageMarkdownTests", () => {
|
||||
it("can parse page", () => {
|
||||
const page: LocalCoursePage = {
|
||||
name: "test title",
|
||||
text: "test text content",
|
||||
dueAt: "07/09/2024 23:59:00",
|
||||
};
|
||||
|
||||
const pageMarkdownString = localPageMarkdownUtils.toMarkdown(page);
|
||||
|
||||
const parsedPage = localPageMarkdownUtils.parseMarkdown(pageMarkdownString);
|
||||
|
||||
expect(parsedPage).toEqual(page);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils";
|
||||
|
||||
describe("Matching Answer Error Messages", () => {
|
||||
it("can parse matching question", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description:
|
||||
---
|
||||
|
||||
question without answer
|
||||
|
||||
`;
|
||||
|
||||
expect(() => quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz)).toThrowError(
|
||||
/question type/
|
||||
);
|
||||
});
|
||||
});
|
||||
134
src/models/local/tests/quizMarkdown/matchingAnswers.test.ts
Normal file
134
src/models/local/tests/quizMarkdown/matchingAnswers.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { QuestionType } from "../../quiz/localQuizQuestion";
|
||||
import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils";
|
||||
import { quizQuestionMarkdownUtils } from "@/models/local/quiz/utils/quizQuestionMarkdownUtils";
|
||||
|
||||
describe("MatchingTests", () => {
|
||||
it("can parse matching question", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description:
|
||||
---
|
||||
Match the following terms & definitions
|
||||
|
||||
^ statement - a single command to be executed
|
||||
^ identifier - name of a variable
|
||||
^ keyword - reserved word that has special meaning in a program (e.g. class, void, static, etc.)
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
|
||||
expect(firstQuestion.questionType).toBe(QuestionType.MATCHING);
|
||||
expect(firstQuestion.text).not.toContain("statement");
|
||||
expect(firstQuestion.answers[0].matchedText).toBe(
|
||||
"a single command to be executed"
|
||||
);
|
||||
});
|
||||
|
||||
it("can create markdown for matching question", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description:
|
||||
---
|
||||
Match the following terms & definitions
|
||||
|
||||
^ statement - a single command to be executed
|
||||
^ identifier - name of a variable
|
||||
^ keyword - reserved word that has special meaning in a program (e.g. class, void, static, etc.)
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
const questionMarkdown = quizQuestionMarkdownUtils.toMarkdown(
|
||||
quiz.questions[0]
|
||||
);
|
||||
const expectedMarkdown = `Points: 1
|
||||
Match the following terms & definitions
|
||||
|
||||
^ statement - a single command to be executed
|
||||
^ identifier - name of a variable
|
||||
^ keyword - reserved word that has special meaning in a program (e.g. class, void, static, etc.)`;
|
||||
|
||||
expect(questionMarkdown).toContain(expectedMarkdown);
|
||||
});
|
||||
|
||||
it("whitespace is optional", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description:
|
||||
---
|
||||
Match the following terms & definitions
|
||||
|
||||
^statement - a single command to be executed
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
expect(quiz.questions[0].answers[0].text).toBe("statement");
|
||||
});
|
||||
|
||||
it("can have distractors", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description:
|
||||
---
|
||||
Match the following terms & definitions
|
||||
|
||||
^ statement - a single command to be executed
|
||||
^ - this is the distractor
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
expect(quiz.questions[0].matchDistractors).toEqual([
|
||||
"this is the distractor",
|
||||
]);
|
||||
});
|
||||
|
||||
it("can have distractors and be persisted", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description:
|
||||
---
|
||||
Match the following terms & definitions
|
||||
|
||||
^ statement - a single command to be executed
|
||||
^ - this is the distractor
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
|
||||
expect(quizMarkdown).toContain(
|
||||
"^ statement - a single command to be executed\n^ - this is the distractor"
|
||||
);
|
||||
});
|
||||
});
|
||||
181
src/models/local/tests/quizMarkdown/multipleAnswers.test.ts
Normal file
181
src/models/local/tests/quizMarkdown/multipleAnswers.test.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
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";
|
||||
|
||||
describe("MultipleAnswersTests", () => {
|
||||
it("quiz markdown includes multiple answer question", () => {
|
||||
const quiz: LocalQuiz = {
|
||||
name: "Test Quiz",
|
||||
description: "desc",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: false,
|
||||
showCorrectAnswers: false,
|
||||
localAssignmentGroupName: "someId",
|
||||
allowedAttempts: -1,
|
||||
questions: [
|
||||
{
|
||||
text: "oneline question",
|
||||
points: 1,
|
||||
questionType: QuestionType.MULTIPLE_ANSWERS,
|
||||
answers: [
|
||||
{ correct: true, text: "true" },
|
||||
{ correct: true, text: "false" },
|
||||
{ correct: false, text: "neither" },
|
||||
],
|
||||
matchDistractors: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const markdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
const expectedQuestionString = `Points: 1
|
||||
oneline question
|
||||
[*] true
|
||||
[*] false
|
||||
[ ] neither`;
|
||||
expect(markdown).toContain(expectedQuestionString);
|
||||
});
|
||||
|
||||
it("can parse question with multiple answers", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description: this is the
|
||||
multi line
|
||||
description
|
||||
---
|
||||
Which events are triggered when the user clicks on an input field?
|
||||
[*] click
|
||||
[*] focus
|
||||
[*] mousedown
|
||||
[ ] submit
|
||||
[ ] change
|
||||
[ ] mouseout
|
||||
[ ] keydown
|
||||
---
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
|
||||
expect(firstQuestion.points).toBe(1);
|
||||
expect(firstQuestion.questionType).toBe(QuestionType.MULTIPLE_ANSWERS);
|
||||
expect(firstQuestion.text).toContain(
|
||||
"Which events are triggered when the user clicks on an input field?"
|
||||
);
|
||||
expect(firstQuestion.answers[0].text).toBe("click");
|
||||
expect(firstQuestion.answers[0].correct).toBe(true);
|
||||
expect(firstQuestion.answers[3].correct).toBe(false);
|
||||
expect(firstQuestion.answers[3].text).toBe("submit");
|
||||
});
|
||||
|
||||
it("can parse question with multiple answers without a space in false answers", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description: this is the
|
||||
multi line
|
||||
description
|
||||
---
|
||||
Which events are triggered when the user clicks on an input field?
|
||||
[*] click
|
||||
[] submit
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
|
||||
expect(firstQuestion.answers.length).toBe(2);
|
||||
expect(firstQuestion.answers[0].correct).toBe(true);
|
||||
expect(firstQuestion.answers[1].correct).toBe(false);
|
||||
});
|
||||
|
||||
it("can parse question with multiple answers without a space in false answers other example", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description: this is the
|
||||
multi line
|
||||
description
|
||||
---
|
||||
Points: 1
|
||||
Which tool(s) will let you: create a database migration or reverse-engineer an existing database
|
||||
[] swagger
|
||||
[] a .http file
|
||||
[*] dotnet ef command line interface
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
|
||||
expect(firstQuestion.answers.length).toBe(3);
|
||||
expect(firstQuestion.answers[0].correct).toBe(false);
|
||||
expect(firstQuestion.answers[1].correct).toBe(false);
|
||||
expect(firstQuestion.answers[2].correct).toBe(true);
|
||||
});
|
||||
|
||||
it("can use braces in answer for multiple answer", () => {
|
||||
const rawMarkdownQuestion = `
|
||||
Which events are triggered when the user clicks on an input field?
|
||||
[*] \`int[] theThing()\`
|
||||
[ ] keydown
|
||||
`;
|
||||
|
||||
const question = quizQuestionMarkdownUtils.parseMarkdown(
|
||||
rawMarkdownQuestion,
|
||||
0
|
||||
);
|
||||
|
||||
expect(question.answers[0].text).toBe("`int[] theThing()`");
|
||||
expect(question.answers.length).toBe(2);
|
||||
});
|
||||
|
||||
it("can use braces in answer for multiple answer with multiline", () => {
|
||||
const rawMarkdownQuestion = `
|
||||
Which events are triggered when the user clicks on an input field?
|
||||
[*]
|
||||
\`\`\`
|
||||
int[] myNumbers = new int[] { };
|
||||
DoSomething(ref myNumbers);
|
||||
static void DoSomething(ref int[] numbers)
|
||||
{
|
||||
// do something
|
||||
}
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
const question = quizQuestionMarkdownUtils.parseMarkdown(
|
||||
rawMarkdownQuestion,
|
||||
0
|
||||
);
|
||||
|
||||
expect(question.answers[0].text).toBe(`\`\`\`
|
||||
int[] myNumbers = new int[] { };
|
||||
DoSomething(ref myNumbers);
|
||||
static void DoSomething(ref int[] numbers)
|
||||
{
|
||||
// do something
|
||||
}
|
||||
\`\`\``);
|
||||
expect(question.answers.length).toBe(1);
|
||||
});
|
||||
});
|
||||
75
src/models/local/tests/quizMarkdown/multipleChoice.test.ts
Normal file
75
src/models/local/tests/quizMarkdown/multipleChoice.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
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";
|
||||
|
||||
describe("MultipleChoiceTests", () => {
|
||||
it("quiz markdown includes multiple choice question", () => {
|
||||
const quiz: LocalQuiz = {
|
||||
name: "Test Quiz",
|
||||
description: "desc",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: false,
|
||||
showCorrectAnswers: false,
|
||||
localAssignmentGroupName: "someId",
|
||||
allowedAttempts: -1,
|
||||
questions: [
|
||||
{
|
||||
points: 2,
|
||||
text: `
|
||||
\`some type\` of question
|
||||
|
||||
with many
|
||||
|
||||
\`\`\`
|
||||
lines
|
||||
\`\`\`
|
||||
`,
|
||||
questionType: QuestionType.MULTIPLE_CHOICE,
|
||||
answers: [
|
||||
{ correct: true, text: "true" },
|
||||
{ correct: false, text: "false\n\nendline" },
|
||||
],
|
||||
matchDistractors: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const markdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
const expectedQuestionString = `
|
||||
Points: 2
|
||||
|
||||
\`some type\` of question
|
||||
|
||||
with many
|
||||
|
||||
\`\`\`
|
||||
lines
|
||||
\`\`\`
|
||||
|
||||
*a) true
|
||||
b) false
|
||||
|
||||
endline`;
|
||||
expect(markdown).toContain(expectedQuestionString);
|
||||
});
|
||||
|
||||
it("letter optional for multiple choice", () => {
|
||||
const questionMarkdown = `
|
||||
Points: 2
|
||||
\`some type\` of question
|
||||
*) true
|
||||
) false
|
||||
`;
|
||||
|
||||
const question = quizQuestionMarkdownUtils.parseMarkdown(
|
||||
questionMarkdown,
|
||||
0
|
||||
);
|
||||
expect(question.answers.length).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,198 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
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
|
||||
describe("QuizDeterministicChecks", () => {
|
||||
it("SerializationIsDeterministic_EmptyQuiz", () => {
|
||||
const quiz: LocalQuiz = {
|
||||
name: "Test Quiz",
|
||||
description: "quiz description",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: true,
|
||||
localAssignmentGroupName: "Assignments",
|
||||
questions: [],
|
||||
allowedAttempts: -1,
|
||||
showCorrectAnswers: true,
|
||||
};
|
||||
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown);
|
||||
|
||||
expect(parsedQuiz).toEqual(quiz);
|
||||
});
|
||||
|
||||
it("SerializationIsDeterministic_ShowCorrectAnswers", () => {
|
||||
const quiz: LocalQuiz = {
|
||||
name: "Test Quiz",
|
||||
description: "quiz description",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
showCorrectAnswers: false,
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: true,
|
||||
localAssignmentGroupName: "Assignments",
|
||||
questions: [],
|
||||
allowedAttempts: -1,
|
||||
};
|
||||
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown);
|
||||
|
||||
expect(parsedQuiz).toEqual(quiz);
|
||||
});
|
||||
|
||||
it("SerializationIsDeterministic_ShortAnswer", () => {
|
||||
const quiz: LocalQuiz = {
|
||||
name: "Test Quiz",
|
||||
description: "quiz description",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: true,
|
||||
localAssignmentGroupName: "Assignments",
|
||||
questions: [
|
||||
{
|
||||
text: "test short answer",
|
||||
questionType: QuestionType.SHORT_ANSWER,
|
||||
points: 1,
|
||||
answers: [],
|
||||
matchDistractors: [],
|
||||
},
|
||||
],
|
||||
allowedAttempts: -1,
|
||||
showCorrectAnswers: true,
|
||||
};
|
||||
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown);
|
||||
|
||||
expect(parsedQuiz).toEqual(quiz);
|
||||
});
|
||||
|
||||
it("SerializationIsDeterministic_Essay", () => {
|
||||
const quiz: LocalQuiz = {
|
||||
name: "Test Quiz",
|
||||
description: "quiz description",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: true,
|
||||
localAssignmentGroupName: "Assignments",
|
||||
questions: [
|
||||
{
|
||||
text: "test essay",
|
||||
questionType: QuestionType.ESSAY,
|
||||
points: 1,
|
||||
matchDistractors: [],
|
||||
answers: [],
|
||||
},
|
||||
],
|
||||
allowedAttempts: -1,
|
||||
showCorrectAnswers: true,
|
||||
};
|
||||
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown);
|
||||
|
||||
expect(parsedQuiz).toEqual(quiz);
|
||||
});
|
||||
|
||||
it("SerializationIsDeterministic_MultipleAnswer", () => {
|
||||
const quiz: LocalQuiz = {
|
||||
name: "Test Quiz",
|
||||
description: "quiz description",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: true,
|
||||
localAssignmentGroupName: "Assignments",
|
||||
questions: [
|
||||
{
|
||||
text: "test multiple answer",
|
||||
questionType: QuestionType.MULTIPLE_ANSWERS,
|
||||
points: 1,
|
||||
matchDistractors: [],
|
||||
answers: [
|
||||
{ text: "yes", correct: true },
|
||||
{ text: "no", correct: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
allowedAttempts: -1,
|
||||
showCorrectAnswers: true,
|
||||
};
|
||||
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown);
|
||||
|
||||
expect(parsedQuiz).toEqual(quiz);
|
||||
});
|
||||
|
||||
it("SerializationIsDeterministic_MultipleChoice", () => {
|
||||
const quiz: LocalQuiz = {
|
||||
name: "Test Quiz",
|
||||
description: "quiz description",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: true,
|
||||
password: undefined,
|
||||
localAssignmentGroupName: "Assignments",
|
||||
questions: [
|
||||
{
|
||||
text: "test multiple choice",
|
||||
questionType: QuestionType.MULTIPLE_CHOICE,
|
||||
points: 1,
|
||||
matchDistractors: [],
|
||||
answers: [
|
||||
{ text: "yes", correct: true },
|
||||
{ text: "no", correct: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
allowedAttempts: -1,
|
||||
showCorrectAnswers: true,
|
||||
};
|
||||
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown);
|
||||
|
||||
expect(parsedQuiz).toEqual(quiz);
|
||||
});
|
||||
|
||||
it("SerializationIsDeterministic_Matching", () => {
|
||||
const quiz: LocalQuiz = {
|
||||
name: "Test Quiz",
|
||||
description: "quiz description",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: true,
|
||||
password: undefined,
|
||||
localAssignmentGroupName: "Assignments",
|
||||
questions: [
|
||||
{
|
||||
text: "test matching",
|
||||
questionType: QuestionType.MATCHING,
|
||||
points: 1,
|
||||
matchDistractors: [],
|
||||
answers: [
|
||||
{ text: "yes", correct: true, matchedText: "testing yes" },
|
||||
{ text: "no", correct: true, matchedText: "testing no" },
|
||||
],
|
||||
},
|
||||
],
|
||||
allowedAttempts: -1,
|
||||
showCorrectAnswers: true,
|
||||
};
|
||||
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown);
|
||||
|
||||
expect(parsedQuiz).toEqual(quiz);
|
||||
});
|
||||
});
|
||||
256
src/models/local/tests/quizMarkdown/quizMarkdown.test.ts
Normal file
256
src/models/local/tests/quizMarkdown/quizMarkdown.test.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
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";
|
||||
|
||||
// Test suite for QuizMarkdown
|
||||
describe("QuizMarkdownTests", () => {
|
||||
it("can serialize quiz to markdown", () => {
|
||||
const quiz: LocalQuiz = {
|
||||
name: "Test Quiz",
|
||||
description: `
|
||||
# quiz description
|
||||
|
||||
this is my description in markdown
|
||||
|
||||
\`here is code\`
|
||||
`,
|
||||
lockAt: new Date(8640000000000000).toISOString(), // DateTime.MaxValue equivalent in TypeScript
|
||||
dueAt: new Date(8640000000000000).toISOString(),
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: false,
|
||||
localAssignmentGroupName: "someId",
|
||||
allowedAttempts: -1,
|
||||
showCorrectAnswers: false,
|
||||
questions: [],
|
||||
};
|
||||
|
||||
const markdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
|
||||
expect(markdown).toContain("Name: Test Quiz");
|
||||
expect(markdown).toContain(quiz.description);
|
||||
expect(markdown).toContain("ShuffleAnswers: true");
|
||||
expect(markdown).toContain("OneQuestionAtATime: false");
|
||||
expect(markdown).toContain("AssignmentGroup: someId");
|
||||
expect(markdown).toContain("AllowedAttempts: -1");
|
||||
});
|
||||
|
||||
it("can parse markdown quiz with no questions", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description: this is the
|
||||
multi line
|
||||
description
|
||||
---
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
|
||||
const expectedDescription = `
|
||||
this is the
|
||||
multi line
|
||||
description`;
|
||||
|
||||
expect(quiz.name).toBe("Test Quiz");
|
||||
expect(quiz.shuffleAnswers).toBe(true);
|
||||
expect(quiz.oneQuestionAtATime).toBe(false);
|
||||
expect(quiz.allowedAttempts).toBe(-1);
|
||||
expect(quiz.description.trim()).toBe(expectedDescription.trim());
|
||||
});
|
||||
|
||||
it("can parse markdown quiz with password", () => {
|
||||
const password = "this-is-the-password";
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
Password: ${password}
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description: this is the
|
||||
multi line
|
||||
description
|
||||
---
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
|
||||
expect(quiz.password).toBe(password);
|
||||
});
|
||||
|
||||
it("can parse markdown quiz and configure to show correct answers", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
ShowCorrectAnswers: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description: this is the
|
||||
multi line
|
||||
description
|
||||
---
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
|
||||
expect(quiz.showCorrectAnswers).toBe(false);
|
||||
});
|
||||
|
||||
it("can parse quiz with questions", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description: this is the
|
||||
multi line
|
||||
description
|
||||
---
|
||||
Points: 2
|
||||
\`some type\` of question
|
||||
|
||||
with many
|
||||
|
||||
\`\`\`
|
||||
lines
|
||||
\`\`\`
|
||||
|
||||
*a) true
|
||||
b) false
|
||||
|
||||
endline`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
|
||||
expect(firstQuestion.questionType).toBe(QuestionType.MULTIPLE_CHOICE);
|
||||
expect(firstQuestion.points).toBe(2);
|
||||
expect(firstQuestion.text).toContain("```");
|
||||
expect(firstQuestion.text).toContain("`some type` of question");
|
||||
expect(firstQuestion.answers[0].text).toBe("true");
|
||||
expect(firstQuestion.answers[0].correct).toBe(true);
|
||||
expect(firstQuestion.answers[1].correct).toBe(false);
|
||||
expect(firstQuestion.answers[1].text).toContain("endline");
|
||||
});
|
||||
|
||||
it("can parse multiple questions", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description: this is the
|
||||
multi line
|
||||
description
|
||||
---
|
||||
Which events are triggered when the user clicks on an input field?
|
||||
[*] click
|
||||
---
|
||||
Points: 2
|
||||
\`some type\` of question
|
||||
*a) true
|
||||
b) false
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
expect(firstQuestion.points).toBe(1);
|
||||
expect(firstQuestion.questionType).toBe(QuestionType.MULTIPLE_ANSWERS);
|
||||
|
||||
const secondQuestion = quiz.questions[1];
|
||||
expect(secondQuestion.points).toBe(2);
|
||||
expect(secondQuestion.questionType).toBe(QuestionType.MULTIPLE_CHOICE);
|
||||
});
|
||||
|
||||
it("short answer to markdown is correct", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description: this is the
|
||||
multi line
|
||||
description
|
||||
---
|
||||
Which events are triggered when the user clicks on an input field?
|
||||
short answer
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
|
||||
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("negative points is allowed", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description: this is the
|
||||
multi line
|
||||
description
|
||||
---
|
||||
Points: -4
|
||||
Which events are triggered when the user clicks on an input field?
|
||||
short answer
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
expect(firstQuestion.points).toBe(-4);
|
||||
});
|
||||
|
||||
it("floating point points is allowed", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description: this is the
|
||||
multi line
|
||||
description
|
||||
---
|
||||
Points: 4.56
|
||||
Which events are triggered when the user clicks on an input field?
|
||||
short answer
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
expect(firstQuestion.points).toBe(4.56);
|
||||
});
|
||||
});
|
||||
137
src/models/local/tests/quizMarkdown/testAnswer.test.ts
Normal file
137
src/models/local/tests/quizMarkdown/testAnswer.test.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
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", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description: this is the
|
||||
multi line
|
||||
description
|
||||
---
|
||||
Which events are triggered when the user clicks on an input field?
|
||||
essay
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
|
||||
expect(firstQuestion.points).toBe(1);
|
||||
expect(firstQuestion.questionType).toBe(QuestionType.ESSAY);
|
||||
expect(firstQuestion.text).not.toContain("essay");
|
||||
});
|
||||
|
||||
it("can parse short answer", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description: this is the
|
||||
multi line
|
||||
description
|
||||
---
|
||||
Which events are triggered when the user clicks on an input field?
|
||||
short answer
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
|
||||
expect(firstQuestion.points).toBe(1);
|
||||
expect(firstQuestion.questionType).toBe(QuestionType.SHORT_ANSWER);
|
||||
expect(firstQuestion.text).not.toContain("short answer");
|
||||
});
|
||||
|
||||
it("short answer to markdown is correct", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description: this is the
|
||||
multi line
|
||||
description
|
||||
---
|
||||
Which events are triggered when the user clicks on an input field?
|
||||
short answer
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
|
||||
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", () => {
|
||||
const rawMarkdownQuiz = `
|
||||
Name: Test Quiz
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
DueAt: 08/21/2023 23:59:00
|
||||
LockAt: 08/21/2023 23:59:00
|
||||
AssignmentGroup: Assignments
|
||||
AllowedAttempts: -1
|
||||
Description: this is the
|
||||
multi line
|
||||
description
|
||||
---
|
||||
Which events are triggered when the user clicks on an input field?
|
||||
essay
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
|
||||
const questionMarkdown =
|
||||
quizQuestionMarkdownUtils.toMarkdown(firstQuestion);
|
||||
const expectedMarkdown = `Points: 1
|
||||
Which events are triggered when the user clicks on an input field?
|
||||
essay`;
|
||||
expect(questionMarkdown).toContain(expectedMarkdown);
|
||||
});
|
||||
|
||||
// it("Can parse short answer with auto graded answers", () => {
|
||||
// const rawMarkdownQuiz = `
|
||||
// Name: Test Quiz
|
||||
// ShuffleAnswers: true
|
||||
// OneQuestionAtATime: false
|
||||
// DueAt: 08/21/2023 23:59:00
|
||||
// LockAt: 08/21/2023 23:59:00
|
||||
// AssignmentGroup: Assignments
|
||||
// AllowedAttempts: -1
|
||||
// Description: this is the
|
||||
// multi line
|
||||
// description
|
||||
// ---
|
||||
// Which events are triggered when the user clicks on an input field?
|
||||
// *a) test
|
||||
// short_answer=
|
||||
// `;
|
||||
|
||||
// const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
|
||||
// const firstQuestion = quiz.questions[0];
|
||||
|
||||
|
||||
// expect(firstQuestion.questionType).toBe(QuestionType.SHORT_ANSWER_WITH_ANSWERS)
|
||||
// });
|
||||
});
|
||||
97
src/models/local/tests/rubricMarkdown.test.ts
Normal file
97
src/models/local/tests/rubricMarkdown.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { RubricItem, rubricItemIsExtraCredit } from "../assignment/rubricItem";
|
||||
import { assignmentMarkdownParser } from "../assignment/utils/assignmentMarkdownParser";
|
||||
|
||||
describe("RubricMarkdownTests", () => {
|
||||
it("can parse one item", () => {
|
||||
const rawRubric = `
|
||||
- 2pts: this is the task
|
||||
`;
|
||||
|
||||
const rubric: RubricItem[] =
|
||||
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
|
||||
expect(rubric.length).toBe(1);
|
||||
expect(rubricItemIsExtraCredit(rubric[0])).toBe(false);
|
||||
expect(rubric[0].label).toBe("this is the task");
|
||||
expect(rubric[0].points).toBe(2);
|
||||
});
|
||||
|
||||
it("can parse multiple items", () => {
|
||||
const rawRubric = `
|
||||
- 2pts: this is the task
|
||||
- 3pts: this is the other task
|
||||
`;
|
||||
|
||||
const rubric: RubricItem[] =
|
||||
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
|
||||
expect(rubric.length).toBe(2);
|
||||
expect(rubricItemIsExtraCredit(rubric[0])).toBe(false);
|
||||
expect(rubric[1].label).toBe("this is the other task");
|
||||
expect(rubric[1].points).toBe(3);
|
||||
});
|
||||
|
||||
it("can parse single point", () => {
|
||||
const rawRubric = `
|
||||
- 1pt: this is the task
|
||||
`;
|
||||
|
||||
const rubric: RubricItem[] =
|
||||
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
|
||||
|
||||
expect(rubricItemIsExtraCredit(rubric[0])).toBe(false);
|
||||
expect(rubric[0].label).toBe("this is the task");
|
||||
expect(rubric[0].points).toBe(1);
|
||||
});
|
||||
|
||||
it("can parse single extra credit (lower case)", () => {
|
||||
const rawRubric = `
|
||||
- 1pt: (extra credit) this is the task
|
||||
`;
|
||||
|
||||
const rubric: RubricItem[] =
|
||||
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
|
||||
expect(rubricItemIsExtraCredit(rubric[0])).toBe(true);
|
||||
expect(rubric[0].label).toBe("(extra credit) this is the task");
|
||||
});
|
||||
|
||||
it("can parse single extra credit (upper case)", () => {
|
||||
const rawRubric = `
|
||||
- 1pt: (Extra Credit) this is the task
|
||||
`;
|
||||
|
||||
const rubric: RubricItem[] =
|
||||
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
|
||||
expect(rubricItemIsExtraCredit(rubric[0])).toBe(true);
|
||||
expect(rubric[0].label).toBe("(Extra Credit) this is the task");
|
||||
});
|
||||
|
||||
it("can parse floating point numbers", () => {
|
||||
const rawRubric = `
|
||||
- 1.5pt: this is the task
|
||||
`;
|
||||
|
||||
const rubric: RubricItem[] =
|
||||
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
|
||||
expect(rubric[0].points).toBe(1.5);
|
||||
});
|
||||
|
||||
it("can parse negative numbers", () => {
|
||||
const rawRubric = `
|
||||
- -2pt: this is the task
|
||||
`;
|
||||
|
||||
const rubric: RubricItem[] =
|
||||
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
|
||||
expect(rubric[0].points).toBe(-2);
|
||||
});
|
||||
|
||||
it("can parse negative floating point numbers", () => {
|
||||
const rawRubric = `
|
||||
- -2895.00053pt: this is the task
|
||||
`;
|
||||
|
||||
const rubric: RubricItem[] =
|
||||
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
|
||||
expect(rubric[0].points).toBe(-2895.00053);
|
||||
});
|
||||
});
|
||||
31
src/models/local/tests/testHolidayParsing.test.ts
Normal file
31
src/models/local/tests/testHolidayParsing.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { parseHolidays } from "../utils/settingsUtils";
|
||||
|
||||
describe("can parse holiday string", () => {
|
||||
it("can parse empty list", () => {
|
||||
const testString = `
|
||||
springBreak:
|
||||
`;
|
||||
const output = parseHolidays(testString);
|
||||
expect(output).toEqual([{ name: "springBreak", days: [] }]);
|
||||
});
|
||||
it("can parse list with date", () => {
|
||||
const testString = `
|
||||
springBreak:
|
||||
- 10/12/2024
|
||||
`;
|
||||
const output = parseHolidays(testString);
|
||||
expect(output).toEqual([{ name: "springBreak", days: ["10/12/2024"] }]);
|
||||
});
|
||||
it("can parse list with two dates", () => {
|
||||
const testString = `
|
||||
springBreak:
|
||||
- 10/12/2024
|
||||
- 10/13/2024
|
||||
`;
|
||||
const output = parseHolidays(testString);
|
||||
expect(output).toEqual([
|
||||
{ name: "springBreak", days: ["10/12/2024", "10/13/2024"] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
222
src/models/local/tests/testSemesterImport.test.ts
Normal file
222
src/models/local/tests/testSemesterImport.test.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { LocalAssignment } from "../assignment/localAssignment";
|
||||
import {
|
||||
prepAssignmentForNewSemester,
|
||||
prepLectureForNewSemester,
|
||||
prepPageForNewSemester,
|
||||
prepQuizForNewSemester,
|
||||
} from "../utils/semesterTransferUtils";
|
||||
import { LocalQuiz } from "../quiz/localQuiz";
|
||||
import { LocalCoursePage } from "../page/localCoursePage";
|
||||
import { Lecture } from "../lecture";
|
||||
|
||||
describe("can take an assignment and template it for a new semester", () => {
|
||||
it("can sanitize assignment github classroom repo url", () => {
|
||||
const assignment: LocalAssignment = {
|
||||
name: "test assignment",
|
||||
description: `
|
||||
## test description
|
||||
|
||||
[GitHub Classroom Assignment](https://classroom.github.com/a/y_eOxTfL)
|
||||
|
||||
other stuff below`,
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
const oldSemesterStartDate = "08/26/2023 23:59:00";
|
||||
const newSemesterStartDate = "01/08/2024 23:59:00";
|
||||
|
||||
const sanitizedAssignment = prepAssignmentForNewSemester(
|
||||
assignment,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
|
||||
expect(sanitizedAssignment.description).toEqual(`
|
||||
## test description
|
||||
|
||||
[GitHub Classroom Assignment](insert_github_classroom_url)
|
||||
|
||||
other stuff below`);
|
||||
});
|
||||
|
||||
it("can sanitize assignment github classroom repo url 2", () => {
|
||||
const assignment: LocalAssignment = {
|
||||
name: "test assignment",
|
||||
description: `
|
||||
<https://classroom.github.com/a/y_eOxTfL>
|
||||
other stuff below`,
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
const oldSemesterStartDate = "08/26/2023 23:59:00";
|
||||
const newSemesterStartDate = "01/08/2024 23:59:00";
|
||||
|
||||
const sanitizedAssignment = prepAssignmentForNewSemester(
|
||||
assignment,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
|
||||
expect(sanitizedAssignment.description).toEqual(`
|
||||
<insert_github_classroom_url>
|
||||
other stuff below`);
|
||||
});
|
||||
|
||||
it("can sanitize assignment github classroom repo url 3", () => {
|
||||
const assignment: LocalAssignment = {
|
||||
name: "test assignment",
|
||||
description: `https://classroom.github.com/a/y_eOxTfL other things`,
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
const oldSemesterStartDate = "08/26/2023 23:59:00";
|
||||
const newSemesterStartDate = "01/08/2024 23:59:00";
|
||||
|
||||
const sanitizedAssignment = prepAssignmentForNewSemester(
|
||||
assignment,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
|
||||
expect(sanitizedAssignment.description).toEqual(
|
||||
`insert_github_classroom_url other things`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("can offset date based on new semester start", () => {
|
||||
it("assignment with new semester start", () => {
|
||||
const assignment: LocalAssignment = {
|
||||
name: "test assignment",
|
||||
description: `https://classroom.github.com/a/y_eOxTfL other things`,
|
||||
dueAt: "08/29/2023 23:59:00",
|
||||
lockAt: "08/29/2023 23:59:00",
|
||||
submissionTypes: [],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
|
||||
const oldSemesterStartDate = "08/26/2023 23:59:00";
|
||||
const newSemesterStartDate = "01/08/2024 23:59:00";
|
||||
|
||||
const sanitizedAssignment = prepAssignmentForNewSemester(
|
||||
assignment,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
|
||||
expect(sanitizedAssignment.dueAt).toEqual("01/11/2024 23:59:00");
|
||||
expect(sanitizedAssignment.lockAt).toEqual("01/11/2024 23:59:00");
|
||||
});
|
||||
it("assignment with new semester start, no lock date", () => {
|
||||
const assignment: LocalAssignment = {
|
||||
name: "test assignment",
|
||||
description: `https://classroom.github.com/a/y_eOxTfL other things`,
|
||||
dueAt: "08/29/2023 23:59:00",
|
||||
lockAt: undefined,
|
||||
submissionTypes: [],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
|
||||
const oldSemesterStartDate = "08/26/2023 23:59:00";
|
||||
const newSemesterStartDate = "01/08/2024 23:59:00";
|
||||
|
||||
const sanitizedAssignment = prepAssignmentForNewSemester(
|
||||
assignment,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
|
||||
expect(sanitizedAssignment.dueAt).toEqual("01/11/2024 23:59:00");
|
||||
expect(sanitizedAssignment.lockAt).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("can prep quizzes", () => {
|
||||
it("quiz gets new lock and due dates", () => {
|
||||
const quiz: LocalQuiz = {
|
||||
name: "Test Quiz",
|
||||
description: `
|
||||
# quiz description
|
||||
`,
|
||||
dueAt: "08/29/2023 23:59:00",
|
||||
lockAt: "08/30/2023 23:59:00",
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: false,
|
||||
localAssignmentGroupName: "someId",
|
||||
allowedAttempts: -1,
|
||||
showCorrectAnswers: false,
|
||||
questions: [],
|
||||
};
|
||||
|
||||
const oldSemesterStartDate = "08/26/2023 23:59:00";
|
||||
const newSemesterStartDate = "01/08/2024 23:59:00";
|
||||
|
||||
const sanitizedQuiz = prepQuizForNewSemester(
|
||||
quiz,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
|
||||
expect(sanitizedQuiz.dueAt).toEqual("01/11/2024 23:59:00");
|
||||
expect(sanitizedQuiz.lockAt).toEqual("01/12/2024 23:59:00");
|
||||
});
|
||||
});
|
||||
|
||||
describe("can prep pages", () => {
|
||||
it("page gets new due date and github url changes", () => {
|
||||
const page: LocalCoursePage = {
|
||||
name: "test title",
|
||||
text: "test text content",
|
||||
dueAt: "08/30/2023 23:59:00",
|
||||
};
|
||||
|
||||
const oldSemesterStartDate = "08/26/2023 23:59:00";
|
||||
const newSemesterStartDate = "01/08/2024 23:59:00";
|
||||
|
||||
const sanitizedPage = prepPageForNewSemester(
|
||||
page,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
|
||||
expect(sanitizedPage.dueAt).toEqual("01/12/2024 23:59:00");
|
||||
});
|
||||
});
|
||||
|
||||
describe("can prep lecture", () => {
|
||||
it("lecture gets new date, github url changes", () => {
|
||||
const lecture: Lecture = {
|
||||
name: "test title",
|
||||
date: "08/30/2023",
|
||||
content: "test text content",
|
||||
};
|
||||
|
||||
const oldSemesterStartDate = "08/26/2023 23:59:00";
|
||||
const newSemesterStartDate = "01/08/2024 23:59:00";
|
||||
|
||||
const sanitizedLecture = prepLectureForNewSemester(
|
||||
lecture,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
|
||||
expect(sanitizedLecture.date).toEqual("01/12/2024");
|
||||
});
|
||||
});
|
||||
72
src/models/local/tests/timeUtils.test.ts
Normal file
72
src/models/local/tests/timeUtils.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { dateToMarkdownString, getDateFromString } from "../utils/timeUtils";
|
||||
|
||||
describe("Can properly handle expected date formats", () => {
|
||||
it("can use AM/PM dates", () => {
|
||||
const dateString = "8/27/2024 1:00:00 AM";
|
||||
const dateObject = getDateFromString(dateString);
|
||||
expect(dateObject).not.toBeUndefined();
|
||||
});
|
||||
it("can use 24 hour dates", () => {
|
||||
const dateString = "8/27/2024 23:95:00";
|
||||
const dateObject = getDateFromString(dateString);
|
||||
expect(dateObject).not.toBeUndefined();
|
||||
});
|
||||
it("can use ISO format", () => {
|
||||
const dateString = "2024-08-26T00:00:00.0000000";
|
||||
const dateObject = getDateFromString(dateString);
|
||||
expect(dateObject).not.toBeUndefined();
|
||||
});
|
||||
it("can use other ISO format", () => {
|
||||
const dateString = "2024-08-26T06:00:00Z";
|
||||
const dateObject = getDateFromString(dateString);
|
||||
expect(dateObject).not.toBeUndefined();
|
||||
});
|
||||
it("can get correct time from format", () => {
|
||||
const dateString = "08/28/2024 23:59:00";
|
||||
const dateObject = getDateFromString(dateString);
|
||||
|
||||
expect(dateObject?.getDate()).toBe(28);
|
||||
expect(dateObject?.getMonth()).toBe(8 - 1); // 0 based
|
||||
expect(dateObject?.getFullYear()).toBe(2024);
|
||||
expect(dateObject?.getMinutes()).toBe(59);
|
||||
expect(dateObject?.getHours()).toBe(23);
|
||||
expect(dateObject?.getSeconds()).toBe(0);
|
||||
});
|
||||
it("can get correct time from format", () => {
|
||||
const dateString = "8/27/2024 1:00:00 AM";
|
||||
const dateObject = getDateFromString(dateString);
|
||||
|
||||
expect(dateObject?.getDate()).toBe(27);
|
||||
expect(dateObject?.getMonth()).toBe(8 - 1); // 0 based
|
||||
expect(dateObject?.getFullYear()).toBe(2024);
|
||||
expect(dateObject?.getMinutes()).toBe(0);
|
||||
expect(dateObject?.getHours()).toBe(1);
|
||||
expect(dateObject?.getSeconds()).toBe(0);
|
||||
});
|
||||
it("can get correct time from format", () => {
|
||||
const dateString = "08/27/2024 23:59:00";
|
||||
const dateObject = getDateFromString(dateString);
|
||||
|
||||
expect(dateObject).not.toBeUndefined()
|
||||
const updatedString = dateToMarkdownString(dateObject!)
|
||||
expect(updatedString).toBe(dateString)
|
||||
});
|
||||
it("can handle canvas time format", () => {
|
||||
const dateString = "8/29/2024, 5:00:00 PM";
|
||||
const dateObject = getDateFromString(dateString);
|
||||
|
||||
expect(dateObject).not.toBeUndefined()
|
||||
const updatedString = dateToMarkdownString(dateObject!)
|
||||
expect(updatedString).toBe("08/29/2024 17:00:00")
|
||||
|
||||
})
|
||||
it("can handle date without time", () => {
|
||||
const dateString = "8/29/2024";
|
||||
const dateObject = getDateFromString(dateString);
|
||||
|
||||
expect(dateObject).not.toBeUndefined()
|
||||
const updatedString = dateToMarkdownString(dateObject!)
|
||||
expect(updatedString).toBe("08/29/2024 00:00:00")
|
||||
})
|
||||
});
|
||||
9
src/models/local/utils/lectureUtils.ts
Normal file
9
src/models/local/utils/lectureUtils.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Lecture } from "../lecture";
|
||||
import { getDateOnlyMarkdownString } from "./timeUtils";
|
||||
|
||||
export function getLectureForDay(weeks: { weekName: string; lectures: Lecture[]; }[], dayAsDate: Date) {
|
||||
return weeks
|
||||
.flatMap((w) => w.lectures)
|
||||
.find((l) => l.date == getDateOnlyMarkdownString(dayAsDate));
|
||||
}
|
||||
|
||||
126
src/models/local/utils/semesterTransferUtils.ts
Normal file
126
src/models/local/utils/semesterTransferUtils.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
|
||||
import { LocalAssignment } from "../assignment/localAssignment";
|
||||
import { Lecture } from "../lecture";
|
||||
import { LocalCoursePage } from "../page/localCoursePage";
|
||||
import { LocalQuiz } from "../quiz/localQuiz";
|
||||
import {
|
||||
dateToMarkdownString,
|
||||
getDateFromStringOrThrow,
|
||||
} from "./timeUtils";
|
||||
|
||||
export const prepAssignmentForNewSemester = (
|
||||
assignment: LocalAssignment,
|
||||
oldSemesterStartDate: string,
|
||||
newSemesterStartDate: string
|
||||
): LocalAssignment => {
|
||||
const descriptionWithoutGithubClassroom = replaceClassroomUrl(
|
||||
assignment.description
|
||||
);
|
||||
return {
|
||||
...assignment,
|
||||
description: descriptionWithoutGithubClassroom,
|
||||
dueAt:
|
||||
newDateOffset(
|
||||
assignment.dueAt,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
) ?? assignment.dueAt,
|
||||
lockAt: newDateOffset(
|
||||
assignment.lockAt,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const prepQuizForNewSemester = (
|
||||
quiz: LocalQuiz,
|
||||
oldSemesterStartDate: string,
|
||||
newSemesterStartDate: string
|
||||
): LocalQuiz => {
|
||||
const descriptionWithoutGithubClassroom = replaceClassroomUrl(
|
||||
quiz.description
|
||||
);
|
||||
return {
|
||||
...quiz,
|
||||
description: descriptionWithoutGithubClassroom,
|
||||
dueAt:
|
||||
newDateOffset(quiz.dueAt, oldSemesterStartDate, newSemesterStartDate) ??
|
||||
quiz.dueAt,
|
||||
lockAt: newDateOffset(
|
||||
quiz.lockAt,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const prepPageForNewSemester = (
|
||||
page: LocalCoursePage,
|
||||
oldSemesterStartDate: string,
|
||||
newSemesterStartDate: string
|
||||
): LocalCoursePage => {
|
||||
const updatedText = replaceClassroomUrl(page.text);
|
||||
return {
|
||||
...page,
|
||||
text: updatedText,
|
||||
dueAt:
|
||||
newDateOffset(page.dueAt, oldSemesterStartDate, newSemesterStartDate) ??
|
||||
page.dueAt,
|
||||
};
|
||||
};
|
||||
export const prepLectureForNewSemester = (
|
||||
lecture: Lecture,
|
||||
oldSemesterStartDate: string,
|
||||
newSemesterStartDate: string
|
||||
): Lecture => {
|
||||
const updatedText = replaceClassroomUrl(lecture.content);
|
||||
const newDate = newDateOffset(
|
||||
lecture.date,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
const newDateOnly = newDate?.split(" ")[0];
|
||||
return {
|
||||
...lecture,
|
||||
content: updatedText,
|
||||
date: newDateOnly ?? lecture.date,
|
||||
};
|
||||
};
|
||||
|
||||
const replaceClassroomUrl = (value: string) => {
|
||||
const classroomPattern =
|
||||
/https:\/\/classroom\.github\.com\/[a-zA-Z0-9\/._-]+/g;
|
||||
const withoutGithubClassroom = value.replace(
|
||||
classroomPattern,
|
||||
"insert_github_classroom_url"
|
||||
);
|
||||
return withoutGithubClassroom;
|
||||
};
|
||||
|
||||
const newDateOffset = (
|
||||
dateString: string | undefined,
|
||||
oldSemesterStartDate: string,
|
||||
newSemesterStartDate: string
|
||||
) => {
|
||||
if (!dateString) return dateString;
|
||||
const oldStart = getDateFromStringOrThrow(
|
||||
oldSemesterStartDate,
|
||||
"semester start date in new semester offset"
|
||||
);
|
||||
const newStart = getDateFromStringOrThrow(
|
||||
newSemesterStartDate,
|
||||
"new semester start date in new semester offset"
|
||||
);
|
||||
const date = getDateFromStringOrThrow(
|
||||
dateString,
|
||||
"date in new semester offset"
|
||||
);
|
||||
const offset = date.getTime() - oldStart.getTime();
|
||||
|
||||
const newUnixTime = offset + newStart.getTime();
|
||||
|
||||
const newDate = new Date(newUnixTime);
|
||||
|
||||
return dateToMarkdownString(newDate);
|
||||
};
|
||||
50
src/models/local/utils/settingsUtils.tsx
Normal file
50
src/models/local/utils/settingsUtils.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
getDateFromStringOrThrow,
|
||||
getDateOnlyMarkdownString,
|
||||
} from "./timeUtils";
|
||||
|
||||
export const parseHolidays = (
|
||||
inputText: string
|
||||
): {
|
||||
name: string;
|
||||
days: string[];
|
||||
}[] => {
|
||||
let holidays: {
|
||||
name: string;
|
||||
days: string[];
|
||||
}[] = [];
|
||||
|
||||
const lines = inputText.split("\n").filter((line) => line.trim() !== "");
|
||||
let currentHoliday: string | null = null;
|
||||
|
||||
lines.forEach((line) => {
|
||||
if (line.includes(":")) {
|
||||
const holidayName = line.split(":")[0].trim();
|
||||
currentHoliday = holidayName;
|
||||
holidays = [...holidays, { name: holidayName, days: [] }];
|
||||
} else if (currentHoliday && line.startsWith("-")) {
|
||||
const date = line.replace("-", "").trim();
|
||||
const dateObject = getDateFromStringOrThrow(date, "parsing holiday text");
|
||||
|
||||
const holiday = holidays.find((h) => h.name == currentHoliday);
|
||||
holiday?.days.push(getDateOnlyMarkdownString(dateObject));
|
||||
}
|
||||
});
|
||||
|
||||
return holidays;
|
||||
};
|
||||
|
||||
export const holidaysToString = (
|
||||
holidays: {
|
||||
name: string;
|
||||
days: string[];
|
||||
}[]
|
||||
) => {
|
||||
const entries = holidays.map((holiday) => {
|
||||
const title = holiday.name + ":\n";
|
||||
const days = holiday.days.map((d) => `- ${d}\n`);
|
||||
return title + days.join("");
|
||||
});
|
||||
|
||||
return entries.join("");
|
||||
};
|
||||
106
src/models/local/utils/timeUtils.ts
Normal file
106
src/models/local/utils/timeUtils.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
const _getDateFromAMPM = (
|
||||
datePart: string,
|
||||
timePart: string,
|
||||
amPmPart: string
|
||||
): Date | undefined => {
|
||||
const [month, day, year] = datePart.split("/").map(Number);
|
||||
const [hours, minutes, seconds] = timePart.split(":").map(Number);
|
||||
|
||||
let adjustedHours = hours;
|
||||
if (amPmPart) {
|
||||
const upperMeridian = amPmPart.toUpperCase();
|
||||
if (upperMeridian === "PM" && hours < 12) {
|
||||
adjustedHours += 12;
|
||||
} else if (upperMeridian === "AM" && hours === 12) {
|
||||
adjustedHours = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const date = new Date(year, month - 1, day, adjustedHours, minutes, seconds);
|
||||
return isNaN(date.getTime()) ? undefined : date;
|
||||
};
|
||||
|
||||
const _getDateFromMilitary = (
|
||||
datePart: string,
|
||||
timePart: string
|
||||
): Date | undefined => {
|
||||
const [month, day, year] = datePart.split("/").map(Number);
|
||||
const [hours, minutes, seconds] = timePart.split(":").map(Number);
|
||||
|
||||
const date = new Date(year, month - 1, day, hours, minutes, seconds);
|
||||
return isNaN(date.getTime()) ? undefined : date;
|
||||
};
|
||||
|
||||
const _getDateFromISO = (value: string): Date | undefined => {
|
||||
const date = new Date(value);
|
||||
return isNaN(date.getTime()) ? undefined : date;
|
||||
};
|
||||
|
||||
const _getDateFromDateOnly = (datePart: string): Date | undefined => {
|
||||
const [month, day, year] = datePart.split("/").map(Number);
|
||||
const date = new Date(year, month - 1, day);
|
||||
return isNaN(date.getTime()) ? undefined : date;
|
||||
};
|
||||
|
||||
export const getDateFromString = (value: string): Date | undefined => {
|
||||
const ampmDateRegex =
|
||||
/^\d{1,2}\/\d{1,2}\/\d{4},? \d{1,2}:\d{2}:\d{2}\s{1}[APap][Mm]$/; //"M/D/YYYY h:mm:ss AM/PM" or "M/D/YYYY, h:mm:ss AM/PM"
|
||||
const militaryDateRegex = /^\d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{2}:\d{2}$/; //"MM/DD/YYYY HH:mm:ss"
|
||||
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}((.\d+)|(Z))$/; //"2024-08-26T00:00:00.0000000"
|
||||
const dateOnlyRegex = /^\d{1,2}\/\d{1,2}\/\d{4}$/; // "M/D/YYYY" or "MM/DD/YYYY"
|
||||
|
||||
if (isoDateRegex.test(value)) {
|
||||
return _getDateFromISO(value);
|
||||
} else if (ampmDateRegex.test(value)) {
|
||||
const [datePart, timePart, amPmPart] = value.split(/,?[\s\u202F]+/);
|
||||
return _getDateFromAMPM(datePart, timePart, amPmPart);
|
||||
} else if (militaryDateRegex.test(value)) {
|
||||
const [datePart, timePart] = value.split(" ");
|
||||
return _getDateFromMilitary(datePart, timePart);
|
||||
} if (dateOnlyRegex.test(value)) {
|
||||
return _getDateFromDateOnly(value);
|
||||
} else {
|
||||
if (value) console.log("invalid date format", value);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const getDateFromStringOrThrow = (
|
||||
value: string,
|
||||
labelForError: string
|
||||
): Date => {
|
||||
const d = getDateFromString(value);
|
||||
if (!d) throw Error(`Invalid date format for ${labelForError}, ${value}`);
|
||||
return d;
|
||||
};
|
||||
|
||||
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 = getDateFromString(value);
|
||||
if (!myDate) throw new Error(`Invalid format for ${labelForError}: ${value}`);
|
||||
return dateToMarkdownString(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();
|
||||
const stringHours = String(date.getHours()).padStart(2, "0");
|
||||
const stringMinutes = String(date.getMinutes()).padStart(2, "0");
|
||||
const stringSeconds = String(date.getSeconds()).padStart(2, "0");
|
||||
|
||||
return `${stringMonth}/${stringDay}/${stringYear} ${stringHours}:${stringMinutes}:${stringSeconds}`;
|
||||
};
|
||||
|
||||
export const getDateOnlyMarkdownString = (date: Date) => {
|
||||
return dateToMarkdownString(date).split(" ")[0];
|
||||
};
|
||||
Reference in New Issue
Block a user