XAPI: Allow import cmi5 package - refs BT#16742

pull/3690/head
Angel Fernando Quiroz Campos 6 years ago
parent 40a4172f03
commit 586a9d609f
  1. 120
      plugin/xapi/cmi5/add.php
  2. 446
      plugin/xapi/src/Entity/Cmi5Item.php
  3. 24
      plugin/xapi/src/Importer/AbstractImporter.php
  4. 45
      plugin/xapi/src/Importer/Cmi5Importer.php
  5. 57
      plugin/xapi/src/Parser/AbstractParser.php
  6. 203
      plugin/xapi/src/Parser/Cmi5Parser.php
  7. 25
      plugin/xapi/src/Parser/TinCanParser.php
  8. 4
      plugin/xapi/src/XApiPlugin.php
  9. 11
      plugin/xapi/tincan/index.php

@ -0,0 +1,120 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\XApi\Importer\Cmi5Importer;
use Chamilo\PluginBundle\XApi\Parser\Cmi5Parser;
require_once __DIR__.'/../../../main/inc/global.inc.php';
api_protect_course_script(true);
api_protect_teacher_script();
$course = api_get_course_entity();
$session = api_get_session_entity();
$plugin = XApiPlugin::create();
$frmActivity = new FormValidator('frm_cmi5', 'post', api_get_self().'?'.api_get_cidreq());
$frmActivity->addFile('file', $plugin->get_lang('Cmi5Package'));
$frmActivity->addButtonAdvancedSettings('lrs_params', $plugin->get_lang('LrsConfiguration'));
$frmActivity->addHtml('<div id="lrs_params_options" style="display:none">');
$frmActivity->addText(
'lrs_url',
[
$plugin->get_lang('lrs_url'),
$plugin->get_lang('lrs_url_help'),
],
false
);
$frmActivity->addText(
'lrs_auth',
[
$plugin->get_lang('lrs_auth_username'),
$plugin->get_lang('lrs_auth_username_help'),
],
false
);
$frmActivity->addText(
'lrs_auth',
[
$plugin->get_lang('lrs_auth_password'),
$plugin->get_lang('lrs_auth_password_help'),
],
false
);
$frmActivity->addHtml('</div>');
$frmActivity->addButtonImport(get_lang('Import'));
$frmActivity->addRule('file', get_lang('ThisFileIsRequired'), 'required');
$frmActivity->addRule(
'file',
$plugin->get_lang('OnlyZipAllowed'),
'filetype',
['zip']
);
$frmActivity->applyFilter('title', 'trim');
$frmActivity->applyFilter('description', 'trim');
$frmActivity->applyFilter('lrs_url', 'trim');
$frmActivity->applyFilter('lrs_auth', 'trim');
if ($frmActivity->validate()) {
$values = $frmActivity->exportValues();
$zipFileInfo = $_FILES['file'];
try {
$packageFile = Cmi5Importer::create($zipFileInfo, $course)->import();
$parser = Cmi5Parser::create($packageFile, $course, $session);
$toolLaunch = $parser->parse();
} catch (Exception $e) {
Display::addFlash(
Display::return_message($e->getMessage(), 'error')
);
exit;
}
if (!empty($values['lrs_url'])
&& !empty($values['lrs_auth_username'])
&& !empty($values['lrs_auth_password'])
) {
$toolLaunch
->setLrsUrl($values['lrs_url'])
->setLrsAuthUsername($values['lrs_auth_username'])
->setLrsAuthUsername($values['lrs_auth_password']);
}
$em = Database::getManager();
$em->persist($toolLaunch);
foreach ($parser->getToc() as $cmi5Item) {
$cmi5Item->setTool($toolLaunch);
$em->persist($cmi5Item);
}
$em->flush();
Display::addFlash(
Display::return_message($plugin->get_lang('ActivityImported'), 'success')
);
header('Location: '.api_get_course_url());
exit;
}
$frmActivity->setDefaults(['allow_multiple_attempts' => true]);
$pageTitle = $plugin->get_title();
$pageContent = $frmActivity->returnForm();
$interbreadcrumb[] = ['url' => '../tincan/index.php', 'name' => $plugin->get_lang('ToolTinCan')];
$langAddActivity = $plugin->get_lang('AddActivity');
$view = new Template($langAddActivity);
$view->assign('header', $langAddActivity);
$view->assign('content', $pageContent);
$view->display_one_col_template();

