feat: füge Skript zur Reparatur von Kinderlektionen hinzu, das Platzhalterübungen ersetzt
All checks were successful
Deploy to production / deploy (push) Successful in 31s
All checks were successful
Deploy to production / deploy (push) Successful in 31s
This commit is contained in:
171
backend/scripts/repair-bisaya-children-lessons.js
Normal file
171
backend/scripts/repair-bisaya-children-lessons.js
Normal file
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Repariert alte/platzhalterhafte Prüfungen für Kinder‑Lektionen (z.B. Lektion 28).
|
||||
*
|
||||
* Nutzung:
|
||||
* node backend/scripts/repair-bisaya-children-lessons.js
|
||||
*
|
||||
* Optional:
|
||||
* VOCAB_FORCE_REBUILD_ALL=1 node backend/scripts/repair-bisaya-children-lessons.js
|
||||
*
|
||||
* Das Script sucht in der DB nach Kurslektionen mit lesson_number=28 oder
|
||||
* mit typischen alten Titeln und ersetzt Platzhalter‑Exercises ("Option A/B/C/D")
|
||||
* durch didaktisch passende Übungen.
|
||||
*/
|
||||
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
|
||||
|
||||
const CONVERSATION_PROFILE = {
|
||||
title: 'Kinder, Spiel & Routine',
|
||||
description: 'Kinderalltag: kurze Anweisungen, Routinen und spielerische Aufforderungen',
|
||||
learningGoals: [
|
||||
'Kurze Aufforderungen und Routinen klar und freundlich formulieren',
|
||||
'Wichtige Alltagswörter (Tasche, Hände, Schlaf) sicher abrufen',
|
||||
'Routinen in der richtigen Reihenfolge ausdrücken'
|
||||
],
|
||||
corePatterns: [
|
||||
{ target: 'Hugas sa kamot.', gloss: 'Wasch dir die Hände.' },
|
||||
{ target: 'Kuhaa imong bag.', gloss: 'Hol deine Tasche.' },
|
||||
{ target: 'Matulog na ta.', gloss: 'Jetzt schlafen wir.' },
|
||||
{ target: 'Patya ang suga.', gloss: 'Mach das Licht aus.' }
|
||||
],
|
||||
speakingPrompts: [
|
||||
{ title: 'Morgenroutine', prompt: 'Sprich eine kurze Morgenroutine: Tasche holen, Hände waschen, Essen.', cue: 'Kuhaa imong bag. Hugas sa kamot. Nikaon na ka?' }
|
||||
],
|
||||
practicalTasks: [
|
||||
{ title: 'Routinekette', text: 'Bilde eine Abfolge von vier kurzen Handlungsanweisungen für Kinder.' }
|
||||
],
|
||||
exercises: [
|
||||
{
|
||||
exerciseTypeId: 2,
|
||||
title: 'Routinebefehl erkennen',
|
||||
instruction: 'Wähle die richtige Bedeutung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Was bedeutet "Hugas sa kamot"?',
|
||||
options: ['Wasch dir die Hände.', 'Hol deine Tasche.', 'Geh schlafen.', 'Iss zuerst.']
|
||||
},
|
||||
answerData: { type: 'multiple_choice', correctAnswer: 0 },
|
||||
explanation: '"Hugas sa kamot" ist eine typische Aufforderung zur Hygiene.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 1,
|
||||
title: 'Lücken: Morgenroutine',
|
||||
instruction: 'Fülle die Lücken.',
|
||||
questionData: { type: 'gap_fill', text: 'Kuhaa imong {gap}. Hugas sa {gap}.', gaps: 2 },
|
||||
answerData: { type: 'gap_fill', answers: ['bag', 'kamot'] },
|
||||
explanation: 'Tasche und Hände sind Kernwörter in Morgenroutinen.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 3,
|
||||
title: 'Routine reihenfolge',
|
||||
instruction: 'Ordne die Sätze zu einer natürlichen Reihenfolge.',
|
||||
questionData: {
|
||||
type: 'sentence_building',
|
||||
question: 'Ordne: Hände waschen, Tasche holen, Licht aus, Schlafen.',
|
||||
tokens: ['Kuhaa imong bag.', 'Hugas sa kamot.', 'Patya ang suga.', 'Matulog na ta.']
|
||||
},
|
||||
answerData: { correct: ['Kuhaa imong bag. Hugas sa kamot. Patya ang suga. Matulog na ta.'] },
|
||||
explanation: 'Typische Abfolge für Abend-/Morgenroutinen.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 10,
|
||||
title: 'Kind-Ansage',
|
||||
instruction: 'Gib eine kurze, freundliche Anweisung.',
|
||||
questionData: {
|
||||
type: 'situational_response',
|
||||
question: 'Du möchtest, dass ein Kind seine Tasche holt und sich die Hände wäscht.',
|
||||
keywords: ['kuhaa', 'bag', 'hugas', 'kamot']
|
||||
},
|
||||
answerData: {
|
||||
modelAnswer: 'Kuhaa imong bag. Hugas sa kamot.',
|
||||
keywords: ['kuhaa', 'bag', 'hugas', 'kamot']
|
||||
},
|
||||
explanation: 'Kurze klare Sätze funktionieren bei Kindern am besten.'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
function isPlaceholderExercise(exercise) {
|
||||
const q = exercise?.questionData || {};
|
||||
const questionData = typeof q === 'string' ? JSON.parse(q) : q;
|
||||
return Array.isArray(questionData?.options)
|
||||
&& questionData.options.some((option) => /^Option [A-D]$/i.test(String(option || '').trim()));
|
||||
}
|
||||
|
||||
async function repairLesson(lesson, ownerUserId, { force = false } = {}) {
|
||||
const profile = CONVERSATION_PROFILE;
|
||||
const existing = await VocabGrammarExercise.findAll({ where: { lessonId: lesson.id }, order: [['exerciseNumber', 'ASC']] });
|
||||
const hasPlaceholder = existing.some((exercise) => isPlaceholderExercise(exercise));
|
||||
const shouldReplaceExercises = force || hasPlaceholder || existing.length < profile.exercises.length;
|
||||
|
||||
await lesson.update({
|
||||
title: profile.title,
|
||||
description: profile.description,
|
||||
learningGoals: profile.learningGoals,
|
||||
corePatterns: profile.corePatterns,
|
||||
speakingPrompts: profile.speakingPrompts,
|
||||
practicalTasks: profile.practicalTasks,
|
||||
targetScorePercent: Math.max(Number(lesson.targetScorePercent) || 0, 80)
|
||||
});
|
||||
|
||||
if (!shouldReplaceExercises) return { updated: true, exercisesReplaced: false, exerciseCount: existing.length };
|
||||
|
||||
await VocabGrammarExercise.destroy({ where: { lessonId: lesson.id } });
|
||||
let exerciseNumber = 1;
|
||||
for (const exercise of profile.exercises) {
|
||||
await VocabGrammarExercise.create({
|
||||
lessonId: lesson.id,
|
||||
exerciseTypeId: exercise.exerciseTypeId,
|
||||
exerciseNumber,
|
||||
title: exercise.title,
|
||||
instruction: exercise.instruction,
|
||||
questionData: exercise.questionData,
|
||||
answerData: exercise.answerData,
|
||||
explanation: exercise.explanation,
|
||||
createdByUserId: ownerUserId
|
||||
});
|
||||
exerciseNumber += 1;
|
||||
}
|
||||
|
||||
return { updated: true, exercisesReplaced: true, exerciseCount: profile.exercises.length };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await sequelize.authenticate();
|
||||
|
||||
const [bisayaLanguage] = await sequelize.query(
|
||||
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
|
||||
{ type: sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
if (!bisayaLanguage) throw new Error('Bisaya language not found');
|
||||
|
||||
const rows = await sequelize.query(
|
||||
`SELECT l.id, l.course_id, l.lesson_number, l.title, l.lesson_type, c.owner_user_id
|
||||
FROM community.vocab_course_lesson l
|
||||
JOIN community.vocab_course c ON c.id = l.course_id
|
||||
WHERE c.language_id = :languageId
|
||||
AND (l.lesson_number = 28 OR l.title IN ('Kinder & Spiel', 'Kinder, Spiel & Routine', 'Kinder & Familie'))
|
||||
ORDER BY c.id, l.lesson_number`,
|
||||
{ replacements: { languageId: bisayaLanguage.id }, type: sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
console.log(`Gefundene Kinderlektionen: ${rows.length}`);
|
||||
let replaced = 0;
|
||||
for (const row of rows) {
|
||||
const lesson = await VocabCourseLesson.findByPk(row.id);
|
||||
const result = await repairLesson(lesson, row.owner_user_id, {
|
||||
force: process.env.VOCAB_FORCE_REBUILD_ALL === '1'
|
||||
});
|
||||
if (result.exercisesReplaced) replaced += 1;
|
||||
console.log(`✅ Kurs ${row.course_id}, Lektion ${row.lesson_number}: ${lesson.title} (${result.exerciseCount} Prüfung(en))`);
|
||||
}
|
||||
|
||||
console.log(`Fertig. Aktualisiert: ${rows.length}, Prüfungen ersetzt/erstellt: ${replaced}`);
|
||||
}
|
||||
|
||||
main()
|
||||
.then(async () => { await sequelize.close(); })
|
||||
.catch(async (error) => { console.error('❌ Reparatur fehlgeschlagen:', error); await sequelize.close(); process.exit(1); });
|
||||
Reference in New Issue
Block a user