361 lines
14 KiB
PHP
361 lines
14 KiB
PHP
<?php
|
|
/* For licensing terms, see /license.txt */
|
|
|
|
class Cc13Convert
|
|
{
|
|
/**
|
|
* Converts a course object into a CC13 package and writes it to disk.
|
|
*
|
|
* @param \Chamilo\CourseBundle\Component\CourseCopy\Course $objCourse The Course object is "augmented" with info from the api_get_course_info() function, into the "$objCourse->info" array attribute.
|
|
*
|
|
* @throws Exception
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function convert(string $packagedir, string $outdir, Chamilo\CourseBundle\Component\CourseCopy\Course $objCourse)
|
|
{
|
|
$dir = realpath($packagedir);
|
|
if (empty($dir)) {
|
|
throw new InvalidArgumentException('Directory does not exist!');
|
|
}
|
|
$odir = realpath($outdir);
|
|
if (empty($odir)) {
|
|
throw new InvalidArgumentException('Directory does not exist!');
|
|
}
|
|
|
|
if (!empty($objCourse)) {
|
|
//Initialize the manifest metadata class
|
|
$meta = new CcMetadataManifest();
|
|
|
|
//Package metadata
|
|
$metageneral = new CcMetadataGeneral();
|
|
$metageneral->setLanguage($objCourse->info['language']);
|
|
$metageneral->setTitle($objCourse->info['title'], $objCourse->info['language']);
|
|
$metageneral->setDescription('', $objCourse->info['language']);
|
|
$metageneral->setCatalog('category');
|
|
$metageneral->setEntry($objCourse->info['categoryName']);
|
|
$meta->addMetadataGeneral($metageneral);
|
|
|
|
// Create the manifest
|
|
$manifest = new CcManifest();
|
|
|
|
$manifest->addMetadataManifest($meta);
|
|
|
|
$organization = null;
|
|
|
|
//Get the course structure - this will be transformed into organization
|
|
//Step 1 - Get the list and order of sections/topics
|
|
|
|
$count = 1;
|
|
$sections = [];
|
|
$resources = $objCourse->resources;
|
|
|
|
// We check the quiz sections
|
|
if (isset($resources['quiz'])) {
|
|
$quizSections = self::getItemSections(
|
|
$resources['quiz'], // array of quiz IDs generated by the course backup process
|
|
'quiz',
|
|
$count,
|
|
$objCourse->info['code'],
|
|
$resources[RESOURCE_QUIZQUESTION] // selected question IDs from the course export form
|
|
);
|
|
$sections = array_merge($sections, $quizSections);
|
|
}
|
|
|
|
// We check the document sections
|
|
if (isset($resources['document'])) {
|
|
$documentSections = self::getItemSections(
|
|
$resources['document'],
|
|
'document',
|
|
$count,
|
|
$objCourse->info['code']
|
|
);
|
|
$sections = array_merge($sections, $documentSections);
|
|
}
|
|
|
|
// We check the wiki sections
|
|
if (isset($resources['wiki'])) {
|
|
$wikiSections = self::getItemSections(
|
|
$resources['wiki'],
|
|
'wiki',
|
|
$count,
|
|
$objCourse->info['code']
|
|
);
|
|
$sections = array_merge($sections, $wikiSections);
|
|
}
|
|
|
|
// We check the forum sections
|
|
if (isset($resources['forum'])) {
|
|
$forumSections = self::getItemSections(
|
|
$resources['forum'],
|
|
'forum',
|
|
$count,
|
|
$objCourse->info['code'],
|
|
$resources['Forum_Category']
|
|
);
|
|
$sections = array_merge($sections, $forumSections);
|
|
}
|
|
|
|
// We check the link sections
|
|
if (isset($resources['link'])) {
|
|
$linkSections = self::getItemSections(
|
|
$resources['link'],
|
|
'link',
|
|
$count,
|
|
$objCourse->info['code'],
|
|
$resources['Link_Category']
|
|
);
|
|
$sections = array_merge($sections, $linkSections);
|
|
}
|
|
|
|
//organization title
|
|
$organization = new CcOrganization();
|
|
foreach ($sections as $sectionid => $values) {
|
|
$item = new CcItem();
|
|
$item->title = $values[0];
|
|
self::processSequence($item, $manifest, $values[1], $dir, $odir);
|
|
$organization->addItem($item);
|
|
}
|
|
$manifest->putNodes();
|
|
|
|
if (!empty($organization)) {
|
|
$manifest->addNewOrganization($organization);
|
|
}
|
|
|
|
$manifestpath = $outdir.DIRECTORY_SEPARATOR.'imsmanifest.xml';
|
|
$saved = $manifest->saveTo($manifestpath);
|
|
|
|
return $saved;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return array of type $sections[1]['quiz', [sequence]] where sequence is the result of a call to getSequence().
|
|
*
|
|
* @param string $courseCode The course code litteral
|
|
* @param ?array $itmesExtraData If defined, represents the items IDs selected in the course export form
|
|
*
|
|
* @return array
|
|
* @reference self::getSequence()
|
|
*/
|
|
protected static function getItemSections(array $itemData, string $itemType, int &$count, string $courseCode, ?array $itmesExtraData = null)
|
|
{
|
|
$sections = [];
|
|
switch ($itemType) {
|
|
case 'quiz':
|
|
case 'document':
|
|
case 'wiki':
|
|
$convertType = $itemType;
|
|
if ($itemType == 'wiki') {
|
|
$convertType = 'Page';
|
|
}
|
|
$sectionid = $count;
|
|
$sectiontitle = ucfirst($itemType);
|
|
$sequence = self::getSequence($itemData, 0, $convertType, $courseCode, $itmesExtraData);
|
|
$sections[$sectionid] = [$sectiontitle, $sequence];
|
|
$count++;
|
|
break;
|
|
case 'link':
|
|
$links = self::getSequence($itemData, null, $itemType);
|
|
foreach ($links as $categoryId => $sequence) {
|
|
$sectionid = $count;
|
|
if (isset($itmesExtraData[$categoryId])) {
|
|
$sectiontitle = $itmesExtraData[$categoryId]->title;
|
|
} else {
|
|
$sectiontitle = 'General';
|
|
}
|
|
$sections[$sectionid] = [$sectiontitle, $sequence];
|
|
$count++;
|
|
}
|
|
break;
|
|
case 'forum':
|
|
if (isset($itmesExtraData)) {
|
|
foreach ($itmesExtraData as $fcategory) {
|
|
if (isset($fcategory->obj)) {
|
|
$objCategory = $fcategory->obj;
|
|
$sectionid = $count;
|
|
$sectiontitle = $objCategory->cat_title;
|
|
$sequence = self::getSequence($itemData, $objCategory->iid, $itemType);
|
|
$sections[$sectionid] = [$sectiontitle, $sequence];
|
|
$count++;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
return $sections;
|
|
}
|
|
|
|
/**
|
|
* Return array of items by category and item ID ("test" level, not "question" level) with details like title,
|
|
* comment (description), type, questions, max_attempt, etc.
|
|
*
|
|
* @param array $objItems Array of item objects as returned by the course backup code
|
|
* @param ?int $categoryId
|
|
* @param ?string $itemType
|
|
* @param ?string $coursecode
|
|
* @param ?array $itemQuestions
|
|
*
|
|
* @return array|mixed
|
|
*/
|
|
protected static function getSequence(array $objItems, ?int $categoryId = null, ?string $itemType = null, ?string $coursecode = null, ?array $itemQuestions = null)
|
|
{
|
|
$sequences = [];
|
|
switch ($itemType) {
|
|
case 'quiz':
|
|
$sequence = [];
|
|
foreach ($objItems as $objItem) {
|
|
if ($categoryId === 0) {
|
|
$questions = [];
|
|
foreach ($objItem->obj->question_ids as $questionId) {
|
|
if (isset($itemQuestions[$questionId])) {
|
|
$questions[$questionId] = $itemQuestions[$questionId];
|
|
}
|
|
}
|
|
// As weird as this may sound, the constructors for the different object types (found in
|
|
// src/CourseBundle/Component/CourseCopy/Resources/[objecttype].php) store the object property
|
|
// in the "obj" attribute. Old code...
|
|
// So here we recover properties from the quiz object, including questions (array built above).
|
|
$sequence[$categoryId][$objItem->obj->iid] = [
|
|
'title' => $objItem->obj->title,
|
|
'comment' => $objItem->obj->description,
|
|
'cc_type' => 'quiz',
|
|
'source_id' => $objItem->obj->iid,
|
|
'questions' => $questions,
|
|
'max_attempt' => $objItem->obj->max_attempt,
|
|
'expired_time' => $objItem->obj->expired_time,
|
|
'pass_percentage' => $objItem->obj->pass_percentage,
|
|
'random_answers' => $objItem->obj->random_answers,
|
|
'course_code' => $coursecode,
|
|
];
|
|
}
|
|
}
|
|
$sequences = $sequence[$categoryId];
|
|
break;
|
|
case 'document':
|
|
$sequence = [];
|
|
foreach ($objItems as $objItem) {
|
|
if ($categoryId === 0) {
|
|
$sequence[$categoryId][$objItem->source_id] = [
|
|
'title' => $objItem->title,
|
|
'comment' => $objItem->comment,
|
|
'cc_type' => ($objItem->file_type == 'folder' ? 'folder' : 'resource'),
|
|
'source_id' => $objItem->source_id,
|
|
'path' => $objItem->path,
|
|
'file_type' => $objItem->file_type,
|
|
'course_code' => $coursecode,
|
|
];
|
|
}
|
|
}
|
|
$sequences = $sequence[$categoryId];
|
|
break;
|
|
case 'forum':
|
|
foreach ($objItems as $objItem) {
|
|
if ($categoryId == $objItem->obj->forum_category) {
|
|
$sequence[$categoryId][$objItem->obj->forum_id] = [
|
|
'title' => $objItem->obj->forum_title,
|
|
'comment' => $objItem->obj->forum_comment,
|
|
'cc_type' => 'forum',
|
|
'source_id' => $objItem->obj->iid,
|
|
];
|
|
}
|
|
}
|
|
$sequences = $sequence[$categoryId];
|
|
break;
|
|
case 'page':
|
|
foreach ($objItems as $objItem) {
|
|
if ($categoryId === 0) {
|
|
$sequence[$categoryId][$objItem->page_id] = [
|
|
'title' => $objItem->title,
|
|
'comment' => $objItem->content,
|
|
'cc_type' => 'page',
|
|
'source_id' => $objItem->page_id,
|
|
'reflink' => $objItem->reflink,
|
|
];
|
|
}
|
|
}
|
|
$sequences = $sequence[$categoryId];
|
|
break;
|
|
case 'link':
|
|
if (!isset($categoryId)) {
|
|
$categories = [];
|
|
foreach ($objItems as $objItem) {
|
|
$categories[$objItem->category_id] = self::getSequence($objItems, $objItem->category_id, $itemType);
|
|
}
|
|
$sequences = $categories;
|
|
} else {
|
|
foreach ($objItems as $objItem) {
|
|
if ($categoryId == $objItem->category_id) {
|
|
$sequence[$categoryId][$objItem->source_id] = [
|
|
'title' => $objItem->title,
|
|
'comment' => $objItem->description,
|
|
'cc_type' => 'url',
|
|
'source_id' => $objItem->source_id,
|
|
'url' => $objItem->url,
|
|
'target' => $objItem->target,
|
|
];
|
|
}
|
|
}
|
|
$sequences = $sequence[$categoryId];
|
|
}
|
|
break;
|
|
}
|
|
|
|
return $sequences;
|
|
}
|
|
|
|
/**
|
|
* Process the different types of activities exported from the course.
|
|
* When dealing with tests, the "sequence" provided here is a test, not a question.
|
|
* For each sequence, call the corresponding CcConverter[Type] object instantiation method in export/src/converter/.
|
|
*
|
|
* @return void
|
|
*/
|
|
protected static function processSequence(CcIItem &$item, CcIManifest &$manifest, array $sequence, string $packageroot, string $outdir)
|
|
{
|
|
if (!empty($sequence)) {
|
|
foreach ($sequence as $seq) {
|
|
$activity_type = ucfirst($seq['cc_type']);
|
|
$activity_indentation = 0;
|
|
$aitem = self::itemIndenter($item, $activity_indentation);
|
|
$caller = "CcConverter{$activity_type}";
|
|
if (class_exists($caller)) {
|
|
$obj = new $caller($aitem, $manifest, $packageroot, $outdir);
|
|
if (!$obj->convert($outdir, $seq)) {
|
|
throw new RuntimeException("failed to convert {$activity_type}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
protected static function itemIndenter(CcIItem &$item, $level = 0)
|
|
{
|
|
$indent = (int) $level;
|
|
$indent = ($indent) <= 0 ? 0 : $indent;
|
|
$nprev = null;
|
|
$nfirst = null;
|
|
for ($pos = 0, $size = $indent; $pos < $size; $pos++) {
|
|
$nitem = new CcItem();
|
|
$nitem->title = '';
|
|
if (empty($nfirst)) {
|
|
$nfirst = $nitem;
|
|
}
|
|
if (!empty($nprev)) {
|
|
$nprev->addChildItem($nitem);
|
|
}
|
|
$nprev = $nitem;
|
|
}
|
|
$result = $item;
|
|
if (!empty($nfirst)) {
|
|
$item->addChildItem($nfirst);
|
|
$result = $nprev;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
}
|