All checks were successful
Deploy to production / deploy (push) Successful in 3m7s
- Updated FalukantService to include age details for partners in relationships. - Added translations for 'age' in English, German, and Spanish localization files. - Enhanced FamilyView component to display age information for lovers and candidates, improving user experience.
736 lines
24 KiB
JavaScript
736 lines
24 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Script zum Erstellen von Übungen für Deutschkurse aus Sicht von Bisaya-Lernenden.
|
|
*
|
|
* Verwendung:
|
|
* node backend/scripts/create-german-for-bisaya-course-content.js
|
|
*/
|
|
|
|
import { sequelize } from '../utils/sequelize.js';
|
|
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
|
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
|
|
import User from '../models/community/user.js';
|
|
import { GERMAN_FOR_BISAYA_PHASE1_DIDACTICS } from './german-for-bisaya-phase1.js';
|
|
import { GERMAN_FOR_BISAYA_PHASE3_DIDACTICS } from './german-for-bisaya-phase3-extension.js';
|
|
import { GERMAN_FOR_BISAYA_PHASE4_DIDACTICS } from './german-for-bisaya-phase4-extension.js';
|
|
import { GERMAN_FOR_BISAYA_PHASE5_DIDACTICS } from './german-for-bisaya-phase5-extension.js';
|
|
|
|
function withTypeName(exerciseTypeName, exercise) {
|
|
return {
|
|
...exercise,
|
|
exerciseTypeName
|
|
};
|
|
}
|
|
|
|
const GERMAN_DIDACTICS = {
|
|
...GERMAN_FOR_BISAYA_PHASE1_DIDACTICS,
|
|
...GERMAN_FOR_BISAYA_PHASE3_DIDACTICS,
|
|
...GERMAN_FOR_BISAYA_PHASE4_DIDACTICS,
|
|
...GERMAN_FOR_BISAYA_PHASE5_DIDACTICS
|
|
};
|
|
|
|
const GENERIC_DISTRACTOR_PATTERNS = Array.from(new Set(
|
|
Object.values(GERMAN_DIDACTICS)
|
|
.flatMap((entry) => Array.isArray(entry?.corePatterns) ? entry.corePatterns : [])
|
|
.map((pattern) => String(pattern || '').trim())
|
|
.filter(Boolean)
|
|
)).slice(0, 300);
|
|
|
|
function normalizeText(value) {
|
|
return String(value || '')
|
|
.trim()
|
|
.replace(/\s+/g, ' ');
|
|
}
|
|
|
|
function simpleHash(value) {
|
|
return Array.from(String(value || '')).reduce((sum, char) => sum + char.charCodeAt(0), 0);
|
|
}
|
|
|
|
function rotateArray(values, offset) {
|
|
if (!Array.isArray(values) || values.length === 0) return [];
|
|
const normalizedOffset = ((offset % values.length) + values.length) % values.length;
|
|
return values.slice(normalizedOffset).concat(values.slice(0, normalizedOffset));
|
|
}
|
|
|
|
function getLessonDidactics(lesson) {
|
|
const staticDidactics = GERMAN_DIDACTICS[lesson.title] || {};
|
|
|
|
return {
|
|
learningGoals: Array.isArray(lesson.learningGoals) ? lesson.learningGoals : (staticDidactics.learningGoals || []),
|
|
corePatterns: (Array.isArray(lesson.corePatterns) ? lesson.corePatterns : (staticDidactics.corePatterns || []))
|
|
.map((entry) => normalizeText(entry))
|
|
.filter(Boolean),
|
|
grammarFocus: Array.isArray(lesson.grammarFocus) ? lesson.grammarFocus : (staticDidactics.grammarFocus || []),
|
|
speakingPrompts: Array.isArray(lesson.speakingPrompts) ? lesson.speakingPrompts : (staticDidactics.speakingPrompts || []),
|
|
practicalTasks: Array.isArray(lesson.practicalTasks) ? lesson.practicalTasks : (staticDidactics.practicalTasks || [])
|
|
};
|
|
}
|
|
|
|
function getScenarioPrompt(lesson, didactics) {
|
|
const speakingPrompt = Array.isArray(didactics.speakingPrompts) ? didactics.speakingPrompts[0] : null;
|
|
const practicalTask = Array.isArray(didactics.practicalTasks) ? didactics.practicalTasks[0] : null;
|
|
|
|
if (speakingPrompt?.prompt) return speakingPrompt.prompt;
|
|
if (practicalTask?.text) return practicalTask.text;
|
|
if (lesson.description) return lesson.description;
|
|
return `Reagiere passend in einer Alltagssituation aus der Lektion "${lesson.title}".`;
|
|
}
|
|
|
|
function getChoiceQuestion(lesson, didactics) {
|
|
const scenarioPrompt = getScenarioPrompt(lesson, didactics);
|
|
|
|
switch (lesson.lessonType) {
|
|
case 'conversation':
|
|
return `${scenarioPrompt} Welche deutsche Formulierung passt am besten?`;
|
|
case 'grammar':
|
|
return `Welche deutsche Struktur passt am besten zum Schwerpunkt "${lesson.title}"?`;
|
|
case 'review':
|
|
return `Welche Formulierung solltest du aus "${lesson.title}" sicher wiedererkennen?`;
|
|
case 'culture':
|
|
return `Welche Formulierung passt besonders gut zum kulturellen Schwerpunkt "${lesson.title}"?`;
|
|
case 'vocab':
|
|
default:
|
|
return `Welche Formulierung gehört thematisch zu "${lesson.title}"?`;
|
|
}
|
|
}
|
|
|
|
function pickDistractors(pattern, allPatterns, count) {
|
|
return allPatterns
|
|
.filter((entry) => entry !== pattern)
|
|
.slice(0, count);
|
|
}
|
|
|
|
function buildChoiceExercise(lesson, didactics, pattern, allPatterns, variant = 0) {
|
|
const distractors = pickDistractors(pattern, allPatterns, 3);
|
|
if (distractors.length < 3) return null;
|
|
|
|
const options = rotateArray([pattern, ...distractors], simpleHash(`${lesson.title}:${pattern}:${variant}`) % 4);
|
|
|
|
return {
|
|
exerciseTypeId: 2,
|
|
title: `${lesson.title}: Passende Formulierung wählen`,
|
|
instruction: 'Wähle die natürlichste deutsche Formulierung für die Situation oder den Schwerpunkt der Lektion.',
|
|
questionData: {
|
|
type: 'multiple_choice',
|
|
question: getChoiceQuestion(lesson, didactics),
|
|
options
|
|
},
|
|
answerData: {
|
|
type: 'multiple_choice',
|
|
correctAnswer: options.indexOf(pattern)
|
|
},
|
|
explanation: `"${pattern}" ist ein zentrales deutsches Muster dieser Lektion.`
|
|
};
|
|
}
|
|
|
|
function pickGapTarget(pattern) {
|
|
const tokens = normalizeText(pattern)
|
|
.split(' ')
|
|
.map((token) => token.trim())
|
|
.filter(Boolean);
|
|
|
|
const candidates = tokens
|
|
.map((token, index) => ({ token, index, score: token.replace(/[.,?!]/g, '').length }))
|
|
.filter(({ token }) => token.replace(/[.,?!]/g, '').length >= 3);
|
|
|
|
if (candidates.length === 0) return null;
|
|
|
|
candidates.sort((left, right) => right.score - left.score);
|
|
return candidates[0];
|
|
}
|
|
|
|
function buildGapExercise(lessonTitle, pattern) {
|
|
const gapTarget = pickGapTarget(pattern);
|
|
if (!gapTarget) return null;
|
|
|
|
const tokens = normalizeText(pattern).split(' ');
|
|
tokens[gapTarget.index] = '{gap}';
|
|
|
|
return {
|
|
exerciseTypeId: 1,
|
|
title: `${lessonTitle}: Muster vervollständigen`,
|
|
instruction: 'Fülle die Lücke mit dem passenden deutschen Ausdruck.',
|
|
questionData: {
|
|
type: 'gap_fill',
|
|
text: tokens.join(' '),
|
|
gaps: 1
|
|
},
|
|
answerData: {
|
|
type: 'gap_fill',
|
|
answers: [gapTarget.token.replace(/[.,?!]/g, '')]
|
|
},
|
|
explanation: `Das vollständige deutsche Kernmuster lautet: "${pattern}".`
|
|
};
|
|
}
|
|
|
|
function buildSentenceExercise(lessonTitle, pattern) {
|
|
const tokens = normalizeText(pattern)
|
|
.replace(/[?!]/g, '')
|
|
.split(' ')
|
|
.filter(Boolean);
|
|
|
|
if (tokens.length < 2) return null;
|
|
|
|
return {
|
|
exerciseTypeId: 3,
|
|
title: `${lessonTitle}: Satz bauen`,
|
|
instruction: 'Ordne die Wörter zu einem korrekten deutschen Satz.',
|
|
questionData: {
|
|
type: 'sentence_building',
|
|
question: `Baue das Kernmuster aus der Lektion "${lessonTitle}".`,
|
|
tokens
|
|
},
|
|
answerData: {
|
|
correct: [normalizeText(pattern)]
|
|
},
|
|
explanation: `Dieses Kernmuster gehört zur Lektion "${lessonTitle}".`
|
|
};
|
|
}
|
|
|
|
function buildSpeakingExercise(lessonTitle, didactics, fallbackPattern) {
|
|
const speakingPrompt = Array.isArray(didactics.speakingPrompts) ? didactics.speakingPrompts[0] : null;
|
|
const expectedText = normalizeText(speakingPrompt?.cue || fallbackPattern);
|
|
if (!expectedText) return null;
|
|
|
|
const keywords = expectedText
|
|
.toLowerCase()
|
|
.replace(/[.,?!]/g, '')
|
|
.split(' ')
|
|
.filter((token) => token.length >= 3)
|
|
.slice(0, 5);
|
|
|
|
return {
|
|
exerciseTypeId: 8,
|
|
title: `${lessonTitle}: Frei sprechen`,
|
|
instruction: 'Sprich das zentrale deutsche Muster frei nach.',
|
|
questionData: {
|
|
type: 'speaking_from_memory',
|
|
question: speakingPrompt?.prompt || `Sprich ein zentrales Muster aus der Lektion "${lessonTitle}".`,
|
|
expectedText,
|
|
keywords
|
|
},
|
|
answerData: {
|
|
type: 'speaking_from_memory'
|
|
},
|
|
explanation: 'Wichtig sind ein flüssiger Abruf und die zentralen deutschen Schlüsselwörter.'
|
|
};
|
|
}
|
|
|
|
function buildSituationalExercise(lessonTitle, didactics, fallbackPattern) {
|
|
const speakingPrompt = Array.isArray(didactics.speakingPrompts) ? didactics.speakingPrompts[0] : null;
|
|
const modelAnswer = normalizeText(speakingPrompt?.cue || fallbackPattern);
|
|
if (!modelAnswer) return null;
|
|
|
|
const keywords = modelAnswer
|
|
.toLowerCase()
|
|
.replace(/[.,?!]/g, '')
|
|
.split(' ')
|
|
.filter((token) => token.length >= 3)
|
|
.slice(0, 5);
|
|
|
|
return withTypeName('situational_response', {
|
|
title: `${lessonTitle}: Situativ reagieren`,
|
|
instruction: 'Antworte kurz und passend auf Deutsch.',
|
|
questionData: {
|
|
type: 'situational_response',
|
|
question: speakingPrompt?.prompt || `Reagiere passend mit einem Ausdruck aus der Lektion "${lessonTitle}".`,
|
|
keywords
|
|
},
|
|
answerData: {
|
|
modelAnswer,
|
|
keywords
|
|
},
|
|
explanation: `Das Kernmuster "${modelAnswer}" passt natürlich zu dieser Situation.`
|
|
});
|
|
}
|
|
|
|
function buildCultureExercise(lesson, pattern, allPatterns) {
|
|
const distractors = pickDistractors(pattern, allPatterns, 3);
|
|
if (distractors.length < 3) return null;
|
|
|
|
const options = rotateArray([pattern, ...distractors], simpleHash(`${lesson.title}:culture`) % 4);
|
|
const culturalNote = normalizeText(lesson.culturalNotes || '');
|
|
|
|
return {
|
|
exerciseTypeId: 2,
|
|
title: `${lesson.title}: Kulturell einordnen`,
|
|
instruction: 'Ordne den Ausdruck dem kulturellen Schwerpunkt der Lektion zu.',
|
|
questionData: {
|
|
type: 'multiple_choice',
|
|
question: culturalNote
|
|
? `${culturalNote} Welche Formulierung passt dazu besonders gut?`
|
|
: `Welche Formulierung passt besonders gut zum Schwerpunkt "${lesson.title}"?`,
|
|
options
|
|
},
|
|
answerData: {
|
|
type: 'multiple_choice',
|
|
correctAnswer: options.indexOf(pattern)
|
|
},
|
|
explanation: `"${pattern}" ist eng mit dem kulturellen Schwerpunkt dieser Lektion verbunden.`
|
|
};
|
|
}
|
|
|
|
function generateExercisesFromDidactics(lesson) {
|
|
const didactics = getLessonDidactics(lesson);
|
|
const corePatterns = didactics.corePatterns;
|
|
if (corePatterns.length === 0) return [];
|
|
|
|
const patternA = corePatterns[0];
|
|
const patternB = corePatterns[1] || corePatterns[0];
|
|
const pool = Array.from(new Set([...corePatterns, ...GENERIC_DISTRACTOR_PATTERNS]));
|
|
|
|
if (lesson.lessonType === 'conversation') {
|
|
return [
|
|
buildChoiceExercise(lesson, didactics, patternA, pool, 0),
|
|
buildGapExercise(lesson.title, patternA),
|
|
buildSentenceExercise(lesson.title, patternB),
|
|
buildSituationalExercise(lesson.title, didactics, patternA),
|
|
buildSpeakingExercise(lesson.title, didactics, patternB)
|
|
].filter(Boolean);
|
|
}
|
|
|
|
if (lesson.lessonType === 'grammar') {
|
|
return [
|
|
buildChoiceExercise(lesson, didactics, patternA, pool, 0),
|
|
buildChoiceExercise(lesson, didactics, patternB, pool, 1),
|
|
buildGapExercise(lesson.title, patternA),
|
|
buildSentenceExercise(lesson.title, patternB),
|
|
buildSpeakingExercise(lesson.title, didactics, patternA)
|
|
].filter(Boolean);
|
|
}
|
|
|
|
if (lesson.lessonType === 'review' || lesson.didacticMode === 'intensive_review') {
|
|
return [
|
|
buildChoiceExercise(lesson, didactics, patternA, pool, 0),
|
|
buildChoiceExercise(lesson, didactics, patternB, pool, 1),
|
|
buildGapExercise(lesson.title, patternA),
|
|
buildSentenceExercise(lesson.title, patternB),
|
|
buildSituationalExercise(lesson.title, didactics, patternA),
|
|
buildSpeakingExercise(lesson.title, didactics, patternB)
|
|
].filter(Boolean);
|
|
}
|
|
|
|
if (lesson.lessonType === 'culture') {
|
|
return [
|
|
buildCultureExercise(lesson, patternA, pool),
|
|
buildGapExercise(lesson.title, patternA),
|
|
buildSpeakingExercise(lesson.title, didactics, patternB)
|
|
].filter(Boolean);
|
|
}
|
|
|
|
return [
|
|
buildChoiceExercise(lesson, didactics, patternA, pool, 0),
|
|
buildChoiceExercise(lesson, didactics, patternB, pool, 1),
|
|
buildGapExercise(lesson.title, patternA),
|
|
buildSentenceExercise(lesson.title, patternB)
|
|
].filter(Boolean);
|
|
}
|
|
|
|
const GERMAN_EXERCISES = {
|
|
'Begrüßung & Vorstellung': [
|
|
{
|
|
exerciseTypeId: 2,
|
|
title: 'Vorstellung erkennen',
|
|
instruction: 'Wähle die passendste deutsche Vorstellung.',
|
|
questionData: {
|
|
type: 'multiple_choice',
|
|
question: 'Du triffst jemanden zum ersten Mal. Welche Formulierung passt?',
|
|
options: ['Hallo, ich heiße Maria.', 'Ich habe Maria.', 'Wo Maria?', 'Nicht Maria.']
|
|
},
|
|
answerData: {
|
|
type: 'multiple_choice',
|
|
correctAnswer: 0
|
|
},
|
|
explanation: '"Hallo, ich heiße Maria." ist eine natürliche deutsche Selbstvorstellung.'
|
|
},
|
|
{
|
|
exerciseTypeId: 1,
|
|
title: 'Herkunft ergänzen',
|
|
instruction: 'Fülle die Lücke mit dem passenden Ausdruck.',
|
|
questionData: {
|
|
type: 'gap_fill',
|
|
text: 'Ich komme {gap} Cebu.',
|
|
gaps: 1
|
|
},
|
|
answerData: {
|
|
type: 'gap_fill',
|
|
answers: ['aus']
|
|
},
|
|
explanation: 'Im Deutschen sagt man "Ich komme aus Cebu."'
|
|
},
|
|
withTypeName('situational_response', {
|
|
title: 'Kurz reagieren bei der Begrüßung',
|
|
instruction: 'Antworte kurz und passend auf Deutsch.',
|
|
questionData: {
|
|
type: 'situational_response',
|
|
question: 'Jemand sagt: "Hallo, ich heiße Anna." Wie reagierst du mit deiner eigenen Vorstellung?',
|
|
keywords: ['hallo', 'heiße', 'ich']
|
|
},
|
|
answerData: {
|
|
modelAnswer: 'Hallo, ich heiße Maria.',
|
|
keywords: ['hallo', 'heiße', 'ich']
|
|
},
|
|
explanation: 'Die sicherste frühe Reaktion ist eine eigene kurze Vorstellung mit "ich heiße".'
|
|
})
|
|
],
|
|
'der / die / das - Einstieg': [
|
|
{
|
|
exerciseTypeId: 2,
|
|
title: 'Artikel als Chunk erkennen',
|
|
instruction: 'Wähle die Form mit dem richtigen Artikel.',
|
|
questionData: {
|
|
type: 'multiple_choice',
|
|
question: 'Welche Form ist als Lern-Chunk richtig?',
|
|
options: ['der Tisch', 'die Tisch', 'das Tasche', 'der Kind']
|
|
},
|
|
answerData: {
|
|
type: 'multiple_choice',
|
|
correctAnswer: 0
|
|
},
|
|
explanation: 'Nomen sollen möglichst zusammen mit dem Artikel gelernt werden.'
|
|
},
|
|
{
|
|
exerciseTypeId: 1,
|
|
title: 'Artikel ergänzen',
|
|
instruction: 'Fülle den passenden Artikel ein.',
|
|
questionData: {
|
|
type: 'gap_fill',
|
|
text: '{gap} Tasche',
|
|
gaps: 1
|
|
},
|
|
answerData: {
|
|
type: 'gap_fill',
|
|
answers: ['die']
|
|
},
|
|
explanation: 'Es heißt "die Tasche".'
|
|
},
|
|
{
|
|
exerciseTypeId: 3,
|
|
title: 'Chunk richtig bauen',
|
|
instruction: 'Ordne die Wörter zu einem korrekten Lern-Chunk.',
|
|
questionData: {
|
|
type: 'sentence_building',
|
|
question: 'Baue den richtigen Chunk für "child".',
|
|
tokens: ['das', 'Kind']
|
|
},
|
|
answerData: {
|
|
correct: ['das Kind']
|
|
},
|
|
explanation: 'Auch sehr kurze Strukturen sollen als vollständiger Chunk gelernt werden.'
|
|
}
|
|
],
|
|
'nicht / kein - Einstieg': [
|
|
{
|
|
exerciseTypeId: 2,
|
|
title: 'Negation unterscheiden',
|
|
instruction: 'Wähle die passende deutsche Negation.',
|
|
questionData: {
|
|
type: 'multiple_choice',
|
|
question: 'Welche Formulierung ist richtig?',
|
|
options: ['Ich habe kein Geld.', 'Ich habe nicht Geld.', 'Ich bin kein müde.', 'Ich kein habe Geld.']
|
|
},
|
|
answerData: {
|
|
type: 'multiple_choice',
|
|
correctAnswer: 0
|
|
},
|
|
explanation: '"Kein" steht hier vor dem Nomen, deshalb ist "Ich habe kein Geld." richtig.'
|
|
},
|
|
{
|
|
exerciseTypeId: 2,
|
|
title: 'Adjektiv negieren',
|
|
instruction: 'Wähle die richtige Form für eine Aussage mit Adjektiv.',
|
|
questionData: {
|
|
type: 'multiple_choice',
|
|
question: 'Wie sagst du korrekt: "Ich bin nicht müde"?',
|
|
options: ['Ich bin nicht müde.', 'Ich bin kein müde.', 'Ich nicht bin müde.', 'Ich bin müde kein.']
|
|
},
|
|
answerData: {
|
|
type: 'multiple_choice',
|
|
correctAnswer: 0
|
|
},
|
|
explanation: 'Adjektive und Aussagen werden im Deutschen oft mit "nicht" negiert.'
|
|
},
|
|
{
|
|
exerciseTypeId: 1,
|
|
title: 'Kein ergänzen',
|
|
instruction: 'Ergänze die passende Negation.',
|
|
questionData: {
|
|
type: 'gap_fill',
|
|
text: 'Ich habe {gap} Zeit.',
|
|
gaps: 1
|
|
},
|
|
answerData: {
|
|
type: 'gap_fill',
|
|
answers: ['keine']
|
|
},
|
|
explanation: 'Bei "Zeit" braucht man hier "keine".'
|
|
}
|
|
],
|
|
'wo / wohin - Kontrast': [
|
|
{
|
|
exerciseTypeId: 2,
|
|
title: 'Ort oder Richtung?',
|
|
instruction: 'Wähle die Frage nach einem Ziel.',
|
|
questionData: {
|
|
type: 'multiple_choice',
|
|
question: 'Welche Frage passt, wenn du nach dem Ziel fragst?',
|
|
options: ['Wohin gehst du?', 'Wo gehst du?', 'Wie gehst du?', 'Wann gehst du?']
|
|
},
|
|
answerData: {
|
|
type: 'multiple_choice',
|
|
correctAnswer: 0
|
|
},
|
|
explanation: '"Wohin" fragt nach einer Richtung oder einem Ziel.'
|
|
},
|
|
{
|
|
exerciseTypeId: 1,
|
|
title: 'Fragewort ergänzen',
|
|
instruction: 'Setze das passende Fragewort ein.',
|
|
questionData: {
|
|
type: 'gap_fill',
|
|
text: '{gap} bist du? Ich bin zu Hause.',
|
|
gaps: 1
|
|
},
|
|
answerData: {
|
|
type: 'gap_fill',
|
|
answers: ['Wo']
|
|
},
|
|
explanation: 'Bei einem Ort fragt man im Deutschen mit "Wo".'
|
|
}
|
|
],
|
|
'du / Sie - Einstieg': [
|
|
{
|
|
exerciseTypeId: 2,
|
|
title: 'Höfliche Anrede erkennen',
|
|
instruction: 'Wähle die höfliche deutsche Form.',
|
|
questionData: {
|
|
type: 'multiple_choice',
|
|
question: 'Du sprichst eine fremde erwachsene Person höflich an. Welche Frage passt?',
|
|
options: ['Wie heißen Sie?', 'Wie heißt du?', 'Wo bist du?', 'Wie bist du?']
|
|
},
|
|
answerData: {
|
|
type: 'multiple_choice',
|
|
correctAnswer: 0
|
|
},
|
|
explanation: '"Sie" markiert im Deutschen die höfliche Anrede.'
|
|
},
|
|
{
|
|
exerciseTypeId: 1,
|
|
title: 'Anredeform ergänzen',
|
|
instruction: 'Fülle die passende Anredeform ein.',
|
|
questionData: {
|
|
type: 'gap_fill',
|
|
text: 'Können {gap} mir helfen?',
|
|
gaps: 1
|
|
},
|
|
answerData: {
|
|
type: 'gap_fill',
|
|
answers: ['Sie']
|
|
},
|
|
explanation: 'In einer höflichen Bitte heißt es "Können Sie mir helfen?".'
|
|
}
|
|
],
|
|
'Perfekt - Einstieg': [
|
|
{
|
|
exerciseTypeId: 2,
|
|
title: 'Perfektform erkennen',
|
|
instruction: 'Wähle die passende deutsche Vergangenheitsform.',
|
|
questionData: {
|
|
type: 'multiple_choice',
|
|
question: 'Welche Form ist ein korrektes frühes Perfekt?',
|
|
options: ['Ich habe gearbeitet.', 'Ich habe arbeiten.', 'Ich bin gearbeitet.', 'Ich arbeitete habe.']
|
|
},
|
|
answerData: {
|
|
type: 'multiple_choice',
|
|
correctAnswer: 0
|
|
},
|
|
explanation: 'Viele frühe Alltagsverben bilden das Perfekt mit "haben" + Partizip II.'
|
|
},
|
|
{
|
|
exerciseTypeId: 1,
|
|
title: 'Hilfsverb ergänzen',
|
|
instruction: 'Ergänze das passende Hilfsverb.',
|
|
questionData: {
|
|
type: 'gap_fill',
|
|
text: 'Ich {gap} gestern gearbeitet.',
|
|
gaps: 1
|
|
},
|
|
answerData: {
|
|
type: 'gap_fill',
|
|
answers: ['habe']
|
|
},
|
|
explanation: 'Im Perfekt braucht man hier das Hilfsverb "habe".'
|
|
}
|
|
]
|
|
};
|
|
|
|
async function resolveExerciseTypeId(exercise) {
|
|
if (exercise.exerciseTypeId) {
|
|
return exercise.exerciseTypeId;
|
|
}
|
|
|
|
const trimmedName =
|
|
exercise.exerciseTypeName != null && exercise.exerciseTypeName !== ''
|
|
? String(exercise.exerciseTypeName).trim()
|
|
: '';
|
|
|
|
if (!trimmedName) {
|
|
throw new Error(`Kein exerciseTypeId oder exerciseTypeName für Übung "${exercise.title || 'unbenannt'}" definiert`);
|
|
}
|
|
|
|
const [type] = await sequelize.query(
|
|
`SELECT id FROM community.vocab_grammar_exercise_type WHERE name = :name LIMIT 1`,
|
|
{
|
|
replacements: { name: trimmedName },
|
|
type: sequelize.QueryTypes.SELECT
|
|
}
|
|
);
|
|
|
|
if (!type) {
|
|
throw new Error(`Übungstyp "${trimmedName}" nicht gefunden`);
|
|
}
|
|
|
|
return Number(type.id);
|
|
}
|
|
|
|
async function findOrCreateSystemUser() {
|
|
let systemUser = await User.findOne({ where: { username: 'system' } });
|
|
if (!systemUser) {
|
|
systemUser = await User.findOne({ where: { username: 'admin' } });
|
|
}
|
|
if (!systemUser) {
|
|
throw new Error('System user not found');
|
|
}
|
|
return systemUser;
|
|
}
|
|
|
|
function getExercisesForLesson(lesson) {
|
|
if (GERMAN_EXERCISES[lesson.title]) {
|
|
return GERMAN_EXERCISES[lesson.title];
|
|
}
|
|
|
|
for (const [key, exercises] of Object.entries(GERMAN_EXERCISES)) {
|
|
if (lesson.title.includes(key) || key.includes(lesson.title)) {
|
|
return exercises;
|
|
}
|
|
}
|
|
|
|
return generateExercisesFromDidactics(lesson);
|
|
}
|
|
|
|
async function createGermanForBisayaCourseContent() {
|
|
await sequelize.authenticate();
|
|
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
|
|
|
|
const systemUser = await findOrCreateSystemUser();
|
|
console.log(`Verwende System-Benutzer: ${systemUser.username} (ID: ${systemUser.id})\n`);
|
|
|
|
const [germanLanguage] = await sequelize.query(
|
|
`SELECT id FROM community.vocab_language WHERE name = 'Deutsch' LIMIT 1`,
|
|
{
|
|
type: sequelize.QueryTypes.SELECT
|
|
}
|
|
);
|
|
|
|
const [bisayaLanguage] = await sequelize.query(
|
|
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
|
|
{
|
|
type: sequelize.QueryTypes.SELECT
|
|
}
|
|
);
|
|
|
|
if (!germanLanguage || !bisayaLanguage) {
|
|
throw new Error('Deutsch oder Bisaya als Sprache nicht gefunden');
|
|
}
|
|
|
|
const courses = await sequelize.query(
|
|
`SELECT id, title, owner_user_id AS "ownerUserId"
|
|
FROM community.vocab_course
|
|
WHERE language_id = :languageId
|
|
AND native_language_id = :nativeLanguageId
|
|
AND title LIKE 'Deutsch für Bisaya-Lernende%'`,
|
|
{
|
|
replacements: {
|
|
languageId: germanLanguage.id,
|
|
nativeLanguageId: bisayaLanguage.id
|
|
},
|
|
type: sequelize.QueryTypes.SELECT
|
|
}
|
|
);
|
|
|
|
console.log(`Gefunden: ${courses.length} Deutsch-für-Bisaya-Kurse\n`);
|
|
|
|
let totalExercisesAdded = 0;
|
|
let totalLessonsProcessed = 0;
|
|
const replaceLessons = new Set(Object.keys(GERMAN_EXERCISES));
|
|
|
|
for (const course of courses) {
|
|
console.log(`📚 Kurs: ${course.title} (ID: ${course.id})`);
|
|
|
|
const lessons = await VocabCourseLesson.findAll({
|
|
where: { courseId: course.id },
|
|
order: [['lessonNumber', 'ASC']]
|
|
});
|
|
|
|
console.log(` ${lessons.length} Lektionen gefunden\n`);
|
|
|
|
for (const lesson of lessons) {
|
|
const exercises = getExercisesForLesson(lesson);
|
|
if (exercises.length === 0) {
|
|
console.log(` ⚠️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - keine Übungen definiert`);
|
|
continue;
|
|
}
|
|
|
|
const existingCount = await VocabGrammarExercise.count({ where: { lessonId: lesson.id } });
|
|
const replaceExisting = replaceLessons.has(lesson.title);
|
|
|
|
if (existingCount > 0 && !replaceExisting) {
|
|
console.log(` ⏭️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - bereits ${existingCount} Übung(en) vorhanden`);
|
|
continue;
|
|
}
|
|
|
|
if (replaceExisting && existingCount > 0) {
|
|
const deleted = await VocabGrammarExercise.destroy({ where: { lessonId: lesson.id } });
|
|
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deleted} bestehende Übung(en) entfernt`);
|
|
}
|
|
|
|
let exerciseNumber = 1;
|
|
for (const exerciseData of exercises) {
|
|
const exerciseTypeId = await resolveExerciseTypeId(exerciseData);
|
|
await VocabGrammarExercise.create({
|
|
lessonId: lesson.id,
|
|
exerciseTypeId,
|
|
exerciseNumber: exerciseNumber++,
|
|
title: exerciseData.title,
|
|
instruction: exerciseData.instruction,
|
|
questionData: JSON.stringify(exerciseData.questionData),
|
|
answerData: JSON.stringify(exerciseData.answerData),
|
|
explanation: exerciseData.explanation,
|
|
createdByUserId: course.ownerUserId || systemUser.id
|
|
});
|
|
totalExercisesAdded++;
|
|
}
|
|
|
|
console.log(` ✅ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${exercises.length} Übung(en) erstellt`);
|
|
totalLessonsProcessed++;
|
|
}
|
|
|
|
console.log('');
|
|
}
|
|
|
|
console.log('\n🎉 Zusammenfassung:');
|
|
console.log(` ${totalLessonsProcessed} Lektionen bearbeitet`);
|
|
console.log(` ${totalExercisesAdded} Übungen erstellt`);
|
|
}
|
|
|
|
createGermanForBisayaCourseContent()
|
|
.then(() => {
|
|
sequelize.close();
|
|
process.exit(0);
|
|
})
|
|
.catch((error) => {
|
|
console.error('❌ Fehler:', error);
|
|
sequelize.close();
|
|
process.exit(1);
|
|
});
|