@ -0,0 +1,446 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\XApi;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* Class Cmi5Item.
*
* @ORM\Table(name="xapi_cmi5_item")
* @ORM\Entity()
*/
class Cmi5Item
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id()
* @ORM\GeneratedValue()
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="identifier", type="string")
*/
private $identifier;
/**
* @var string
*
* @ORM\Column(name="type", type="string")
*/
private $type;
/**
* @var array;
*
* @ORM\Column(name="title", type="json")
*/
private $title;
/**
* @var array
*
* @ORM\Column(name="description", type="json")
*/
private $description;
/**
* @var string|null
*
* @ORM\Column(name="url", type="string", nullable=true)
*/
private $url;
/**
* @var string|null
*
* @ORM\Column(name="activity_type", type="string", nullable=true)
*/
private $activityType;
/**
* @var string|null
*
* @ORM\Column(name="launch_method", type="string", nullable=true)
*/
private $launchMethod;
/**
* @var string|null
*
* @ORM\Column(name="move_on", type="string", nullable=true)
*/
private $moveOn;
/**
* @var float|null
*
* @ORM\Column(name="mastery_score", type="float", nullable=true)
*/
private $masteryScore;
/**
* @var string|null
*
* @ORM\Column(name="launch_parameters", type="string", nullable=true)
*/
private $launchParameters;
/**
* @var string|null
*
* @ORM\Column(name="entitlement_key", type="string", nullable=true)
*/
private $entitlementKey;
/**
* @var \Chamilo\PluginBundle\Entity\XApi\Cmi5Item|null
*
* @ORM\ManyToOne(targetEntity="Chamilo\PluginBundle\Entity\XApi\Cmi5Item", inversedBy="children")
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id")
*/
private $parent;
/**
* @var \Doctrine\Common\Collections\ArrayCollection
*
* @ORM\OneToMany(targetEntity="Chamilo\PluginBundle\Entity\XApi\Cmi5Item", mappedBy="parent")
*/
private $children;
/**
* @var string|null
*
* @ORM\Column(name="status", type="string", nullable=true)
*/
private $status;
/**
* @var \Chamilo\PluginBundle\Entity\XApi\ToolLaunch
*
* @ORM\ManyToOne(targetEntity="Chamilo\PluginBundle\Entity\XApi\ToolLaunch")
* @ORM\JoinColumn(name="tool_id", referencedColumnName="id")
*/
private $tool;
/**
* Cmi5Item constructor.
*/
public function __construct()
{
$this->children = new ArrayCollection();
}
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @param int $id
*
* @return Cmi5Item
*/
public function setId(int $id): Cmi5Item
{
$this->id = $id;
return $this;
}
/**
* @return string
*/
public function getIdentifier(): string
{
return $this->identifier;
}
/**
* @param string $identifier
*
* @return Cmi5Item
*/
public function setIdentifier(string $identifier): Cmi5Item
{
$this->identifier = $identifier;
return $this;
}
/**
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* @param string $type
*
* @return Cmi5Item
*/
public function setType(string $type): Cmi5Item
{
$this->type = $type;
return $this;
}
/**
* @return array
*/
public function getTitle(): array
{
return $this->title;
}
/**
* @param array $title
*
* @return Cmi5Item
*/
public function setTitle(array $title): Cmi5Item
{
$this->title = $title;
return $this;
}
/**
* @return array
*/
public function getDescription(): array
{
return $this->description;
}
/**
* @param array $description
*
* @return Cmi5Item
*/
public function setDescription(array $description): Cmi5Item
{
$this->description = $description;
return $this;
}
/**
* @return string|null
*/
public function getUrl(): ?string
{
return $this->url;
}
/**
* @param string|null $url
*
* @return Cmi5Item
*/
public function setUrl(?string $url): Cmi5Item
{
$this->url = $url;
return $this;
}
/**
* @return string|null
*/
public function getActivityType(): ?string
{
return $this->activityType;
}
/**
* @param string|null $activityType
*
* @return Cmi5Item
*/
public function setActivityType(?string $activityType): Cmi5Item
{
$this->activityType = $activityType;
return $this;
}
/**
* @return string|null
*/
public function getLaunchMethod(): ?string
{
return $this->launchMethod;
}
/**
* @param string|null $launchMethod
*
* @return Cmi5Item
*/
public function setLaunchMethod(?string $launchMethod): Cmi5Item
{
$this->launchMethod = $launchMethod;
return $this;
}
/**
* @return string|null
*/
public function getMoveOn(): ?string
{
return $this->moveOn;
}
/**
* @param string|null $moveOn
*
* @return Cmi5Item
*/
public function setMoveOn(?string $moveOn): Cmi5Item
{
$this->moveOn = $moveOn;
return $this;
}
/**
* @return float|null
*/
public function getMasteryScore(): ?float
{
return $this->masteryScore;
}
/**
* @param float|null $masteryScore
*
* @return Cmi5Item
*/
public function setMasteryScore(?float $masteryScore): Cmi5Item
{
$this->masteryScore = $masteryScore;
return $this;
}
/**
* @return string|null
*/
public function getLaunchParameters(): ?string
{
return $this->launchParameters;
}
/**
* @param string|null $launchParameters
*
* @return Cmi5Item
*/
public function setLaunchParameters(?string $launchParameters): Cmi5Item
{
$this->launchParameters = $launchParameters;
return $this;
}
/**
* @return string|null
*/
public function getEntitlementKey(): ?string
{
return $this->entitlementKey;
}
/**
* @param string|null $entitlementKey
*
* @return Cmi5Item
*/
public function setEntitlementKey(?string $entitlementKey): Cmi5Item
{
$this->entitlementKey = $entitlementKey;
return $this;
}
/**
* @return \Chamilo\PluginBundle\Entity\XApi\Cmi5Item|null
*/
public function getParent(): ?Cmi5Item
{
return $this->parent;
}
/**
* @param \Chamilo\PluginBundle\Entity\XApi\Cmi5Item|null $parent
*
* @return Cmi5Item
*/
public function setParent(?Cmi5Item $parent): Cmi5Item
{
$this->parent = $parent;
return $this;
}
/**
* @return \Doctrine\Common\Collections\ArrayCollection
*/
public function getChildren(): ArrayCollection
{
return $this->children;
}
/**
* @param \Doctrine\Common\Collections\ArrayCollection $children
*
* @return Cmi5Item
*/
public function setChildren(ArrayCollection $children): Cmi5Item
{
$this->children = $children;
return $this;
}
/**
* @return string|null
*/
public function getStatus(): ?string
{
return $this->status;
}
/**
* @param string|null $status
*
* @return Cmi5Item
*/
public function setStatus(?string $status): Cmi5Item
{
$this->status = $status;
return $this;
}
/**
* @return \Chamilo\PluginBundle\Entity\XApi\ToolLaunch
*/
public function getTool(): ToolLaunch
{
return $this->tool;
}
/**
* @param \Chamilo\PluginBundle\Entity\XApi\ToolLaunch $tool
*
* @return Cmi5Item
*/
public function setTool(ToolLaunch $tool): Cmi5Item
{
$this->tool = $tool;
return $this;
}
}

