399 lines
16 KiB
PHP
399 lines
16 KiB
PHP
|
<?php
|
||
|
|
||
|
/**
|
||
|
* This program is free software; you can redistribute it and/or
|
||
|
* modify it under the terms of the GNU General Public License
|
||
|
* as published by the Free Software Foundation; under version 2
|
||
|
* of the License (non-upgradable).
|
||
|
*
|
||
|
* This program is distributed in the hope that it will be useful,
|
||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
* GNU General Public License for more details.
|
||
|
*
|
||
|
* You should have received a copy of the GNU General Public License
|
||
|
* along with this program; if not, write to the Free Software
|
||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
|
*
|
||
|
* Copyright (c) 2016-2020 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
declare(strict_types=1);
|
||
|
|
||
|
namespace oat\taoResultServer\models\classes;
|
||
|
|
||
|
use common_Exception;
|
||
|
use common_exception_InvalidArgumentType;
|
||
|
use common_exception_NotFound;
|
||
|
use common_exception_NotImplemented;
|
||
|
use common_exception_ResourceNotFound;
|
||
|
use core_kernel_classes_Resource;
|
||
|
use DOMDocument;
|
||
|
use DOMElement;
|
||
|
use finfo;
|
||
|
use oat\oatbox\service\ConfigurableService;
|
||
|
use oat\oatbox\service\exception\InvalidServiceManagerException;
|
||
|
use oat\taoDelivery\model\execution\DeliveryExecution as DeliveryExecutionInterface;
|
||
|
use oat\taoDelivery\model\execution\ServiceProxy;
|
||
|
use oat\taoResultServer\models\Exceptions\DuplicateVariableException;
|
||
|
use oat\taoResultServer\models\Mapper\ResultMapper;
|
||
|
use oat\taoResultServer\models\Parser\QtiResultParser;
|
||
|
use qtism\common\enums\Cardinality;
|
||
|
use qtism\data\storage\xml\XmlStorageException;
|
||
|
use tao_helpers_Date;
|
||
|
use taoResultServer_models_classes_WritableResultStorage as WritableResultStorage;
|
||
|
|
||
|
class QtiResultsService extends ConfigurableService implements ResultService
|
||
|
{
|
||
|
protected $deliveryExecutionService;
|
||
|
|
||
|
private const QTI_NS = 'http://www.imsglobal.org/xsd/imsqti_result_v2p1';
|
||
|
|
||
|
public const CLASS_RESPONSE_VARIABLE = 'http://www.tao.lu/Ontologies/TAOResult.rdf#ResponseVariable';
|
||
|
public const CLASS_OUTCOME_VARIABLE = 'http://www.tao.lu/Ontologies/TAOResult.rdf#OutcomeVariable';
|
||
|
|
||
|
/**
|
||
|
* Get the implementation of delivery execution service
|
||
|
*
|
||
|
* @return ServiceProxy
|
||
|
* @throws \Zend\ServiceManager\Exception\ServiceNotFoundException
|
||
|
*/
|
||
|
protected function getDeliveryExecutionService()
|
||
|
{
|
||
|
if (!$this->deliveryExecutionService) {
|
||
|
$this->deliveryExecutionService = $this->getServiceLocator()->get(ServiceProxy::SERVICE_ID);
|
||
|
}
|
||
|
return $this->deliveryExecutionService;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get last delivery execution from $delivery & $testtaker uri
|
||
|
*
|
||
|
* @param string $delivery uri
|
||
|
* @param string $testtaker uri
|
||
|
* @return \oat\taoDelivery\model\execution\DeliveryExecutionInterface
|
||
|
* @throws
|
||
|
*/
|
||
|
public function getDeliveryExecutionByTestTakerAndDelivery($delivery, $testtaker)
|
||
|
{
|
||
|
$delivery = new core_kernel_classes_Resource($delivery);
|
||
|
$deliveryExecutions = $this->getDeliveryExecutionService()->getUserExecutions($delivery, $testtaker);
|
||
|
if (empty($deliveryExecutions)) {
|
||
|
throw new common_exception_NotFound('Provided parameters don\'t match with any delivery execution.');
|
||
|
}
|
||
|
return array_pop($deliveryExecutions);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get Delivery execution from resource
|
||
|
*
|
||
|
* @param $deliveryExecutionId
|
||
|
* @return DeliveryExecutionInterface
|
||
|
* @throws common_exception_NotFound
|
||
|
*/
|
||
|
public function getDeliveryExecutionById($deliveryExecutionId)
|
||
|
{
|
||
|
$deliveryExecution = $this->getDeliveryExecutionService()->getDeliveryExecution($deliveryExecutionId);
|
||
|
try {
|
||
|
$deliveryExecution->getDelivery();
|
||
|
} catch (common_exception_NotFound $e) {
|
||
|
throw new common_exception_NotFound('Provided parameters don\'t match with any delivery execution.');
|
||
|
}
|
||
|
return $deliveryExecution;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return delivery execution as xml of testtaker based on delivery
|
||
|
*
|
||
|
* @param DeliveryExecutionInterface $deliveryExecution
|
||
|
* @return string
|
||
|
*/
|
||
|
public function getDeliveryExecutionXml(DeliveryExecutionInterface $deliveryExecution)
|
||
|
{
|
||
|
return $this->getQtiResultXml($deliveryExecution->getDelivery()->getUri(), $deliveryExecution->getIdentifier());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param $deliveryId
|
||
|
* @param $resultId
|
||
|
* @param bool $fetchOnlyLastAttemptResult
|
||
|
* @return string
|
||
|
* @throws common_Exception
|
||
|
* @throws InvalidServiceManagerException
|
||
|
*/
|
||
|
public function getQtiResultXml($deliveryId, $resultId, $fetchOnlyLastAttemptResult = false)
|
||
|
{
|
||
|
$deId = $this->getServiceManager()->get(ResultAliasServiceInterface::SERVICE_ID)->getDeliveryExecutionId($resultId);
|
||
|
if ($deId === null) {
|
||
|
$deId = $resultId;
|
||
|
}
|
||
|
|
||
|
$resultService = $this->getServiceLocator()->get(ResultServerService::SERVICE_ID);
|
||
|
$resultServer = $resultService->getResultStorage();
|
||
|
|
||
|
$crudService = new CrudResultsService();
|
||
|
|
||
|
$dom = new DOMDocument('1.0', 'UTF-8');
|
||
|
$dom->formatOutput = true;
|
||
|
|
||
|
$itemResultsByAttempt = $crudService->format($resultServer, $deId, CrudResultsService::GROUP_BY_ITEM, $fetchOnlyLastAttemptResult, true);
|
||
|
$testResults = $crudService->format($resultServer, $deId, CrudResultsService::GROUP_BY_TEST);
|
||
|
|
||
|
$assessmentResultElt = $dom->createElementNS(self::QTI_NS, 'assessmentResult');
|
||
|
$dom->appendChild($assessmentResultElt);
|
||
|
|
||
|
/** Context */
|
||
|
$contextElt = $dom->createElementNS(self::QTI_NS, 'context');
|
||
|
$userId = $resultServer->getTestTaker($deId);
|
||
|
|
||
|
if ($userId === false) {
|
||
|
throw new common_exception_ResourceNotFound('Provided parameters don\'t match with any delivery execution.');
|
||
|
}
|
||
|
|
||
|
if (\common_Utils::isUri($userId)) {
|
||
|
$userId = \tao_helpers_Uri::getUniqueId($userId);
|
||
|
}
|
||
|
|
||
|
$contextElt->setAttribute('sourcedId', $userId);
|
||
|
$assessmentResultElt->appendChild($contextElt);
|
||
|
|
||
|
/** Test Result */
|
||
|
foreach ($testResults as $testResultIdentifier => $testResult) {
|
||
|
$identifierParts = explode('.', $testResultIdentifier);
|
||
|
$testIdentifier = array_pop($identifierParts);
|
||
|
|
||
|
$testResultElement = $dom->createElementNS(self::QTI_NS, 'testResult');
|
||
|
$testResultElement->setAttribute('identifier', $testIdentifier);
|
||
|
$testResultElement->setAttribute('datestamp', $this->getDisplayDate($testResult[0]['epoch']));
|
||
|
|
||
|
/** Item Variable */
|
||
|
foreach ($testResult as $itemVariable) {
|
||
|
$isResponseVariable = $itemVariable['type']->getUri() === self::CLASS_RESPONSE_VARIABLE;
|
||
|
$testVariableElement = $dom->createElementNS(self::QTI_NS, ($isResponseVariable) ? 'responseVariable' : 'outcomeVariable');
|
||
|
$testVariableElement->setAttribute('identifier', $itemVariable['identifier']);
|
||
|
$testVariableElement->setAttribute('cardinality', $itemVariable['cardinality']);
|
||
|
$testVariableElement->setAttribute('baseType', $itemVariable['basetype']);
|
||
|
|
||
|
$valueElement = $this->createCDATANode($dom, 'value', trim($itemVariable['value']));
|
||
|
|
||
|
if ($isResponseVariable) {
|
||
|
$candidateResponseElement = $dom->createElementNS(self::QTI_NS, 'candidateResponse');
|
||
|
$candidateResponseElement->appendChild($valueElement);
|
||
|
$testVariableElement->appendChild($candidateResponseElement);
|
||
|
} else {
|
||
|
$testVariableElement->appendChild($valueElement);
|
||
|
}
|
||
|
|
||
|
$testResultElement->appendChild($testVariableElement);
|
||
|
}
|
||
|
|
||
|
$assessmentResultElt->appendChild($testResultElement);
|
||
|
}
|
||
|
|
||
|
/** Item Result */
|
||
|
foreach ($itemResultsByAttempt as $itemResultIdentifier => $itemResults) {
|
||
|
/** Iterates variables */
|
||
|
foreach ($itemResults as $itemResult) {
|
||
|
$itemElement = $this->createItemResultNode($dom, $itemResultIdentifier, $itemResult);
|
||
|
/** Item variables */
|
||
|
foreach ($itemResult as $key => $itemVariable) {
|
||
|
$isResponseVariable = $itemVariable['type']->getUri() === self::CLASS_RESPONSE_VARIABLE;
|
||
|
|
||
|
if ($itemVariable['identifier'] == 'comment') {
|
||
|
/** Comment */
|
||
|
$itemVariableElement = $dom->createElementNS(self::QTI_NS,'candidateComment', $itemVariable['value']);
|
||
|
} else {
|
||
|
$itemVariableElement = $this->createItemVariableNode($dom, $isResponseVariable, $itemVariable);
|
||
|
}
|
||
|
$itemElement->appendChild($itemVariableElement);
|
||
|
}
|
||
|
$assessmentResultElt->appendChild($itemElement);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $dom->saveXML();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse the xml to save including variables into given deliveryExecution
|
||
|
*
|
||
|
* @param string $deliveryExecutionId
|
||
|
* @param string $xml
|
||
|
* @throws common_exception_InvalidArgumentType
|
||
|
* @throws common_exception_NotFound
|
||
|
* @throws common_exception_NotImplemented
|
||
|
* @throws XmlStorageException
|
||
|
* @throws DuplicateVariableException
|
||
|
*/
|
||
|
public function injectXmlResultToDeliveryExecution($deliveryExecutionId, $xml)
|
||
|
{
|
||
|
$deliveryExecution = $this->getDeliveryExecutionById($deliveryExecutionId);
|
||
|
|
||
|
/** @var QtiResultParser $parser */
|
||
|
$parser = $this->getServiceLocator()->get(QtiResultParser::class);
|
||
|
/** @var ResultMapper $map */
|
||
|
$map = $parser->parse($xml);
|
||
|
|
||
|
/** @var WritableResultStorage $resultStorage */
|
||
|
$resultStorage = $this->getServiceLocator()
|
||
|
->get(ResultServerService::SERVICE_ID)
|
||
|
->getResultStorage();
|
||
|
|
||
|
|
||
|
$this->storeTestVariables($resultStorage, $deliveryExecutionId, $map->getTestVariables());
|
||
|
$this->storeItemVariables($resultStorage, $deliveryExecutionId, $map->getItemVariables());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Store test variables associated to a delivery execution
|
||
|
*
|
||
|
* @param WritableResultStorage $resultStorage
|
||
|
* @param string $deliveryExecutionId
|
||
|
* @param array $itemVariablesByTestResult
|
||
|
* @throws DuplicateVariableException
|
||
|
*/
|
||
|
protected function storeTestVariables(WritableResultStorage $resultStorage, $deliveryExecutionId, array $itemVariablesByTestResult)
|
||
|
{
|
||
|
$test = ' ';
|
||
|
foreach ($itemVariablesByTestResult as $test => $testVariables) {
|
||
|
$resultStorage->storeTestVariables($deliveryExecutionId, $test, $testVariables, $test);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Store item variables associated to a delivery execution
|
||
|
*
|
||
|
* @param WritableResultStorage $resultStorage
|
||
|
* @param string $deliveryExecutionId
|
||
|
* @param array $itemVariablesByItemResult
|
||
|
* @throws DuplicateVariableException
|
||
|
*/
|
||
|
protected function storeItemVariables(WritableResultStorage $resultStorage, $deliveryExecutionId, array $itemVariablesByItemResult)
|
||
|
{
|
||
|
$test = null;
|
||
|
foreach ($itemVariablesByItemResult as $itemResultIdentifier => $itemVariables) {
|
||
|
$callIdItem = $deliveryExecutionId . '.' . $itemResultIdentifier;
|
||
|
foreach ($itemVariables as $variable) {
|
||
|
if ($variable->getIdentifier() == 'numAttempts') {
|
||
|
$callIdItem .= '.' . (int)$variable->getValue();
|
||
|
}
|
||
|
}
|
||
|
$resultStorage->storeItemVariables($deliveryExecutionId, $test, $itemResultIdentifier, $itemVariables, $callIdItem);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param DOMDocument $dom
|
||
|
* @param string $tag Xml tag to create
|
||
|
* @param string $data Data to escape
|
||
|
* @return DOMElement
|
||
|
*/
|
||
|
protected function createCDATANode($dom, $tag, $data)
|
||
|
{
|
||
|
$node = $dom->createCDATASection($data);
|
||
|
$returnValue = $dom->createElementNS(self::QTI_NS, $tag);
|
||
|
$returnValue->appendChild($node);
|
||
|
return $returnValue;
|
||
|
}
|
||
|
|
||
|
private function createItemResultNode(DOMDocument $dom, string $itemResultIdentifier, array $itemResult): DOMElement
|
||
|
{
|
||
|
$identifierParts = explode('.', $itemResultIdentifier);
|
||
|
$occurrenceNumber = array_pop($identifierParts);
|
||
|
$refIdentifier = array_pop($identifierParts);
|
||
|
|
||
|
$itemElement = $dom->createElementNS(self::QTI_NS, 'itemResult');
|
||
|
$itemElement->setAttribute('identifier', $refIdentifier);
|
||
|
$itemElement->setAttribute('datestamp', $this->getDisplayDate($itemResult[0]['epoch']));
|
||
|
$itemElement->setAttribute('sessionStatus', 'final');
|
||
|
|
||
|
return $itemElement;
|
||
|
}
|
||
|
|
||
|
private function createItemVariableNode(DOMDocument $dom, bool $isResponseVariable, $itemVariable): DOMElement
|
||
|
{
|
||
|
/** Item variable */
|
||
|
$itemVariableElement = $dom->createElementNS(
|
||
|
self::QTI_NS,
|
||
|
($isResponseVariable) ? 'responseVariable' : 'outcomeVariable'
|
||
|
);
|
||
|
$itemVariableElement->setAttribute('identifier', $itemVariable['identifier']);
|
||
|
$itemVariableElement->setAttribute('cardinality', $itemVariable['cardinality']);
|
||
|
$itemVariableElement->setAttribute('baseType', $itemVariable['basetype']);
|
||
|
|
||
|
/** Split multiple response */
|
||
|
$itemVariable['value'] = $this->prepareItemVariableValue($itemVariable['value'], $itemVariable['basetype']);
|
||
|
if ($itemVariable['cardinality'] !== Cardinality::getNameByConstant(Cardinality::SINGLE)) {
|
||
|
$values = explode(';', $itemVariable['value']);
|
||
|
$returnValue = [];
|
||
|
foreach ($values as $value) {
|
||
|
$returnValue[] = $this->createCDATANode($dom, 'value', $value);
|
||
|
}
|
||
|
} else {
|
||
|
$returnValue = $this->createCDATANode($dom, 'value', $itemVariable['value']);
|
||
|
}
|
||
|
|
||
|
/** Get response parent element */
|
||
|
if ($isResponseVariable) {
|
||
|
/** Response variable */
|
||
|
$responseElement = $dom->createElementNS(self::QTI_NS, 'candidateResponse');
|
||
|
} else {
|
||
|
/** Outcome variable */
|
||
|
$responseElement = $itemVariableElement;
|
||
|
}
|
||
|
|
||
|
/** Write a response node foreach answer */
|
||
|
if (is_array($returnValue)) {
|
||
|
foreach ($returnValue as $valueElement) {
|
||
|
$responseElement->appendChild($valueElement);
|
||
|
}
|
||
|
} else {
|
||
|
$responseElement->appendChild($returnValue);
|
||
|
}
|
||
|
|
||
|
if ($isResponseVariable) {
|
||
|
$itemVariableElement->appendChild($responseElement);
|
||
|
}
|
||
|
|
||
|
return $itemVariableElement;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @throws common_Exception
|
||
|
*/
|
||
|
private function getDisplayDate(string $epoch): string
|
||
|
{
|
||
|
return tao_helpers_Date::displayeDate($epoch, tao_helpers_Date::FORMAT_ISO8601);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Prepares a variable value depending on it's baseType
|
||
|
*/
|
||
|
private function prepareItemVariableValue($value, $basetype): string
|
||
|
{
|
||
|
if ($basetype === 'file') {
|
||
|
return self::renderBinaryContentAsVariableValue($value);
|
||
|
}
|
||
|
|
||
|
return trim($value, '[]');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Tries to guess a MIME type from passed binary content and builds a properly formatted string
|
||
|
* @param string $binaryContent
|
||
|
* @return string
|
||
|
*/
|
||
|
public static function renderBinaryContentAsVariableValue(string $binaryContent): string
|
||
|
{
|
||
|
if (extension_loaded('fileinfo')) {
|
||
|
$info = new finfo(FILEINFO_MIME_TYPE);
|
||
|
$mimeType = $info->buffer($binaryContent);
|
||
|
} else {
|
||
|
$mimeType = 'application/octet-stream';
|
||
|
}
|
||
|
|
||
|
return sprintf('%s,base64,%s', $mimeType, base64_encode($binaryContent));
|
||
|
}
|
||
|
}
|