feat: enhance API and middleware functionality: add public API routes, implement program and course member checks, and improve rate limiting for invite acceptance. Update OpenAPI spec generation to focus on public API endpoints and add new middleware for automation key scopes. Include new documentation for lesson AI features and public API tests.
This commit is contained in:
+32
-1
@@ -13,8 +13,10 @@
|
||||
ORDERING_FIXTURE,
|
||||
RADIO_FIXTURE,
|
||||
SHORT_ANSWER_FIXTURE,
|
||||
STAR_FIXTURE,
|
||||
TEXTAREA_FIXTURE,
|
||||
TRUE_FALSE_FIXTURE
|
||||
TRUE_FALSE_FIXTURE,
|
||||
WORD_BANK_FIXTURE
|
||||
} from './question-fixtures';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
@@ -57,6 +59,16 @@
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="WordBank">
|
||||
{#snippet template()}
|
||||
<QuestionTypeModes
|
||||
question={WORD_BANK_FIXTURE.question}
|
||||
answer={WORD_BANK_FIXTURE.answer}
|
||||
wrongAnswer={WORD_BANK_FIXTURE.wrongAnswer}
|
||||
/>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story
|
||||
name="Hotspot"
|
||||
parameters={{
|
||||
@@ -127,6 +139,25 @@
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story
|
||||
name="Star"
|
||||
parameters={{
|
||||
docs: {
|
||||
description: {
|
||||
story: '**Premium in the app** – Gated behind a paid plan in the exercise editor.'
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#snippet template()}
|
||||
<QuestionTypeModes
|
||||
question={STAR_FIXTURE.question}
|
||||
answer={STAR_FIXTURE.answer}
|
||||
wrongAnswer={STAR_FIXTURE.wrongAnswer}
|
||||
/>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Ordering">
|
||||
{#snippet template()}
|
||||
<QuestionTypeModes
|
||||
|
||||
@@ -131,6 +131,18 @@ export const NUMERIC_FIXTURE: QuestionStoryFixture = {
|
||||
wrongAnswer: { type: 'NUMERIC', value: 42 }
|
||||
};
|
||||
|
||||
export const STAR_FIXTURE: QuestionStoryFixture = {
|
||||
question: {
|
||||
id: 'q-star',
|
||||
key: 'q-star',
|
||||
title: 'How clear were the lesson objectives? [STAR]',
|
||||
questionType: 'STAR',
|
||||
settings: { correctValue: 4, maxStars: 5 }
|
||||
},
|
||||
answer: { type: 'STAR', value: 4 },
|
||||
wrongAnswer: { type: 'STAR', value: 2 }
|
||||
};
|
||||
|
||||
export const FILL_BLANK_FIXTURE: QuestionStoryFixture = {
|
||||
question: {
|
||||
id: 'q-fill-blank',
|
||||
@@ -143,6 +155,29 @@ export const FILL_BLANK_FIXTURE: QuestionStoryFixture = {
|
||||
wrongAnswer: { type: 'FILL_BLANK', values: ['SOAP'] }
|
||||
};
|
||||
|
||||
export const WORD_BANK_FIXTURE: QuestionStoryFixture = {
|
||||
question: {
|
||||
id: 'q-word-bank',
|
||||
key: 'q-word-bank',
|
||||
title: 'Match each term with the sentence. [WORD_BANK]',
|
||||
questionType: 'WORD_BANK',
|
||||
settings: {
|
||||
template:
|
||||
'Measurements to describe our data are called ___. The ___ shows values and how often they occur. Outliers appear when data is skewed or has ___.',
|
||||
correctAnswers: ['summary statistics', 'distribution', 'outliers'],
|
||||
distractors: ['categorical variables', 'linear relationships', 'numeric variables']
|
||||
}
|
||||
},
|
||||
answer: {
|
||||
type: 'WORD_BANK',
|
||||
filledBlanks: ['summary statistics', 'distribution', 'outliers']
|
||||
},
|
||||
wrongAnswer: {
|
||||
type: 'WORD_BANK',
|
||||
filledBlanks: ['numeric variables', 'distribution', 'linear relationships']
|
||||
}
|
||||
};
|
||||
|
||||
export const FILE_UPLOAD_FIXTURE: QuestionStoryFixture = {
|
||||
question: {
|
||||
id: 'q-file-upload',
|
||||
|
||||
@@ -66,10 +66,35 @@ export const QUESTION_LABELS = {
|
||||
'numeric.review.your_answer_label': 'Submitted:',
|
||||
'numeric.review.in_range': 'Within tolerance',
|
||||
'numeric.review.out_of_range': 'Outside tolerance',
|
||||
'star.edit.correct_value_label': 'Correct star value',
|
||||
'star.edit.correct_value_info': 'Expected star count for full credit.',
|
||||
'star.edit.max_stars_label': 'Maximum stars',
|
||||
'star.edit.max_stars_info': 'Stars shown to learners (1–10).',
|
||||
'star.edit.correct_value_placeholder': 'e.g. 3',
|
||||
'star.edit.max_stars_placeholder': 'e.g. 5',
|
||||
'star.take.helper': 'Select your rating.',
|
||||
'star.take.star_sr': 'Star {n} of {max}',
|
||||
'star.preview.correct_label': 'Correct answer',
|
||||
'fill_blank.edit.accepted_answers_placeholder': 'Accepted answers (comma separated)',
|
||||
'fill_blank.take.placeholder': 'Enter answers separated by commas',
|
||||
'fill_blank.preview.accepted_answers_label': 'Accepted answers',
|
||||
'fill_blank.review.your_answer_label': 'Submitted:',
|
||||
'word_bank.edit.template_label': 'Sentence template',
|
||||
'word_bank.edit.template_placeholder': 'Use ___ for each blank...',
|
||||
'word_bank.edit.template_helper': 'Use three underscores (___) for each blank.',
|
||||
'word_bank.edit.no_blanks_warning': 'Add at least one ___ blank marker in your template.',
|
||||
'word_bank.edit.correct_answers_heading': 'Correct answer for each blank',
|
||||
'word_bank.edit.blank_label': 'Blank',
|
||||
'word_bank.edit.correct_placeholder': 'Correct term or phrase',
|
||||
'word_bank.edit.distractors_heading': 'Extra words (distractors)',
|
||||
'word_bank.edit.distractor_placeholder': 'Wrong option',
|
||||
'word_bank.edit.add_distractor': 'Add distractor',
|
||||
'word_bank.take.helper': 'Click or drag words into blanks.',
|
||||
'word_bank.take.bank_heading': 'Word bank',
|
||||
'word_bank.take.clear_blank_sr': 'Remove word from blank',
|
||||
'word_bank.preview.filled_label': 'Expected answers in context',
|
||||
'word_bank.review.your_answer_label': 'Your answers',
|
||||
'word_bank.review.correct_label': 'Correct answers',
|
||||
'file_upload.edit.accepted_types_placeholder': 'Select accepted file formats',
|
||||
'file_upload.edit.max_size_placeholder': 'Max size (MB)',
|
||||
'file_upload.take.selected_file_label': 'Selected file',
|
||||
|
||||
@@ -63,11 +63,45 @@
|
||||
return { type: 'SHORT_ANSWER', text: 'Use deterministic fixtures' };
|
||||
case 'NUMERIC':
|
||||
return { type: 'NUMERIC', value: 88 + (index % 6) };
|
||||
case 'STAR': {
|
||||
const rawMax = storyQuestion.settings?.maxStars;
|
||||
const maxStars =
|
||||
typeof rawMax === 'number' && Number.isFinite(rawMax) ? Math.min(10, Math.max(1, Math.floor(rawMax))) : 5;
|
||||
const rawCorrect = storyQuestion.settings?.correctValue;
|
||||
const correct =
|
||||
typeof rawCorrect === 'number' && Number.isFinite(rawCorrect)
|
||||
? Math.min(maxStars, Math.max(1, Math.floor(rawCorrect)))
|
||||
: Math.min(3, maxStars);
|
||||
if (index % 4 === 0) return { type: 'STAR', value: correct };
|
||||
return { type: 'STAR', value: (index % maxStars) + 1 };
|
||||
}
|
||||
case 'FILL_BLANK':
|
||||
return {
|
||||
type: 'FILL_BLANK',
|
||||
values: index % 2 === 0 ? ['API'] : ['endpoint']
|
||||
};
|
||||
case 'WORD_BANK': {
|
||||
const correct = Array.isArray(storyQuestion.settings?.correctAnswers)
|
||||
? (storyQuestion.settings.correctAnswers as string[]).map((s) => String(s ?? ''))
|
||||
: ['alpha', 'beta'];
|
||||
const blankCount = Math.max(1, correct.length);
|
||||
if (index % 4 === 0) {
|
||||
return { type: 'WORD_BANK', filledBlanks: correct.slice(0, blankCount) };
|
||||
}
|
||||
if (index % 4 === 1) {
|
||||
const wrongFirst = ['wrong term', ...correct.slice(1)].slice(0, blankCount);
|
||||
return { type: 'WORD_BANK', filledBlanks: wrongFirst };
|
||||
}
|
||||
if (index % 4 === 2 && blankCount > 1) {
|
||||
const swapped = [...correct.slice(0, blankCount)];
|
||||
[swapped[0], swapped[1]] = [swapped[1], swapped[0]];
|
||||
return { type: 'WORD_BANK', filledBlanks: swapped };
|
||||
}
|
||||
return {
|
||||
type: 'WORD_BANK',
|
||||
filledBlanks: Array.from({ length: blankCount }, () => '')
|
||||
};
|
||||
}
|
||||
case 'FILE_UPLOAD':
|
||||
return {
|
||||
type: 'FILE_UPLOAD',
|
||||
|
||||
Reference in New Issue
Block a user