<?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)); } }