feat(bisaya-course): add new lesson and update course creation logic
All checks were successful
Deploy to production / deploy (push) Successful in 2m51s
All checks were successful
Deploy to production / deploy (push) Successful in 2m51s
- Introduced a new lesson titled "Zahlen & Preise" to enhance the numerical curriculum in Bisaya. - Updated the course creation logic to check for existing "Familien"-Bisaya courses, ensuring idempotency in course creation. - Enhanced the lesson didactics by mapping legacy titles to current lesson keys, improving data consistency. - Adjusted the course generation script to limit Bisaya courses to German-speaking learners only, streamlining course offerings.
This commit is contained in:
@@ -5169,6 +5169,10 @@ async function findOrCreateSystemUser() {
|
|||||||
|
|
||||||
function getExercisesForLesson(lesson) {
|
function getExercisesForLesson(lesson) {
|
||||||
const lessonTitle = lesson.title;
|
const lessonTitle = lesson.title;
|
||||||
|
// Alte Kurstitel (DB noch nicht migriert)
|
||||||
|
if (lessonTitle === 'Zahlen & Preise' && BISAYA_EXERCISES['Zahlen 1–20']) {
|
||||||
|
return BISAYA_EXERCISES['Zahlen 1–20'];
|
||||||
|
}
|
||||||
// Suche nach exaktem Titel
|
// Suche nach exaktem Titel
|
||||||
if (BISAYA_EXERCISES[lessonTitle]) {
|
if (BISAYA_EXERCISES[lessonTitle]) {
|
||||||
return BISAYA_EXERCISES[lessonTitle];
|
return BISAYA_EXERCISES[lessonTitle];
|
||||||
@@ -5254,6 +5258,7 @@ async function createBisayaCourseContent() {
|
|||||||
'Zeitformen - Grundlagen',
|
'Zeitformen - Grundlagen',
|
||||||
'Zeit & Datum',
|
'Zeit & Datum',
|
||||||
'Einkaufen & Preise',
|
'Einkaufen & Preise',
|
||||||
|
'Zahlen & Preise',
|
||||||
'Zahlen 1–20',
|
'Zahlen 1–20',
|
||||||
'Zahlen: Zehner',
|
'Zahlen: Zehner',
|
||||||
'Zahlen: Hunderter',
|
'Zahlen: Hunderter',
|
||||||
|
|||||||
@@ -1076,6 +1076,8 @@ const LESSONS = [
|
|||||||
...BISAYA_PHASE5_LESSONS
|
...BISAYA_PHASE5_LESSONS
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const BISAYA_FAMILY_COURSE_TITLE = 'Bisaya für Familien - Alltag & Stabilisierung';
|
||||||
|
|
||||||
async function createBisayaCourse(languageId, ownerHashedId) {
|
async function createBisayaCourse(languageId, ownerHashedId) {
|
||||||
try {
|
try {
|
||||||
// Finde User
|
// Finde User
|
||||||
@@ -1093,11 +1095,25 @@ async function createBisayaCourse(languageId, ownerHashedId) {
|
|||||||
throw new Error(`Sprache mit ID ${languageId} nicht gefunden`);
|
throw new Error(`Sprache mit ID ${languageId} nicht gefunden`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existing = await VocabCourse.findOne({
|
||||||
|
where: {
|
||||||
|
languageId: Number(languageId),
|
||||||
|
title: BISAYA_FAMILY_COURSE_TITLE
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (existing) {
|
||||||
|
console.log(
|
||||||
|
`⏭️ Es existiert bereits ein „Familien“-Bisaya-Kurs für diese Zielsprache (language_id=${languageId}, Kurs-ID ${existing.id}).`
|
||||||
|
);
|
||||||
|
console.log(' Es wird kein zweiter Kurs angelegt (idempotent).');
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
// Erstelle Kurs
|
// Erstelle Kurs
|
||||||
const shareCode = crypto.randomBytes(8).toString('hex');
|
const shareCode = crypto.randomBytes(8).toString('hex');
|
||||||
const course = await VocabCourse.create({
|
const course = await VocabCourse.create({
|
||||||
ownerUserId: user.id,
|
ownerUserId: user.id,
|
||||||
title: 'Bisaya für Familien - Alltag & Stabilisierung',
|
title: BISAYA_FAMILY_COURSE_TITLE,
|
||||||
description: 'Lerne Bisaya (Cebuano) praxisnah für den Familienalltag. Der Pfad verbindet Schnellstart, Alltagsmodule und Stabilisierungsblöcke mit Spiralwiederholung, Fehlertraining und freier Produktion.',
|
description: 'Lerne Bisaya (Cebuano) praxisnah für den Familienalltag. Der Pfad verbindet Schnellstart, Alltagsmodule und Stabilisierungsblöcke mit Spiralwiederholung, Fehlertraining und freier Produktion.',
|
||||||
languageId: Number(languageId),
|
languageId: Number(languageId),
|
||||||
difficultyLevel: 1,
|
difficultyLevel: 1,
|
||||||
|
|||||||
@@ -5,10 +5,13 @@
|
|||||||
* Verwendung:
|
* Verwendung:
|
||||||
* node backend/scripts/create-language-courses.js
|
* node backend/scripts/create-language-courses.js
|
||||||
*
|
*
|
||||||
* Erstellt öffentliche Kurse für alle Kombinationen von:
|
* Erstellt öffentliche Kurse für Kombinationen aus Ziel- und Muttersprache:
|
||||||
* - Zielsprachen: Bisaya, Französisch, Spanisch, Latein, Italienisch, Portugiesisch, Tagalog
|
* - Zielsprachen: Bisaya, Französisch, Spanisch, Latein, Italienisch, Portugiesisch, Tagalog
|
||||||
* - Muttersprachen: Deutsch, Englisch, Spanisch, Französisch, Italienisch, Portugiesisch
|
* - Muttersprachen: Deutsch, Englisch, Spanisch, Französisch, Italienisch, Portugiesisch
|
||||||
*
|
*
|
||||||
|
* Ausnahme Bisaya: Es wird nur ein Schnellstart-Kurs „Bisaya für Deutschsprachige“ erzeugt
|
||||||
|
* (Zielgruppe der Plattform). Andere Muttersprachen-Kombinationen für Bisaya entfallen.
|
||||||
|
*
|
||||||
* Die Kurse werden automatisch einem System-Benutzer zugeordnet und sind öffentlich zugänglich.
|
* Die Kurse werden automatisch einem System-Benutzer zugeordnet und sind öffentlich zugänglich.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -271,7 +274,10 @@ function generateCourseConfigs() {
|
|||||||
for (const nativeLang of NATIVE_LANGUAGES) {
|
for (const nativeLang of NATIVE_LANGUAGES) {
|
||||||
// Überspringe, wenn Zielsprache = Muttersprache
|
// Überspringe, wenn Zielsprache = Muttersprache
|
||||||
if (targetLang === nativeLang) continue;
|
if (targetLang === nativeLang) continue;
|
||||||
|
|
||||||
|
// Bisaya: nur Kurs für deutschsprachige Lernende (eine Zielsprache, eine Muttersprache)
|
||||||
|
if (targetLang === 'Bisaya' && nativeLang !== 'Deutsch') continue;
|
||||||
|
|
||||||
const title = `${targetLang} für ${nativeLang}sprachige - Schnellstart in 4 Wochen`;
|
const title = `${targetLang} für ${nativeLang}sprachige - Schnellstart in 4 Wochen`;
|
||||||
let description = `Lerne ${targetLang} schnell und praktisch für den Alltag. `;
|
let description = `Lerne ${targetLang} schnell und praktisch für den Alltag. `;
|
||||||
|
|
||||||
|
|||||||
139
backend/scripts/dedupe-bisaya-family-courses.js
Normal file
139
backend/scripts/dedupe-bisaya-family-courses.js
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Entfernt doppelte „Bisaya für Familien - Alltag & Stabilisierung“-Kurse und behält
|
||||||
|
* den Kurs mit der kleinsten ID (üblicherweise der erste / in der UI verlinkte).
|
||||||
|
*
|
||||||
|
* Standard: Nur Anzeige (Dry-Run). Löschen nur mit:
|
||||||
|
* VOCAB_DEDUPE_BISAYA_FAMILY_APPLY=1 node backend/scripts/dedupe-bisaya-family-courses.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { sequelize } from '../utils/sequelize.js';
|
||||||
|
|
||||||
|
const CANONICAL_TITLE = 'Bisaya für Familien - Alltag & Stabilisierung';
|
||||||
|
|
||||||
|
async function deleteCourseCascade(courseId, transaction) {
|
||||||
|
const lessonRows = await sequelize.query(
|
||||||
|
`SELECT id FROM community.vocab_course_lesson WHERE course_id = :cid`,
|
||||||
|
{ replacements: { cid: courseId }, transaction, type: sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
const lessonIds = lessonRows.map((r) => r.id);
|
||||||
|
|
||||||
|
if (lessonIds.length > 0) {
|
||||||
|
const exRows = await sequelize.query(
|
||||||
|
`SELECT id FROM community.vocab_grammar_exercise WHERE lesson_id IN (:lids)`,
|
||||||
|
{ replacements: { lids: lessonIds }, transaction, type: sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
const exerciseIds = exRows.map((r) => r.id);
|
||||||
|
|
||||||
|
if (exerciseIds.length > 0) {
|
||||||
|
await sequelize.query(
|
||||||
|
`DELETE FROM community.vocab_grammar_exercise_progress WHERE exercise_id IN (:eids)`,
|
||||||
|
{ replacements: { eids: exerciseIds }, transaction }
|
||||||
|
);
|
||||||
|
await sequelize.query(`DELETE FROM community.vocab_grammar_exercise WHERE id IN (:eids)`, {
|
||||||
|
replacements: { eids: exerciseIds },
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await sequelize.query(
|
||||||
|
`DELETE FROM community.vocab_course_progress WHERE lesson_id IN (:lids) OR course_id = :cid`,
|
||||||
|
{ replacements: { lids: lessonIds, cid: courseId }, transaction }
|
||||||
|
);
|
||||||
|
await sequelize.query(
|
||||||
|
`DELETE FROM community.vocab_course_lesson WHERE course_id = :cid`,
|
||||||
|
{ replacements: { cid: courseId }, transaction }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await sequelize.query(
|
||||||
|
`DELETE FROM community.vocab_course_progress WHERE course_id = :cid`,
|
||||||
|
{ replacements: { cid: courseId }, transaction }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await sequelize.query(
|
||||||
|
`DELETE FROM community.vocab_course_enrollment WHERE course_id = :cid`,
|
||||||
|
{ replacements: { cid: courseId }, transaction }
|
||||||
|
);
|
||||||
|
await sequelize.query(`DELETE FROM community.vocab_course WHERE id = :cid`, {
|
||||||
|
replacements: { cid: courseId },
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const apply = process.env.VOCAB_DEDUPE_BISAYA_FAMILY_APPLY === '1';
|
||||||
|
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) {
|
||||||
|
console.error('❌ Bisaya-Sprache nicht gefunden.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const courses = await sequelize.query(
|
||||||
|
`SELECT id, title, owner_user_id AS "ownerUserId", created_at AS "createdAt"
|
||||||
|
FROM community.vocab_course
|
||||||
|
WHERE language_id = :languageId AND title = :title
|
||||||
|
ORDER BY id ASC`,
|
||||||
|
{
|
||||||
|
replacements: { languageId: bisayaLanguage.id, title: CANONICAL_TITLE },
|
||||||
|
type: sequelize.QueryTypes.SELECT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (courses.length <= 1) {
|
||||||
|
console.log(
|
||||||
|
courses.length === 0
|
||||||
|
? `Kein Kurs mit Titel „${CANONICAL_TITLE}“ gefunden.`
|
||||||
|
: `Nur ein Kurs – nichts zu bereinigen (ID ${courses[0].id}).`
|
||||||
|
);
|
||||||
|
await sequelize.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keep = courses[0];
|
||||||
|
const remove = courses.slice(1);
|
||||||
|
|
||||||
|
console.log(`Behalten: Kurs-ID ${keep.id} (älteste ID)\n`);
|
||||||
|
console.log(`${apply ? 'Löschen' : 'Würde löschen'} (${remove.length} Duplikate):`);
|
||||||
|
for (const c of remove) {
|
||||||
|
const stat = await sequelize.query(
|
||||||
|
`SELECT
|
||||||
|
(SELECT COUNT(*)::int FROM community.vocab_course_enrollment WHERE course_id = :cid) AS enroll,
|
||||||
|
(SELECT COUNT(*)::int FROM community.vocab_course_progress WHERE course_id = :cid) AS prog`,
|
||||||
|
{ replacements: { cid: c.id }, type: sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
const row = stat[0];
|
||||||
|
console.log(
|
||||||
|
` – ID ${c.id} (owner ${c.ownerUserId}, Enrollments ${row?.enroll ?? '?'}, Fortschrittszeilen ${row?.prog ?? '?'})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apply) {
|
||||||
|
console.log(
|
||||||
|
'\nDry-Run. Zum tatsächlichen Löschen: VOCAB_DEDUPE_BISAYA_FAMILY_APPLY=1 node backend/scripts/dedupe-bisaya-family-courses.js'
|
||||||
|
);
|
||||||
|
await sequelize.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sequelize.transaction(async (t) => {
|
||||||
|
for (const c of remove) {
|
||||||
|
await deleteCourseCascade(c.id, t);
|
||||||
|
console.log(`🗑️ Kurs ${c.id} entfernt.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`\n✅ Fertig. Verbleibender Kurs: ID ${keep.id}`);
|
||||||
|
await sequelize.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error('❌ Fehler:', e);
|
||||||
|
sequelize.close();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
202
backend/scripts/migrate-bisaya-zahlen-split.js
Normal file
202
backend/scripts/migrate-bisaya-zahlen-split.js
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Migriert ältere Bisaya-Kurse von einer kombinierten Lektion „Zahlen & Preise“ (nur #18)
|
||||||
|
* zur viergeteilten Zahlenreihe (#18–#21). Erwartet vorher noch das alte Layout:
|
||||||
|
* Lektion 19 = „Woche 2 - Wiederholung“ (liegt im neuen Kurs an Position 22).
|
||||||
|
*
|
||||||
|
* Ablauf je Kurs:
|
||||||
|
* - Lektionen mit lesson_number >= 19 um +3 nach oben schieben (von oben nach unten)
|
||||||
|
* - Lektion 18 auf „Zahlen 1–20“ setzen
|
||||||
|
* - Neue Lektionen 19–21 („Zahlen: Zehner/Hunderter/Tausender“) einfügen
|
||||||
|
*
|
||||||
|
* Verwendung:
|
||||||
|
* node backend/scripts/migrate-bisaya-zahlen-split.js
|
||||||
|
*
|
||||||
|
* Anschließend:
|
||||||
|
* node backend/scripts/update-bisaya-didactics.js
|
||||||
|
* node backend/scripts/create-bisaya-course-content.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { sequelize } from '../utils/sequelize.js';
|
||||||
|
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||||
|
import { LESSON_DIDACTICS } from './update-bisaya-didactics.js';
|
||||||
|
import { getBisayaLessonPedagogy } from './bisaya-course-phase2-pedagogy.js';
|
||||||
|
|
||||||
|
const INSERTED_LESSONS = [
|
||||||
|
{
|
||||||
|
lessonNumber: 19,
|
||||||
|
title: 'Zahlen: Zehner',
|
||||||
|
description: 'Runde Zehner von 20 bis 90',
|
||||||
|
weekNumber: 2,
|
||||||
|
dayNumber: 4,
|
||||||
|
lessonType: 'vocab',
|
||||||
|
culturalNotes: null,
|
||||||
|
targetMinutes: 20,
|
||||||
|
targetScorePercent: 85,
|
||||||
|
requiresReview: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lessonNumber: 20,
|
||||||
|
title: 'Zahlen: Hunderter',
|
||||||
|
description: 'Hunderter bis 900 (usa ka gatos … siyam ka gatos)',
|
||||||
|
weekNumber: 2,
|
||||||
|
dayNumber: 4,
|
||||||
|
lessonType: 'vocab',
|
||||||
|
culturalNotes: null,
|
||||||
|
targetMinutes: 20,
|
||||||
|
targetScorePercent: 85,
|
||||||
|
requiresReview: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lessonNumber: 21,
|
||||||
|
title: 'Zahlen: Tausender',
|
||||||
|
description: 'Tausender und große Beträge (libo)',
|
||||||
|
weekNumber: 2,
|
||||||
|
dayNumber: 4,
|
||||||
|
lessonType: 'vocab',
|
||||||
|
culturalNotes: null,
|
||||||
|
targetMinutes: 18,
|
||||||
|
targetScorePercent: 85,
|
||||||
|
requiresReview: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function applyPedagogy(patch, lessonNumber) {
|
||||||
|
const pedagogy = getBisayaLessonPedagogy(lessonNumber) || {};
|
||||||
|
patch.didacticMode = pedagogy.didacticMode || null;
|
||||||
|
patch.phaseLabel = pedagogy.phaseLabel || null;
|
||||||
|
patch.blockNumber = pedagogy.blockNumber ?? null;
|
||||||
|
patch.difficultyWeight = pedagogy.difficultyWeight ?? null;
|
||||||
|
patch.newUnitTarget = pedagogy.newUnitTarget ?? null;
|
||||||
|
patch.reviewWeight = pedagogy.reviewWeight ?? null;
|
||||||
|
patch.isIntensiveReview = Boolean(pedagogy.isIntensiveReview);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDidactics(patch, title) {
|
||||||
|
const d = LESSON_DIDACTICS[title];
|
||||||
|
if (!d) return;
|
||||||
|
patch.learningGoals = d.learningGoals || [];
|
||||||
|
patch.corePatterns = d.corePatterns || [];
|
||||||
|
patch.grammarFocus = d.grammarFocus || [];
|
||||||
|
patch.speakingPrompts = d.speakingPrompts || [];
|
||||||
|
patch.practicalTasks = d.practicalTasks || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function migrateCourse(courseId) {
|
||||||
|
const maxRow = await sequelize.query(
|
||||||
|
`SELECT MAX(lesson_number) AS max FROM community.vocab_course_lesson WHERE course_id = :courseId`,
|
||||||
|
{ replacements: { courseId }, type: sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
const maxNum = Number(maxRow[0]?.max || 0);
|
||||||
|
if (maxNum < 19) {
|
||||||
|
return { status: 'skip', reason: 'weniger als 19 Lektionen' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const l19 = await VocabCourseLesson.findOne({
|
||||||
|
where: { courseId, lessonNumber: 19 }
|
||||||
|
});
|
||||||
|
if (!l19) {
|
||||||
|
return { status: 'skip', reason: 'keine Lektion 19' };
|
||||||
|
}
|
||||||
|
if (l19.title !== 'Woche 2 - Wiederholung') {
|
||||||
|
return { status: 'skip', reason: 'Lektion 19 ist nicht „Woche 2 - Wiederholung“ (bereits migriert oder anderer Stand)' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const l18 = await VocabCourseLesson.findOne({
|
||||||
|
where: { courseId, lessonNumber: 18 }
|
||||||
|
});
|
||||||
|
if (!l18) {
|
||||||
|
return { status: 'skip', reason: 'keine Lektion 18' };
|
||||||
|
}
|
||||||
|
if (l18.title !== 'Zahlen & Preise' && l18.title !== 'Zahlen 1–20') {
|
||||||
|
return { status: 'skip', reason: `Lektion 18 unerwartet: „${l18.title}“` };
|
||||||
|
}
|
||||||
|
|
||||||
|
await sequelize.transaction(async (t) => {
|
||||||
|
for (let n = maxNum; n >= 19; n -= 1) {
|
||||||
|
await VocabCourseLesson.update(
|
||||||
|
{ lessonNumber: n + 3 },
|
||||||
|
{ where: { courseId, lessonNumber: n }, transaction: t }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lesson18 = await VocabCourseLesson.findOne({
|
||||||
|
where: { courseId, lessonNumber: 18 },
|
||||||
|
transaction: t
|
||||||
|
});
|
||||||
|
const patch18 = {
|
||||||
|
title: 'Zahlen 1–20',
|
||||||
|
description: 'Grundzahlen und Zahlen bis 20 (usa … baynte)',
|
||||||
|
weekNumber: 2,
|
||||||
|
dayNumber: 4,
|
||||||
|
lessonType: 'vocab',
|
||||||
|
culturalNotes: null,
|
||||||
|
targetMinutes: 22,
|
||||||
|
targetScorePercent: 85,
|
||||||
|
requiresReview: true
|
||||||
|
};
|
||||||
|
applyDidactics(patch18, 'Zahlen 1–20');
|
||||||
|
applyPedagogy(patch18, 18);
|
||||||
|
await lesson18.update(patch18, { transaction: t });
|
||||||
|
|
||||||
|
for (const def of INSERTED_LESSONS) {
|
||||||
|
const createPayload = {
|
||||||
|
courseId,
|
||||||
|
chapterId: null,
|
||||||
|
lessonNumber: def.lessonNumber,
|
||||||
|
title: def.title,
|
||||||
|
description: def.description,
|
||||||
|
weekNumber: def.weekNumber,
|
||||||
|
dayNumber: def.dayNumber,
|
||||||
|
lessonType: def.lessonType,
|
||||||
|
culturalNotes: def.culturalNotes,
|
||||||
|
targetMinutes: def.targetMinutes,
|
||||||
|
targetScorePercent: def.targetScorePercent,
|
||||||
|
requiresReview: def.requiresReview
|
||||||
|
};
|
||||||
|
applyDidactics(createPayload, def.title);
|
||||||
|
applyPedagogy(createPayload, def.lessonNumber);
|
||||||
|
await VocabCourseLesson.create(createPayload, { transaction: t });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { status: 'ok' };
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
console.error('❌ Bisaya-Sprache nicht gefunden.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const courses = await sequelize.query(
|
||||||
|
`SELECT id, title FROM community.vocab_course WHERE language_id = :languageId ORDER BY id`,
|
||||||
|
{ replacements: { languageId: bisayaLanguage.id }, type: sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Gefunden: ${courses.length} Bisaya-Kurs(e)\n`);
|
||||||
|
|
||||||
|
for (const course of courses) {
|
||||||
|
const result = await migrateCourse(course.id);
|
||||||
|
if (result.status === 'ok') {
|
||||||
|
console.log(`✅ Kurs ${course.id}: „${course.title}“ – Zahlen-Split angewendet (Lektionen verschoben, 19–21 eingefügt).`);
|
||||||
|
} else {
|
||||||
|
console.log(`⏭️ Kurs ${course.id}: „${course.title}“ – ${result.reason}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\nFertig. Danach: node backend/scripts/update-bisaya-didactics.js && node backend/scripts/create-bisaya-course-content.js');
|
||||||
|
await sequelize.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error('❌ Fehler:', e);
|
||||||
|
sequelize.close();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -10,7 +10,12 @@ import { sequelize } from '../utils/sequelize.js';
|
|||||||
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||||
import { getBisayaLessonPedagogy } from './bisaya-course-phase2-pedagogy.js';
|
import { getBisayaLessonPedagogy } from './bisaya-course-phase2-pedagogy.js';
|
||||||
|
|
||||||
const LESSON_DIDACTICS = {
|
/** Alte Kurstitel → aktueller Schlüssel in LESSON_DIDACTICS (bestehende Datenbanken). */
|
||||||
|
export const LEGACY_DIDACTICS_TITLE_MAP = {
|
||||||
|
'Zahlen & Preise': 'Zahlen 1–20'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LESSON_DIDACTICS = {
|
||||||
'Begrüßungen & Höflichkeit': {
|
'Begrüßungen & Höflichkeit': {
|
||||||
learningGoals: [
|
learningGoals: [
|
||||||
'Einfache Begrüßungen verstehen und selbst verwenden.',
|
'Einfache Begrüßungen verstehen und selbst verwenden.',
|
||||||
@@ -438,6 +443,14 @@ const LESSON_DIDACTICS = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function resolveDidacticsForLesson(lesson) {
|
||||||
|
const direct = LESSON_DIDACTICS[lesson.title];
|
||||||
|
if (direct) return direct;
|
||||||
|
const mappedTitle = LEGACY_DIDACTICS_TITLE_MAP[lesson.title];
|
||||||
|
if (mappedTitle) return LESSON_DIDACTICS[mappedTitle];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
async function updateBisayaDidactics() {
|
async function updateBisayaDidactics() {
|
||||||
await sequelize.authenticate();
|
await sequelize.authenticate();
|
||||||
const [bisayaLanguage] = await sequelize.query(
|
const [bisayaLanguage] = await sequelize.query(
|
||||||
@@ -464,25 +477,29 @@ async function updateBisayaDidactics() {
|
|||||||
let updated = 0;
|
let updated = 0;
|
||||||
for (const row of lessons) {
|
for (const row of lessons) {
|
||||||
const lesson = await VocabCourseLesson.findByPk(row.id);
|
const lesson = await VocabCourseLesson.findByPk(row.id);
|
||||||
const didactics = LESSON_DIDACTICS[lesson.title];
|
const didactics = resolveDidacticsForLesson(lesson);
|
||||||
const pedagogy = getBisayaLessonPedagogy(lesson.lessonNumber);
|
const pedagogy = getBisayaLessonPedagogy(lesson.lessonNumber);
|
||||||
if (!didactics && !pedagogy) continue;
|
if (!didactics && !pedagogy) continue;
|
||||||
const normalizedDidactics = didactics || {};
|
|
||||||
|
|
||||||
await lesson.update({
|
const patch = {};
|
||||||
learningGoals: normalizedDidactics.learningGoals || [],
|
if (didactics) {
|
||||||
corePatterns: normalizedDidactics.corePatterns || [],
|
patch.learningGoals = didactics.learningGoals || [];
|
||||||
grammarFocus: normalizedDidactics.grammarFocus || [],
|
patch.corePatterns = didactics.corePatterns || [];
|
||||||
speakingPrompts: normalizedDidactics.speakingPrompts || [],
|
patch.grammarFocus = didactics.grammarFocus || [];
|
||||||
practicalTasks: normalizedDidactics.practicalTasks || [],
|
patch.speakingPrompts = didactics.speakingPrompts || [];
|
||||||
didacticMode: pedagogy?.didacticMode || null,
|
patch.practicalTasks = didactics.practicalTasks || [];
|
||||||
phaseLabel: pedagogy?.phaseLabel || null,
|
}
|
||||||
blockNumber: pedagogy?.blockNumber ?? null,
|
if (pedagogy) {
|
||||||
difficultyWeight: pedagogy?.difficultyWeight ?? null,
|
patch.didacticMode = pedagogy.didacticMode || null;
|
||||||
newUnitTarget: pedagogy?.newUnitTarget ?? null,
|
patch.phaseLabel = pedagogy.phaseLabel || null;
|
||||||
reviewWeight: pedagogy?.reviewWeight ?? null,
|
patch.blockNumber = pedagogy.blockNumber ?? null;
|
||||||
isIntensiveReview: Boolean(pedagogy?.isIntensiveReview)
|
patch.difficultyWeight = pedagogy.difficultyWeight ?? null;
|
||||||
});
|
patch.newUnitTarget = pedagogy.newUnitTarget ?? null;
|
||||||
|
patch.reviewWeight = pedagogy.reviewWeight ?? null;
|
||||||
|
patch.isIntensiveReview = Boolean(pedagogy.isIntensiveReview);
|
||||||
|
}
|
||||||
|
|
||||||
|
await lesson.update(patch);
|
||||||
updated++;
|
updated++;
|
||||||
console.log(`✅ Didaktik aktualisiert: Lektion ${lesson.lessonNumber} - ${lesson.title}`);
|
console.log(`✅ Didaktik aktualisiert: Lektion ${lesson.lessonNumber} - ${lesson.title}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user