From 41d6a4f5d9c99435033ca3d40af7a1697b6aba87 Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Mon, 20 Oct 2025 20:00:24 -0500 Subject: [PATCH 1/5] Course: Moodle export: mirror Documents tree; use LP item titles; restore document items - refs BT#21977 --- main/inc/lib/moodleexport/ActivityExport.php | 23 ++ main/inc/lib/moodleexport/FileExport.php | 9 +- main/inc/lib/moodleexport/FolderExport.php | 22 +- main/inc/lib/moodleexport/ForumExport.php | 11 +- main/inc/lib/moodleexport/MoodleExport.php | 237 ++++++++++++++----- main/inc/lib/moodleexport/PageExport.php | 7 +- main/inc/lib/moodleexport/QuizExport.php | 13 +- main/inc/lib/moodleexport/ResourceExport.php | 7 +- main/inc/lib/moodleexport/SectionExport.php | 7 +- main/inc/lib/moodleexport/UrlExport.php | 7 +- 10 files changed, 253 insertions(+), 90 deletions(-) diff --git a/main/inc/lib/moodleexport/ActivityExport.php b/main/inc/lib/moodleexport/ActivityExport.php index e7cd8951326..d87c0636fba 100644 --- a/main/inc/lib/moodleexport/ActivityExport.php +++ b/main/inc/lib/moodleexport/ActivityExport.php @@ -14,6 +14,7 @@ abstract class ActivityExport { protected $course; + public const DOCS_MODULE_ID = 1000000; public function __construct($course) { @@ -252,4 +253,26 @@ protected function createCalendarXml(array $activityData, string $destinationDir $this->createXmlFile('calendar', $xmlContent, $destinationDir); } + + /** + * Returns the title of the item in the LP (if it exists); otherwise, $fallback. + */ + protected function lpItemTitle(int $sectionId, string $itemType, int $resourceId, ?string $fallback): string + { + if (!isset($this->course->resources[RESOURCE_LEARNPATH])) { + return $fallback ?? ''; + } + foreach ($this->course->resources[RESOURCE_LEARNPATH] as $lp) { + if ((int) $lp->source_id !== $sectionId || empty($lp->items)) { + continue; + } + foreach ($lp->items as $it) { + $type = $it['item_type'] === 'student_publication' ? 'work' : $it['item_type']; + if ($type === $itemType && (int) $it['path'] === $resourceId) { + return $it['title'] ?? ($fallback ?? ''); + } + } + } + return $fallback ?? ''; + } } diff --git a/main/inc/lib/moodleexport/FileExport.php b/main/inc/lib/moodleexport/FileExport.php index 4f982ec3628..dc17b7c14ea 100644 --- a/main/inc/lib/moodleexport/FileExport.php +++ b/main/inc/lib/moodleexport/FileExport.php @@ -256,22 +256,23 @@ private function getFileData(object $document): array /** * Get file data for files inside a folder. */ - private function getFolderFileData(array $file, int $sourceId, string $parentPath = '/Documents/'): array + private function getFolderFileData(array $file, int $sourceId, string $parentPath = '/'): array { $adminData = MoodleExport::getAdminUserData(); $adminId = $adminData['id']; $contenthash = hash('sha1', basename($file['path'])); $mimetype = $this->getMimeType($file['path']); $filename = basename($file['path']); - $filepath = $this->ensureTrailingSlash($parentPath); + $relDir = dirname($file['path']); + $filepath = $this->ensureTrailingSlash($relDir === '.' ? '/' : '/'.$relDir.'/'); return [ 'id' => $file['id'], 'contenthash' => $contenthash, - 'contextid' => $sourceId, + 'contextid' => ActivityExport::DOCS_MODULE_ID, 'component' => 'mod_folder', 'filearea' => 'content', - 'itemid' => (int) $file['id'], + 'itemid' => ActivityExport::DOCS_MODULE_ID, 'filepath' => $filepath, 'documentpath' => 'document/'.$file['path'], 'filename' => $filename, diff --git a/main/inc/lib/moodleexport/FolderExport.php b/main/inc/lib/moodleexport/FolderExport.php index a1b1ac6dd0f..0ba0295e823 100644 --- a/main/inc/lib/moodleexport/FolderExport.php +++ b/main/inc/lib/moodleexport/FolderExport.php @@ -44,12 +44,12 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void */ public function getData(int $folderId, int $sectionId): ?array { - if ($folderId === 0) { + if ($folderId === 0 || $folderId === ActivityExport::DOCS_MODULE_ID) { return [ - 'id' => 0, - 'moduleid' => 0, + 'id' => ActivityExport::DOCS_MODULE_ID, + 'moduleid' => ActivityExport::DOCS_MODULE_ID, 'modulename' => 'folder', - 'contextid' => 0, + 'contextid' => ActivityExport::DOCS_MODULE_ID, 'name' => 'Documents', 'sectionid' => $sectionId, 'timemodified' => time(), @@ -98,17 +98,13 @@ private function createFolderXml(array $folderData, string $folderDir): void private function getFilesForFolder(int $folderId): array { $files = []; - if ($folderId === 0) { + if ($folderId === ActivityExport::DOCS_MODULE_ID) { foreach ($this->course->resources[RESOURCE_DOCUMENT] as $doc) { if ($doc->file_type === 'file') { - $files[] = [ - 'id' => (int) $doc->source_id, - 'contenthash' => hash('sha1', basename($doc->path)), - 'filename' => basename($doc->path), - 'filepath' => '/Documents/', - 'filesize' => (int) $doc->size, - 'mimetype' => $this->getMimeType($doc->path), - ]; + $ext = strtolower(pathinfo($doc->path, PATHINFO_EXTENSION)); + if (!in_array($ext, ['html', 'htm'], true)) { + $files[] = ['id' => (int) $doc->source_id]; + } } } } diff --git a/main/inc/lib/moodleexport/ForumExport.php b/main/inc/lib/moodleexport/ForumExport.php index 36cda8a9a8b..5044d3ea9d3 100644 --- a/main/inc/lib/moodleexport/ForumExport.php +++ b/main/inc/lib/moodleexport/ForumExport.php @@ -45,7 +45,7 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void */ public function getData(int $forumId, int $sectionId): ?array { - $forum = $this->course->resources['forum'][$forumId]->obj; + $forum = $this->course->resources[RESOURCE_FORUM][$forumId]->obj; $adminData = MoodleExport::getAdminUserData(); $adminId = $adminData['id']; @@ -78,14 +78,17 @@ public function getData(int $forumId, int $sectionId): ?array } } - $fileIds = []; + $name = $forum->forum_title ?? ''; + if ($sectionId > 0) { + $name = $this->lpItemTitle($sectionId, RESOURCE_FORUM, $forumId, $name); + } return [ 'id' => $forumId, 'moduleid' => $forumId, 'modulename' => 'forum', 'contextid' => $this->course->info['real_id'], - 'name' => $forum->forum_title, + 'name' => $name, 'description' => $forum->forum_comment, 'timecreated' => time(), 'timemodified' => time(), @@ -94,7 +97,7 @@ public function getData(int $forumId, int $sectionId): ?array 'userid' => $adminId, 'threads' => $threads, 'users' => [$adminId], - 'files' => $fileIds, + 'files' => [], ]; } diff --git a/main/inc/lib/moodleexport/MoodleExport.php b/main/inc/lib/moodleexport/MoodleExport.php index bc8d7cf6e80..791cc932658 100644 --- a/main/inc/lib/moodleexport/MoodleExport.php +++ b/main/inc/lib/moodleexport/MoodleExport.php @@ -443,105 +443,171 @@ private function getActivities(): array $activities = []; $glossaryAdded = false; - $documentsFolder = [ - 'id' => 0, + $lpIndex = []; + if (!empty($this->course->resources[RESOURCE_LEARNPATH])) { + foreach ($this->course->resources[RESOURCE_LEARNPATH] as $lp) { + $sid = (int) $lp->source_id; + foreach ($lp->items ?? [] as $it) { + $type = $it['item_type']; + if ($type === 'student_publication') { $type = 'assign'; } + elseif ($type === 'link') { $type = 'url'; } + elseif ($type === 'survey') { $type = 'feedback'; } + elseif ($type === 'document') { $type = 'document'; } + + $rid = $it['path']; + $title = $it['title'] ?? ''; + + if (ctype_digit((string) $rid)) { + $lpIndex[$sid][$type]['id'][(int) $rid] = $title; + } else { + $lpIndex[$sid][$type]['path'][(string) $rid] = $title; + } + } + } + } + + $titleFromLp = function (int $sectionId, string $moduleName, int $resourceId, string $fallback) use ($lpIndex) { + $type = in_array($moduleName, ['page','resource'], true) ? 'document' : $moduleName; + + if (isset($lpIndex[$sectionId][$type]['id'][$resourceId])) { + return $lpIndex[$sectionId][$type]['id'][$resourceId]; + } + + if ($type === 'assign' && isset($lpIndex[$sectionId]['student_publication']['id'][$resourceId])) { + return $lpIndex[$sectionId]['student_publication']['id'][$resourceId]; + } + + if ($type === 'document') { + $doc = \DocumentManager::get_document_data_by_id($resourceId, $this->course->code); + if (!empty($doc['path'])) { + $p = (string) $doc['path']; + foreach ([$p, 'document/'.$p, '/'.$p] as $cand) { + if (isset($lpIndex[$sectionId]['document']['path'][$cand])) { + return $lpIndex[$sectionId]['document']['path'][$cand]; + } + } + } + } + + return $fallback; + }; + + $activities[] = [ + 'id' => ActivityExport::DOCS_MODULE_ID, 'sectionid' => 0, - 'modulename' => 'folder', - 'moduleid' => 0, - 'title' => 'Documents', + 'modulename'=> 'folder', + 'moduleid' => ActivityExport::DOCS_MODULE_ID, + 'title' => 'Documents', ]; - $activities[] = $documentsFolder; + $htmlPageIds = []; foreach ($this->course->resources as $resourceType => $resources) { foreach ($resources as $resource) { $exportClass = null; - $moduleName = ''; - $title = ''; - $id = 0; + $moduleName = ''; + $title = ''; + $id = 0; + $sectionId = 0; - // Handle quizzes + // QUIZ if ($resourceType === RESOURCE_QUIZ && $resource->obj->iid > 0) { $exportClass = QuizExport::class; - $moduleName = 'quiz'; - $id = $resource->obj->iid; - $title = $resource->obj->title; + $moduleName = 'quiz'; + $id = (int) $resource->obj->iid; + $title = $resource->obj->title ?? ''; + $sectionId = (new QuizExport($this->course))->getSectionIdForActivity($id, $resourceType); + $title = $titleFromLp($sectionId, $moduleName, $id, $title); } - // Handle links - if ($resourceType === RESOURCE_LINK && $resource->source_id > 0) { + // URL + elseif ($resourceType === RESOURCE_LINK && $resource->source_id > 0) { $exportClass = UrlExport::class; - $moduleName = 'url'; - $id = $resource->source_id; - $title = $resource->title; + $moduleName = 'url'; + $id = (int) $resource->source_id; + $title = $resource->title ?? ''; + $sectionId = (new UrlExport($this->course))->getSectionIdForActivity($id, $resourceType); + $title = $titleFromLp($sectionId, $moduleName, $id, $title); } - // Handle glossaries + // GLOSSARY (uno solo) elseif ($resourceType === RESOURCE_GLOSSARY && $resource->glossary_id > 0 && !$glossaryAdded) { $exportClass = GlossaryExport::class; - $moduleName = 'glossary'; - $id = 1; - $title = get_lang('Glossary'); + $moduleName = 'glossary'; + $id = 1; + $title = get_lang('Glossary'); + $sectionId = 0; $glossaryAdded = true; } - // Handle forums + // FORUM elseif ($resourceType === RESOURCE_FORUM && $resource->source_id > 0) { $exportClass = ForumExport::class; - $moduleName = 'forum'; - $id = $resource->obj->iid; - $title = $resource->obj->forum_title; + $moduleName = 'forum'; + $id = (int) $resource->obj->iid; + $title = $resource->obj->forum_title ?? ''; + $sectionId = (new ForumExport($this->course))->getSectionIdForActivity($id, $resourceType); + $title = $titleFromLp($sectionId, $moduleName, $id, $title); } - // Handle documents (HTML pages) + // DOCUMENT elseif ($resourceType === RESOURCE_DOCUMENT && $resource->source_id > 0) { $document = \DocumentManager::get_document_data_by_id($resource->source_id, $this->course->code); - if ('html' === pathinfo($document['path'], PATHINFO_EXTENSION) && substr_count($resource->path, '/') === 1) { + $ext = strtolower(pathinfo($document['path'], PATHINFO_EXTENSION)); + + // HTML → page + if ($ext === 'html' || $ext === 'htm') { $exportClass = PageExport::class; - $moduleName = 'page'; - $id = $resource->source_id; - $title = $document['title']; + $moduleName = 'page'; + $id = (int) $resource->source_id; + $title = $document['title'] ?? ''; + $sectionId = (new PageExport($this->course))->getSectionIdForActivity($id, $resourceType); + $title = $titleFromLp($sectionId, $moduleName, $id, $title); $htmlPageIds[] = $id; } - if ('file' === $resource->file_type && !in_array($resource->source_id, $htmlPageIds)) { + + if ($resource->file_type === 'file' && !in_array($resource->source_id, $htmlPageIds, true)) { $resourceExport = new ResourceExport($this->course); - if ($resourceExport->getSectionIdForActivity($resource->source_id, $resourceType) > 0) { - $isRoot = substr_count($resource->path, '/') === 1; - if ($isRoot) { - $exportClass = ResourceExport::class; - $moduleName = 'resource'; - $id = $resource->source_id; - $title = $resource->title; - } + $sectionTmp = $resourceExport->getSectionIdForActivity((int) $resource->source_id, $resourceType); + if ($sectionTmp > 0) { + $exportClass = ResourceExport::class; + $moduleName = 'resource'; + $id = (int) $resource->source_id; + $title = $resource->title ?? ''; + $sectionId = $sectionTmp; + $title = $titleFromLp($sectionId, $moduleName, $id, $title); } } } - // Handle course introduction (page) + // INTRO → page elseif ($resourceType === RESOURCE_TOOL_INTRO && $resource->source_id == 'course_homepage') { $exportClass = PageExport::class; - $moduleName = 'page'; - $id = 0; - $title = get_lang('Introduction'); + $moduleName = 'page'; + $id = 0; + $title = get_lang('Introduction'); + $sectionId = 0; } - // Handle assignments (work) + // ASSIGN elseif ($resourceType === RESOURCE_WORK && $resource->source_id > 0) { $exportClass = AssignExport::class; - $moduleName = 'assign'; - $id = $resource->source_id; - $title = $resource->params['title'] ?? ''; + $moduleName = 'assign'; + $id = (int) $resource->source_id; + $title = $resource->params['title'] ?? ''; + $sectionId = (new AssignExport($this->course))->getSectionIdForActivity($id, $resourceType); + $title = $titleFromLp($sectionId, $moduleName, $id, $title); } - // Handle feedback (survey) + // FEEDBACK / SURVEY elseif ($resourceType === RESOURCE_SURVEY && $resource->source_id > 0) { $exportClass = FeedbackExport::class; - $moduleName = 'feedback'; - $id = $resource->source_id; - $title = $resource->params['title'] ?? ''; + $moduleName = 'feedback'; + $id = (int) $resource->source_id; + $title = $resource->params['title'] ?? ''; + $sectionId = (new FeedbackExport($this->course))->getSectionIdForActivity($id, $resourceType); + $title = $titleFromLp($sectionId, $moduleName, $id, $title); } - // Add the activity if the class and module name are set if ($exportClass && $moduleName) { - $exportInstance = new $exportClass($this->course); $activities[] = [ - 'id' => $id, - 'sectionid' => $exportInstance->getSectionIdForActivity($id, $resourceType), - 'modulename' => $moduleName, - 'moduleid' => $id, - 'title' => $title, + 'id' => $id, + 'sectionid' => $sectionId, + 'modulename'=> $moduleName, + 'moduleid' => $id, + 'title' => $title, ]; } } @@ -854,4 +920,57 @@ private function exportBackupSettings(array $sections, array $activities): array return $settings; } + + /** + * Maps module name to item_type of c_lp_item. + * (c_lp_item.item_type: document, quiz, link, forum, student_publication, survey) + */ + private function mapToLpItemType(string $moduleOrItemType): string + { + switch ($moduleOrItemType) { + case 'page': + case 'resource': + return 'document'; + case 'assign': + return 'student_publication'; + case 'url': + return 'link'; + case 'feedback': + return 'survey'; + default: + return $moduleOrItemType; // quiz, forum... + } + } + + /** Index titles by section + type + id from the LP items (c_lp_item.title). */ + private function buildLpTitleIndex(): array + { + $idx = []; + if (empty($this->course->resources[RESOURCE_LEARNPATH])) { + return $idx; + } + foreach ($this->course->resources[RESOURCE_LEARNPATH] as $lp) { + $sid = (int) $lp->source_id; + if (empty($lp->items)) { + continue; + } + foreach ($lp->items as $it) { + $type = $this->mapToLpItemType($it['item_type']); + $rid = (string) $it['path']; + $idx[$sid][$type][$rid] = $it['title'] ?? ''; + } + } + return $idx; + } + + /** Returns the LP title if it exists; otherwise, use the fallback. */ + private function titleFromLp(array $idx, int $sectionId, string $moduleName, int $resourceId, string $fallback): string + { + if ($sectionId <= 0) { + return $fallback; + } + $type = $this->mapToLpItemType($moduleName); + $rid = (string) $resourceId; + return $idx[$sectionId][$type][$rid] ?? $fallback; + } } diff --git a/main/inc/lib/moodleexport/PageExport.php b/main/inc/lib/moodleexport/PageExport.php index 7e69deef9d1..c2aa97305f4 100644 --- a/main/inc/lib/moodleexport/PageExport.php +++ b/main/inc/lib/moodleexport/PageExport.php @@ -111,12 +111,17 @@ public function getData(int $pageId, int $sectionId): ?array $pageResources = $this->course->resources[RESOURCE_DOCUMENT] ?? []; foreach ($pageResources as $page) { if ($page->source_id == $pageId) { + $name = $page->title ?? ''; + if ($sectionId > 0) { + $name = $this->lpItemTitle($sectionId, RESOURCE_DOCUMENT, $page->source_id, $name); + } + return [ 'id' => $page->source_id, 'moduleid' => $page->source_id, 'modulename' => 'page', 'contextid' => $contextid, - 'name' => $page->title, + 'name' => $name, 'intro' => $page->comment ?? '', 'content' => $this->normalizeContent($this->getPageContent($page)), 'sectionid' => $sectionId, diff --git a/main/inc/lib/moodleexport/QuizExport.php b/main/inc/lib/moodleexport/QuizExport.php index 69c11285833..8f303de07e1 100644 --- a/main/inc/lib/moodleexport/QuizExport.php +++ b/main/inc/lib/moodleexport/QuizExport.php @@ -49,19 +49,23 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void */ public function getData(int $quizId, int $sectionId): array { - $quizResources = $this->course->resources[RESOURCE_QUIZ]; + $quizResources = $this->course->resources[RESOURCE_QUIZ] ?? []; foreach ($quizResources as $quiz) { if ($quiz->obj->iid == -1) { continue; } - - if ($quiz->obj->iid == $quizId) { + if ((int) $quiz->obj->iid === $quizId) { $contextid = $quiz->obj->c_id; + $name = $quiz->obj->title ?? ''; + if ($sectionId > 0) { + $name = $this->lpItemTitle($sectionId, RESOURCE_QUIZ, $quizId, $name); + } + return [ 'id' => $quiz->obj->iid, - 'name' => $quiz->obj->title, + 'name' => $name, 'intro' => $quiz->obj->description, 'timeopen' => $quiz->obj->start_time ?? 0, 'timeclose' => $quiz->obj->end_time ?? 0, @@ -105,6 +109,7 @@ public function getData(int $quizId, int $sectionId): array 'completionpass' => $quiz->obj->completionpass ?? 0, 'completionminattempts' => $quiz->obj->completionminattempts ?? 0, 'allowofflineattempts' => $quiz->obj->allowofflineattempts ?? 0, + 'navmethod' => $quiz->obj->navmethod ?? 'free', 'users' => [], 'files' => [], ]; diff --git a/main/inc/lib/moodleexport/ResourceExport.php b/main/inc/lib/moodleexport/ResourceExport.php index 68fad4e41e4..1c6c97e1860 100644 --- a/main/inc/lib/moodleexport/ResourceExport.php +++ b/main/inc/lib/moodleexport/ResourceExport.php @@ -46,12 +46,17 @@ public function getData(int $resourceId, int $sectionId): array { $resource = $this->course->resources[RESOURCE_DOCUMENT][$resourceId]; + $name = $resource->title; + if ($sectionId > 0) { + $name = $this->lpItemTitle($sectionId, RESOURCE_DOCUMENT, $resourceId, $name); + } + return [ 'id' => $resourceId, 'moduleid' => $resource->source_id, 'modulename' => 'resource', 'contextid' => $resource->source_id, - 'name' => $resource->title, + 'name' => $name, 'intro' => $resource->comment ?? '', 'sectionid' => $sectionId, 'sectionnumber' => 1, diff --git a/main/inc/lib/moodleexport/SectionExport.php b/main/inc/lib/moodleexport/SectionExport.php index 2a83bc4790a..b633fdd1856 100644 --- a/main/inc/lib/moodleexport/SectionExport.php +++ b/main/inc/lib/moodleexport/SectionExport.php @@ -113,10 +113,11 @@ public function getActivitiesForGeneral(): array $activities = $this->getActivitiesForSection($generalLearnpath, true); - if (!in_array('folder', array_column($activities, 'modulename'))) { + $ya = array_column($activities, 'modulename'); + if (!in_array('folder', $ya, true)) { $activities[] = [ - 'id' => 0, - 'moduleid' => 0, + 'id' => ActivityExport::DOCS_MODULE_ID, + 'moduleid' => ActivityExport::DOCS_MODULE_ID, 'modulename' => 'folder', 'name' => 'Documents', 'sectionid' => 0, diff --git a/main/inc/lib/moodleexport/UrlExport.php b/main/inc/lib/moodleexport/UrlExport.php index c92b96a0fc5..3652437a93d 100644 --- a/main/inc/lib/moodleexport/UrlExport.php +++ b/main/inc/lib/moodleexport/UrlExport.php @@ -47,13 +47,18 @@ public function getData(int $activityId, int $sectionId): ?array // Extract the URL information from the course data $url = $this->course->resources['link'][$activityId]; + $name = $url->title; + if ($sectionId > 0) { + $name = $this->lpItemTitle($sectionId, RESOURCE_LINK, $activityId, $name); + } + // Return the URL data formatted for export return [ 'id' => $activityId, 'moduleid' => $activityId, 'modulename' => 'url', 'contextid' => $this->course->info['real_id'], - 'name' => $url->title, + 'name' => $name, 'description' => $url->description, 'externalurl' => $url->url, 'timecreated' => time(), From 8aec3e8a6b48981eae6e5b7f8262394403dcf203 Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Mon, 10 Nov 2025 14:49:06 -0500 Subject: [PATCH 2/5] Course: Moodle export: add activities meta exports (Announcements, Attendance, Calendar, Gradebook, etc.) - refs #6958 --- composer.json | 202 -------------------------------------------------- 1 file changed, 202 deletions(-) delete mode 100755 composer.json diff --git a/composer.json b/composer.json deleted file mode 100755 index 486f75734f6..00000000000 --- a/composer.json +++ /dev/null @@ -1,202 +0,0 @@ -{ - "name": "chamilo/chamilo-lms", - "description": "E-learning and collaboration software", - "type": "project", - "homepage": "http://www.chamilo.org", - "license": "GPL-3.0", - "support": { - "docs": "https://docs.chamilo.org/", - "forum": "https://forum.chamilo.org/", - "issues": "https://github.com/chamilo/chamilo-lms/issues", - "source": "https://github.com/chamilo/chamilo-lms" - }, - "autoload": { - "psr-4": { - "Application\\": "app/", - "Chamilo\\": "src/Chamilo/" - }, - "classmap": [ - "main/admin", - "main/auth", - "main/course_description", - "main/cron/lang", - "main/dropbox", - "main/exercise", - "main/gradebook/lib", - "main/inc/lib", - "main/inc/lib/hook", - "main/install", - "main/lp", - "main/survey", - "main/common_cartridge/export", - "main/common_cartridge/import", - "plugin" - ] - }, - "require": { - "php": "^7.4", - "ext-curl": "*", - "ext-dom": "*", - "ext-fileinfo": "*", - "ext-gd": "*", - "ext-intl": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "ext-zip": "*", - "ext-zlib": "*", - "angelfqc/vimeo-api": "2.0.6", - "apereo/phpcas": "^1.6", - "chamilo/pclzip": "~2.8", - "clue/graph": "~0.9.0", - "culqi/culqi-php": "1.3.4", - "ddeboer/data-import": "@stable", - "doctrine/data-fixtures": "~1.0@dev", - "doctrine/dbal": "~2.5", - "doctrine/migrations": "~1.0@dev", - "doctrine/orm": "~2.5", - "emojione/emojione": "1.3.0", - "endroid/qr-code": "2.5.*", - "enshrined/svg-sanitize": "^0.16.0", - "essence/essence": "2.6.1", - "ezyang/htmlpurifier": "~4.9", - "facebook/graph-sdk": "^5.7", - "firebase/php-jwt": "~5.0", - "gedmo/doctrine-extensions": "~2.3", - "graphp/algorithms": "~0.8.0", - "graphp/graphviz": "~0.2.0", - "guzzlehttp/guzzle": "~6.0", - "h5p/h5p-core": "*", - "imagine/imagine": "0.6.3", - "ircmaxell/password-compat": "~1.0.4", - "jbroadway/urlify": "1.1.0-stable", - "jeroendesloovere/vcard": "~1.7", - "jimmiw/php-time-ago": "0.4.15", - "kigkonsult/icalcreator": "2.24", - "knplabs/doctrine-behaviors": "~1.1", - "knplabs/gaufrette": "~0.3", - "knplabs/knp-components": "~1.3", - "league/csv": "~8.0", - "media-alchemyst/media-alchemyst": "~0.5", - "michelf/php-markdown": "~1.7", - "monolog/monolog": "~1.0", - "mpdf/mpdf": "^8.0", - "ocramius/proxy-manager": "~1.0|2.0.*", - "onelogin/php-saml": "^3.0", - "paragonie/random-lib": "2.0.0", - "patchwork/utf8": "~1.2", - "php-ffmpeg/php-ffmpeg": "0.5.1", - "php-http/guzzle6-adapter": "^2.0", - "php-xapi/client": "0.7.x-dev", - "php-xapi/repository-api": "dev-master as 0.3.1", - "php-xapi/repository-doctrine": "dev-master", - "php-xapi/symfony-serializer": "2.1.0 as 2.0", - "phpmailer/phpmailer": "~6.1", - "phpoffice/phpexcel": "~1.8", - "phpoffice/phpword": "~0.14", - "phpseclib/phpseclib": "^2.0", - "robrichards/xmlseclibs": "3.0.*", - "sabre/vobject": "~3.1", - "sonata-project/admin-bundle": "~3.1|~4.0", - "sonata-project/core-bundle": "~3.1|~4.0", - "sonata-project/user-bundle": "~3.0|~4.0", - "stripe/stripe-php": "*", - "studio-42/elfinder": "2.1.*", - "sunra/php-simple-html-dom-parser": "~1.5.0", - "sylius/attribute": "0.13.0", - "sylius/translation": "0.13.0", - "symfony/console": "~3.0|~4.0", - "symfony/doctrine-bridge": "~2.8", - "symfony/dom-crawler": "~3.4|~4.0", - "symfony/filesystem": "~3.0|~4.0", - "symfony/http-foundation": "~2.8|~3.0", - "symfony/security": "~3.0|~4.0", - "symfony/serializer": "~3.0|~4.0", - "symfony/validator": "~3.0|~4.0", - "symfony/yaml": "~3.0|~4.0", - "szymach/c-pchart": "~3.0", - "thenetworg/oauth2-azure": "^1.4", - "twig/extensions": "~1.0", - "twig/twig": "1.*", - "webit/eval-math": "1.0.1", - "yuloh/bccomp-polyfill": "dev-master", - "packbackbooks/lti-1p3-tool": "1.1.1.x-dev", - "zendframework/zend-config": "~3.0", - "zendframework/zend-feed": "~2.6|^3.0", - "zendframework/zend-http": "~2.6|^3.0", - "zendframework/zend-soap": "~2.6|^3.0" - }, - "require-dev": { - "behat/behat": "~3.5", - "behat/mink": "1.7.1", - "behat/mink-extension": "*", - "behat/mink-goutte-driver": "*", - "behat/mink-selenium2-driver": "*", - "phpunit/phpunit": "*" - }, - "scripts": { - "pre-install-cmd": [ - "Chamilo\\CoreBundle\\Composer\\ScriptHandler::deleteOldFilesFrom19x" - ], - "pre-update-cmd": [ - "Chamilo\\CoreBundle\\Composer\\ScriptHandler::deleteOldFilesFrom19x" - ], - "post-install-cmd": [ - "Chamilo\\CoreBundle\\Composer\\ScriptHandler::dumpCssFiles", - "Chamilo\\CoreBundle\\Composer\\ScriptHandler::generateDoctrineProxies" - ], - "post-update-cmd": [ - "Chamilo\\CoreBundle\\Composer\\ScriptHandler::dumpCssFiles", - "Chamilo\\CoreBundle\\Composer\\ScriptHandler::generateDoctrineProxies" - ], - "update-css": "Chamilo\\CoreBundle\\Composer\\ScriptHandler::updateCss" - }, - "extra": { - "asset-installer-paths": { - "bower-asset-library": "web/assets/" - }, - "branch-alias": { - "dev-master": "1.11.x-dev" - }, - "incenteev-parameters": { - "file": "app/config/parameters.yml" - }, - "symfony-app-dir": "app", - "symfony-assets-install": "relative", - "symfony-bin-dir": "bin", - "symfony-tests-dir": "tests", - "symfony-web-dir": "web" - }, - "repositories": [ - { - "type": "github", - "url": "https://github.com/AngelFQC/vimeo.php.git", - "no-api": true - }, - { - "type": "github", - "url": "https://github.com/AngelFQC/xapi-model.git", - "no-api": true - }, - { - "type": "github", - "url": "https://github.com/AngelFQC/xapi-repository-doctrine.git", - "no-api": true - }, - { - "type": "github", - "url": "https://github.com/AngelFQC/xapi-symfony-serializer.git", - "no-api": true - }, - { - "type": "github", - "url": "https://github.com/chamilo/lti-1-3-php-library.git", - "no-api": true - } - ], - "config": { - "sort-packages": true, - "component-dir": "web/assets" - } -} From 18ab7d9d47dff39e8755009c77f48c4022e16a87 Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Wed, 26 Nov 2025 15:39:40 -0500 Subject: [PATCH 3/5] Course: Fix Moodle export: include root Documents in folder and keep learnpath items ordered by display_order - refs BT#21977 --- main/inc/lib/moodleexport/ActivityExport.php | 37 ++- main/inc/lib/moodleexport/FileExport.php | 45 ++-- main/inc/lib/moodleexport/FolderExport.php | 5 +- main/inc/lib/moodleexport/MoodleExport.php | 233 +++++++++++++++---- main/inc/lib/moodleexport/SectionExport.php | 54 +++-- 5 files changed, 286 insertions(+), 88 deletions(-) diff --git a/main/inc/lib/moodleexport/ActivityExport.php b/main/inc/lib/moodleexport/ActivityExport.php index d87c0636fba..f7d66519f78 100644 --- a/main/inc/lib/moodleexport/ActivityExport.php +++ b/main/inc/lib/moodleexport/ActivityExport.php @@ -28,15 +28,44 @@ public function __construct($course) abstract public function export($activityId, $exportDir, $moduleId, $sectionId); /** - * Get the section ID for a given activity ID. + * Get the section ID (learnpath source_id) for a given activity. */ public function getSectionIdForActivity(int $activityId, string $itemType): int { + if (empty($this->course->resources[RESOURCE_LEARNPATH])) { + return 0; + } + foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) { + if (empty($learnpath->items)) { + continue; + } + foreach ($learnpath->items as $item) { - $item['item_type'] = $item['item_type'] === 'student_publication' ? 'work' : $item['item_type']; - if ($item['item_type'] == $itemType && $item['path'] == $activityId) { - return $learnpath->source_id; + $normalizedType = $item['item_type'] === 'student_publication' + ? 'work' + : $item['item_type']; + + if ($normalizedType !== $itemType) { + continue; + } + + // Classic case: LP stores the numeric id in "path" + if (ctype_digit((string) $item['path']) && (int) $item['path'] === $activityId) { + return (int) $learnpath->source_id; + } + + // Fallback for documents when LP stores the path instead of the id + if ($itemType === RESOURCE_DOCUMENT) { + $doc = \DocumentManager::get_document_data_by_id($activityId, $this->course->code); + if (!empty($doc['path'])) { + $p = (string) $doc['path']; + foreach ([$p, 'document/'.$p, '/'.$p] as $candidate) { + if ((string) $item['path'] === $candidate) { + return (int) $learnpath->source_id; + } + } + } } } } diff --git a/main/inc/lib/moodleexport/FileExport.php b/main/inc/lib/moodleexport/FileExport.php index 19208ff0d0c..d42fbf2d889 100644 --- a/main/inc/lib/moodleexport/FileExport.php +++ b/main/inc/lib/moodleexport/FileExport.php @@ -197,37 +197,28 @@ private function createFileXmlEntry(array $file): string */ private function processDocument(array $filesData, object $document): array { - if ( - $document->file_type === 'file' && - isset($this->course->used_page_doc_ids) && - in_array($document->source_id, $this->course->used_page_doc_ids) - ) { + // Only real files are exported; folders are represented implicitly by "filepath" + if ($document->file_type !== 'file') { return $filesData; } - if ( - $document->file_type === 'file' && - pathinfo($document->path, PATHINFO_EXTENSION) === 'html' && - substr_count($document->path, '/') === 1 - ) { - return $filesData; - } + // Base file data (contenthash, size, title, etc.) + $fileData = $this->getFileData($document); - if ($document->file_type === 'file') { - $extension = pathinfo($document->path, PATHINFO_EXTENSION); - if (!in_array(strtolower($extension), ['html', 'htm'])) { - $fileData = $this->getFileData($document); - $fileData['filepath'] = '/Documents/'; - $fileData['contextid'] = 0; - $fileData['component'] = 'mod_folder'; - $filesData['files'][] = $fileData; - } - } elseif ($document->file_type === 'folder') { - $folderFiles = \DocumentManager::getAllDocumentsByParentId($this->course->info, $document->source_id); - foreach ($folderFiles as $file) { - $filesData['files'][] = $this->getFolderFileData($file, (int) $document->source_id, '/Documents/'.dirname($file['path']).'/'); - } - } + // Rebuild the relative filepath so Moodle can recreate the Documents tree + $relDir = dirname($document->path); + $filepath = $this->ensureTrailingSlash( + $relDir === '.' ? '/' : '/'.$relDir.'/' + ); + + // Attach this file to the global "Documents" folder activity + $fileData['filepath'] = $filepath; + $fileData['contextid'] = ActivityExport::DOCS_MODULE_ID; + $fileData['component'] = 'mod_folder'; + $fileData['filearea'] = 'content'; + $fileData['itemid'] = ActivityExport::DOCS_MODULE_ID; + + $filesData['files'][] = $fileData; return $filesData; } diff --git a/main/inc/lib/moodleexport/FolderExport.php b/main/inc/lib/moodleexport/FolderExport.php index 0ba0295e823..9cd9f3be7e8 100644 --- a/main/inc/lib/moodleexport/FolderExport.php +++ b/main/inc/lib/moodleexport/FolderExport.php @@ -101,10 +101,7 @@ private function getFilesForFolder(int $folderId): array if ($folderId === ActivityExport::DOCS_MODULE_ID) { foreach ($this->course->resources[RESOURCE_DOCUMENT] as $doc) { if ($doc->file_type === 'file') { - $ext = strtolower(pathinfo($doc->path, PATHINFO_EXTENSION)); - if (!in_array($ext, ['html', 'htm'], true)) { - $files[] = ['id' => (int) $doc->source_id]; - } + $files[] = ['id' => (int) $doc->source_id]; } } } diff --git a/main/inc/lib/moodleexport/MoodleExport.php b/main/inc/lib/moodleexport/MoodleExport.php index 086e29ecadb..9c557fcc853 100644 --- a/main/inc/lib/moodleexport/MoodleExport.php +++ b/main/inc/lib/moodleexport/MoodleExport.php @@ -407,15 +407,27 @@ private function createMoodleBackupXml(string $destinationDir, int $version): vo } /** - * Get all sections from the course. + * Get all sections from the course ordered by LP display_order. */ private function getSections(): array { $sectionExport = new SectionExport($this->course); $sections = []; - foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) { - if ($learnpath->lp_type == '1') { + // Safety: if there is no learnpath resource, return only the general section + $learnpaths = $this->course->resources[RESOURCE_LEARNPATH] ?? []; + + // Sort LPs by display_order to respect the order defined in c_lp + usort($learnpaths, static function ($a, $b): int { + $aOrder = (int) ($a->display_order ?? 0); + $bOrder = (int) ($b->display_order ?? 0); + + return $aOrder <=> $bOrder; + }); + + foreach ($learnpaths as $learnpath) { + // We only export "real" LPs (type 1) + if ((int) $learnpath->lp_type === 1) { $sections[] = $sectionExport->getSectionData($learnpath); } } @@ -437,44 +449,85 @@ private function getSections(): array /** * Get all activities from the course. + * Activities are ordered by learnpath display_order when available. */ private function getActivities(): array { - $activities = []; + $activities = []; $glossaryAdded = false; + // ----------------------------------------------------------------- + // 1) Build LP index: titles + display_order per section/type/resource + // ----------------------------------------------------------------- $lpIndex = []; if (!empty($this->course->resources[RESOURCE_LEARNPATH])) { foreach ($this->course->resources[RESOURCE_LEARNPATH] as $lp) { $sid = (int) $lp->source_id; + foreach ($lp->items ?? [] as $it) { - $type = $it['item_type']; - if ($type === 'student_publication') { $type = 'assign'; } - elseif ($type === 'link') { $type = 'url'; } - elseif ($type === 'survey') { $type = 'feedback'; } - elseif ($type === 'document') { $type = 'document'; } + $type = $it['item_type'] ?? ''; + if ($type === 'student_publication') { + $type = 'assign'; + } elseif ($type === 'link') { + $type = 'url'; + } elseif ($type === 'survey') { + $type = 'feedback'; + } elseif ($type === 'document') { + $type = 'document'; + } - $rid = $it['path']; + $rid = $it['path'] ?? ''; $title = $it['title'] ?? ''; + $order = isset($it['display_order']) ? (int) $it['display_order'] : 0; + + $entry = [ + 'title' => $title, + 'order' => $order, + ]; if (ctype_digit((string) $rid)) { - $lpIndex[$sid][$type]['id'][(int) $rid] = $title; + $rid = (int) $rid; + + // If the same resource appears multiple times, keep the lowest order. + if (!isset($lpIndex[$sid][$type]['id'][$rid])) { + $lpIndex[$sid][$type]['id'][$rid] = $entry; + } else { + $existingOrder = (int) ($lpIndex[$sid][$type]['id'][$rid]['order'] ?? 0); + if ($order > 0 && ($existingOrder === 0 || $order < $existingOrder)) { + $lpIndex[$sid][$type]['id'][$rid]['order'] = $order; + } + // Keep the first title to avoid random renames. + } } else { - $lpIndex[$sid][$type]['path'][(string) $rid] = $title; + $rid = (string) $rid; + + if (!isset($lpIndex[$sid][$type]['path'][$rid])) { + $lpIndex[$sid][$type]['path'][$rid] = $entry; + } else { + $existingOrder = (int) ($lpIndex[$sid][$type]['path'][$rid]['order'] ?? 0); + if ($order > 0 && ($existingOrder === 0 || $order < $existingOrder)) { + $lpIndex[$sid][$type]['path'][$rid]['order'] = $order; + } + } } } } } + // Helper: get title from LP index. $titleFromLp = function (int $sectionId, string $moduleName, int $resourceId, string $fallback) use ($lpIndex) { - $type = in_array($moduleName, ['page','resource'], true) ? 'document' : $moduleName; + $type = in_array($moduleName, ['page', 'resource'], true) ? 'document' : $moduleName; - if (isset($lpIndex[$sectionId][$type]['id'][$resourceId])) { - return $lpIndex[$sectionId][$type]['id'][$resourceId]; + $entry = $lpIndex[$sectionId][$type]['id'][$resourceId] ?? null; + if (is_array($entry) && !empty($entry['title'])) { + return $entry['title']; } - if ($type === 'assign' && isset($lpIndex[$sectionId]['student_publication']['id'][$resourceId])) { - return $lpIndex[$sectionId]['student_publication']['id'][$resourceId]; + if ($type === 'assign') { + $entry = $lpIndex[$sectionId]['student_publication']['id'][$resourceId] ?? null; + if (is_array($entry) && !empty($entry['title'])) { + return $entry['title']; + } } if ($type === 'document') { @@ -482,8 +535,9 @@ private function getActivities(): array if (!empty($doc['path'])) { $p = (string) $doc['path']; foreach ([$p, 'document/'.$p, '/'.$p] as $cand) { - if (isset($lpIndex[$sectionId]['document']['path'][$cand])) { - return $lpIndex[$sectionId]['document']['path'][$cand]; + $entry = $lpIndex[$sectionId]['document']['path'][$cand] ?? null; + if (is_array($entry) && !empty($entry['title'])) { + return $entry['title']; } } } @@ -492,14 +546,53 @@ private function getActivities(): array return $fallback; }; + // Helper: get display_order from LP index. + $orderFromLp = function (int $sectionId, string $moduleName, int $resourceId) use ($lpIndex): int { + $type = in_array($moduleName, ['page', 'resource'], true) ? 'document' : $moduleName; + + $entry = $lpIndex[$sectionId][$type]['id'][$resourceId] ?? null; + if (is_array($entry) && !empty($entry['order'])) { + return (int) $entry['order']; + } + + if ($type === 'assign') { + $entry = $lpIndex[$sectionId]['student_publication']['id'][$resourceId] ?? null; + if (is_array($entry) && !empty($entry['order'])) { + return (int) $entry['order']; + } + } + + if ($type === 'document') { + $doc = \DocumentManager::get_document_data_by_id($resourceId, $this->course->code); + if (!empty($doc['path'])) { + $p = (string) $doc['path']; + foreach ([$p, 'document/'.$p, '/'.$p] as $cand) { + $entry = $lpIndex[$sectionId]['document']['path'][$cand] ?? null; + if (is_array($entry) && !empty($entry['order'])) { + return (int) $entry['order']; + } + } + } + } + + return 0; + }; + + // ----------------------------------------------------------------- + // 2) "Documents" folder pseudo-activity (section 0) + // ----------------------------------------------------------------- $activities[] = [ 'id' => ActivityExport::DOCS_MODULE_ID, 'sectionid' => 0, 'modulename'=> 'folder', 'moduleid' => ActivityExport::DOCS_MODULE_ID, 'title' => 'Documents', + 'order' => 0, ]; + // ----------------------------------------------------------------- + // 3) Loop over all course resources (original logic) + // ----------------------------------------------------------------- $htmlPageIds = []; foreach ($this->course->resources as $resourceType => $resources) { foreach ($resources as $resource) { @@ -515,7 +608,8 @@ private function getActivities(): array $moduleName = 'quiz'; $id = (int) $resource->obj->iid; $title = $resource->obj->title ?? ''; - $sectionId = (new QuizExport($this->course))->getSectionIdForActivity($id, $resourceType); + $sectionId = (new QuizExport($this->course)) + ->getSectionIdForActivity($id, $resourceType); $title = $titleFromLp($sectionId, $moduleName, $id, $title); } // URL @@ -524,16 +618,17 @@ private function getActivities(): array $moduleName = 'url'; $id = (int) $resource->source_id; $title = $resource->title ?? ''; - $sectionId = (new UrlExport($this->course))->getSectionIdForActivity($id, $resourceType); + $sectionId = (new UrlExport($this->course)) + ->getSectionIdForActivity($id, $resourceType); $title = $titleFromLp($sectionId, $moduleName, $id, $title); } - // GLOSSARY (uno solo) + // GLOSSARY (only one) elseif ($resourceType === RESOURCE_GLOSSARY && $resource->glossary_id > 0 && !$glossaryAdded) { - $exportClass = GlossaryExport::class; - $moduleName = 'glossary'; - $id = 1; - $title = get_lang('Glossary'); - $sectionId = 0; + $exportClass = GlossaryExport::class; + $moduleName = 'glossary'; + $id = 1; + $title = get_lang('Glossary'); + $sectionId = 0; $glossaryAdded = true; } // FORUM @@ -542,28 +637,38 @@ private function getActivities(): array $moduleName = 'forum'; $id = (int) $resource->obj->iid; $title = $resource->obj->forum_title ?? ''; - $sectionId = (new ForumExport($this->course))->getSectionIdForActivity($id, $resourceType); + $sectionId = (new ForumExport($this->course)) + ->getSectionIdForActivity($id, $resourceType); $title = $titleFromLp($sectionId, $moduleName, $id, $title); } - // DOCUMENT + // DOCUMENTS elseif ($resourceType === RESOURCE_DOCUMENT && $resource->source_id > 0) { - $document = \DocumentManager::get_document_data_by_id($resource->source_id, $this->course->code); - $ext = strtolower(pathinfo($document['path'], PATHINFO_EXTENSION)); + $document = \DocumentManager::get_document_data_by_id( + $resource->source_id, + $this->course->code + ); + $ext = strtolower(pathinfo($document['path'] ?? '', PATHINFO_EXTENSION)); - // HTML → page + // HTML → Moodle "page" if ($ext === 'html' || $ext === 'htm') { $exportClass = PageExport::class; $moduleName = 'page'; $id = (int) $resource->source_id; $title = $document['title'] ?? ''; - $sectionId = (new PageExport($this->course))->getSectionIdForActivity($id, $resourceType); + $sectionId = (new PageExport($this->course)) + ->getSectionIdForActivity($id, $resourceType); $title = $titleFromLp($sectionId, $moduleName, $id, $title); $htmlPageIds[] = $id; } - if ($resource->file_type === 'file' && !in_array($resource->source_id, $htmlPageIds, true)) { + // Other files → Moodle "resource" (but not if already exported as page) + if ($resource->file_type === 'file' + && !in_array($resource->source_id, $htmlPageIds, true) + ) { $resourceExport = new ResourceExport($this->course); - $sectionTmp = $resourceExport->getSectionIdForActivity((int) $resource->source_id, $resourceType); + $sectionTmp = $resourceExport + ->getSectionIdForActivity((int) $resource->source_id, $resourceType); + if ($sectionTmp > 0) { $exportClass = ResourceExport::class; $moduleName = 'resource'; @@ -574,8 +679,10 @@ private function getActivities(): array } } } - // INTRO → page - elseif ($resourceType === RESOURCE_TOOL_INTRO && $resource->source_id == 'course_homepage') { + // INTRODUCTION → Moodle "page" + elseif ($resourceType === RESOURCE_TOOL_INTRO + && $resource->source_id === 'course_homepage' + ) { $exportClass = PageExport::class; $moduleName = 'page'; $id = 0; @@ -588,7 +695,8 @@ private function getActivities(): array $moduleName = 'assign'; $id = (int) $resource->source_id; $title = $resource->params['title'] ?? ''; - $sectionId = (new AssignExport($this->course))->getSectionIdForActivity($id, $resourceType); + $sectionId = (new AssignExport($this->course)) + ->getSectionIdForActivity($id, $resourceType); $title = $titleFromLp($sectionId, $moduleName, $id, $title); } // FEEDBACK / SURVEY @@ -597,22 +705,69 @@ private function getActivities(): array $moduleName = 'feedback'; $id = (int) $resource->source_id; $title = $resource->params['title'] ?? ''; - $sectionId = (new FeedbackExport($this->course))->getSectionIdForActivity($id, $resourceType); + $sectionId = (new FeedbackExport($this->course)) + ->getSectionIdForActivity($id, $resourceType); $title = $titleFromLp($sectionId, $moduleName, $id, $title); } if ($exportClass && $moduleName) { + $order = $orderFromLp($sectionId, $moduleName, $id); + $activities[] = [ 'id' => $id, 'sectionid' => $sectionId, 'modulename'=> $moduleName, 'moduleid' => $id, 'title' => $title, + 'order' => $order, ]; } } } + // ----------------------------------------------------------------- + // 4) Sort activities per section by LP order (display_order) + // ----------------------------------------------------------------- + if (!empty($activities)) { + $grouped = []; + $seqBySec = []; + + foreach ($activities as $activity) { + $sid = (int) ($activity['sectionid'] ?? 0); + + if (!isset($grouped[$sid])) { + $grouped[$sid] = []; + $seqBySec[$sid] = 0; + } + + $order = (int) ($activity['order'] ?? 0); + if ($order <= 0) { + // Keep relative insertion order for items without LP order, + // but make sure they come *after* the LP-ordered items. + $seqBySec[$sid]++; + $order = 1000 + $seqBySec[$sid]; + } + + $activity['_sort'] = $order; + $grouped[$sid][] = $activity; + } + + $activities = []; + foreach ($grouped as $sid => $list) { + usort( + $list, + static function (array $a, array $b): int { + return $a['_sort'] <=> $b['_sort']; + } + ); + + foreach ($list as $item) { + unset($item['order'], $item['_sort']); + $activities[] = $item; + } + } + } + return $activities; } diff --git a/main/inc/lib/moodleexport/SectionExport.php b/main/inc/lib/moodleexport/SectionExport.php index 426ec5a7941..948b6c33f21 100644 --- a/main/inc/lib/moodleexport/SectionExport.php +++ b/main/inc/lib/moodleexport/SectionExport.php @@ -113,15 +113,23 @@ public function getActivitiesForGeneral(): array $activities = $this->getActivitiesForSection($generalLearnpath, true); - $ya = array_column($activities, 'modulename'); - if (!in_array('folder', $ya, true)) { - $activities[] = [ - 'id' => ActivityExport::DOCS_MODULE_ID, - 'moduleid' => ActivityExport::DOCS_MODULE_ID, - 'modulename' => 'folder', - 'name' => 'Documents', - 'sectionid' => 0, - ]; + // Ensure we always have one "Documents" folder activity at the top of the general section. + $hasFolder = false; + foreach ($activities as $activity) { + if ($activity['modulename'] === 'folder') { + $hasFolder = true; + break; + } + } + + if (!$hasFolder) { + array_unshift($activities, [ + 'id' => ActivityExport::DOCS_MODULE_ID, + 'moduleid' => ActivityExport::DOCS_MODULE_ID, + 'type' => 'folder', + 'modulename'=> 'folder', + 'name' => 'Documents', + ]); } return $activities; @@ -159,14 +167,32 @@ public function getSectionData(object $learnpath): array } /** - * Get the activities for a specific section. + * Get the activities for a specific section (learnpath). + * + * Items are sorted using c_lp_item.display_order so that the order + * in Moodle matches the order defined in the Chamilo LP. */ public function getActivitiesForSection(object $learnpath, bool $isGeneral = false): array { $activities = []; - $sectionId = $isGeneral ? 0 : $learnpath->source_id; + $sectionId = $isGeneral ? 0 : (int) $learnpath->source_id; + + $items = $learnpath->items ?? []; + + // Ensure items follow the same order as in Chamilo (c_lp_item.display_order). + usort($items, static function (array $a, array $b): int { + $aOrder = $a['display_order'] ?? 0; + $bOrder = $b['display_order'] ?? 0; + + if ($aOrder === $bOrder) { + // Fallback to id to keep a deterministic order. + return (int) ($a['id'] ?? 0) <=> (int) ($b['id'] ?? 0); + } + + return $aOrder <=> $bOrder; + }); - foreach ($learnpath->items as $item) { + foreach ($items as $item) { $this->addActivityToList($item, $sectionId, $activities); } @@ -226,7 +252,7 @@ private function isItemInLearnpath(object $item, string $type): bool */ private function addActivityToList(array $item, int $sectionId, array &$activities): void { - static $documentsFolderAdded = false; + /*static $documentsFolderAdded = false; if (!$documentsFolderAdded && $sectionId === 0) { $activities[] = [ 'id' => 0, @@ -236,7 +262,7 @@ private function addActivityToList(array $item, int $sectionId, array &$activiti 'name' => 'Documents', ]; $documentsFolderAdded = true; - } + }*/ $activityData = null; $activityClassMap = [ From e34cc0bd3929028e7e33d0626f21634aa7d93db8 Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Mon, 5 Jan 2026 19:23:36 -0500 Subject: [PATCH 4/5] Course: Fix lesson document exports and LP activity mapping - refs BT#21977 --- main/inc/lib/moodleexport/ActivityExport.php | 2 +- main/inc/lib/moodleexport/MoodleExport.php | 487 +++++++------------ main/inc/lib/moodleexport/PageExport.php | 7 +- main/inc/lib/moodleexport/SectionExport.php | 117 ++--- 4 files changed, 251 insertions(+), 362 deletions(-) diff --git a/main/inc/lib/moodleexport/ActivityExport.php b/main/inc/lib/moodleexport/ActivityExport.php index f7d66519f78..2665890e7d5 100644 --- a/main/inc/lib/moodleexport/ActivityExport.php +++ b/main/inc/lib/moodleexport/ActivityExport.php @@ -14,7 +14,7 @@ abstract class ActivityExport { protected $course; - public const DOCS_MODULE_ID = 1000000; + public const DOCS_MODULE_ID = 0; public function __construct($course) { diff --git a/main/inc/lib/moodleexport/MoodleExport.php b/main/inc/lib/moodleexport/MoodleExport.php index 9c557fcc853..5f89e71c28d 100644 --- a/main/inc/lib/moodleexport/MoodleExport.php +++ b/main/inc/lib/moodleexport/MoodleExport.php @@ -81,7 +81,7 @@ public function export(string $courseId, string $exportDir, int $version) $fileExport->exportFiles($filesData, $tempDir); // Export sections of the course - $this->exportSections($tempDir); + $this->exportSections($tempDir, $activities); // Export all root XML files $this->exportRootXmlFiles($tempDir); @@ -277,9 +277,10 @@ private function exportRootXmlFiles(string $exportDir): void $activities = $this->getActivities(); $questionsData = []; foreach ($activities as $activity) { - if ($activity['modulename'] === 'quiz') { + if (($activity['modulename'] ?? '') === 'quiz') { $quizExport = new QuizExport($this->course); - $quizData = $quizExport->getData($activity['id'], $activity['sectionid']); + $quizData = $quizExport->getData((int) $activity['id'], (int) $activity['sectionid']); + $quizData['moduleid'] = (int) $activity['moduleid']; $questionsData[] = $quizData; } } @@ -408,12 +409,23 @@ private function createMoodleBackupXml(string $destinationDir, int $version): vo /** * Get all sections from the course ordered by LP display_order. + * Uses the SAME activities list (and moduleid) as moodle_backup.xml. */ - private function getSections(): array + private function getSections(?array $activities = null): array { - $sectionExport = new SectionExport($this->course); $sections = []; + // Compute activities once if not provided + if ($activities === null) { + $activities = $this->getActivities(); + } + + $activitiesBySection = $this->groupActivitiesBySection($activities); + + // We only need SectionExport for metadata (name/summary/visible/timemodified), + // but it MUST reuse the precomputed activities to keep moduleid consistent. + $sectionExport = new SectionExport($this->course, $activitiesBySection); + // Safety: if there is no learnpath resource, return only the general section $learnpaths = $this->course->resources[RESOURCE_LEARNPATH] ?? []; @@ -453,337 +465,198 @@ private function getSections(): array */ private function getActivities(): array { - $activities = []; - $glossaryAdded = false; - - // ----------------------------------------------------------------- - // 1) Build LP index: titles + display_order per section/type/resource - // ----------------------------------------------------------------- - $lpIndex = []; - if (!empty($this->course->resources[RESOURCE_LEARNPATH])) { - foreach ($this->course->resources[RESOURCE_LEARNPATH] as $lp) { - $sid = (int) $lp->source_id; - - foreach ($lp->items ?? [] as $it) { - $type = $it['item_type'] ?? ''; - if ($type === 'student_publication') { - $type = 'assign'; - } elseif ($type === 'link') { - $type = 'url'; - } elseif ($type === 'survey') { - $type = 'feedback'; - } elseif ($type === 'document') { - $type = 'document'; - } + $activities = []; + // "Documents" folder pseudo-activity (always in section 0) + $activities[] = [ + 'id' => ActivityExport::DOCS_MODULE_ID, + 'sectionid' => 0, + 'modulename' => 'folder', + 'moduleid' => ActivityExport::DOCS_MODULE_ID, + 'title' => 'Documents', + 'order' => 0, + ]; - $rid = $it['path'] ?? ''; - $title = $it['title'] ?? ''; - $order = isset($it['display_order']) ? (int) $it['display_order'] : 0; - - $entry = [ - 'title' => $title, - 'order' => $order, - ]; - - if (ctype_digit((string) $rid)) { - $rid = (int) $rid; - - // If the same resource appears multiple times, keep the lowest order. - if (!isset($lpIndex[$sid][$type]['id'][$rid])) { - $lpIndex[$sid][$type]['id'][$rid] = $entry; - } else { - $existingOrder = (int) ($lpIndex[$sid][$type]['id'][$rid]['order'] ?? 0); - if ($order > 0 && ($existingOrder === 0 || $order < $existingOrder)) { - $lpIndex[$sid][$type]['id'][$rid]['order'] = $order; - } - // Keep the first title to avoid random renames. - } - } else { - $rid = (string) $rid; - - if (!isset($lpIndex[$sid][$type]['path'][$rid])) { - $lpIndex[$sid][$type]['path'][$rid] = $entry; - } else { - $existingOrder = (int) ($lpIndex[$sid][$type]['path'][$rid]['order'] ?? 0); - if ($order > 0 && ($existingOrder === 0 || $order < $existingOrder)) { - $lpIndex[$sid][$type]['path'][$rid]['order'] = $order; - } - } - } - } - } - } + // Build activities from LP items (one course module per LP item) + $learnpaths = $this->course->resources[RESOURCE_LEARNPATH] ?? []; - // Helper: get title from LP index. - $titleFromLp = function (int $sectionId, string $moduleName, int $resourceId, string $fallback) use ($lpIndex) { - $type = in_array($moduleName, ['page', 'resource'], true) ? 'document' : $moduleName; + // Sort by LP display_order to respect c_lp order + usort($learnpaths, static function ($a, $b): int { + return (int)($a->display_order ?? 0) <=> (int)($b->display_order ?? 0); + }); - $entry = $lpIndex[$sectionId][$type]['id'][$resourceId] ?? null; - if (is_array($entry) && !empty($entry['title'])) { - return $entry['title']; + foreach ($learnpaths as $lp) { + // Only "real" LPs + if ((int)($lp->lp_type ?? 0) !== 1) { + continue; } - if ($type === 'assign') { - $entry = $lpIndex[$sectionId]['student_publication']['id'][$resourceId] ?? null; - if (is_array($entry) && !empty($entry['title'])) { - return $entry['title']; - } + $sectionId = (int)($lp->source_id ?? 0); + if ($sectionId <= 0 || empty($lp->items)) { + continue; } - if ($type === 'document') { - $doc = \DocumentManager::get_document_data_by_id($resourceId, $this->course->code); - if (!empty($doc['path'])) { - $p = (string) $doc['path']; - foreach ([$p, 'document/'.$p, '/'.$p] as $cand) { - $entry = $lpIndex[$sectionId]['document']['path'][$cand] ?? null; - if (is_array($entry) && !empty($entry['title'])) { - return $entry['title']; + foreach ($lp->items as $it) { + $lpItemId = isset($it['id']) ? (int)$it['id'] : 0; + $itemType = (string)($it['item_type'] ?? ''); + $path = $it['path'] ?? null; + $title = (string)($it['title'] ?? ''); + $order = isset($it['display_order']) ? (int)$it['display_order'] : 0; + + // Map LP item_type to Moodle modulename + $moduleName = null; + $instanceId = null; + + if ($itemType === 'quiz') { + $moduleName = 'quiz'; + $instanceId = is_numeric($path) ? (int)$path : null; + } elseif ($itemType === 'link') { + $moduleName = 'url'; + $instanceId = is_numeric($path) ? (int)$path : null; + } elseif ($itemType === 'student_publication') { + $moduleName = 'assign'; + $instanceId = is_numeric($path) ? (int)$path : null; + } elseif ($itemType === 'survey') { + $moduleName = 'feedback'; + $instanceId = is_numeric($path) ? (int)$path : null; + } elseif ($itemType === 'forum') { + $moduleName = 'forum'; + $instanceId = is_numeric($path) ? (int)$path : null; + } elseif ($itemType === 'document') { + $docId = is_numeric($path) ? (int)$path : 0; + if ($docId > 0) { + $doc = \DocumentManager::get_document_data_by_id($docId, $this->course->code); + if (!empty($doc)) { + $docPath = (string)($doc['path'] ?? ''); + $ext = strtolower(pathinfo($docPath, PATHINFO_EXTENSION)); + + if ($ext === 'html' || $ext === 'htm') { + $moduleName = 'page'; + $instanceId = $docId; + if ($title === '') { + $title = (string)($doc['title'] ?? ''); + } + } elseif (($doc['filetype'] ?? '') === 'file') { + $moduleName = 'resource'; + $instanceId = $docId; + if ($title === '') { + $title = (string)($doc['title'] ?? ''); + } + } } } } - } - - return $fallback; - }; - - // Helper: get display_order from LP index. - $orderFromLp = function (int $sectionId, string $moduleName, int $resourceId) use ($lpIndex): int { - $type = in_array($moduleName, ['page', 'resource'], true) ? 'document' : $moduleName; - $entry = $lpIndex[$sectionId][$type]['id'][$resourceId] ?? null; - if (is_array($entry) && !empty($entry['order'])) { - return (int) $entry['order']; - } - - if ($type === 'assign') { - $entry = $lpIndex[$sectionId]['student_publication']['id'][$resourceId] ?? null; - if (is_array($entry) && !empty($entry['order'])) { - return (int) $entry['order']; + // Skip unsupported / invalid + if (empty($moduleName) || empty($instanceId)) { + continue; } - } - if ($type === 'document') { - $doc = \DocumentManager::get_document_data_by_id($resourceId, $this->course->code); - if (!empty($doc['path'])) { - $p = (string) $doc['path']; - foreach ([$p, 'document/'.$p, '/'.$p] as $cand) { - $entry = $lpIndex[$sectionId]['document']['path'][$cand] ?? null; - if (is_array($entry) && !empty($entry['order'])) { - return (int) $entry['order']; - } - } - } + // Generic unique course module id per LP occurrence + $moduleId = $this->resolveLpModuleId($moduleName, $lpItemId, (int)$instanceId); + + $activities[] = [ + 'id' => (int)$instanceId, + 'sectionid' => $sectionId, + 'modulename' => $moduleName, + 'moduleid' => $moduleId, + 'title' => $title !== '' ? $title : $moduleName, + 'order' => $order, + ]; } + } - return 0; - }; - - // ----------------------------------------------------------------- - // 2) "Documents" folder pseudo-activity (section 0) - // ----------------------------------------------------------------- - $activities[] = [ - 'id' => ActivityExport::DOCS_MODULE_ID, - 'sectionid' => 0, - 'modulename'=> 'folder', - 'moduleid' => ActivityExport::DOCS_MODULE_ID, - 'title' => 'Documents', - 'order' => 0, - ]; - - // ----------------------------------------------------------------- - // 3) Loop over all course resources (original logic) - // ----------------------------------------------------------------- - $htmlPageIds = []; - foreach ($this->course->resources as $resourceType => $resources) { - foreach ($resources as $resource) { - $exportClass = null; - $moduleName = ''; - $title = ''; - $id = 0; - $sectionId = 0; - - // QUIZ - if ($resourceType === RESOURCE_QUIZ && $resource->obj->iid > 0) { - $exportClass = QuizExport::class; - $moduleName = 'quiz'; - $id = (int) $resource->obj->iid; - $title = $resource->obj->title ?? ''; - $sectionId = (new QuizExport($this->course)) - ->getSectionIdForActivity($id, $resourceType); - $title = $titleFromLp($sectionId, $moduleName, $id, $title); - } - // URL - elseif ($resourceType === RESOURCE_LINK && $resource->source_id > 0) { - $exportClass = UrlExport::class; - $moduleName = 'url'; - $id = (int) $resource->source_id; - $title = $resource->title ?? ''; - $sectionId = (new UrlExport($this->course)) - ->getSectionIdForActivity($id, $resourceType); - $title = $titleFromLp($sectionId, $moduleName, $id, $title); - } - // GLOSSARY (only one) - elseif ($resourceType === RESOURCE_GLOSSARY && $resource->glossary_id > 0 && !$glossaryAdded) { - $exportClass = GlossaryExport::class; - $moduleName = 'glossary'; - $id = 1; - $title = get_lang('Glossary'); - $sectionId = 0; - $glossaryAdded = true; - } - // FORUM - elseif ($resourceType === RESOURCE_FORUM && $resource->source_id > 0) { - $exportClass = ForumExport::class; - $moduleName = 'forum'; - $id = (int) $resource->obj->iid; - $title = $resource->obj->forum_title ?? ''; - $sectionId = (new ForumExport($this->course)) - ->getSectionIdForActivity($id, $resourceType); - $title = $titleFromLp($sectionId, $moduleName, $id, $title); - } - // DOCUMENTS - elseif ($resourceType === RESOURCE_DOCUMENT && $resource->source_id > 0) { - $document = \DocumentManager::get_document_data_by_id( - $resource->source_id, - $this->course->code - ); - $ext = strtolower(pathinfo($document['path'] ?? '', PATHINFO_EXTENSION)); - - // HTML → Moodle "page" - if ($ext === 'html' || $ext === 'htm') { - $exportClass = PageExport::class; - $moduleName = 'page'; - $id = (int) $resource->source_id; - $title = $document['title'] ?? ''; - $sectionId = (new PageExport($this->course)) - ->getSectionIdForActivity($id, $resourceType); - $title = $titleFromLp($sectionId, $moduleName, $id, $title); - $htmlPageIds[] = $id; - } - - // Other files → Moodle "resource" (but not if already exported as page) - if ($resource->file_type === 'file' - && !in_array($resource->source_id, $htmlPageIds, true) - ) { - $resourceExport = new ResourceExport($this->course); - $sectionTmp = $resourceExport - ->getSectionIdForActivity((int) $resource->source_id, $resourceType); - - if ($sectionTmp > 0) { - $exportClass = ResourceExport::class; - $moduleName = 'resource'; - $id = (int) $resource->source_id; - $title = $resource->title ?? ''; - $sectionId = $sectionTmp; - $title = $titleFromLp($sectionId, $moduleName, $id, $title); - } - } - } - // INTRODUCTION → Moodle "page" - elseif ($resourceType === RESOURCE_TOOL_INTRO - && $resource->source_id === 'course_homepage' - ) { - $exportClass = PageExport::class; - $moduleName = 'page'; - $id = 0; - $title = get_lang('Introduction'); - $sectionId = 0; - } - // ASSIGN - elseif ($resourceType === RESOURCE_WORK && $resource->source_id > 0) { - $exportClass = AssignExport::class; - $moduleName = 'assign'; - $id = (int) $resource->source_id; - $title = $resource->params['title'] ?? ''; - $sectionId = (new AssignExport($this->course)) - ->getSectionIdForActivity($id, $resourceType); - $title = $titleFromLp($sectionId, $moduleName, $id, $title); - } - // FEEDBACK / SURVEY - elseif ($resourceType === RESOURCE_SURVEY && $resource->source_id > 0) { - $exportClass = FeedbackExport::class; - $moduleName = 'feedback'; - $id = (int) $resource->source_id; - $title = $resource->params['title'] ?? ''; - $sectionId = (new FeedbackExport($this->course)) - ->getSectionIdForActivity($id, $resourceType); - $title = $titleFromLp($sectionId, $moduleName, $id, $title); - } + // Add general section activities (items not in any LP) + $sectionExport = new SectionExport($this->course); + $generalActivities = $sectionExport->getActivitiesForGeneral(); - if ($exportClass && $moduleName) { - $order = $orderFromLp($sectionId, $moduleName, $id); - - $activities[] = [ - 'id' => $id, - 'sectionid' => $sectionId, - 'modulename'=> $moduleName, - 'moduleid' => $id, - 'title' => $title, - 'order' => $order, - ]; - } + foreach ($generalActivities as $ga) { + // Avoid duplicating the Documents folder (we added it already) + if (($ga['modulename'] ?? '') === 'folder') { + continue; } - } - - // ----------------------------------------------------------------- - // 4) Sort activities per section by LP order (display_order) - // ----------------------------------------------------------------- - if (!empty($activities)) { - $grouped = []; - $seqBySec = []; - foreach ($activities as $activity) { - $sid = (int) ($activity['sectionid'] ?? 0); + $activities[] = [ + 'id' => (int)($ga['id'] ?? 0), + 'sectionid' => 0, + 'modulename' => (string)($ga['modulename'] ?? ''), + 'moduleid' => (int)($ga['moduleid'] ?? 0), + 'title' => (string)($ga['name'] ?? ''), + 'order' => 0, + ]; + } - if (!isset($grouped[$sid])) { - $grouped[$sid] = []; - $seqBySec[$sid] = 0; - } + // Sort activities per section by LP display_order + $grouped = []; + $seqBySec = []; - $order = (int) ($activity['order'] ?? 0); - if ($order <= 0) { - // Keep relative insertion order for items without LP order, - // but make sure they come *after* the LP-ordered items. - $seqBySec[$sid]++; - $order = 1000 + $seqBySec[$sid]; - } + foreach ($activities as $a) { + $sid = (int)($a['sectionid'] ?? 0); + if (!isset($grouped[$sid])) { + $grouped[$sid] = []; + $seqBySec[$sid] = 0; + } - $activity['_sort'] = $order; - $grouped[$sid][] = $activity; + $ord = (int)($a['order'] ?? 0); + if ($ord <= 0) { + $seqBySec[$sid]++; + $ord = 1000 + $seqBySec[$sid]; } - $activities = []; - foreach ($grouped as $sid => $list) { - usort( - $list, - static function (array $a, array $b): int { - return $a['_sort'] <=> $b['_sort']; - } - ); + $a['_sort'] = $ord; + $grouped[$sid][] = $a; + } - foreach ($list as $item) { - unset($item['order'], $item['_sort']); - $activities[] = $item; - } + $sorted = []; + foreach ($grouped as $sid => $list) { + usort($list, static fn(array $x, array $y): int => $x['_sort'] <=> $y['_sort']); + foreach ($list as $x) { + unset($x['_sort'], $x['order']); + $sorted[] = $x; } } - return $activities; + return $sorted; } /** * Export the sections of the course. */ - private function exportSections(string $exportDir): void + private function exportSections(string $exportDir, array $activities): void { - $sections = $this->getSections(); + $sections = $this->getSections($activities); + $activitiesBySection = $this->groupActivitiesBySection($activities); + + // Reuse ONE instance to keep any internal caches stable + $sectionExport = new SectionExport($this->course, $activitiesBySection); foreach ($sections as $section) { - $sectionExport = new SectionExport($this->course); - $sectionExport->exportSection($section['id'], $exportDir); + $sectionExport->exportSection((int) $section['id'], $exportDir); } } + /** + * Convert MoodleExport::getActivities() output into the structure SectionExport expects. + * Ensures section.xml sequence uses the same moduleid as moodle_backup.xml. + */ + private function groupActivitiesBySection(array $activities): array + { + $bySection = []; + + foreach ($activities as $a) { + $sid = (int) ($a['sectionid'] ?? 0); + + $bySection[$sid][] = [ + 'id' => (int) ($a['id'] ?? 0), + 'moduleid' => (int) ($a['moduleid'] ?? 0), + 'modulename'=> (string) ($a['modulename'] ?? ''), + 'name' => (string) ($a['title'] ?? ''), + 'sectionid' => $sid, + ]; + } + + return $bySection; + } + /** * Create a .mbz (ZIP) file from the exported data. */ @@ -1128,4 +1001,22 @@ private function titleFromLp(array $idx, int $sectionId, string $moduleName, int $rid = (string) $resourceId; return $idx[$sectionId][$type][$rid] ?? $fallback; } + + /** + * Generic resolver for Moodle course module id from an LP item occurrence. + * Keep folder/glossary stable (if you treat glossary as singleton). + */ + private function resolveLpModuleId(string $moduleName, int $lpItemId, int $fallback): int + { + if ($lpItemId <= 0) { + return $fallback; + } + + // Keep special/singleton modules stable if needed + if (in_array($moduleName, ['folder', 'glossary'], true)) { + return $fallback; + } + + return 900000000 + $lpItemId; + } } diff --git a/main/inc/lib/moodleexport/PageExport.php b/main/inc/lib/moodleexport/PageExport.php index edd49a66b0e..f9601b5e588 100644 --- a/main/inc/lib/moodleexport/PageExport.php +++ b/main/inc/lib/moodleexport/PageExport.php @@ -111,17 +111,12 @@ public function getData(int $pageId, int $sectionId): ?array $pageResources = $this->course->resources[RESOURCE_DOCUMENT] ?? []; foreach ($pageResources as $page) { if ($page->source_id == $pageId) { - $name = $page->title ?? ''; - if ($sectionId > 0) { - $name = $this->lpItemTitle($sectionId, RESOURCE_DOCUMENT, $page->source_id, $name); - } - return [ 'id' => $page->source_id, 'moduleid' => $page->source_id, 'modulename' => 'page', 'contextid' => $contextid, - 'name' => $name, + 'name' => $page->title, 'intro' => $page->comment ?? '', 'content' => $this->normalizeContent($this->getPageContent($page)), 'sectionid' => $sectionId, diff --git a/main/inc/lib/moodleexport/SectionExport.php b/main/inc/lib/moodleexport/SectionExport.php index 948b6c33f21..53546519858 100644 --- a/main/inc/lib/moodleexport/SectionExport.php +++ b/main/inc/lib/moodleexport/SectionExport.php @@ -15,15 +15,17 @@ class SectionExport { private $course; + private $activitiesBySection = []; /** * Constructor to initialize the course object. * * @param object $course The course object to be exported. */ - public function __construct($course) + public function __construct($course, array $activitiesBySection = []) { $this->course = $course; + $this->activitiesBySection = $activitiesBySection; } /** @@ -106,6 +108,12 @@ public function getGeneralItems(): array */ public function getActivitiesForGeneral(): array { + // Preferred path: reuse precomputed activities from MoodleExport + if (isset($this->activitiesBySection[0]) && is_array($this->activitiesBySection[0])) { + return $this->activitiesBySection[0]; + } + + // Fallback to legacy behavior (kept for safety) $generalLearnpath = (object) [ 'items' => $this->getGeneralItems(), 'source_id' => 0, @@ -113,23 +121,14 @@ public function getActivitiesForGeneral(): array $activities = $this->getActivitiesForSection($generalLearnpath, true); - // Ensure we always have one "Documents" folder activity at the top of the general section. - $hasFolder = false; - foreach ($activities as $activity) { - if ($activity['modulename'] === 'folder') { - $hasFolder = true; - break; - } - } - - if (!$hasFolder) { - array_unshift($activities, [ - 'id' => ActivityExport::DOCS_MODULE_ID, - 'moduleid' => ActivityExport::DOCS_MODULE_ID, - 'type' => 'folder', - 'modulename'=> 'folder', - 'name' => 'Documents', - ]); + if (!in_array('folder', array_column($activities, 'modulename'), true)) { + $activities[] = [ + 'id' => 0, + 'moduleid' => 0, + 'modulename' => 'folder', + 'name' => 'Documents', + 'sectionid' => 0, + ]; } return $activities; @@ -154,12 +153,14 @@ public function getLearnpathById(int $sectionId): ?object */ public function getSectionData(object $learnpath): array { + $sectionId = (int) $learnpath->source_id; + return [ - 'id' => $learnpath->source_id, + 'id' => $sectionId, 'number' => $learnpath->display_order, 'name' => $learnpath->name, 'summary' => $learnpath->description, - 'sequence' => $learnpath->source_id, + 'sequence' => $sectionId, 'visible' => $learnpath->visibility, 'timemodified' => strtotime($learnpath->modified_on), 'activities' => $this->getActivitiesForSection($learnpath), @@ -167,32 +168,20 @@ public function getSectionData(object $learnpath): array } /** - * Get the activities for a specific section (learnpath). - * - * Items are sorted using c_lp_item.display_order so that the order - * in Moodle matches the order defined in the Chamilo LP. + * Get the activities for a specific section. */ public function getActivitiesForSection(object $learnpath, bool $isGeneral = false): array { - $activities = []; $sectionId = $isGeneral ? 0 : (int) $learnpath->source_id; - $items = $learnpath->items ?? []; - - // Ensure items follow the same order as in Chamilo (c_lp_item.display_order). - usort($items, static function (array $a, array $b): int { - $aOrder = $a['display_order'] ?? 0; - $bOrder = $b['display_order'] ?? 0; - - if ($aOrder === $bOrder) { - // Fallback to id to keep a deterministic order. - return (int) ($a['id'] ?? 0) <=> (int) ($b['id'] ?? 0); - } - - return $aOrder <=> $bOrder; - }); + // Preferred path: reuse precomputed activities from MoodleExport + if (isset($this->activitiesBySection[$sectionId]) && is_array($this->activitiesBySection[$sectionId])) { + return $this->activitiesBySection[$sectionId]; + } - foreach ($items as $item) { + // Fallback to legacy behavior + $activities = []; + foreach ($learnpath->items as $item) { $this->addActivityToList($item, $sectionId, $activities); } @@ -249,10 +238,13 @@ private function isItemInLearnpath(object $item, string $type): bool /** * Add an activity to the activities list. + * Generic approach: for any activity in a real section (LP sectionId > 0), + * force a unique moduleid per LP item occurrence: + * moduleid = 900000000 + lp_item_id */ private function addActivityToList(array $item, int $sectionId, array &$activities): void { - /*static $documentsFolderAdded = false; + static $documentsFolderAdded = false; if (!$documentsFolderAdded && $sectionId === 0) { $activities[] = [ 'id' => 0, @@ -262,7 +254,7 @@ private function addActivityToList(array $item, int $sectionId, array &$activiti 'name' => 'Documents', ]; $documentsFolderAdded = true; - }*/ + } $activityData = null; $activityClassMap = [ @@ -276,14 +268,14 @@ private function addActivityToList(array $item, int $sectionId, array &$activiti 'feedback' => FeedbackExport::class, ]; - if ($item['id'] == 'course_homepage') { + if (($item['id'] ?? null) == 'course_homepage') { $item['item_type'] = 'page'; $item['path'] = 0; } - $itemType = $item['item_type'] === 'link' ? 'url' : - ($item['item_type'] === 'work' || $item['item_type'] === 'student_publication' ? 'assign' : - ($item['item_type'] === 'survey' ? 'feedback' : $item['item_type'])); + $itemType = ($item['item_type'] ?? '') === 'link' ? 'url' : + (($item['item_type'] ?? '') === 'work' || ($item['item_type'] ?? '') === 'student_publication' ? 'assign' : + (($item['item_type'] ?? '') === 'survey' ? 'feedback' : ($item['item_type'] ?? ''))); switch ($itemType) { case 'quiz': @@ -293,33 +285,43 @@ private function addActivityToList(array $item, int $sectionId, array &$activiti case 'forum': case 'feedback': case 'page': - $activityId = $itemType === 'glossary' ? 1 : (int) $item['path']; + $activityId = $itemType === 'glossary' ? 1 : (int) ($item['path'] ?? 0); $exportClass = $activityClassMap[$itemType]; $exportInstance = new $exportClass($this->course); $activityData = $exportInstance->getData($activityId, $sectionId); break; case 'document': - $documentId = (int) $item['path']; + $documentId = (int) ($item['path'] ?? 0); $document = \DocumentManager::get_document_data_by_id($documentId, $this->course->code); if ($document) { - $isRoot = substr_count($document['path'], '/') === 1; $documentType = $this->getDocumentType($document['filetype'], $document['path']); - if ($documentType === 'page' && $isRoot) { - $activityClass = $activityClassMap['page']; - $exportInstance = new $activityClass($this->course); - $activityData = $exportInstance->getData($item['path'], $sectionId); - } elseif ($sectionId > 0 && $documentType && isset($activityClassMap[$documentType])) { + + if ($sectionId > 0 && $documentType && isset($activityClassMap[$documentType])) { $activityClass = $activityClassMap[$documentType]; $exportInstance = new $activityClass($this->course); - $activityData = $exportInstance->getData($item['path'], $sectionId); + $activityData = $exportInstance->getData($documentId, $sectionId); + } elseif ($sectionId === 0 && $documentType === 'page') { + // Keep your original behavior for general section if you want + $activityClass = $activityClassMap['page']; + $exportInstance = new $activityClass($this->course); + $activityData = $exportInstance->getData($documentId, $sectionId); } } break; } - // Add the activity to the list if the data exists + // Generic override: unique moduleid per LP occurrence (sectionId > 0) + if (!empty($activityData) && $sectionId > 0) { + $lpItemId = isset($item['id']) ? (int)$item['id'] : 0; + $modName = (string)($activityData['modulename'] ?? ''); + + if ($lpItemId > 0 && !in_array($modName, ['folder', 'glossary'], true)) { + $activityData['moduleid'] = 900000000 + $lpItemId; + } + } + if ($activityData) { $activities[] = [ 'id' => $activityData['id'], @@ -376,7 +378,8 @@ private function createInforefXml(array $sectionData, string $destinationDir): v $xmlContent .= ''.PHP_EOL; foreach ($sectionData['activities'] as $activity) { - $xmlContent .= ' '.htmlspecialchars($activity['name']).''.PHP_EOL; + $refId = $activity['moduleid'] ?? $activity['id']; + $xmlContent .= ' '.htmlspecialchars($activity['name']).''.PHP_EOL; } $xmlContent .= ''.PHP_EOL; From 3b7f46da828e6683762e78ebe3d476cc401a8cb2 Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Tue, 6 Jan 2026 13:35:55 -0500 Subject: [PATCH 5/5] Restore composer.json --- composer.json | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100755 composer.json diff --git a/composer.json b/composer.json new file mode 100755 index 00000000000..486f75734f6 --- /dev/null +++ b/composer.json @@ -0,0 +1,202 @@ +{ + "name": "chamilo/chamilo-lms", + "description": "E-learning and collaboration software", + "type": "project", + "homepage": "http://www.chamilo.org", + "license": "GPL-3.0", + "support": { + "docs": "https://docs.chamilo.org/", + "forum": "https://forum.chamilo.org/", + "issues": "https://github.com/chamilo/chamilo-lms/issues", + "source": "https://github.com/chamilo/chamilo-lms" + }, + "autoload": { + "psr-4": { + "Application\\": "app/", + "Chamilo\\": "src/Chamilo/" + }, + "classmap": [ + "main/admin", + "main/auth", + "main/course_description", + "main/cron/lang", + "main/dropbox", + "main/exercise", + "main/gradebook/lib", + "main/inc/lib", + "main/inc/lib/hook", + "main/install", + "main/lp", + "main/survey", + "main/common_cartridge/export", + "main/common_cartridge/import", + "plugin" + ] + }, + "require": { + "php": "^7.4", + "ext-curl": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-intl": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-zip": "*", + "ext-zlib": "*", + "angelfqc/vimeo-api": "2.0.6", + "apereo/phpcas": "^1.6", + "chamilo/pclzip": "~2.8", + "clue/graph": "~0.9.0", + "culqi/culqi-php": "1.3.4", + "ddeboer/data-import": "@stable", + "doctrine/data-fixtures": "~1.0@dev", + "doctrine/dbal": "~2.5", + "doctrine/migrations": "~1.0@dev", + "doctrine/orm": "~2.5", + "emojione/emojione": "1.3.0", + "endroid/qr-code": "2.5.*", + "enshrined/svg-sanitize": "^0.16.0", + "essence/essence": "2.6.1", + "ezyang/htmlpurifier": "~4.9", + "facebook/graph-sdk": "^5.7", + "firebase/php-jwt": "~5.0", + "gedmo/doctrine-extensions": "~2.3", + "graphp/algorithms": "~0.8.0", + "graphp/graphviz": "~0.2.0", + "guzzlehttp/guzzle": "~6.0", + "h5p/h5p-core": "*", + "imagine/imagine": "0.6.3", + "ircmaxell/password-compat": "~1.0.4", + "jbroadway/urlify": "1.1.0-stable", + "jeroendesloovere/vcard": "~1.7", + "jimmiw/php-time-ago": "0.4.15", + "kigkonsult/icalcreator": "2.24", + "knplabs/doctrine-behaviors": "~1.1", + "knplabs/gaufrette": "~0.3", + "knplabs/knp-components": "~1.3", + "league/csv": "~8.0", + "media-alchemyst/media-alchemyst": "~0.5", + "michelf/php-markdown": "~1.7", + "monolog/monolog": "~1.0", + "mpdf/mpdf": "^8.0", + "ocramius/proxy-manager": "~1.0|2.0.*", + "onelogin/php-saml": "^3.0", + "paragonie/random-lib": "2.0.0", + "patchwork/utf8": "~1.2", + "php-ffmpeg/php-ffmpeg": "0.5.1", + "php-http/guzzle6-adapter": "^2.0", + "php-xapi/client": "0.7.x-dev", + "php-xapi/repository-api": "dev-master as 0.3.1", + "php-xapi/repository-doctrine": "dev-master", + "php-xapi/symfony-serializer": "2.1.0 as 2.0", + "phpmailer/phpmailer": "~6.1", + "phpoffice/phpexcel": "~1.8", + "phpoffice/phpword": "~0.14", + "phpseclib/phpseclib": "^2.0", + "robrichards/xmlseclibs": "3.0.*", + "sabre/vobject": "~3.1", + "sonata-project/admin-bundle": "~3.1|~4.0", + "sonata-project/core-bundle": "~3.1|~4.0", + "sonata-project/user-bundle": "~3.0|~4.0", + "stripe/stripe-php": "*", + "studio-42/elfinder": "2.1.*", + "sunra/php-simple-html-dom-parser": "~1.5.0", + "sylius/attribute": "0.13.0", + "sylius/translation": "0.13.0", + "symfony/console": "~3.0|~4.0", + "symfony/doctrine-bridge": "~2.8", + "symfony/dom-crawler": "~3.4|~4.0", + "symfony/filesystem": "~3.0|~4.0", + "symfony/http-foundation": "~2.8|~3.0", + "symfony/security": "~3.0|~4.0", + "symfony/serializer": "~3.0|~4.0", + "symfony/validator": "~3.0|~4.0", + "symfony/yaml": "~3.0|~4.0", + "szymach/c-pchart": "~3.0", + "thenetworg/oauth2-azure": "^1.4", + "twig/extensions": "~1.0", + "twig/twig": "1.*", + "webit/eval-math": "1.0.1", + "yuloh/bccomp-polyfill": "dev-master", + "packbackbooks/lti-1p3-tool": "1.1.1.x-dev", + "zendframework/zend-config": "~3.0", + "zendframework/zend-feed": "~2.6|^3.0", + "zendframework/zend-http": "~2.6|^3.0", + "zendframework/zend-soap": "~2.6|^3.0" + }, + "require-dev": { + "behat/behat": "~3.5", + "behat/mink": "1.7.1", + "behat/mink-extension": "*", + "behat/mink-goutte-driver": "*", + "behat/mink-selenium2-driver": "*", + "phpunit/phpunit": "*" + }, + "scripts": { + "pre-install-cmd": [ + "Chamilo\\CoreBundle\\Composer\\ScriptHandler::deleteOldFilesFrom19x" + ], + "pre-update-cmd": [ + "Chamilo\\CoreBundle\\Composer\\ScriptHandler::deleteOldFilesFrom19x" + ], + "post-install-cmd": [ + "Chamilo\\CoreBundle\\Composer\\ScriptHandler::dumpCssFiles", + "Chamilo\\CoreBundle\\Composer\\ScriptHandler::generateDoctrineProxies" + ], + "post-update-cmd": [ + "Chamilo\\CoreBundle\\Composer\\ScriptHandler::dumpCssFiles", + "Chamilo\\CoreBundle\\Composer\\ScriptHandler::generateDoctrineProxies" + ], + "update-css": "Chamilo\\CoreBundle\\Composer\\ScriptHandler::updateCss" + }, + "extra": { + "asset-installer-paths": { + "bower-asset-library": "web/assets/" + }, + "branch-alias": { + "dev-master": "1.11.x-dev" + }, + "incenteev-parameters": { + "file": "app/config/parameters.yml" + }, + "symfony-app-dir": "app", + "symfony-assets-install": "relative", + "symfony-bin-dir": "bin", + "symfony-tests-dir": "tests", + "symfony-web-dir": "web" + }, + "repositories": [ + { + "type": "github", + "url": "https://github.com/AngelFQC/vimeo.php.git", + "no-api": true + }, + { + "type": "github", + "url": "https://github.com/AngelFQC/xapi-model.git", + "no-api": true + }, + { + "type": "github", + "url": "https://github.com/AngelFQC/xapi-repository-doctrine.git", + "no-api": true + }, + { + "type": "github", + "url": "https://github.com/AngelFQC/xapi-symfony-serializer.git", + "no-api": true + }, + { + "type": "github", + "url": "https://github.com/chamilo/lti-1-3-php-library.git", + "no-api": true + } + ], + "config": { + "sort-packages": true, + "component-dir": "web/assets" + } +}