@ -28,7 +28,7 @@ abstract class AbstractImporter
/**
* @var string
*/
protected $toolDirectoryPath;
protected $toolDirectory;
/**
* @var string
*/
@ -41,22 +41,34 @@ abstract class AbstractImporter
/**
* AbstractImporter constructor.
*
* @param string $toolDirectory
* @param array $fileInfo
* @param string $toolDirectory
* @param \Chamilo\CoreBundle\Entity\Course $course
*/
protected function __construct(array $fileInfo, $toolDirectory, Course $course)
protected function __construct(array $fileInfo, string $toolDirectory, Course $course)
{
$this->course = $course;
$pathInfo = pathinfo($fileInfo['name']);
$this->courseDirectoryPath = api_get_path(SYS_COURSE_PATH).$this->course->getDirectory();
$this->toolDirectoryPath = $this->courseDirectoryPath.'/'.$toolDirectory;
$this->packageDirectoryPath = $this->toolDirectoryPath.'/'.api_replace_dangerous_char($pathInfo['filename']);
$this->toolDirectory = $toolDirectory;
$this->packageDirectoryPath = implode(
'/',
[
$this->courseDirectoryPath,
$this->toolDirectory,
api_replace_dangerous_char($pathInfo['filename'])
]
);
$this->zipFile = new PclZip($fileInfo['tmp_name']);
}
/**
* @param array $fileInfo
* @param \Chamilo\CoreBundle\Entity\Course $course
*
* @return \Chamilo\PluginBundle\XApi\Importer\AbstractImporter
*/
abstract public static function create(array $fileInfo, Course $course);
@ -80,7 +92,7 @@ abstract class AbstractImporter
$this->zipFile->extract($this->packageDirectoryPath);
return "{$this->packageDirectoryPath}/tincan.xml";
return "{$this->packageDirectoryPath}/{$this->toolDirectory}.xml";
}
/**

@ -0,0 +1,45 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Importer;
use Chamilo\CoreBundle\Entity\Course;
/**
* Class Cmi5Importer
*
* @package Chamilo\PluginBundle\XApi\Importer
*/
class Cmi5Importer extends AbstractImporter
{
/**
* @inheritDoc
*/
public static function create(array $fileInfo, Course $course)
{
return new self($fileInfo, 'cmi5', $course);
}
/**
* {@inheritdoc}
*/
protected function validPackage()
{
parent::validPackage();
$zipContent = $this->zipFile->listContent();
$isValid = false;
foreach ($zipContent as $zipEntry) {
if ('cmi5.xml' === $zipEntry['filename']) {
$isValid = true;
}
}
if (!$isValid) {
throw new \Exception('Incorrect package. Missing "cmi5.xml" file');
}
}
}

