Skip to content

Internal: Improvements and structure adjustments for moodle import - refs BT#21977 #6146

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions main/coursecopy/export_moodle.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
// Rebuild the course object based on selected resources
$cb = new CourseBuilder('partial');
$course = $cb->build(0, null, false, array_keys($selectedResources), $selectedResources);
$course = CourseSelectForm::get_posted_course(null, 0, '', $course);

// Get admin details
$adminId = (int) $_POST['admin_id'];
Expand Down
2 changes: 1 addition & 1 deletion main/inc/lib/moodleexport/CourseExport.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class CourseExport
private $courseInfo;
private $activities;

public function __construct($course, $activities)
public function __construct($course, $activities = [])
{
$this->course = $course;
$this->courseInfo = api_get_course_info($course->code);
Expand Down
24 changes: 17 additions & 7 deletions main/inc/lib/moodleexport/FileExport.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public function getFilesData(): array
'component' => 'mod_assign',
'filearea' => 'introattachment',
'itemid' => (int) $work->params['id'],
'filepath' => '/',
'filepath' => '/Documents/',
'documentpath' => 'document/'.$docData['path'],
'filename' => basename($docData['path']),
'userid' => $adminId,
Expand Down Expand Up @@ -187,11 +187,15 @@ private function createFileXmlEntry(array $file): string
private function processDocument(array $filesData, object $document): array
{
if ($document->file_type === 'file') {
$filesData['files'][] = $this->getFileData($document);
$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);
$filesData['files'][] = $this->getFolderFileData($file, (int) $document->source_id, '/Documents/'.dirname($file['path']).'/');
}
}

Expand Down Expand Up @@ -233,14 +237,14 @@ private function getFileData(object $document): array
/**
* Get file data for files inside a folder.
*/
private function getFolderFileData(array $file, int $sourceId): array
private function getFolderFileData(array $file, int $sourceId, string $parentPath = '/Documents/'): 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(dirname($file['path']));
$filepath = $this->ensureTrailingSlash($parentPath);

