Files
yourpart3/backend/scripts/create-language-courses.js
Torsten Schulz (local) 0572a0eb50 Add grammar exercise creation in course generation
- Integrated functionality to create example grammar exercises for grammar lessons during course creation.
- Added a new helper function to generate gap fill and multiple choice exercises based on lesson data.
- Enhanced logging to confirm the number of grammar exercises created, improving feedback during course setup.
2026-01-19 15:15:24 +01:00

558 lines
20 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Script zum Erstellen von öffentlichen Sprachkursen für verschiedene Sprachen
*
* Verwendung:
* node backend/scripts/create-language-courses.js
*
* Erstellt öffentliche Kurse für alle Kombinationen von:
* - Zielsprachen: Bisaya, Französisch, Spanisch, Latein, Italienisch, Portugiesisch, Tagalog
* - Muttersprachen: Deutsch, Englisch, Spanisch, Französisch, Italienisch, Portugiesisch
*
* Die Kurse werden automatisch einem System-Benutzer zugeordnet und sind öffentlich zugänglich.
*/
import { sequelize } from '../utils/sequelize.js';
import VocabCourse from '../models/community/vocab_course.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 crypto from 'crypto';
import bcrypt from 'bcryptjs';
// Kursstruktur für alle Sprachen (4 Wochen, 40 Lektionen)
const LESSON_TEMPLATE = [
// WOCHE 1: Grundlagen & Aussprache
{ week: 1, day: 1, num: 1, type: 'conversation', title: 'Begrüßungen & Höflichkeit',
desc: 'Lerne die wichtigsten Begrüßungen und Höflichkeitsformeln',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Höflichkeit ist wichtig. Lächeln hilft!' },
{ week: 1, day: 1, num: 2, type: 'vocab', title: 'Überlebenssätze - Teil 1',
desc: 'Die 10 wichtigsten Sätze für den Alltag',
targetMin: 20, targetScore: 85, review: true,
cultural: 'Diese Sätze helfen dir sofort im Alltag weiter.' },
{ week: 1, day: 2, num: 3, type: 'vocab', title: 'Familienwörter',
desc: 'Mama, Papa, Geschwister, Großeltern und mehr',
targetMin: 20, targetScore: 85, review: true,
cultural: 'Familienwörter sind wichtig für echte Gespräche.' },
{ week: 1, day: 2, num: 4, type: 'conversation', title: 'Familien-Gespräche',
desc: 'Einfache Gespräche mit Familienmitgliedern',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Familienkonversationen sind herzlicher als formelle Gespräche.' },
{ week: 1, day: 3, num: 5, type: 'conversation', title: 'Gefühle & Zuneigung',
desc: 'Wie man Gefühle ausdrückt',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Gefühle auszudrücken ist wichtig für echte Verbindung.' },
{ week: 1, day: 3, num: 6, type: 'vocab', title: 'Überlebenssätze - Teil 2',
desc: 'Weitere wichtige Alltagssätze',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 1, day: 4, num: 7, type: 'conversation', title: 'Essen & Fürsorge',
desc: 'Gespräche rund ums Essen',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Essen verbindet Menschen überall auf der Welt.' },
{ week: 1, day: 4, num: 8, type: 'vocab', title: 'Essen & Trinken',
desc: 'Wichtige Wörter rund ums Essen',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 1, day: 5, num: 9, type: 'review', title: 'Woche 1 - Wiederholung',
desc: 'Wiederhole alle Inhalte der ersten Woche',
targetMin: 30, targetScore: 80, review: false,
cultural: 'Wiederholung ist der Schlüssel zum Erfolg!' },
{ week: 1, day: 5, num: 10, type: 'vocab', title: 'Woche 1 - Vokabeltest',
desc: 'Teste dein Wissen aus Woche 1',
targetMin: 15, targetScore: 80, review: true,
cultural: null },
// WOCHE 2: Alltag & Familie
{ week: 2, day: 1, num: 11, type: 'conversation', title: 'Alltagsgespräche - Teil 1',
desc: 'Wie war dein Tag? Was machst du?',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Alltagsgespräche sind wichtig für echte Kommunikation.' },
{ week: 2, day: 1, num: 12, type: 'vocab', title: 'Haus & Familie',
desc: 'Wörter für Haus, Zimmer, Familie',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 2, day: 2, num: 13, type: 'conversation', title: 'Alltagsgespräche - Teil 2',
desc: 'Wohin gehst du? Was machst du heute?',
targetMin: 15, targetScore: 80, review: false,
cultural: null },
{ week: 2, day: 2, num: 14, type: 'vocab', title: 'Ort & Richtung',
desc: 'Wo, hier, dort, gehen zu',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 2, day: 3, num: 15, type: 'grammar', title: 'Zeitformen - Grundlagen',
desc: 'Vergangenheit, Gegenwart, Zukunft',
targetMin: 25, targetScore: 75, review: true,
cultural: 'Zeitformen sind wichtig für präzise Kommunikation.' },
{ week: 2, day: 3, num: 16, type: 'vocab', title: 'Zeit & Datum',
desc: 'Jetzt, morgen, gestern, heute',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 2, day: 4, num: 17, type: 'conversation', title: 'Einkaufen & Preise',
desc: 'Wie viel kostet das? Kann es billiger sein?',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Einkaufen ist eine wichtige Alltagssituation.' },
{ week: 2, day: 4, num: 18, type: 'vocab', title: 'Zahlen & Preise',
desc: 'Zahlen 1-100, Preise, Mengen',
targetMin: 25, targetScore: 85, review: true,
cultural: null },
{ week: 2, day: 5, num: 19, type: 'review', title: 'Woche 2 - Wiederholung',
desc: 'Wiederhole alle Inhalte der zweiten Woche',
targetMin: 30, targetScore: 80, review: false,
cultural: null },
{ week: 2, day: 5, num: 20, type: 'vocab', title: 'Woche 2 - Vokabeltest',
desc: 'Teste dein Wissen aus Woche 2',
targetMin: 15, targetScore: 80, review: true,
cultural: null },
// WOCHE 3: Vertiefung
{ week: 3, day: 1, num: 21, type: 'conversation', title: 'Gefühle & Emotionen',
desc: 'Wie man verschiedene Gefühle ausdrückt',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Emotionen auszudrücken ist wichtig für echte Verbindung.' },
{ week: 3, day: 1, num: 22, type: 'vocab', title: 'Gefühle & Emotionen',
desc: 'Wörter für verschiedene Gefühle',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 3, day: 2, num: 23, type: 'conversation', title: 'Gesundheit & Wohlbefinden',
desc: 'Gespräche über Gesundheit',
targetMin: 15, targetScore: 80, review: false,
cultural: null },
{ week: 3, day: 2, num: 24, type: 'vocab', title: 'Körper & Gesundheit',
desc: 'Wörter rund um den Körper und Gesundheit',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 3, day: 3, num: 25, type: 'grammar', title: 'Höflichkeitsformen',
desc: 'Wie man höflich spricht',
targetMin: 20, targetScore: 75, review: true,
cultural: 'Höflichkeit ist extrem wichtig in jeder Kultur.' },
{ week: 3, day: 3, num: 26, type: 'conversation', title: 'Bitten & Fragen',
desc: 'Wie man höflich fragt und bittet',
targetMin: 15, targetScore: 80, review: false,
cultural: null },
{ week: 3, day: 4, num: 27, type: 'conversation', title: 'Kinder & Familie',
desc: 'Gespräche mit und über Kinder',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Kinder sind sehr wichtig in Familien.' },
{ week: 3, day: 4, num: 28, type: 'vocab', title: 'Kinder & Spiel',
desc: 'Wörter für Kinder und Spielsachen',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 3, day: 5, num: 29, type: 'review', title: 'Woche 3 - Wiederholung',
desc: 'Wiederhole alle Inhalte der dritten Woche',
targetMin: 30, targetScore: 80, review: false,
cultural: null },
{ week: 3, day: 5, num: 30, type: 'vocab', title: 'Woche 3 - Vokabeltest',
desc: 'Teste dein Wissen aus Woche 3',
targetMin: 15, targetScore: 80, review: true,
cultural: null },
// WOCHE 4: Freies Sprechen
{ week: 4, day: 1, num: 31, type: 'conversation', title: 'Freies Gespräch - Thema 1',
desc: 'Übe freies Sprechen zu verschiedenen Themen',
targetMin: 20, targetScore: 75, review: false,
cultural: 'Fehler sind okay! Muttersprachler schätzen das Bemühen.' },
{ week: 4, day: 1, num: 32, type: 'vocab', title: 'Wiederholung - Woche 1 & 2',
desc: 'Wiederhole wichtige Vokabeln aus den ersten beiden Wochen',
targetMin: 25, targetScore: 85, review: true,
cultural: null },
{ week: 4, day: 2, num: 33, type: 'conversation', title: 'Freies Gespräch - Thema 2',
desc: 'Weitere Übung im freien Sprechen',
targetMin: 20, targetScore: 75, review: false,
cultural: null },
{ week: 4, day: 2, num: 34, type: 'vocab', title: 'Wiederholung - Woche 3',
desc: 'Wiederhole wichtige Vokabeln aus Woche 3',
targetMin: 25, targetScore: 85, review: true,
cultural: null },
{ week: 4, day: 3, num: 35, type: 'conversation', title: 'Komplexere Gespräche',
desc: 'Längere Gespräche zu verschiedenen Themen',
targetMin: 25, targetScore: 75, review: false,
cultural: 'Je mehr du sprichst, desto besser wirst du!' },
{ week: 4, day: 3, num: 36, type: 'review', title: 'Gesamtwiederholung',
desc: 'Wiederhole alle wichtigen Inhalte des Kurses',
targetMin: 30, targetScore: 80, review: false,
cultural: null },
{ week: 4, day: 4, num: 37, type: 'conversation', title: 'Praktische Übung',
desc: 'Simuliere echte Gesprächssituationen',
targetMin: 25, targetScore: 75, review: false,
cultural: null },
{ week: 4, day: 4, num: 38, type: 'vocab', title: 'Abschlusstest - Vokabeln',
desc: 'Finaler Vokabeltest über den gesamten Kurs',
targetMin: 20, targetScore: 80, review: true,
cultural: null },
{ week: 4, day: 5, num: 39, type: 'review', title: 'Abschlussprüfung',
desc: 'Finale Prüfung über alle Kursinhalte',
targetMin: 30, targetScore: 80, review: false,
cultural: 'Gratulation zum Abschluss des Kurses!' },
{ week: 4, day: 5, num: 40, type: 'culture', title: 'Kulturelle Tipps & Tricks',
desc: 'Wichtige kulturelle Hinweise für den Alltag',
targetMin: 15, targetScore: 0, review: false,
cultural: 'Kulturelles Verständnis ist genauso wichtig wie die Sprache selbst.' }
];
// Zielsprachen (die zu lernenden Sprachen)
const TARGET_LANGUAGES = [
'Bisaya',
'Französisch',
'Spanisch',
'Latein',
'Italienisch',
'Portugiesisch',
'Tagalog'
];
// Muttersprachen (für die Kurse erstellt werden)
const NATIVE_LANGUAGES = [
'Deutsch',
'Englisch',
'Spanisch',
'Französisch',
'Italienisch',
'Portugiesisch'
];
// Generiere Kurskonfigurationen für alle Kombinationen
function generateCourseConfigs() {
const configs = [];
for (const targetLang of TARGET_LANGUAGES) {
for (const nativeLang of NATIVE_LANGUAGES) {
// Überspringe, wenn Zielsprache = Muttersprache
if (targetLang === nativeLang) continue;
const title = `${targetLang} für ${nativeLang}sprachige - Schnellstart in 4 Wochen`;
let description = `Lerne ${targetLang} schnell und praktisch für den Alltag. `;
if (targetLang === 'Latein') {
description = `Lerne ${targetLang} systematisch mit Fokus auf Grammatik und Vokabular. `;
} else if (targetLang === 'Bisaya') {
description = `Lerne ${targetLang} (Cebuano) schnell und praktisch für den Familienalltag. `;
}
description += `Fokus auf Sprechen & Hören mit strukturiertem 4-Wochen-Plan.`;
configs.push({
targetLanguageName: targetLang,
nativeLanguageName: nativeLang,
title,
description,
difficultyLevel: 1
});
}
}
return configs;
}
const LANGUAGE_COURSES = generateCourseConfigs();
async function findOrCreateLanguage(languageName, ownerUserId) {
// Suche zuerst nach vorhandener Sprache
const [existing] = await sequelize.query(
`SELECT id FROM community.vocab_language WHERE name = :name LIMIT 1`,
{
replacements: { name: languageName },
type: sequelize.QueryTypes.SELECT
}
);
if (existing) {
console.log(` ✅ Sprache "${languageName}" bereits vorhanden (ID: ${existing.id})`);
return existing.id;
}
// Erstelle neue Sprache
const shareCode = crypto.randomBytes(8).toString('hex');
const [created] = await sequelize.query(
`INSERT INTO community.vocab_language (owner_user_id, name, share_code)
VALUES (:ownerUserId, :name, :shareCode)
RETURNING id`,
{
replacements: { ownerUserId, name: languageName, shareCode },
type: sequelize.QueryTypes.SELECT
}
);
console.log(` ✅ Sprache "${languageName}" erstellt (ID: ${created.id})`);
return created.id;
}
async function createCourseForLanguage(targetLanguageId, nativeLanguageId, languageConfig, ownerUserId) {
const shareCode = crypto.randomBytes(8).toString('hex');
const course = await VocabCourse.create({
ownerUserId,
title: languageConfig.title,
description: languageConfig.description,
languageId: Number(targetLanguageId),
nativeLanguageId: nativeLanguageId ? Number(nativeLanguageId) : null,
difficultyLevel: languageConfig.difficultyLevel || 1,
isPublic: true,
shareCode
});
console.log(` ✅ Kurs erstellt: "${course.title}" (ID: ${course.id}, Share-Code: ${shareCode})`);
// Erstelle Lektionen
const createdLessons = [];
for (const lessonData of LESSON_TEMPLATE) {
const lesson = await VocabCourseLesson.create({
courseId: course.id,
chapterId: null,
lessonNumber: lessonData.num,
title: lessonData.title,
description: lessonData.desc,
weekNumber: lessonData.week,
dayNumber: lessonData.day,
lessonType: lessonData.type,
culturalNotes: lessonData.cultural,
targetMinutes: lessonData.targetMin,
targetScorePercent: lessonData.targetScore,
requiresReview: lessonData.review
});
createdLessons.push({ lesson, lessonData });
}
console.log(`${LESSON_TEMPLATE.length} Lektionen erstellt`);
// Erstelle Beispiel-Grammatik-Übungen für Grammar-Lektionen
let grammarExerciseCount = 0;
for (const { lesson, lessonData } of createdLessons) {
if (lessonData.type === 'grammar') {
// Erstelle 2-3 Beispiel-Übungen für jede Grammar-Lektion
const exercises = createExampleGrammarExercises(lesson.id, lessonData, ownerUserId);
for (const exercise of exercises) {
await VocabGrammarExercise.create(exercise);
grammarExerciseCount++;
}
}
}
if (grammarExerciseCount > 0) {
console.log(`${grammarExerciseCount} Grammatik-Übungen erstellt`);
}
return course;
}
// Erstelle Beispiel-Grammatik-Übungen für eine Grammar-Lektion
function createExampleGrammarExercises(lessonId, lessonData, ownerUserId) {
const exercises = [];
// Beispiel-Übung 1: Gap Fill (Lückentext)
exercises.push({
lessonId: lessonId,
exerciseTypeId: 1, // gap_fill
exerciseNumber: 1,
title: `${lessonData.title} - Übung 1`,
instruction: 'Fülle die Lücken mit den richtigen Wörtern.',
questionData: JSON.stringify({
type: 'gap_fill',
text: 'Hallo! Wie geht es {gap}? Mir geht es {gap}, danke!',
gaps: 2
}),
answerData: JSON.stringify({
type: 'gap_fill',
answers: ['dir', 'gut']
}),
explanation: 'Die richtigen Antworten sind "dir" und "gut".',
createdByUserId: ownerUserId
});
// Beispiel-Übung 2: Multiple Choice
exercises.push({
lessonId: lessonId,
exerciseTypeId: 2, // multiple_choice
exerciseNumber: 2,
title: `${lessonData.title} - Übung 2`,
instruction: 'Wähle die richtige Antwort aus.',
questionData: JSON.stringify({
type: 'multiple_choice',
question: 'Wie sagt man "Guten Tag" auf ' + lessonData.title.split(' - ')[0] + '?',
options: ['Option A', 'Option B', 'Option C', 'Option D']
}),
answerData: JSON.stringify({
type: 'multiple_choice',
correctAnswer: 0
}),
explanation: 'Die richtige Antwort ist Option A.',
createdByUserId: ownerUserId
});
return exercises;
}
async function findOrCreateSystemUser() {
// Versuche zuerst einen System-Benutzer zu finden (z.B. mit username "system" oder "admin")
let systemUser = await User.findOne({
where: {
username: 'system'
}
});
if (!systemUser) {
// Versuche Admin-Benutzer
systemUser = await User.findOne({
where: {
username: 'admin'
}
});
}
if (!systemUser) {
// Erstelle einen System-Benutzer
console.log(' Erstelle System-Benutzer für öffentliche Kurse...');
const saltRounds = 10;
const randomPassword = crypto.randomBytes(32).toString('hex');
const hashedPassword = await bcrypt.hash(randomPassword, saltRounds);
systemUser = await User.create({
username: 'system',
email: 'system@your-part.de',
password: hashedPassword,
active: true,
registrationDate: new Date()
});
// hashedId wird automatisch vom Hook gesetzt, aber warte kurz darauf
await systemUser.reload();
console.log(` ✅ System-Benutzer erstellt (ID: ${systemUser.id}, hashedId: ${systemUser.hashedId})`);
} else {
console.log(` ✅ System-Benutzer gefunden (ID: ${systemUser.id}, Username: ${systemUser.username})`);
}
return systemUser;
}
async function createAllLanguageCourses() {
try {
// Finde oder erstelle System-Benutzer
const systemUser = await findOrCreateSystemUser();
console.log(`\n🚀 Erstelle öffentliche Sprachkurse (Besitzer: System-Benutzer ID ${systemUser.id})\n`);
const createdCourses = [];
// Stelle sicher, dass alle benötigten Sprachen existieren
console.log(`\n🌍 Stelle sicher, dass alle Sprachen existieren...`);
const allLanguages = [...new Set([...TARGET_LANGUAGES, ...NATIVE_LANGUAGES])];
const languageMap = new Map();
for (const langName of allLanguages) {
const langId = await findOrCreateLanguage(langName, systemUser.id);
languageMap.set(langName, langId);
}
for (const langConfig of LANGUAGE_COURSES) {
console.log(`\n📚 Verarbeite: ${langConfig.targetLanguageName} für ${langConfig.nativeLanguageName}sprachige`);
const targetLanguageId = languageMap.get(langConfig.targetLanguageName);
const nativeLanguageId = languageMap.get(langConfig.nativeLanguageName);
// Prüfe, ob Kurs bereits existiert (unabhängig vom Besitzer, wenn öffentlich)
const existingCourse = await VocabCourse.findOne({
where: {
languageId: targetLanguageId,
nativeLanguageId: nativeLanguageId,
isPublic: true
}
});
if (existingCourse) {
console.log(` ⚠️ Kurs "${langConfig.title}" existiert bereits (ID: ${existingCourse.id})`);
createdCourses.push({
...langConfig,
courseId: existingCourse.id,
targetLanguageId,
nativeLanguageId,
skipped: true
});
continue;
}
// Erstelle Kurs
const course = await createCourseForLanguage(targetLanguageId, nativeLanguageId, langConfig, systemUser.id);
createdCourses.push({
...langConfig,
courseId: course.id,
targetLanguageId,
nativeLanguageId,
shareCode: course.shareCode
});
}
console.log(`\n\n🎉 Zusammenfassung:\n`);
console.log(` Gesamt: ${LANGUAGE_COURSES.length} Sprachen`);
console.log(` Erstellt: ${createdCourses.filter(c => !c.skipped).length} Kurse`);
console.log(` Übersprungen: ${createdCourses.filter(c => c.skipped).length} Kurse`);
console.log(`\n📋 Erstellte Kurse:\n`);
for (const course of createdCourses) {
if (course.skipped) {
console.log(` ⚠️ ${course.languageName}: Bereits vorhanden (ID: ${course.courseId})`);
} else {
console.log(`${course.languageName}: ${course.title}`);
console.log(` Share-Code: ${course.shareCode}`);
}
}
console.log(`\n💡 Nächste Schritte:`);
console.log(` 1. Füge Vokabeln zu den Vokabel-Lektionen hinzu`);
console.log(` 2. Erstelle Grammatik-Übungen für die Grammatik-Lektionen`);
console.log(` 3. Teile die Kurse mit anderen (Share-Codes oben)`);
return createdCourses;
} catch (error) {
console.error('❌ Fehler beim Erstellen der Kurse:', error);
throw error;
}
}
// CLI-Aufruf
// Keine Parameter mehr nötig - verwendet automatisch System-Benutzer
createAllLanguageCourses()
.then(() => {
process.exit(0);
})
.catch((error) => {
console.error(error);
process.exit(1);
});