@ -0,0 +1,57 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Parser;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
/**
* Class AbstractParser.
*
* @package Chamilo\PluginBundle\XApi\Parser
*/
abstract class AbstractParser
{
/**
* @var string
*/
protected $filePath;
/**
* @var Course
*/
protected $course;
/**
* @var Session|null
*/
protected $session;
/**
* AbstractParser constructor.
*
* @param $filePath
* @param \Chamilo\CoreBundle\Entity\Course $course
* @param \Chamilo\CoreBundle\Entity\Session|null $session
*/
protected function __construct($filePath, Course $course, Session $session = null)
{
$this->filePath = $filePath;
$this->course = $course;
$this->session = $session;
}
/**
* @param string $filePath
* @param \Chamilo\CoreBundle\Entity\Course $course
* @param \Chamilo\CoreBundle\Entity\Session|null $session
*
* @return mixed
*/
abstract public static function create($filePath, Course $course, Session $session = null);
/**
* @return \Chamilo\PluginBundle\Entity\XApi\ToolLaunch
*/
abstract public function parse();
}

@ -0,0 +1,203 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Parser;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\PluginBundle\Entity\XApi\Cmi5Item;
use Chamilo\PluginBundle\Entity\XApi\ToolLaunch;
use Symfony\Component\DomCrawler\Crawler;
use Xabbuh\XApi\Model\LanguageMap;
/**
* Class Cmi5Parser.
*
* @package Chamilo\PluginBundle\XApi\Parser
*/
class Cmi5Parser extends AbstractParser
{
/**
* @var array|\Chamilo\PluginBundle\Entity\XApi\Cmi5Item[]
*/
private $toc;
/**
* @inheritDoc
*/
public static function create($filePath, Course $course, Session $session = null)
{
return new self($filePath, $course, $session);
}
/**
* @inheritDoc
*/
public function parse()
{
$content = file_get_contents($this->filePath);
$xml = new Crawler($content);
$courseNode = $xml->filterXPath('//courseStructure/course');
$toolLaunch = new ToolLaunch();
$toolLaunch
->setTitle(
current(
$this->getLanguageStrings(
$courseNode->filterXPath('//title')
)
)
)
->setDescription(
current(
$this->getLanguageStrings(
$courseNode->filterXPath('//description')
)
)
)
->setLaunchUrl('')
->setActivityId($courseNode->attr('id'))
->setActivityType('cmi5')
->setAllowMultipleAttempts(false)
->setCreatedAt(api_get_utc_datetime(null, false, true))
->setCourse($this->course)
->setSession($this->session);
$this->toc = $this->generateToC($xml);
return $toolLaunch;
}
/**
* @param \Symfony\Component\DomCrawler\Crawler $node
*
* @return array
*/
private function getLanguageStrings(Crawler $node)
{
$map = [];
foreach ($node->children() as $child) {
$key = $child->attributes['lang']->value;
$value = trim($child->textContent);
$map[$key] = $value;
}
return $map;
}
/**
* @param \Symfony\Component\DomCrawler\Crawler $xml
*
* @return array|\Chamilo\PluginBundle\Entity\XApi\Cmi5Item[]
*/
private function generateToC(Crawler $xml)
{
$blocksMap = [];
/** @var array|Cmi5Item[] $items */
$items = $xml
->filterXPath('//*')
->reduce(
function (Crawler $node, $i) {
return in_array($node->nodeName(), ['au', 'block']);
}
)
->each(
function (Crawler $node, $i) use (&$blocksMap) {
$attributes = ['id', 'activityType', 'launchMethod', 'moveOn', 'masteryScore'];
list($id, $activityType, $launchMethod, $moveOn, $masteryMode) = $node->extract($attributes)[0];
$item = new Cmi5Item();
$item
->setIdentifier($id)
->setType($node->nodeName())
->setTitle(
$this->getLanguageStrings(
$node->filterXPath('//title')
)
)
->setDescription(
$this->getLanguageStrings(
$node->filterXPath('//description')
)
);
if ('au' === $node->nodeName()) {
$launchParametersNode = $node->filterXPath('//launchParameters');
$entitlementKeyNode = $node->filterXPath('//entitlementKey');
$url =
$item
->setUrl(
$this->parseLaunchUrl(
trim($node->filterXPath('//url')->text())
)
)
->setActivityType($activityType ?: null)
->setLaunchMethod($launchMethod ?: null)
->setMoveOn($moveOn ?: 'NotApplicable')
->setMasteryScore((float) $masteryMode ?: null)
->setLaunchParameters(
$launchParametersNode->count() > 0 ? trim($launchParametersNode->text()) : null
)
->setEntitlementKey(
$entitlementKeyNode->count() > 0 ? trim($entitlementKeyNode->text()) : null
);
}
$parentNode = $node->parents()->first();
if ('block' === $parentNode->nodeName()) {
$blocksMap[$i] = $parentNode->attr('id');
}
return $item;
}
);
foreach ($blocksMap as $itemPos => $parentIdentifier) {
foreach ($items as $item) {
if ($parentIdentifier === $item->getIdentifier()) {
$items[$itemPos]->setParent($item);
}
}
}
return $items;
}
/**
* @return array|\Chamilo\PluginBundle\Entity\XApi\Cmi5Item[]
*/
public function getToc()
{
return $this->toc;
}
/**
* @param string $url
*
* @return string
*/
private function parseLaunchUrl($url)
{
$urlInfo = parse_url($url);
if (empty($urlInfo['scheme'])) {
$baseUrl = str_replace(
api_get_path(SYS_COURSE_PATH),
api_get_path(WEB_COURSE_PATH),
dirname($this->filePath)
);
return "$baseUrl/$url";
}
return $url;
}
}

