feat(bisaya): update choice exercise question format and add question number to review

This commit is contained in:
Torsten Schulz (local)
2026-08-24 15:18:36 +02:00
parent e175eb1436
commit b8b45cd5ba
3 changed files with 52 additions and 3 deletions

View File

@@ -0,0 +1,40 @@
'use strict';
const AMBIGUOUS_QUESTION = 'Erzähle einen Tagesplan mit drei Schritten und einem Wunsch. Welche Bisaya-Formulierung passt am besten?';
const REPAIRED_QUESTION = 'Wie sagt man auf Bisaya: „Zuerst gehe ich zum Markt.“?';
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.transaction(async (transaction) => {
// The former free-production prompt has more than one defensible answer,
// while its multiple-choice data accepts only the first core pattern.
await queryInterface.sequelize.query(`
UPDATE community.vocab_grammar_exercise AS exercise
SET question_data = JSONB_SET(exercise.question_data, '{question}', TO_JSONB(CAST(:repairedQuestion AS text))),
updated_at = NOW()
FROM community.vocab_course_lesson AS lesson
WHERE exercise.lesson_id = lesson.id
AND (lesson.id = 38 OR (lesson.course_id = 1 AND lesson.lesson_number = 38))
AND exercise.question_data->>'type' = 'multiple_choice'
AND exercise.question_data->>'question' = :ambiguousQuestion;
`, {
replacements: { ambiguousQuestion: AMBIGUOUS_QUESTION, repairedQuestion: REPAIRED_QUESTION },
transaction
});
// This lesson is designed for a 78% target. With six questions, five
// correct answers (83%) must therefore pass.
await queryInterface.sequelize.query(`
UPDATE community.vocab_course_lesson
SET target_score_percent = 78,
updated_at = NOW()
WHERE id = 38
OR (course_id = 1 AND lesson_number = 38);
`, { transaction });
});
},
async down() {
// The repair is intentionally not reverted: the old data is ambiguous.
}
};

View File

@@ -529,6 +529,14 @@ function buildChoiceExercise(lesson, didactics, pattern, allPatterns, variant =
const seed = simpleHash(`${lesson.title}:${pattern}:${variant}`);
const options = rotateArray([pattern, ...distractors], seed % 4);
const correctAnswer = options.indexOf(pattern);
const normalizedPattern = normalizeCorePatternEntry(
didactics.corePatterns.find((entry) => corePatternTarget(entry) === pattern)
);
// A free speaking prompt can have several valid answers. It must never be
// used as the question for a single-answer multiple-choice exercise.
const question = normalizedPattern?.gloss
? `Wie sagt man auf Bisaya: „${normalizedPattern.gloss}“?`
: getChoiceQuestion(lesson, didactics);
return {
exerciseTypeId: 2,
@@ -536,7 +544,7 @@ function buildChoiceExercise(lesson, didactics, pattern, allPatterns, variant =
instruction: 'Wähle die natürlichste Formulierung für die Situation oder den Schwerpunkt der Lektion.',
questionData: {
type: 'multiple_choice',
question: getChoiceQuestion(lesson, didactics),
question,
options
},
answerData: {

View File

@@ -1045,9 +1045,9 @@
<div v-if="chapterExamFailedDetails.length > 0" class="exam-failed-review">
<h4>{{ $t('socialnetwork.vocab.courses.exerciseExamFailedReviewTitle') }}</h4>
<ul>
<li v-for="(item, index) in chapterExamFailedDetails" :key="`failed-${index}-${item.id}`">
<li v-for="item in chapterExamFailedDetails" :key="`failed-${item.id}`">
<p class="exam-failed-review__question">
<strong>{{ $t('socialnetwork.vocab.courses.exerciseExamFailedQuestionLabel') }} {{ index + 1 }}:</strong>
<strong>{{ $t('socialnetwork.vocab.courses.exerciseExamFailedQuestionLabel') }} {{ item.questionNumber }}:</strong>
{{ item.question }}
</p>
<p class="exam-failed-review__answer">
@@ -2450,6 +2450,7 @@ export default {
const userAnswerRaw = this.exerciseAnswers[exercise.id];
out.push({
id: exercise.id,
questionNumber: all.findIndex((candidate) => candidate.id === exercise.id) + 1,
question,
userAnswer: this.formatExerciseAnswerForReview(exercise, userAnswerRaw),
correctAnswer: String(result.correctAnswer || '').trim()