return [
'id' => $file['id'],
Expand All @@ -267,9 +271,15 @@ private function getFolderFileData(array $file, int $sourceId): array
/**
* Ensure the directory path has a trailing slash.
*/
private function ensureTrailingSlash($path): string
private function ensureTrailingSlash(string $path): string
{
return empty($path) || $path === '.' || $path === '/' ? '/' : rtrim($path, '/').'/';
if (empty($path) || $path === '.' || $path === '/') {
return '/';
}

$path = preg_replace('/\/+/', '/', $path);

return rtrim($path, '/') . '/';
}

/**
Expand Down
62 changes: 34 additions & 28 deletions main/inc/lib/moodleexport/FolderExport.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,26 +44,32 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void
*/
public function getData(int $folderId, int $sectionId): ?array
{
$folder = $this->course->resources['document'][$folderId];

$folderPath = $folder->path.'/';
foreach ($this->course->resources['document'] as $resource) {
if ($resource->path !== $folder->path && str_starts_with($resource->path, $folderPath)) {
return [
'id' => $folderId,
'moduleid' => $folder->source_id,
'modulename' => 'folder',
'contextid' => $folder->source_id,
'name' => $folder->title,
'sectionid' => $sectionId,
'timemodified' => time(),
];
}
if ($folderId === 0) {
return [
'id' => 0,
'moduleid' => 0,
'modulename' => 'folder',
'contextid' => 0,
'name' => 'Documents',
'sectionid' => $sectionId,
'timemodified' => time(),
];
}

return null;
$folder = $this->course->resources['document'][$folderId];

return [
'id' => $folderId,
'moduleid' => $folder->source_id,
'modulename' => 'folder',
'contextid' => $folder->source_id,
'name' => $folder->title,
'sectionid' => $sectionId,
'timemodified' => time(),
];
}


/**
* Create the XML file for the folder.
*/
Expand Down Expand Up @@ -92,19 +98,19 @@ private function createFolderXml(array $folderData, string $folderDir): void
*/
private function getFilesForFolder(int $folderId): array
{
$documentData = \DocumentManager::getAllDocumentsByParentId($this->course->info, $folderId);

$files = [];
foreach ($documentData as $doc) {
if ($doc['filetype'] === 'file') {
$files[] = [
'id' => (int) $doc['id'],
'contenthash' => 'hash'.$doc['id'],
'filename' => $doc['basename'],
'filepath' => $doc['path'],
'filesize' => (int) $doc['size'],
'mimetype' => $this->getMimeType($doc['basename']),
];
if ($folderId === 0) {
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),
];
}
}
}

Expand Down
124 changes: 98 additions & 26 deletions main/inc/lib/moodleexport/MoodleExport.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace moodleexport;

use Chamilo\CourseBundle\Component\CourseCopy\CourseBuilder;
use Exception;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
Expand All @@ -25,7 +26,18 @@ class MoodleExport
*/
public function __construct(object $course)
{
// Build the complete course object
$cb = new CourseBuilder('complete');
$complete = $cb->build();

// Store the selected course
$this->course = $course;

// Fill missing resources from learnpath
$this->fillResourcesFromLearnpath($complete);

// Fill missing quiz questions
$this->fillQuestionsFromQuiz($complete);
}

/**
Expand Down Expand Up @@ -177,6 +189,67 @@ public static function getAdminUserData(): array
return self::$adminUserData;
}

/**
* Fills missing resources from the learnpath into the course structure.
*
* This method checks if the course has a learnpath and ensures that all
* referenced resources (documents, quizzes, etc.) exist in the course's
* resources array by pulling them from the complete course object.
*/
private function fillResourcesFromLearnpath(object $complete): void
{
// Check if the course has learnpath
if (!isset($this->course->resources['learnpath'])) {
return;
}

foreach ($this->course->resources['learnpath'] as $learnpathId => $learnpath) {
if (!isset($learnpath->items)) {
continue;
}

foreach ($learnpath->items as $item) {
$type = $item['item_type']; // Resource type (document, quiz, etc.)
$resourceId = $item['path']; // Resource ID in resources

// Check if the resource exists in the complete object and is not yet in the course resources
if (isset($complete->resources[$type][$resourceId]) && !isset($this->course->resources[$type][$resourceId])) {
// Add the resource directly to the original course resources structure
$this->course->resources[$type][$resourceId] = $complete->resources[$type][$resourceId];
}
}
}
}

/**
* Fills missing exercise questions related to quizzes in the course.
*
* This method checks if the course has quizzes and ensures that all referenced
* questions exist in the course's resources array by pulling them from the complete
* course object.
*/
private function fillQuestionsFromQuiz(object $complete): void
{
// Check if the course has quizzes
if (!isset($this->course->resources['quiz'])) {
return;
}

foreach ($this->course->resources['quiz'] as $quizId => $quiz) {
if (!isset($quiz->obj->question_ids)) {
continue;
}

foreach ($quiz->obj->question_ids as $questionId) {
// Check if the question exists in the complete object and is not yet in the course resources
if (isset($complete->resources['Exercise_Question'][$questionId]) && !isset($this->course->resources['Exercise_Question'][$questionId])) {
// Add the question directly to the original course resources structure
$this->course->resources['Exercise_Question'][$questionId] = $complete->resources['Exercise_Question'][$questionId];
}
}
}
}

/**
* Export root XML files such as badges, completion, gradebook, etc.
*/
Expand Down Expand Up @@ -359,6 +432,14 @@ private function getActivities(): array
$activities = [];
$glossaryAdded = false;

$documentsFolder = [
'id' => 0,
'sectionid' => 0,
'modulename' => 'folder',
'moduleid' => 0,
'title' => 'Documents',
];
$activities[] = $documentsFolder;
foreach ($this->course->resources as $resourceType => $resources) {
foreach ($resources as $resource) {
$exportClass = null;
Expand Down Expand Up @@ -403,36 +484,27 @@ private function getActivities(): array
$moduleName = 'page';
$id = $resource->source_id;
$title = $document['title'];
} elseif ('file' === $resource->file_type) {
$isRoot = substr_count($resource->path, '/') === 1;

if ($isRoot) {
$exportClass = ResourceExport::class;
$moduleName = 'resource';
$id = $resource->source_id;
$title = $resource->title;
}
} elseif ('folder' === $resource->file_type) {
$isEmpty = true;
$folderPath = $resource->path.'/';

foreach ($this->course->resources['document'] as $childResource) {
if (str_starts_with($childResource->path, $folderPath) && $childResource->path !== $resource->path) {
$isEmpty = false;
break;
}
if ('file' === $resource->file_type) {
$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;
}
}

$isRoot = substr_count($resource->path, '/') === 1;

if (!$isEmpty && $isRoot) {
$exportClass = FolderExport::class;
$moduleName = 'folder';
$id = $resource->source_id;
$title = $resource->title;
}
}
}
// Handle course introduction (page)
elseif ($resourceType === RESOURCE_TOOL_INTRO && $resource->source_id == 'course_homepage') {
$exportClass = PageExport::class;
$moduleName = 'page';
$id = 0;
$title = get_lang('Introduction');
}
// Handle assignments (work)
elseif ($resourceType === RESOURCE_WORK && $resource->source_id > 0) {
$exportClass = AssignExport::class;
Expand Down
29 changes: 26 additions & 3 deletions main/inc/lib/moodleexport/PageExport.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,35 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void
*/
public function getData(int $pageId, int $sectionId): ?array
{
$pageResources = $this->course->resources[RESOURCE_DOCUMENT];
$contextid = $this->course->info['real_id'];
if ($pageId === 0) {
if (
isset($this->course->resources[RESOURCE_TOOL_INTRO]['course_homepage']) &&
is_object($this->course->resources[RESOURCE_TOOL_INTRO]['course_homepage']) &&
!empty($this->course->resources[RESOURCE_TOOL_INTRO]['course_homepage']->intro_text)
) {

return [
'id' => 0,
'moduleid' => 0,
'modulename' => 'page',
'contextid' => $contextid,
'name' => get_lang('Introduction'),
'intro' => '',
'content' => trim($this->course->resources[RESOURCE_TOOL_INTRO]['course_homepage']->intro_text),
'sectionid' => $sectionId,
'sectionnumber' => 1,
'display' => 0,
'timemodified' => time(),
'users' => [],
'files' => [],
];
}
}

$pageResources = $this->course->resources[RESOURCE_DOCUMENT] ?? [];
foreach ($pageResources as $page) {
if ($page->source_id == $pageId) {
$contextid = $this->course->info['real_id'];

return [
'id' => $page->source_id,
'moduleid' => $page->source_id,
Expand Down
Loading
Loading