/** * ## Exam Specification * * The general structure of an exam specification is: * - [[QuestionSpecification]]s with content for individual questions. * - [[SectionSpecification]]s with content for sections and that specify which questions they contain. * - An [[ExamSpecification]] with content for the overall exam and that specifies which sections it contains. * * ### IDs * * Question, section, and exam specifications require unique IDs. (They must be unique within their individual * component specification category - e.g. it would be ok to have a question and section with the same ID.) * * Valid IDs must start with a letter and may contain letters, numbers, underscores, and dashes (i.e. they * must match the regex `/^[a-zA-Z][a-zA-Z0-9_\-]*$/`). The [[isValidID]] function tests this. * * Student "uniqnames" are also required to meet these same criteria. * * When a question/section/exam is assigned to a student, it is given a UUID, with these original IDs used * as part of the seed (assuming one of the deterministic UUID generation policies is used). So, don't change * the IDs after you've started giving/grading an exam (or ideally don't change them EVER once a question is * put into some use). * * ### Specifying Question/Section Choosers * * Questions within a [[SectionSpecification]] are given as a list of specific questions or [[QuestionChooser]]s * that assign a (potentially different) subset of questions to each individual student. You may create custom * question choosers or use predefined factory functions to create them: * - [[RANDOM_QUESTION]] * - [[RANDOM_BY_TAG]] * * Sections within an [[ExamSpecification]] are given as a list of specific sections or [[SectionChooser]]s * that assign a (potentially different) subset of sections to each individual student. You may create custom * section choosers or use predefined factory functions to create them: * - [[RANDOM_SECTION]] * * ### Randomizing Question/Section Ordering * * Use [[SHUFFLE]] to randomize the ordering (on a per-student basis) of a list of questions or sections. * * ### Customizing Existing Specifications * * Use the [[CUSTOMIZE]] function if you've got an existing specification (e.g. from a question bank) that * you just need to tweak slightly. * * ### Student information * * Presently, the only information needed for students are names and "uniqnames", which are a unique * identifier for each student. These must satisfy the same * * @module */ import { Randomizer } from "./randomization"; import { QuestionBank } from "./QuestionBank"; import { ResponseKind } from "../response/common"; import { ResponseSpecification } from "../response/responses"; import { ExamComponentSkin } from "./skins"; import { Exam, Question, Section } from "./exam_components"; import { GraderSpecification } from "../graders/QuestionGrader"; import { QuestionVerifierSpecification } from "../verifiers/QuestionVerifier"; import { DateTime } from "luxon"; export type ExamCompletionSpecification = { threshold: number; tooltip: string; endpoints: { check: string; submit: string; }; local_deadline?: { when: DateTime; grace_minutes?: number; }; }; export type QuestionSpecification = { readonly component_kind?: "specification"; /** * A unique ID for the question. Must be distinct from all other question IDs. * Also used as part of the composite seed for UUID generation * by an [[ExamGenerator]] using the `"plain"` or "uuidv5" strategies. */ readonly question_id: string; /** * An optional question title. */ readonly title?: string; /** * The number of points the question is worth overall. */ readonly points: number; /** * Markdown-formatted question description. */ readonly mk_description: string; /** * Optional, markdown-formatted question postscript. Appears below the response element. */ readonly mk_postscript?: string; /** * The response for this question, which is the part of the question students interact with to enter their answer. * Depending on the kind of response, this may also contain a significant amount of content as well. */ readonly response: ResponseSpecification; /** * Optional, a verifier for this question used to check the answer on the clientside. */ readonly verifier?: QuestionVerifierSpecification; /** * Tags for this question that may be used to pick it out of a question bank. */ readonly tags?: readonly string[]; /** * A skin for this question, or a "chooser" that selects a skin from a set of possible * skins. A question's skin is used in rendering its description and response. * @see [[core/skins]] */ readonly skin?: ExamComponentSkin | SkinChooserSpecification; /** * An absolute path to a directory containing assets for this question. Assets will be * available to the frontend at {{frontend_assets_dir}}/question/{{question_id}}/ where * {{frontend_assets_dir}} is configured by the exam generator (defaults to "assets"). * Tip: Use `__dirname` to get an absolute path to an assets folder located in the * same directory as the file in which you define your specification. For example, * `__dirname + "/assets"`. */ readonly assets_dir?: string; }; export type SectionSpecification = { readonly component_kind?: "specification"; /** * A unique ID for the section. Must be distinct from all other section IDs. * Also used as part of the composite seed for UUID generation * by an [[ExamGenerator]] using the `"plain"` or "uuidv5" strategies. */ readonly section_id: string; /** * The section title. */ readonly title: string; /** * Markdown-formatted section description, shown before its questions. */ readonly mk_description: string; /** * Markdown-formatted section reference material, shown to the side of its description * and questions. */ readonly mk_reference?: string; /** * Specifies the sequence of questions in this section. Each entry in the array may either specify * a particular question or a "chooser" that selects one (or more) from a set of possible * questions that an individual student might be assigned. * @see [[QuestionSpecification]] * @see [[QuestionChooser]] */ readonly questions: readonly (QuestionSpecification | QuestionChooserSpecification)[]; /** * A skin for this section, or a "chooser" that selects a skin from a set of possible * skins. A section's skin is used in rendering its description and response, and also * affects the rendering of its questions (each question uses a compound skin created * by layering its own skin on top of the section skin). * @see [[core/skins]] */ readonly skin?: ExamComponentSkin | SkinChooserSpecification; /** * The initial width, in percent 0-100, of the right panel for this section. */ readonly right_column_width?: number; /** * An absolute path to a directory containing assets for this section. Assets will be * available to the frontend at {{frontend_assets_dir}}/section/{{section_id}}/ where * {{frontend_assets_dir}} is configured by the exam generator (defaults to "assets"). * Tip: Use `__dirname` to get an absolute path to an assets folder located in the * same directory as the file in which you define your specification. For example, * `__dirname + "/assets"`. */ readonly assets_dir?: string; }; export type CredentialsStrategy = { strategy: "google_local"; client_id: string; auth_endpoint: string; message?: string; header?: string; }; export type ExamSpecification = { readonly component_kind?: "specification"; /** * A unique ID for the exam. Also used as part of the composite seed for UUID generation * by an [[ExamGenerator]] using the `"plain"` or "uuidv5" strategies. */ readonly exam_id: string; /** * Title shown at the top of the exam. */ readonly title: string; /** * Markdown-formatted exam instructions, shown at the top of the exam. */ readonly mk_intructions: string; /** * Specifies the sequence of sections on this exam. Each entry in the array may either specify * a particular section or a "chooser" that selects one (or more) from a set of possible * sections that an individual student might be assigned. * @see [[SectionSpecification]] * @see [[SectionChooser]] */ readonly sections: readonly (SectionSpecification | SectionChooserSpecification)[]; /** * A completion checker for this exam */ readonly completion?: ExamCompletionSpecification; /** * Markdown-formatted announcements that will be shown in an "alert" style box at the top of the exam, * right below the main instructions. These are intended to stand out from the regular instructions. */ readonly mk_announcements?: string[]; /** * A markdown-formatted message that appears at the bottom left of the page, right above the * "Submission" button. A suggested use is to specify how students can ask questions during * the exam, perhaps including a link to e.g. a course forum or video meeting with proctors. */ readonly mk_questions_message?: string; /** * A markdown-formatted message that appears at the bottom left of the page, right below the * "Submission" button. A suggested use is to remind students why to click the "Submission" * button, e.g. "Download an answers file to submit to Canvas". */ readonly mk_download_message?: string; /** * A markdown-formatted message that appears when students open the "Submission" modal. * A suggested use is to give students instructions for downloading and turning in their * answers file. */ readonly mk_saver_message?: string; /** * A markdown-formatted message that appears at the bottom of the exam. A suggested use is * to confirm that students have reached the bottom of the exam and remind them to download * the answers file and turn it in elsewhere (e.g. to Canvas or some other LMS). * * Defaults to [[MK_DEFAULT_DOWNLOAD_MESSAGE]] */ readonly mk_bottom_message?: string; /** * If true, an "I'm Finished" button will be shown at the bottom of the exam. * When clicked, it opens a modal to confirm. * Defaults to undefined (interpreted as false). */ readonly enable_bottom_im_finished_button?: boolean; /** * TODO this will probably be moved elsewhere. */ readonly enable_regrades?: boolean; /** * An absolute path to a directory containing assets for this exam. Assets will be * available to the frontend at {{frontend_assets_dir}}/exam/{{exam_id}}/ where * {{frontend_assets_dir}} is configured by the exam generator (defaults to "assets"). * Tip: Use `__dirname` to get an absolute path to an assets folder located in the * same directory as the file in which you define your specification. For example, * `__dirname + "/assets"`. */ readonly assets_dir?: string; /** * Whether or not the exam content is available in the clientside exam spec, * which is written to spec/exam_spec.json. Defaults to undefined (interpreted as false). * * Enabling this is required for certain client-side features, for example, local * grading of questions. * * CAUTION! This should NOT be enabled unless you are OK with the * full content, including the exam structure, randomization details, and all questions * being potentially available to savvy users. If the exam specification contains * sample solutions or encodings of graders, those would also be accessible. */ readonly allow_clientside_content?: boolean; /** * Enable login on the clientside to retrieve credentials that may be used * to authenticate requests to a backend server. */ readonly credentials_strategy?: CredentialsStrategy; }; export declare function isValidID(id: string): boolean; export declare function without_content(spec: ExamSpecification): ExamSpecification; export declare function question_spec_without_assets_dir(spec: QuestionSpecification): QuestionSpecification; export declare function question_spec_without_assets_dir(spec: QuestionChooserSpecification): QuestionChooserSpecification; export declare function question_spec_without_assets_dir(spec: QuestionSpecification | QuestionChooserSpecification): QuestionSpecification | QuestionChooserSpecification; export declare function section_spec_without_assets_dir(spec: SectionSpecification): SectionSpecification; export declare function section_spec_without_assets_dir(spec: SectionChooserSpecification): SectionChooserSpecification; export declare function section_spec_without_assets_dir(spec: SectionSpecification | SectionChooserSpecification): SectionSpecification | SectionChooserSpecification; export declare function exam_spec_without_assets_dirs(spec: ExamSpecification): ExamSpecification; export type ExamComponentSpecification = ExamSpecification | SectionSpecification | QuestionSpecification | ExamComponentSkin; export declare function isExamSpecification(spec: ExamComponentSpecification): spec is ExamSpecification; export declare function isSectionSpecification(spec: ExamComponentSpecification): spec is SectionSpecification; export declare function isQuestionSpecification(spec: ExamComponentSpecification): spec is QuestionSpecification; export declare function isSkinSpecificatoin(spec: ExamComponentSpecification): spec is ExamComponentSkin; export declare function getExamComponentSpecificationID(spec: ExamComponentSpecification): string; export type ExamComponentOrChooserSpecification = ExamComponentSpecification | ExamComponentChooserSpecification<"section"> | ExamComponentChooserSpecification<"question"> | ExamComponentChooserSpecification<"skin">; type ChooserStrategySpecification = { kind: "group"; } | { kind: "random_1"; } | { kind: "random_n"; n: number; } | { kind: "shuffle"; }; type ChooserKind = "section" | "question" | "skin"; type ChoiceKind = { "section": SectionSpecification; "question": QuestionSpecification; "skin": ExamComponentSkin; }; type ExamComponentChooserSpecification = { readonly component_kind: "chooser_specification"; readonly chooser_kind: CK; readonly strategy: ChooserStrategySpecification; readonly choices: readonly (ChoiceKind[CK] | ExamComponentChooserSpecification)[]; }; export type SectionChooserSpecification = ExamComponentChooserSpecification<"section">; export type QuestionChooserSpecification = ExamComponentChooserSpecification<"question">; export type SkinChooserSpecification = ExamComponentChooserSpecification<"skin">; declare class ExamComponentChooser { readonly spec: ExamComponentChooserSpecification; readonly component_kind = "chooser"; readonly chooser_kind: CK; readonly all_choices: readonly ChoiceKind[CK][]; readonly n_chosen: MinMax; constructor(spec: ExamComponentChooserSpecification); choose(exam: Exam, student: StudentInfo, rand: Randomizer): readonly ChoiceKind[CK][]; } export type SectionChooser = ExamComponentChooser<"section">; export type QuestionChooser = ExamComponentChooser<"question">; export type SkinChooser = ExamComponentChooser<"skin">; export declare function realizeChooser(spec: ExamComponentChooserSpecification): ExamComponentChooser; type MinMax = { readonly min: number; readonly max: number; }; export type MinMaxPoints = MinMax & { _t_min_max_points?: never; }; export type MinMaxItems = MinMax & { _t_min_max_items?: never; }; export declare function minMaxPoints(component: QuestionChooserSpecification | SectionChooserSpecification | SectionSpecification | QuestionSpecification | ExamSpecification): MinMaxPoints; export declare function minMaxChosenItems(chooser_spec: QuestionChooserSpecification | SectionChooserSpecification | SkinChooserSpecification): MinMaxItems; export declare function realizeSection(s: SectionSpecification | Section): Section; export declare function realizeSection(s: SectionSpecification | Section | SectionChooserSpecification | SectionChooser): Section | SectionChooser; export declare function realizeSections(sections: readonly (SectionSpecification | Section)[]): readonly Section[]; export declare function realizeSections(sections: readonly (SectionSpecification | Section | SectionChooserSpecification | SectionChooser)[]): readonly (Section | SectionChooser)[]; export declare function chooseSections(chooser: Section | SectionChooser, exam: Exam, student: StudentInfo, rand: Randomizer): Section[] | readonly SectionSpecification[]; export declare function chooseAllSections(chooser: Section | SectionChooser): Section[] | readonly SectionSpecification[]; export declare function realizeQuestion(q: QuestionSpecification | Question): Question; export declare function realizeQuestion(q: QuestionSpecification | Question | QuestionChooserSpecification | QuestionChooser): Question | QuestionChooser; export declare function realizeQuestions(questions: readonly (QuestionSpecification | Question)[]): readonly Question[]; export declare function realizeQuestions(questions: readonly (QuestionSpecification | Question | QuestionChooserSpecification | QuestionChooser)[]): readonly (Question | QuestionChooser)[]; export declare function chooseQuestions(chooser: Question | QuestionChooser, exam: Exam, student: StudentInfo, rand: Randomizer): Question[] | readonly QuestionSpecification[]; export declare function chooseAllQuestions(chooser: Question | QuestionChooser): Question[] | readonly QuestionSpecification[]; export declare function chooseSkins(chooser: ExamComponentSkin | SkinChooser, exam: Exam, student: StudentInfo, rand: Randomizer): readonly ExamComponentSkin[]; export declare function chooseAllSkins(chooser: ExamComponentSkin | SkinChooser): readonly ExamComponentSkin[]; /** * This factory function returns a specification for a [[SectionChooser]] that will randomly * select a set of n questions from the given set of questions or question bank. If there are * not enough to choose n of them, the chooser will throw an exception. * @param n * @param sections * @returns [[SectionChooserSpecification]] */ export declare function RANDOM_SECTION(n: number, sections: readonly (SectionSpecification | SectionChooserSpecification)[]): SectionChooserSpecification; /** * This factory function returns a [[QuestionChooser]] that will randomly select a set * of n questions matching the given tag. If there are not enough questions matching that * tag, the chooser will throw an exception. * @param tag Choose only questions with this tag * @param n The number of questions to choose * @param questionBank The bank to choose questions from * @returns [[SectionChooserSpecification]] */ export declare function RANDOM_BY_TAG(tag: string, n: number, questions: QuestionBank | readonly QuestionSpecification[]): QuestionChooserSpecification; /** * This factory function returns a specification for a [[QuestionChooser]] that will randomly * select a set of n questions from the given set of questions or question bank. If there are * not enough to choose n of them, the chooser will throw an exception. * @param n * @param sections * @returns [[QuestionChooserSpecification]] */ export declare function RANDOM_QUESTION(n: number, questions: QuestionBank | readonly (QuestionSpecification | QuestionChooserSpecification)[]): QuestionChooserSpecification; export declare function CUSTOMIZE(spec: QuestionSpecification, customizations: Partial>): QuestionSpecification; export declare function CUSTOMIZE(spec: SectionSpecification, customizations: Partial>): SectionSpecification; export declare function CUSTOMIZE(spec: ExamSpecification, customizations: Partial>): ExamSpecification; export declare function CUSTOMIZE(spec: Omit, customizations: Partial> & Pick): QuestionSpecification; export declare function CUSTOMIZE(spec: Omit, customizations: Partial> & Pick): SectionSpecification; export declare function CUSTOMIZE(spec: Omit, customizations: Partial> & Pick): ExamSpecification; /** * This factory function returns a chooser that simply selects all its choices. The * intended use is to ensure they are grouped together when nested within other * randomization constructs. * @param questions */ export declare function GROUP(questions: QuestionSpecification | QuestionChooserSpecification | readonly (QuestionSpecification | QuestionChooserSpecification)[]): QuestionChooserSpecification; /** * This factory function returns a chooser that simply selects all its choices. The * intended use is to ensure they are grouped together when nested within other * randomization constructs. * @param sections */ export declare function GROUP(sections: SectionSpecification | SectionChooserSpecification | readonly (SectionSpecification | SectionChooserSpecification)[]): SectionChooserSpecification; export declare function SHUFFLE(questions: QuestionSpecification | QuestionChooserSpecification | readonly (QuestionSpecification | QuestionChooserSpecification)[]): QuestionChooserSpecification; export declare function SHUFFLE(sections: SectionSpecification | SectionChooserSpecification | readonly (SectionSpecification | SectionChooserSpecification)[]): SectionChooserSpecification; export interface StudentInfo { readonly uniqname: string; readonly name: string; } export declare function parseExamSpecification(str: string): ExamSpecification; export declare function parseSectionSpecification(str: string): SectionSpecification; export declare function parseQuestionSpecification(str: string): QuestionSpecification; export declare function parseExamSkinSpecification(str: string): ExamComponentSkin; export declare function parseExamComponentSpecification(str: string): ExamComponentOrChooserSpecification | ResponseSpecification | GraderSpecification; export declare function stringifyExamComponentSpecification(spec: ExamComponentOrChooserSpecification | ResponseSpecification | GraderSpecification): string; export {};