@ -14,35 +14,18 @@ use Symfony\Component\DomCrawler\Crawler;
*
* @package Chamilo\PluginBundle\XApi\Parser
*/
class TinCanParser
class TinCanParser extends AbstractParser
{
/**
* @var string
* @inheritDoc
*/
private $filePath;
/**
* @var Course
*/
private $course;
/**
* @var Session|null
*/
private $session;
protected function __construct($filePath, Course $course, Session $session = null)
{
$this->filePath = $filePath;
$this->course = $course;
$this->session = $session;
}
public static function create($filePath, Course $course, Session $session = null)
{
return new self($filePath, $course, $session);
}
/**
* @return \Chamilo\PluginBundle\Entity\XApi\ToolLaunch
* @inheritDoc
*/
public function parse()
{
@ -76,6 +59,8 @@ class TinCanParser
}
/**
* @param \Symfony\Component\DomCrawler\Crawler $launchNode
*
* @return string
*/
private function parseLaunchUrl(Crawler $launchNode)

@ -2,6 +2,7 @@
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\XApi\Cmi5Item;
use Chamilo\PluginBundle\Entity\XApi\SharedStatement;
use Chamilo\PluginBundle\Entity\XApi\ToolLaunch;
use Doctrine\ORM\EntityManager;
@ -104,6 +105,7 @@ class XApiPlugin extends Plugin implements HookPluginInterface
'xapi_shared_statement',
'xapi_tool_launch',
'xapi_lrs_auth',
'xapi_cmi5_item',
'xapi_attachment',
'xapi_object',
@ -167,6 +169,7 @@ class XApiPlugin extends Plugin implements HookPluginInterface
$em->getClassMetadata(SharedStatement::class),
$em->getClassMetadata(ToolLaunch::class),
$em->getClassMetadata(LrsAuth::class),
$em->getClassMetadata(Cmi5Item::class),
]
);
@ -444,6 +447,7 @@ class XApiPlugin extends Plugin implements HookPluginInterface
$em->getClassMetadata(SharedStatement::class),
$em->getClassMetadata(ToolLaunch::class),
$em->getClassMetadata(LrsAuth::class),
$em->getClassMetadata(Cmi5Item::class),
]
);

@ -110,9 +110,14 @@ $view->assign('header', $pageTitle);
if ($isAllowedToEdit) {
$actions = Display::url(
Display::return_icon('add.png', get_lang('Add'), [], ICON_SIZE_MEDIUM),
"add.php?$cidReq"
);
Display::return_icon('add.png', get_lang('Add'), [], ICON_SIZE_MEDIUM),
"add.php?$cidReq"
)
.PHP_EOL
.Display::url(
Display::return_icon('add.png', get_lang('Add'), [], ICON_SIZE_MEDIUM),
"../cmi5/add.php?$cidReq"
);
$view->assign(
'actions',

Loading…
Cancel
Save