1727 lines
61 KiB
PHP
1727 lines
61 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) 2013-2017 Open Assessment Technologies S.A.
|
|
*
|
|
*
|
|
* @access public
|
|
* @author Joel Bout, <joel.bout@tudor.lu>
|
|
* @package taoOutcomeUi
|
|
*/
|
|
|
|
namespace oat\taoOutcomeUi\model;
|
|
|
|
use common_Exception;
|
|
use common_exception_Error;
|
|
use common_Logger;
|
|
use core_kernel_classes_Resource;
|
|
use League\Flysystem\FileNotFoundException;
|
|
use oat\generis\model\GenerisRdf;
|
|
use oat\generis\model\OntologyRdfs;
|
|
use oat\oatbox\service\ServiceManager;
|
|
use oat\oatbox\user\User;
|
|
use oat\tao\helpers\metadata\ResourceCompiledMetadataHelper;
|
|
use oat\tao\model\metadata\compiler\ResourceJsonMetadataCompiler;
|
|
use oat\tao\model\metadata\compiler\ResourceMetadataCompilerInterface;
|
|
use oat\tao\model\OntologyClassService;
|
|
use oat\taoDelivery\model\execution\DeliveryExecution;
|
|
use oat\taoDelivery\model\execution\ServiceProxy;
|
|
use oat\taoDelivery\model\RuntimeService;
|
|
use oat\taoDeliveryRdf\model\DeliveryAssemblyService;
|
|
use oat\taoItems\model\ItemCompilerIndex;
|
|
use oat\taoOutcomeUi\helper\Datatypes;
|
|
use oat\taoOutcomeUi\model\table\ContextTypePropertyColumn;
|
|
use oat\taoOutcomeUi\model\table\GradeColumn;
|
|
use oat\taoOutcomeUi\model\table\ResponseColumn;
|
|
use oat\taoOutcomeUi\model\table\TraceVariableColumn;
|
|
use oat\taoOutcomeUi\model\table\VariableColumn;
|
|
use oat\taoOutcomeUi\model\Wrapper\ResultServiceWrapper;
|
|
use oat\taoQtiTest\models\QtiTestCompilerIndex;
|
|
use oat\taoResultServer\models\classes\NoResultStorage;
|
|
use oat\taoResultServer\models\classes\NoResultStorageException;
|
|
use oat\taoResultServer\models\classes\ResultManagement;
|
|
use oat\taoResultServer\models\classes\ResultServerService;
|
|
use oat\taoResultServer\models\classes\ResultService;
|
|
use oat\taoResultServer\models\Formatter\ItemResponseVariableSplitter;
|
|
use tao_helpers_Date;
|
|
use tao_models_classes_service_StorageDirectory;
|
|
use taoQtiTest_models_classes_QtiTestService;
|
|
use taoResultServer_models_classes_ReadableResultStorage;
|
|
use taoResultServer_models_classes_Variable as Variable;
|
|
use oat\taoOutcomeUi\model\table\TestCenterColumn;
|
|
|
|
class ResultsService extends OntologyClassService
|
|
{
|
|
public const SERVICE_ID = 'taoOutcomeUi/OutcomeUiResultService';
|
|
|
|
public const VARIABLES_FILTER_LAST_SUBMITTED = 'lastSubmitted';
|
|
public const VARIABLES_FILTER_FIRST_SUBMITTED = 'firstSubmitted';
|
|
public const VARIABLES_FILTER_ALL = 'all';
|
|
|
|
// Only need to correct formatting trace variables
|
|
protected const VARIABLES_FILTER_TRACE = 'trace';
|
|
|
|
public const PERSISTENCE_CACHE_KEY = 'resultCache';
|
|
|
|
public const PERIODS = [self::FILTER_START_FROM, self::FILTER_START_TO, self::FILTER_END_FROM, self::FILTER_END_TO];
|
|
public const DELIVERY_EXECUTION_STARTED_AT = 'delivery_execution_started_at';
|
|
public const DELIVERY_EXECUTION_FINISHED_AT = 'delivery_execution_finished_at';
|
|
public const FILTER_START_FROM = 'startfrom';
|
|
public const FILTER_START_TO = 'startto';
|
|
public const FILTER_END_FROM = 'endfrom';
|
|
public const FILTER_END_TO = 'endto';
|
|
|
|
public const OPTION_ALLOW_SQL_EXPORT = 'allow_sql_export';
|
|
public const OPTION_ALLOW_TRACE_VARIABLES_EXPORT = 'allow_trace_variable_export';
|
|
|
|
public const SEPARATOR = ' | ';
|
|
|
|
/** @var taoResultServer_models_classes_ReadableResultStorage */
|
|
private $implementation;
|
|
|
|
/**
|
|
* Internal cache for item info.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $itemInfoCache = [];
|
|
|
|
/**
|
|
* External cache.
|
|
*
|
|
* @var \common_persistence_KvDriver
|
|
*/
|
|
private $resultCache;
|
|
|
|
/** @var array */
|
|
private $indexerCache = [];
|
|
|
|
/** @var array */
|
|
private $executionCache = [];
|
|
|
|
/** @var array */
|
|
private $testMetadataCache = [];
|
|
/**
|
|
* @return \common_persistence_KvDriver|null
|
|
*/
|
|
public function getCache()
|
|
{
|
|
if (is_null($this->resultCache)) {
|
|
/** @var \common_persistence_Manager $persistenceManager */
|
|
$persistenceManager = $this->getServiceLocator()->get(\common_persistence_Manager::SERVICE_ID);
|
|
if ($persistenceManager->hasPersistence(self::PERSISTENCE_CACHE_KEY)) {
|
|
$this->resultCache = $persistenceManager->getPersistenceById(self::PERSISTENCE_CACHE_KEY);
|
|
}
|
|
}
|
|
|
|
return $this->resultCache;
|
|
}
|
|
|
|
public function getCacheKey($resultIdentifier, $suffix = '')
|
|
{
|
|
return 'resultPageCache:' . $resultIdentifier . ':' . $suffix;
|
|
}
|
|
|
|
protected function getContainerCacheKey($resultIdentifier)
|
|
{
|
|
return $this->getCacheKey($resultIdentifier, 'keys');
|
|
}
|
|
|
|
public function setCacheValue($resultIdentifier, $fullKey, $value)
|
|
{
|
|
if (is_null($this->getCache())) {
|
|
return false;
|
|
}
|
|
|
|
$fullKeys = [];
|
|
|
|
$containerKey = $this->getContainerCacheKey($resultIdentifier);
|
|
if ($this->getCache()->exists($containerKey)) {
|
|
$fullKeys = $this->getContainerCacheValue($containerKey);
|
|
}
|
|
|
|
$fullKeys[] = $fullKey;
|
|
|
|
if ($this->getCache()->set($fullKey, $value)) {
|
|
// let's save the container of the keys as well
|
|
return $this->setContainerCacheValue($containerKey, $fullKeys);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function deleteCacheFor($resultIdentifier)
|
|
{
|
|
if (is_null($this->getCache())) {
|
|
return false;
|
|
}
|
|
|
|
$containerKey = $this->getContainerCacheKey($resultIdentifier);
|
|
if (!$this->getCache()->exists($containerKey)) {
|
|
return false;
|
|
}
|
|
|
|
$fullKeys = $this->getContainerCacheValue($containerKey);
|
|
$initialCount = count($fullKeys);
|
|
|
|
foreach ($fullKeys as $i => $key) {
|
|
if ($this->getCache()->del($key)) {
|
|
unset($fullKeys[$i]);
|
|
}
|
|
}
|
|
|
|
if (empty($fullKeys)) {
|
|
// delete the whole container
|
|
return $this->getCache()->del($containerKey);
|
|
} elseif (count($fullKeys) < $initialCount) {
|
|
// update the container
|
|
return $this->setContainerCacheValue($containerKey, $fullKeys);
|
|
}
|
|
|
|
// no cache has been deleted
|
|
return false;
|
|
}
|
|
|
|
protected function setContainerCacheValue($containerKey, array $fullKeys)
|
|
{
|
|
return $this->getCache()->set($containerKey, gzencode(json_encode(array_unique($fullKeys)), 9));
|
|
}
|
|
|
|
protected function getContainerCacheValue($containerKey)
|
|
{
|
|
return json_decode(gzdecode($this->getCache()->get($containerKey)), true);
|
|
}
|
|
|
|
/**
|
|
* (non-PHPdoc)
|
|
* @see tao_models_classes_ClassService::getRootClass()
|
|
*/
|
|
public function getRootClass()
|
|
{
|
|
return $this->getClass(ResultService::DELIVERY_RESULT_CLASS_URI);
|
|
}
|
|
|
|
public function setImplementation(ResultManagement $implementation)
|
|
{
|
|
$this->implementation = $implementation;
|
|
}
|
|
|
|
/**
|
|
* @return ResultManagement
|
|
* @throws common_exception_Error
|
|
*/
|
|
public function getImplementation()
|
|
{
|
|
if ($this->implementation == null) {
|
|
throw new \common_exception_Error('No result storage defined');
|
|
}
|
|
|
|
return $this->implementation;
|
|
}
|
|
|
|
/**
|
|
* return all variable for that deliveryResults (uri identifiers)
|
|
*
|
|
* @access public
|
|
*
|
|
* @param string $resultIdentifier
|
|
* @param boolean $flat a flat array is returned or a structured delvieryResult-ItemResult-Variable
|
|
*
|
|
* @return array
|
|
* @author Joel Bout, <joel.bout@tudor.lu>
|
|
*/
|
|
public function getVariables($resultIdentifier, $flat = true)
|
|
{
|
|
$variables = [];
|
|
//this service is slow due to the way the data model design
|
|
//if the delvieryResult related execution is finished, the data is stored in cache.
|
|
|
|
$serial = 'deliveryResultVariables:' . $resultIdentifier;
|
|
//if (common_cache_FileCache::singleton()->has($serial)) {
|
|
// $variables = common_cache_FileCache::singleton()->get($serial);
|
|
//} else {
|
|
$resultVariables = $this->getImplementation()->getDeliveryVariables($resultIdentifier);
|
|
foreach ($resultVariables as $resultVariable) {
|
|
$currentItem = current($resultVariable);
|
|
$key = isset($currentItem->callIdItem) ? $currentItem->callIdItem : $currentItem->callIdTest;
|
|
$variables[$key][] = $resultVariable;
|
|
}
|
|
|
|
// impossible to determine state DeliveryExecution::STATE_FINISHIED
|
|
// if (false) {
|
|
// common_cache_FileCache::singleton()->put($variables, $serial);
|
|
// }
|
|
//}
|
|
if ($flat) {
|
|
$returnValue = [];
|
|
foreach ($variables as $key => $itemResultVariables) {
|
|
$newKeys = [];
|
|
$oldKeys = array_keys($itemResultVariables);
|
|
foreach ($oldKeys as $oldKey) {
|
|
$newKeys[] = $key . '_' . $oldKey;
|
|
}
|
|
$itemResultVariables = array_combine($newKeys, array_values($itemResultVariables));
|
|
$returnValue = array_merge($returnValue, $itemResultVariables);
|
|
}
|
|
} else {
|
|
$returnValue = $variables;
|
|
}
|
|
|
|
|
|
return (array)$returnValue;
|
|
}
|
|
|
|
/**
|
|
* @param string|array $itemResult
|
|
* @param array $wantedTypes
|
|
*
|
|
* @return array
|
|
* @throws common_exception_Error
|
|
*/
|
|
public function getVariablesFromObjectResult($itemResult, $wantedTypes = [\taoResultServer_models_classes_ResponseVariable::class, \taoResultServer_models_classes_OutcomeVariable::class, \taoResultServer_models_classes_TraceVariable::class])
|
|
{
|
|
$returnedVariables = [];
|
|
$variables = $this->getImplementation()->getVariables($itemResult);
|
|
|
|
foreach ($variables as $itemVariables) {
|
|
foreach ($itemVariables as $variable) {
|
|
if (in_array(get_class($variable->variable), $wantedTypes)) {
|
|
$returnedVariables[] = [$variable];
|
|
}
|
|
}
|
|
}
|
|
|
|
unset($variables);
|
|
|
|
return $returnedVariables;
|
|
}
|
|
|
|
/**
|
|
* Return the corresponding delivery
|
|
*
|
|
* @param string $resultIdentifier
|
|
*
|
|
* @return core_kernel_classes_Resource delviery
|
|
* @author Patrick Plichart, <patrick@taotesting.com>
|
|
*/
|
|
public function getDelivery($resultIdentifier)
|
|
{
|
|
return new core_kernel_classes_Resource($this->getImplementation()->getDelivery($resultIdentifier));
|
|
}
|
|
|
|
/**
|
|
* Ges the type of items contained by the delivery
|
|
*
|
|
* @param string $resultIdentifier
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getDeliveryItemType($resultIdentifier)
|
|
{
|
|
$resultsViewerService = $this->getServiceLocator()->get(ResultsViewerService::SERVICE_ID);
|
|
|
|
return $resultsViewerService->getDeliveryItemType($resultIdentifier);
|
|
}
|
|
|
|
/**
|
|
* Returns all label of itemResults related to the delvieryResults
|
|
*
|
|
* @param string $resultIdentifier
|
|
*
|
|
* @return array string uri
|
|
* */
|
|
public function getItemResultsFromDeliveryResult($resultIdentifier)
|
|
{
|
|
return $this->getImplementation()->getRelatedItemCallIds($resultIdentifier);
|
|
}
|
|
|
|
/**
|
|
* Returns all label of itemResults related to the delvieryResults
|
|
*
|
|
* @param string $resultIdentifier
|
|
*
|
|
* @return array string uri
|
|
* */
|
|
public function getTestsFromDeliveryResult($resultIdentifier)
|
|
{
|
|
return $this->getImplementation()->getRelatedTestCallIds($resultIdentifier);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param string $itemCallId
|
|
* @param array $itemVariables already retrieved variables
|
|
*
|
|
* @return array|null
|
|
* @throws \common_exception_NotFound
|
|
* @throws common_exception_Error
|
|
*/
|
|
public function getItemFromItemResult($itemCallId, $itemVariables = [])
|
|
{
|
|
$item = null;
|
|
|
|
if (empty($itemVariables)) {
|
|
$itemVariables = $this->getImplementation()->getVariables($itemCallId);
|
|
}
|
|
|
|
//get the first variable (item are the same in all)
|
|
$tmpItems = array_shift($itemVariables);
|
|
|
|
//get the first object
|
|
$itemUri = $tmpItems[0]->item;
|
|
|
|
$delivery = $this->getDeliveryByResultId($tmpItems[0]->deliveryResultIdentifier);
|
|
|
|
$itemIndexer = $this->getItemIndexer($delivery);
|
|
|
|
if (!is_null($itemUri)) {
|
|
$langItem = $itemIndexer->getItem($itemUri, $this->getResultLanguage());
|
|
$item = array_merge(is_array($langItem) ? $langItem : [], ['uriResource' => $itemUri]);
|
|
}
|
|
|
|
return $item;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param string $test
|
|
*
|
|
* @return \core_kernel_classes_Resource
|
|
*/
|
|
public function getVariableFromTest($test)
|
|
{
|
|
$returnTest = null;
|
|
$tests = $this->getImplementation()->getVariables($test);
|
|
|
|
//get the first variable (item are the same in all)
|
|
$tmpTests = array_shift($tests);
|
|
|
|
//get the first object
|
|
if (!is_null($tmpTests[0]->test)) {
|
|
$returnTest = new core_kernel_classes_Resource($tmpTests[0]->test);
|
|
}
|
|
|
|
return $returnTest;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param string $variableUri
|
|
*
|
|
* @return string
|
|
*
|
|
*/
|
|
public function getVariableCandidateResponse($variableUri)
|
|
{
|
|
return $this->getImplementation()->getVariableProperty($variableUri, 'candidateResponse');
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param string $variableUri
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getVariableBaseType($variableUri)
|
|
{
|
|
return $this->getImplementation()->getVariableProperty($variableUri, 'baseType');
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
* @param array $variablesData
|
|
*
|
|
* @return array ["nbResponses" => x,"nbCorrectResponses" => y,"nbIncorrectResponses" => z,"nbUnscoredResponses" =>
|
|
* a,"data" => $variableData]
|
|
*/
|
|
public function calculateResponseStatistics($variablesData)
|
|
{
|
|
$numberOfResponseVariables = 0;
|
|
$numberOfCorrectResponseVariables = 0;
|
|
$numberOfInCorrectResponseVariables = 0;
|
|
$numberOfUnscoredResponseVariables = 0;
|
|
foreach ($variablesData as $epoch => $itemVariables) {
|
|
foreach ($itemVariables as $key => $value) {
|
|
if ($key == \taoResultServer_models_classes_ResponseVariable::class) {
|
|
foreach ($value as $variable) {
|
|
$numberOfResponseVariables++;
|
|
switch ($variable['isCorrect']) {
|
|
case 'correct':
|
|
$numberOfCorrectResponseVariables++;
|
|
break;
|
|
case 'incorrect':
|
|
$numberOfInCorrectResponseVariables++;
|
|
break;
|
|
case 'unscored':
|
|
$numberOfUnscoredResponseVariables++;
|
|
break;
|
|
default:
|
|
common_Logger::w('The value ' . $variable['isCorrect'] . ' is not a valid value');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$stats = [
|
|
"nbResponses" => $numberOfResponseVariables,
|
|
"nbCorrectResponses" => $numberOfCorrectResponseVariables,
|
|
"nbIncorrectResponses" => $numberOfInCorrectResponseVariables,
|
|
"nbUnscoredResponses" => $numberOfUnscoredResponseVariables,
|
|
];
|
|
|
|
return $stats;
|
|
}
|
|
|
|
/**
|
|
* @param $itemCallId
|
|
* @param $itemVariables
|
|
*
|
|
* @return array item information ['uri' => xxx, 'label' => yyy]
|
|
*/
|
|
private function getItemInfos($itemCallId, $itemVariables)
|
|
{
|
|
$undefinedStr = __('unknown'); //some data may have not been submitted
|
|
|
|
try {
|
|
common_Logger::d("Retrieving related Item for item call " . $itemCallId . "");
|
|
$relatedItem = $this->getItemFromItemResult($itemCallId, $itemVariables);
|
|
} catch (common_Exception $e) {
|
|
common_Logger::w("The item call '" . $itemCallId . "' is not linked to a valid item. (deleted item ?)");
|
|
$relatedItem = null;
|
|
}
|
|
|
|
$itemIdentifier = $undefinedStr;
|
|
$itemLabel = $undefinedStr;
|
|
|
|
if ($relatedItem) {
|
|
$itemIdentifier = $relatedItem['uriResource'];
|
|
|
|
// check item info in internal cache
|
|
if (isset($this->itemInfoCache[$itemIdentifier])) {
|
|
common_Logger::t("Item info found in internal cache for item " . $itemIdentifier . "");
|
|
|
|
return $this->itemInfoCache[$itemIdentifier];
|
|
}
|
|
$itemLabel = $relatedItem['label'];
|
|
}
|
|
|
|
$item['itemModel'] = '---';
|
|
$item['label'] = $itemLabel;
|
|
$item['uri'] = $itemIdentifier;
|
|
|
|
// storing item info in memory to not hit the db for the same item again and again
|
|
// when method "getStructuredVariables" are called multiple times in the same request
|
|
if ($relatedItem) {
|
|
$this->itemInfoCache[$itemIdentifier] = $item;
|
|
}
|
|
|
|
return $item;
|
|
}
|
|
|
|
|
|
/**
|
|
* prepare a data set as an associative array, service intended to populate gui controller
|
|
*
|
|
* @param string $resultIdentifier
|
|
* @param string $filter 'lastSubmitted', 'firstSubmitted', 'all'
|
|
* @param array $wantedTypes ['taoResultServer_models_classes_ResponseVariable',
|
|
* 'taoResultServer_models_classes_OutcomeVariable',
|
|
* 'taoResultServer_models_classes_TraceVariable']
|
|
*
|
|
* @return array
|
|
* [
|
|
* 'epoch1' => [
|
|
* 'label' => Example_0_Introduction,
|
|
* 'uri' => http://tao.local/mytao.rdf#i1462952280695832,
|
|
* 'internalIdentifier' => item-1,
|
|
* 'taoResultServer_models_classes_Variable class name' => [
|
|
* 'Variable identifier 1' => [
|
|
* 'uri' => 1,
|
|
* 'var' => taoResultServer_models_classes_Variable object,
|
|
* 'isCorrect' => correct
|
|
* ],
|
|
* 'Variable identifier 2' => [
|
|
* 'uri' => 2,
|
|
* 'var' => taoResultServer_models_classes_Variable object,
|
|
* 'isCorrect' => unscored
|
|
* ]
|
|
* ]
|
|
* ]
|
|
* ]
|
|
*
|
|
* @throws common_exception_Error
|
|
*/
|
|
public function getStructuredVariables($resultIdentifier, $filter, $wantedTypes = [])
|
|
{
|
|
$itemCallIds = $this->getItemResultsFromDeliveryResult($resultIdentifier);
|
|
|
|
// splitting call ids into chunks to perform bulk queries
|
|
$itemCallIdChunks = array_chunk($itemCallIds, 50);
|
|
|
|
$itemVariables = [];
|
|
foreach ($itemCallIdChunks as $ids) {
|
|
$itemVariables = array_merge($itemVariables, $this->getVariablesFromObjectResult($ids, $wantedTypes));
|
|
}
|
|
|
|
return $this->structureItemVariables($itemVariables, $filter);
|
|
}
|
|
|
|
public function structureItemVariables($itemVariables, $filter)
|
|
{
|
|
usort($itemVariables, function ($a, $b) {
|
|
$variableA = $a[0]->variable;
|
|
$variableB = $b[0]->variable;
|
|
[$usec, $sec] = explode(" ", $variableA->getEpoch());
|
|
$floata = ((float)$usec + (float)$sec);
|
|
[$usec, $sec] = explode(" ", $variableB->getEpoch());
|
|
$floatb = ((float)$usec + (float)$sec);
|
|
|
|
if ((floatval($floata) - floatval($floatb)) > 0) {
|
|
return 1;
|
|
} elseif ((floatval($floata) - floatval($floatb)) < 0) {
|
|
return -1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
});
|
|
|
|
$attempts = $this->splitByItemAndAttempt($itemVariables, $filter);
|
|
$variablesByItem = [];
|
|
foreach ($attempts as $time => $variables) {
|
|
$variablesByItem[$time] = [];
|
|
foreach ($variables as $itemVariable) {
|
|
$variable = $itemVariable->variable;
|
|
$itemCallId = $itemVariable->callIdItem;
|
|
if ($variable->getIdentifier() == 'numAttempts') {
|
|
$variablesByItem[$time] = array_merge($variablesByItem[$time], $this->getItemInfos($itemCallId, [[$itemVariable]]));
|
|
$variablesByItem[$time]['attempt'] = $variable->getValue();
|
|
}
|
|
$variableDescription = [
|
|
'uri' => $itemVariable->uri,
|
|
'var' => $variable,
|
|
];
|
|
if ($variable instanceof \taoResultServer_models_classes_ResponseVariable && !is_null($variable->getCorrectResponse())) {
|
|
$variableDescription['isCorrect'] = $variable->getCorrectResponse() >= 1 ? 'correct' : 'incorrect';
|
|
} else {
|
|
$variableDescription['isCorrect'] = 'unscored';
|
|
}
|
|
|
|
// some dangerous assumptions about the call Id structure
|
|
$callIdParts = explode('.', $itemCallId);
|
|
$variablesByItem[$time]['internalIdentifier'] = $callIdParts[count($callIdParts) - 2];
|
|
$variablesByItem[$time][get_class($variable)][$variable->getIdentifier()] = $variableDescription;
|
|
}
|
|
}
|
|
|
|
return $variablesByItem;
|
|
}
|
|
|
|
public function splitByItemAndAttempt($itemVariables, $filter)
|
|
{
|
|
$sorted = [];
|
|
foreach ($this->splitByItem($itemVariables) as $variables) {
|
|
$itemCallId = current($variables)->callIdItem;
|
|
$byAttempt = $this->splitByAttempt($variables);
|
|
switch ($filter) {
|
|
case self::VARIABLES_FILTER_ALL:
|
|
foreach ($byAttempt as $time => $attempt) {
|
|
$sorted[$time . $itemCallId] = $attempt;
|
|
}
|
|
break;
|
|
case self::VARIABLES_FILTER_FIRST_SUBMITTED:
|
|
reset($byAttempt);
|
|
$sorted[key($byAttempt) . $itemCallId] = current($byAttempt);
|
|
break;
|
|
case self::VARIABLES_FILTER_LAST_SUBMITTED:
|
|
end($byAttempt);
|
|
$sorted[key($byAttempt) . $itemCallId] = current($byAttempt);
|
|
break;
|
|
default:
|
|
throw new \common_exception_InconsistentData('Unknown Filter ' . $filter);
|
|
}
|
|
}
|
|
ksort($sorted);
|
|
|
|
return $sorted;
|
|
}
|
|
|
|
/**
|
|
* Split item variables by item
|
|
*/
|
|
public function splitByItem($itemVariables)
|
|
{
|
|
$byItem = [];
|
|
foreach ($itemVariables as $variable) {
|
|
$itemVariable = $variable[0];
|
|
if (!is_null($itemVariable->callIdItem)) {
|
|
if (!isset($byItem[$itemVariable->callIdItem])) {
|
|
$byItem[$itemVariable->callIdItem] = [$itemVariable];
|
|
} else {
|
|
$byItem[$itemVariable->callIdItem][] = $itemVariable;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $byItem;
|
|
}
|
|
|
|
public function splitByAttempt($itemVariables)
|
|
{
|
|
return $this->getItemResponseSplitter()->splitObjByAttempt($itemVariables);
|
|
}
|
|
|
|
private function getItemResponseSplitter(): ItemResponseVariableSplitter
|
|
{
|
|
return $this->getServiceLocator()->get(ItemResponseVariableSplitter::class);
|
|
}
|
|
|
|
/**
|
|
* Filters the complex array structure for variable classes
|
|
*
|
|
* @param array $structure as defined by getStructuredVariables()
|
|
* @param array $filter classes to keep
|
|
*
|
|
* @return array as defined by getStructuredVariables()
|
|
*/
|
|
public function filterStructuredVariables(array $structure, array $filter)
|
|
{
|
|
$all = [
|
|
\taoResultServer_models_classes_ResponseVariable::class,
|
|
\taoResultServer_models_classes_OutcomeVariable::class,
|
|
\taoResultServer_models_classes_TraceVariable::class,
|
|
];
|
|
$toRemove = array_diff($all, $filter);
|
|
$filtered = $structure;
|
|
foreach ($filtered as $timestamp => $entry) {
|
|
foreach ($entry as $key => $value) {
|
|
if (in_array($key, $toRemove)) {
|
|
unset($filtered[$timestamp][$key]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $filtered;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param $resultIdentifier
|
|
* @param string $filter 'lastSubmitted', 'firstSubmitted'
|
|
*
|
|
* @return array ["nbResponses" => x,"nbCorrectResponses" => y,"nbIncorrectResponses" => z,"nbUnscoredResponses" =>
|
|
* a,"data" => $variableData]
|
|
* @deprecated
|
|
*/
|
|
public function getItemVariableDataStatsFromDeliveryResult($resultIdentifier, $filter = null)
|
|
{
|
|
$numberOfResponseVariables = 0;
|
|
$numberOfCorrectResponseVariables = 0;
|
|
$numberOfInCorrectResponseVariables = 0;
|
|
$numberOfUnscoredResponseVariables = 0;
|
|
$numberOfOutcomeVariables = 0;
|
|
$variablesData = $this->getItemVariableDataFromDeliveryResult($resultIdentifier, $filter);
|
|
foreach ($variablesData as $itemVariables) {
|
|
foreach ($itemVariables['sortedVars'] as $key => $value) {
|
|
if ($key == \taoResultServer_models_classes_ResponseVariable::class) {
|
|
foreach ($value as $variable) {
|
|
$variable = array_shift($variable);
|
|
$numberOfResponseVariables++;
|
|
switch ($variable['isCorrect']) {
|
|
case 'correct':
|
|
$numberOfCorrectResponseVariables++;
|
|
break;
|
|
case 'incorrect':
|
|
$numberOfInCorrectResponseVariables++;
|
|
break;
|
|
case 'unscored':
|
|
$numberOfUnscoredResponseVariables++;
|
|
break;
|
|
default:
|
|
common_Logger::w('The value ' . $variable['isCorrect'] . ' is not a valid value');
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
$numberOfOutcomeVariables++;
|
|
}
|
|
}
|
|
}
|
|
$stats = [
|
|
"nbResponses" => $numberOfResponseVariables,
|
|
"nbCorrectResponses" => $numberOfCorrectResponseVariables,
|
|
"nbIncorrectResponses" => $numberOfInCorrectResponseVariables,
|
|
"nbUnscoredResponses" => $numberOfUnscoredResponseVariables,
|
|
"data" => $variablesData,
|
|
];
|
|
|
|
return $stats;
|
|
}
|
|
|
|
/**
|
|
* prepare a data set as an associative array, service intended to populate gui controller
|
|
*
|
|
* @param string $resultIdentifier
|
|
* @param string $filter 'lastSubmitted', 'firstSubmitted'
|
|
*
|
|
* @return array
|
|
* @deprecated
|
|
*/
|
|
public function getItemVariableDataFromDeliveryResult($resultIdentifier, $filter)
|
|
{
|
|
$itemCallIds = $this->getItemResultsFromDeliveryResult($resultIdentifier);
|
|
$variablesByItem = [];
|
|
foreach ($itemCallIds as $itemCallId) {
|
|
$itemVariables = $this->getVariablesFromObjectResult($itemCallId);
|
|
|
|
$item = $this->getItemInfos($itemCallId, $itemVariables);
|
|
$itemIdentifier = $item['uri'];
|
|
$itemLabel = $item['label'];
|
|
$variablesByItem[$itemIdentifier]['itemModel'] = $item['itemModel'];
|
|
foreach ($itemVariables as $variable) {
|
|
//retrieve the type of the variable
|
|
$variableTemp = $variable[0]->variable;
|
|
$variableDescription = [];
|
|
$type = get_class($variableTemp);
|
|
|
|
|
|
$variableIdentifier = $variableTemp->getIdentifier();
|
|
|
|
$variableDescription["uri"] = $variable[0]->uri;
|
|
$variableDescription["var"] = $variableTemp;
|
|
|
|
if (method_exists($variableTemp, 'getCorrectResponse') && !is_null($variableTemp->getCorrectResponse())) {
|
|
if ($variableTemp->getCorrectResponse() >= 1) {
|
|
$variableDescription["isCorrect"] = "correct";
|
|
} else {
|
|
$variableDescription["isCorrect"] = "incorrect";
|
|
}
|
|
} else {
|
|
$variableDescription["isCorrect"] = "unscored";
|
|
}
|
|
|
|
$variablesByItem[$itemIdentifier]['sortedVars'][$type][$variableIdentifier][$variableTemp->getEpoch()] = $variableDescription;
|
|
$variablesByItem[$itemIdentifier]['label'] = $itemLabel;
|
|
}
|
|
}
|
|
//sort by epoch and filter
|
|
foreach ($variablesByItem as $itemIdentifier => $itemVariables) {
|
|
foreach ($itemVariables['sortedVars'] as $variableType => $variables) {
|
|
foreach ($variables as $variableIdentifier => $observation) {
|
|
uksort($variablesByItem[$itemIdentifier]['sortedVars'][$variableType][$variableIdentifier], "self::sortTimeStamps");
|
|
|
|
switch ($filter) {
|
|
case self::VARIABLES_FILTER_LAST_SUBMITTED:
|
|
{
|
|
$variablesByItem[$itemIdentifier]['sortedVars'][$variableType][$variableIdentifier] = [array_pop($variablesByItem[$itemIdentifier]['sortedVars'][$variableType][$variableIdentifier])];
|
|
break;
|
|
}
|
|
case self::VARIABLES_FILTER_FIRST_SUBMITTED:
|
|
{
|
|
$variablesByItem[$itemIdentifier]['sortedVars'][$variableType][$variableIdentifier] = [array_shift($variablesByItem[$itemIdentifier]['sortedVars'][$variableType][$variableIdentifier])];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $variablesByItem;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param string $a epoch
|
|
* @param string $b epoch
|
|
*
|
|
* @return number
|
|
*/
|
|
public static function sortTimeStamps($a, $b)
|
|
{
|
|
[$usec, $sec] = explode(" ", $a);
|
|
$floata = ((float)$usec + (float)$sec);
|
|
[$usec, $sec] = explode(" ", $b);
|
|
$floatb = ((float)$usec + (float)$sec);
|
|
|
|
//the callback is expecting an int returned, for the case where the difference is of less than a second
|
|
//intval(round(floatval($b) - floatval($a),1, PHP_ROUND_HALF_EVEN));
|
|
if ((floatval($floata) - floatval($floatb)) > 0) {
|
|
return 1;
|
|
} elseif ((floatval($floata) - floatval($floatb)) < 0) {
|
|
return -1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* return all variables linked to the delviery result and that are not linked to a particular itemResult
|
|
*
|
|
* @param string $resultIdentifier
|
|
* @param array $wantedTypes
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getVariableDataFromDeliveryResult($resultIdentifier, $wantedTypes = [\taoResultServer_models_classes_ResponseVariable::class, \taoResultServer_models_classes_OutcomeVariable::class, \taoResultServer_models_classes_TraceVariable::class])
|
|
{
|
|
$testCallIds = $this->getTestsFromDeliveryResult($resultIdentifier);
|
|
|
|
return $this->extractTestVariables($this->getVariablesFromObjectResult($testCallIds), $wantedTypes);
|
|
}
|
|
|
|
public function extractTestVariables(array $variableObjects, array $wantedTypes, string $filter = self::VARIABLES_FILTER_ALL)
|
|
{
|
|
$variableObjects = array_filter($variableObjects, static function (array $variableObject) use ($wantedTypes) {
|
|
$variable = current($variableObject);
|
|
|
|
return $variable->callIdItem === null && in_array(get_class($variable->variable), $wantedTypes, true);
|
|
});
|
|
|
|
$variableObjects = array_map(static function (array $variableObject) {
|
|
return current($variableObject)->variable;
|
|
}, $variableObjects);
|
|
|
|
usort($variableObjects, static function (
|
|
Variable $a,
|
|
Variable $b
|
|
) use ($filter) {
|
|
if ($filter === self::VARIABLES_FILTER_LAST_SUBMITTED) {
|
|
return $b->getCreationTime() - $a->getCreationTime();
|
|
}
|
|
|
|
return $a->getCreationTime() - $b->getCreationTime();
|
|
});
|
|
|
|
if (in_array($filter, [self::VARIABLES_FILTER_FIRST_SUBMITTED, self::VARIABLES_FILTER_LAST_SUBMITTED], true)) {
|
|
$uniqueVariableIdentifiers = [];
|
|
|
|
$variableObjects = array_filter($variableObjects, static function (
|
|
Variable $variable
|
|
) use (&$uniqueVariableIdentifiers) {
|
|
if (in_array($variable->getIdentifier(), $uniqueVariableIdentifiers, true)) {
|
|
return false;
|
|
}
|
|
$uniqueVariableIdentifiers[] = $variable->getIdentifier();
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
return $variableObjects;
|
|
}
|
|
|
|
/**
|
|
* returns the test taker related to the delivery
|
|
*
|
|
* @param string $resultIdentifier
|
|
*
|
|
* @return User
|
|
*/
|
|
public function getTestTaker($resultIdentifier)
|
|
{
|
|
$testTaker = $this->getImplementation()->getTestTaker($resultIdentifier);
|
|
/** @var \tao_models_classes_UserService $userService */
|
|
$userService = $this->getServiceLocator()->get(\tao_models_classes_UserService::SERVICE_ID);
|
|
$user = $userService->getUserById($testTaker);
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* Delete a delivery result
|
|
*
|
|
* @param string $resultIdentifier
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function deleteResult($resultIdentifier)
|
|
{
|
|
return $this->getImplementation()->deleteResult($resultIdentifier);
|
|
}
|
|
|
|
|
|
/**
|
|
* Return the file data associate to a variable
|
|
*
|
|
* @param $variableUri
|
|
*
|
|
* @return array file data
|
|
* @throws \core_kernel_persistence_Exception
|
|
*/
|
|
public function getVariableFile($variableUri)
|
|
{
|
|
//distinguish QTI file from other "file" base type
|
|
$baseType = $this->getVariableBaseType($variableUri);
|
|
|
|
switch ($baseType) {
|
|
case "file":
|
|
{
|
|
$value = $this->getVariableCandidateResponse($variableUri);
|
|
common_Logger::i(var_export(strlen($value), true));
|
|
$decodedFile = Datatypes::decodeFile($value);
|
|
common_Logger::i("FileName:");
|
|
common_Logger::i(var_export($decodedFile["name"], true));
|
|
common_Logger::i("Mime Type:");
|
|
common_Logger::i(var_export($decodedFile["mime"], true));
|
|
$file = [
|
|
"data" => $decodedFile["data"],
|
|
"mimetype" => "Content-type: " . $decodedFile["mime"],
|
|
"filename" => $decodedFile["name"]];
|
|
break;
|
|
}
|
|
default:
|
|
{ //legacy files
|
|
$file = [
|
|
"data" => $this->getVariableCandidateResponse($variableUri),
|
|
"mimetype" => "Content-type: text/xml",
|
|
"filename" => "trace.xml"];
|
|
}
|
|
}
|
|
|
|
return $file;
|
|
}
|
|
|
|
/**
|
|
* To be reviewed as it implies a dependency towards taoSubjects
|
|
*
|
|
* @param string $resultIdentifier
|
|
*
|
|
* @return array test taker properties values
|
|
*/
|
|
public function getTestTakerData($resultIdentifier)
|
|
{
|
|
$testTaker = $this->gettestTaker($resultIdentifier);
|
|
if (get_class($testTaker) == 'core_kernel_classes_Literal') {
|
|
return $testTaker;
|
|
} elseif (empty($testTaker)) {
|
|
return null;
|
|
} else {
|
|
$arrayOfProperties = [
|
|
OntologyRdfs::RDFS_LABEL,
|
|
GenerisRdf::PROPERTY_USER_LOGIN,
|
|
GenerisRdf::PROPERTY_USER_FIRSTNAME,
|
|
GenerisRdf::PROPERTY_USER_LASTNAME,
|
|
GenerisRdf::PROPERTY_USER_MAIL,
|
|
];
|
|
$propValues = [];
|
|
foreach ($arrayOfProperties as $property) {
|
|
$values = [];
|
|
foreach ($testTaker->getPropertyValues($property) as $value) {
|
|
$values[] = new \core_kernel_classes_Literal($value);
|
|
}
|
|
$propValues[$property] = $values;
|
|
}
|
|
}
|
|
|
|
return $propValues;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param \core_kernel_classes_Resource $delivery
|
|
*
|
|
* @return taoResultServer_models_classes_ReadableResultStorage
|
|
* @throws common_exception_Error
|
|
*/
|
|
public function getReadableImplementation(\core_kernel_classes_Resource $delivery)
|
|
{
|
|
/** @var ResultServerService $service */
|
|
$service = $this->getServiceLocator()->get(ResultServerService::SERVICE_ID);
|
|
$resultStorage = $service->getResultStorage($delivery);
|
|
|
|
/** NoResultStorage it's not readable only writable */
|
|
if ($resultStorage instanceof NoResultStorage) {
|
|
throw NoResultStorageException::create();
|
|
}
|
|
|
|
if (!$resultStorage instanceof taoResultServer_models_classes_ReadableResultStorage) {
|
|
throw new \common_exception_Error('The results storage it is not readable');
|
|
}
|
|
|
|
return $resultStorage;
|
|
}
|
|
|
|
/**
|
|
* Get the array of column names indexed by their unique column id.
|
|
*
|
|
* @param \tao_models_classes_table_Column[] $columns
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getColumnNames(array $columns)
|
|
{
|
|
return array_reduce($columns, function ($carry, \tao_models_classes_table_Column $column) {
|
|
/** @var ContextTypePropertyColumn|VariableColumn $column */
|
|
$carry[$this->getColumnId($column)] = $column->getLabel();
|
|
|
|
return $carry;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param \tao_models_classes_table_Column|ContextTypePropertyColumn|VariableColumn $column
|
|
*
|
|
* @return string
|
|
*/
|
|
private function getColumnId(\tao_models_classes_table_Column $column)
|
|
{
|
|
if ($column instanceof ContextTypePropertyColumn) {
|
|
$id = $column->getProperty()->getUri() . '_' . $column->getContextType();
|
|
} elseif ($column instanceof TestCenterColumn) {
|
|
$id = $column->getProperty()->getUri();
|
|
} else {
|
|
$id = $column->getContextIdentifier() . '_' . $column->getIdentifier();
|
|
}
|
|
|
|
return $id;
|
|
}
|
|
|
|
/**
|
|
* @param core_kernel_classes_Resource $delivery
|
|
* @param array $storageOptions
|
|
* @param array $filters
|
|
*
|
|
* @throws common_Exception
|
|
* @throws common_exception_Error
|
|
*/
|
|
public function getResultsByDelivery(\core_kernel_classes_Resource $delivery, array $storageOptions = [], array $filters = [])
|
|
{
|
|
//The list of delivery Results matching the current selection filters
|
|
$this->setImplementation($this->getReadableImplementation($delivery));
|
|
|
|
return $this->findResultsByDeliveryAndFilters($delivery, $filters, $storageOptions);
|
|
}
|
|
|
|
/**
|
|
* @param string $result
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function shouldResultBeSkipped($result)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param array $results
|
|
* @param $columns - columns to be exported
|
|
* @param $filter 'lastSubmitted' or 'firstSubmitted'
|
|
* @param array $filters
|
|
* @param int $offset
|
|
* @param int $limit
|
|
*
|
|
* @return array
|
|
* @throws common_Exception
|
|
* @throws common_exception_Error
|
|
*/
|
|
public function getCellsByResults(array $results, $columns, $filter, array $filters = [], $offset = 0, $limit = null)
|
|
{
|
|
$rows = [];
|
|
$dataProviderMap = $this->collectColumnDataProviderMap($columns);
|
|
|
|
if (!array_key_exists($offset, $results)) {
|
|
return null;
|
|
}
|
|
|
|
/** @var DeliveryExecution $result */
|
|
for ($i = $offset; $i < ($offset + $limit); $i++) {
|
|
if (!array_key_exists($i, $results)) {
|
|
break;
|
|
}
|
|
$result = $results[$i];
|
|
if ($this->shouldResultBeSkipped($result)) {
|
|
continue;
|
|
}
|
|
|
|
// initialize column data providers for single result
|
|
foreach ($dataProviderMap as $element) {
|
|
$element['instance']->prepare([$result], $element['columns']);
|
|
}
|
|
|
|
$cellData = [];
|
|
|
|
/** @var ContextTypePropertyColumn|VariableColumn $column */
|
|
foreach ($columns as $column) {
|
|
$cellKey = $this->getColumnId($column);
|
|
|
|
$cellData[$cellKey] = null;
|
|
if ($column instanceof TraceVariableColumn && count($column->getDataProvider()->getCache()) > 0) {
|
|
$cellData[$cellKey] = self::filterCellData($column->getDataProvider()->getValue(new core_kernel_classes_Resource($result), $column), self::VARIABLES_FILTER_TRACE);
|
|
} elseif (count($column->getDataProvider()->cache) > 0) {
|
|
// grade or response column values
|
|
$cellData[$cellKey] = self::filterCellData($column->getDataProvider()->getValue(new core_kernel_classes_Resource($result), $column), $filter);
|
|
|
|
continue;
|
|
} elseif ($column instanceof ContextTypePropertyColumn) {
|
|
// test taker or delivery property values
|
|
$resource = $column->isTestTakerType()
|
|
? $this->getTestTaker($result)
|
|
: $this->getDelivery($result);
|
|
|
|
$property = $column->getProperty();
|
|
if ($resource instanceof User) {
|
|
$property = $column->getProperty()->getUri();
|
|
}
|
|
$values = $resource->getPropertyValues($property);
|
|
|
|
$values = array_map(function ($value) use ($column) {
|
|
if (\common_Utils::isUri($value)) {
|
|
$value = (new core_kernel_classes_Resource($value))->getLabel();
|
|
} else {
|
|
$value = (string)$value;
|
|
}
|
|
|
|
if (in_array($column->getProperty()->getUri(), [DeliveryAssemblyService::PROPERTY_START, DeliveryAssemblyService::PROPERTY_END])) {
|
|
$value = tao_helpers_Date::displayeDate($value, tao_helpers_Date::FORMAT_VERBOSE);
|
|
}
|
|
|
|
return $value;
|
|
}, $values);
|
|
|
|
// if it's a guest test taker (it has no property values at all), let's display the uri as label
|
|
if ($column->isTestTakerType() && empty($values) && $column->getProperty()->getUri() == OntologyRdfs::RDFS_LABEL) {
|
|
switch (true) {
|
|
case $resource instanceof core_kernel_classes_Resource:
|
|
$values[] = $resource->getUri();
|
|
break;
|
|
case $resource instanceof User:
|
|
$values[] = $resource->getIdentifier();
|
|
break;
|
|
default:
|
|
throw new \Exception('Invalid type of resource property values.');
|
|
}
|
|
}
|
|
|
|
} elseif ($column instanceof TestCenterColumn) {
|
|
$property = $column->getProperty();
|
|
$testTaker = $this->getTestTaker($result);
|
|
$values = $testTaker->getPropertyValues($property);
|
|
|
|
$values = array_map(function ($value) use ($column, $result) {
|
|
$currentDelivery = $this->getDelivery($result);
|
|
|
|
return $column->getTestCenterLabel($value, $currentDelivery);
|
|
}, $values);
|
|
}
|
|
|
|
$cellData[$cellKey] = [self::filterCellData(implode(self::SEPARATOR, array_filter($values)), $filter)];
|
|
}
|
|
if ($this->filterData($cellData, $filters)) {
|
|
$this->convertDates($cellData);
|
|
$rows[] = [
|
|
'id' => $result,
|
|
'cell' => $cellData,
|
|
];
|
|
}
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* @param $columns
|
|
*
|
|
* @return array
|
|
*/
|
|
private function collectColumnDataProviderMap($columns)
|
|
{
|
|
$dataProviderMap = [];
|
|
foreach ($columns as $column) {
|
|
$dataProvider = $column->getDataProvider();
|
|
$found = false;
|
|
foreach ($dataProviderMap as $index => $element) {
|
|
if ($element['instance'] == $dataProvider) {
|
|
$dataProviderMap[$index]['columns'][] = $column;
|
|
$found = true;
|
|
}
|
|
}
|
|
if (!$found) {
|
|
$dataProviderMap[] = [
|
|
'instance' => $dataProvider,
|
|
'columns' => [$column],
|
|
];
|
|
}
|
|
}
|
|
|
|
return $dataProviderMap;
|
|
}
|
|
|
|
/**
|
|
* @param $data
|
|
*
|
|
* @throws common_Exception
|
|
*/
|
|
private function convertDates(&$data)
|
|
{
|
|
$sd = current($data[self::DELIVERY_EXECUTION_STARTED_AT]);
|
|
$data[self::DELIVERY_EXECUTION_STARTED_AT][0] = $sd ? tao_helpers_Date::displayeDate($sd) : '';
|
|
$ed = current($data[self::DELIVERY_EXECUTION_FINISHED_AT]);
|
|
$data[self::DELIVERY_EXECUTION_FINISHED_AT][0] = $ed ? tao_helpers_Date::displayeDate($ed) : '';
|
|
}
|
|
|
|
/**
|
|
* Check that data is apply to these filter params
|
|
*
|
|
* @param $row
|
|
* @param array $filters
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function filterData($row, array $filters)
|
|
{
|
|
$matched = true;
|
|
if (count($filters) && count(array_intersect(self::PERIODS, array_keys($filters)))) {
|
|
$startDate = current($row[self::DELIVERY_EXECUTION_STARTED_AT]);
|
|
$startTime = $startDate ? tao_helpers_Date::getTimeStamp($startDate) : 0;
|
|
|
|
$endDate = current($row[self::DELIVERY_EXECUTION_FINISHED_AT]);
|
|
$endTime = $endDate ? tao_helpers_Date::getTimeStamp($endDate) : 0;
|
|
|
|
if ($matched && array_key_exists(self::FILTER_START_FROM, $filters) && $filters[self::FILTER_START_FROM]) {
|
|
$matched = $startTime >= $filters[self::FILTER_START_FROM];
|
|
}
|
|
if ($matched && array_key_exists(self::FILTER_START_TO, $filters) && $filters[self::FILTER_START_TO]) {
|
|
$matched = $startTime <= $filters[self::FILTER_START_TO];
|
|
}
|
|
if ($matched && array_key_exists(self::FILTER_END_FROM, $filters) && $filters[self::FILTER_END_FROM]) {
|
|
$matched = $endTime >= $filters[self::FILTER_END_FROM];
|
|
}
|
|
if ($matched && array_key_exists(self::FILTER_END_TO, $filters) && $filters[self::FILTER_END_TO]) {
|
|
$matched = $endTime <= $filters[self::FILTER_END_TO];
|
|
}
|
|
}
|
|
|
|
return $matched;
|
|
}
|
|
|
|
/**
|
|
* @param array $deliveryUris
|
|
*
|
|
* @return int
|
|
* @throws common_exception_Error
|
|
*/
|
|
public function countResultByDelivery(array $deliveryUris)
|
|
{
|
|
return $this->getImplementation()->countResultByDelivery($deliveryUris);
|
|
}
|
|
|
|
/**
|
|
* @param array|string $resultsIds
|
|
*
|
|
* @return mixed
|
|
* @throws common_exception_Error
|
|
*/
|
|
protected function getResultsVariables($resultsIds)
|
|
{
|
|
return $this->getImplementation()->getDeliveryVariables($resultsIds);
|
|
}
|
|
|
|
/**
|
|
* Retrieve the different variables columns pertainign to the current selection of results
|
|
* Implementation note : it nalyses all the data collected to identify the different response variables submitted
|
|
* by the items in the context of activities
|
|
*/
|
|
public function getVariableColumns($delivery, $variableClassUri, array $filters = [], array $storageOptions = [])
|
|
{
|
|
$columns = [];
|
|
/** @var ResultServiceWrapper $resultServiceWrapper */
|
|
$resultServiceWrapper = $this->getServiceLocator()->get(ResultServiceWrapper::SERVICE_ID);
|
|
|
|
$this->setImplementation($this->getReadableImplementation($delivery));
|
|
//The list of delivery Results matching the current selection filters
|
|
$resultsIds = $this->findResultsByDeliveryAndFilters($delivery, $filters, $storageOptions);
|
|
|
|
//retrieveing all individual response variables referring to the selected delivery results
|
|
$itemIndex = $this->getItemIndexer($delivery);
|
|
|
|
//retrieving The list of the variables identifiers per activities defintions as observed
|
|
$variableTypes = [];
|
|
|
|
$resultLanguage = $this->getResultLanguage();
|
|
|
|
foreach (array_chunk($resultsIds, $resultServiceWrapper->getOption(ResultServiceWrapper::RESULT_COLUMNS_CHUNK_SIZE_OPTION)) as $resultsIdsItem) {
|
|
$selectedVariables = $this->getResultsVariables($resultsIdsItem);
|
|
foreach ($selectedVariables as $variable) {
|
|
$variable = $variable[0];
|
|
if ($this->isResultVariable($variable, $variableClassUri)) {
|
|
//variableIdentifier
|
|
$variableIdentifier = $variable->variable->getIdentifier();
|
|
if (!is_null($variable->item)) {
|
|
$uri = $variable->item;
|
|
$contextIdentifierLabel = $itemIndex->getItemValue($uri, $resultLanguage, 'label');
|
|
} else {
|
|
$uri = $variable->test;
|
|
$testData = $this->getTestMetadata($delivery, $variable->test);
|
|
$contextIdentifierLabel = $testData->getLabel();
|
|
}
|
|
|
|
$columnType = $this->defineTypeColumn($variable->variable);
|
|
|
|
$variableTypes[$uri . $variableIdentifier] = [
|
|
"contextLabel" => $contextIdentifierLabel,
|
|
"contextId" => $uri,
|
|
"variableIdentifier" => $variableIdentifier,
|
|
"columnType" => $columnType
|
|
];
|
|
|
|
if ($variable->variable instanceof \taoResultServer_models_classes_ResponseVariable
|
|
&& $variable->variable->getCorrectResponse() !== null) {
|
|
$variableTypes[$uri . $variableIdentifier . '_is_correct'] = [
|
|
"contextLabel" => $contextIdentifierLabel,
|
|
"contextId" => $uri,
|
|
"variableIdentifier" => $variableIdentifier . '_is_correct',
|
|
"columnType" => Variable::TYPE_VARIABLE_IDENTIFIER
|
|
];
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($variableTypes as $variableType) {
|
|
switch ($variableClassUri) {
|
|
case \taoResultServer_models_classes_OutcomeVariable::class:
|
|
$columns[] = new GradeColumn($variableType["contextId"], $variableType["contextLabel"], $variableType["variableIdentifier"], $variableType["columnType"]);
|
|
break;
|
|
case \taoResultServer_models_classes_ResponseVariable::class:
|
|
$columns[] = new ResponseColumn($variableType["contextId"], $variableType["contextLabel"], $variableType["variableIdentifier"], $variableType["columnType"]);
|
|
break;
|
|
default:
|
|
$columns[] = new ResponseColumn($variableType["contextId"], $variableType["contextLabel"], $variableType["variableIdentifier"], $variableType["columnType"]);
|
|
}
|
|
}
|
|
$arr = [];
|
|
foreach ($columns as $column) {
|
|
$arr[] = $column->toArray();
|
|
}
|
|
|
|
return $arr;
|
|
}
|
|
|
|
/**
|
|
* Check if provided variable is a result variable.
|
|
*
|
|
* @param $variable
|
|
* @param $variableClassUri
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function isResultVariable($variable, $variableClassUri)
|
|
{
|
|
$responseVariableClass = \taoResultServer_models_classes_ResponseVariable::class;
|
|
$outcomeVariableClass = \taoResultServer_models_classes_OutcomeVariable::class;
|
|
$class = isset($variable->class) ? $variable->class : get_class($variable->variable);
|
|
|
|
return (null != $variable->item || null != $variable->test)
|
|
&& (
|
|
$class == $outcomeVariableClass
|
|
&& $variableClassUri == $outcomeVariableClass
|
|
) || (
|
|
$class == $responseVariableClass
|
|
&& $variableClassUri == $responseVariableClass
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param Variable $variable
|
|
* @return string|null
|
|
*/
|
|
private function defineTypeColumn(Variable $variable)
|
|
{
|
|
$stringColumns = [
|
|
'SCORE',
|
|
'MAXSCORE',
|
|
'numAttempts',
|
|
'duration'
|
|
];
|
|
|
|
if (in_array($variable->getIdentifier(), $stringColumns) ||
|
|
($variable instanceof \taoResultServer_models_classes_ResponseVariable && $variable->getCorrectResponse() !== null)) {
|
|
return Variable::TYPE_VARIABLE_IDENTIFIER;
|
|
}
|
|
|
|
return $variable->getBaseType();
|
|
}
|
|
|
|
/**
|
|
* Sort the list of variables by filters
|
|
*
|
|
* List of variables contains the response for an interaction.
|
|
* Each attempts is an entry in $observationList
|
|
*
|
|
* 3 allowed filters: firstSubmitted, lastSubmitted, all, trace
|
|
*
|
|
* @param array $observationsList The list of variable values
|
|
* @param string $filterData The filter
|
|
* @param string $allDelimiter $delimiter to separate values in "all" filter context
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function filterCellData($observationsList, $filterData, $allDelimiter = '|')
|
|
{
|
|
//if the cell content is not an array with multiple entries, do not filter
|
|
if (!is_array($observationsList)) {
|
|
return $observationsList;
|
|
}
|
|
|
|
// Sort by TimeStamps
|
|
uksort($observationsList, "oat\\taoOutcomeUi\\model\\ResultsService::sortTimeStamps");
|
|
|
|
// Extract the value to make this array flat
|
|
$observationsList = array_map(function ($obs) {
|
|
return $obs[0];
|
|
}, $observationsList);
|
|
|
|
switch ($filterData) {
|
|
case self::VARIABLES_FILTER_LAST_SUBMITTED:
|
|
$value = array_pop($observationsList);
|
|
break;
|
|
|
|
case self::VARIABLES_FILTER_FIRST_SUBMITTED:
|
|
$value = array_shift($observationsList);
|
|
break;
|
|
|
|
case self::VARIABLES_FILTER_TRACE:
|
|
$value = json_encode(array_map(function ($value) {
|
|
return json_decode($value, true);
|
|
}, array_values($observationsList)));
|
|
break;
|
|
|
|
case self::VARIABLES_FILTER_ALL:
|
|
default:
|
|
$value = implode($allDelimiter, $observationsList);
|
|
break;
|
|
}
|
|
|
|
return [$value];
|
|
}
|
|
|
|
/**
|
|
* @param $delivery
|
|
*
|
|
* @return ItemCompilerIndex
|
|
* @throws common_exception_Error
|
|
*/
|
|
private function getItemIndexer($delivery)
|
|
{
|
|
$deliveryUri = $delivery->getUri();
|
|
if (!array_key_exists($deliveryUri, $this->indexerCache)) {
|
|
$directory = $this->getPrivateDirectory($delivery);
|
|
$indexer = $this->getDecompiledIndexer($directory);
|
|
$this->indexerCache[$deliveryUri] = $indexer;
|
|
}
|
|
$indexer = $this->indexerCache[$deliveryUri];
|
|
|
|
return $indexer;
|
|
}
|
|
|
|
/**
|
|
* @param $delivery
|
|
* @param $testUri
|
|
*
|
|
* @return ResourceCompiledMetadataHelper
|
|
*/
|
|
private function getTestMetadata(core_kernel_classes_Resource $delivery, $testUri)
|
|
{
|
|
if (isset($this->testMetadataCache[$testUri])) {
|
|
return $this->testMetadataCache[$testUri];
|
|
}
|
|
|
|
$compiledMetadataHelper = new ResourceCompiledMetadataHelper();
|
|
|
|
try {
|
|
$directory = $this->getPrivateDirectory($delivery);
|
|
$testMetadata = $this->loadTestMetadata($directory, $testUri);
|
|
if (!empty($testMetadata)) {
|
|
$compiledMetadataHelper->unserialize($testMetadata);
|
|
}
|
|
} catch (\Exception $e) {
|
|
\common_Logger::d('Ignoring data not found exception for Test Metadata');
|
|
}
|
|
|
|
$this->testMetadataCache[$testUri] = $compiledMetadataHelper;
|
|
|
|
return $this->testMetadataCache[$testUri];
|
|
}
|
|
|
|
/**
|
|
* Load test metadata from file. For deliveries without compiled file try to compile test metadata.
|
|
*
|
|
* @param tao_models_classes_service_StorageDirectory $directory
|
|
* @param string $testUri
|
|
*
|
|
* @return false|string
|
|
* @throws \FileNotFoundException
|
|
* @throws common_Exception
|
|
*/
|
|
private function loadTestMetadata(tao_models_classes_service_StorageDirectory $directory, $testUri)
|
|
{
|
|
try {
|
|
$testMetadata = $this->loadTestMetadataFromFile($directory);
|
|
} catch (FileNotFoundException $e) {
|
|
\common_Logger::d('Compiled test metadata file not found. Try to compile a new file.');
|
|
|
|
$this->compileTestMetadata($directory, $testUri);
|
|
$testMetadata = $this->loadTestMetadataFromFile($directory);
|
|
}
|
|
|
|
return $testMetadata;
|
|
}
|
|
|
|
/**
|
|
* Get teast metadata from file.
|
|
*
|
|
* @param tao_models_classes_service_StorageDirectory $directory
|
|
*
|
|
* @return false|string
|
|
*/
|
|
private function loadTestMetadataFromFile(tao_models_classes_service_StorageDirectory $directory)
|
|
{
|
|
return $directory->read(taoQtiTest_models_classes_QtiTestService::TEST_COMPILED_METADATA_FILENAME);
|
|
}
|
|
|
|
/**
|
|
* Compile test metadata and store into file.
|
|
* Added for backward compatibility for deliveries without compiled test metadata.
|
|
*
|
|
* @param tao_models_classes_service_StorageDirectory $directory
|
|
* @param $testUri
|
|
*
|
|
* @throws \FileNotFoundException
|
|
* @throws common_Exception
|
|
*/
|
|
private function compileTestMetadata(tao_models_classes_service_StorageDirectory $directory, $testUri)
|
|
{
|
|
$resource = $this->getResource($testUri);
|
|
|
|
/** @var ResourceMetadataCompilerInterface $resourceMetadataCompiler */
|
|
$resourceMetadataCompiler = $this->getServiceLocator()->get(ResourceJsonMetadataCompiler::SERVICE_ID);
|
|
$metadata = $resourceMetadataCompiler->compile($resource);
|
|
|
|
$directory->write(taoQtiTest_models_classes_QtiTestService::TEST_COMPILED_METADATA_FILENAME, json_encode($metadata));
|
|
}
|
|
|
|
/**
|
|
* @param string $directory
|
|
*
|
|
* @return QtiTestCompilerIndex
|
|
*/
|
|
private function getDecompiledIndexer(tao_models_classes_service_StorageDirectory $directory)
|
|
{
|
|
$itemIndex = new QtiTestCompilerIndex();
|
|
try {
|
|
$data = $directory->read(taoQtiTest_models_classes_QtiTestService::TEST_COMPILED_INDEX);
|
|
if ($data) {
|
|
$itemIndex->unserialize($data);
|
|
}
|
|
} catch (\Exception $e) {
|
|
\common_Logger::d('Ignoring file not found exception for Items Index');
|
|
}
|
|
|
|
return $itemIndex;
|
|
}
|
|
|
|
/**
|
|
* Should be changed if real result language would matter
|
|
*
|
|
* @return string
|
|
*/
|
|
private function getResultLanguage()
|
|
{
|
|
return DEFAULT_LANG;
|
|
}
|
|
|
|
/**
|
|
* @param $executionUri
|
|
*
|
|
* @return core_kernel_classes_Resource
|
|
* @throws \common_exception_NotFound
|
|
*/
|
|
private function getDeliveryByResultId($executionUri)
|
|
{
|
|
if (!array_key_exists($executionUri, $this->executionCache)) {
|
|
/** @var DeliveryExecution $execution */
|
|
$execution = $this->getServiceManager()->get(ServiceProxy::class)->getDeliveryExecution($executionUri);
|
|
$delivery = $execution->getDelivery();
|
|
$this->executionCache[$executionUri] = $delivery;
|
|
}
|
|
$delivery = $this->executionCache[$executionUri];
|
|
|
|
return $delivery;
|
|
}
|
|
|
|
/**
|
|
* @param $delivery
|
|
*
|
|
* @return array
|
|
* @throws common_exception_Error
|
|
*/
|
|
private function getDirectoryIds($delivery)
|
|
{
|
|
$runtime = $this->getServiceLocator()->get(RuntimeService::SERVICE_ID)->getRuntime($delivery);
|
|
$inputParameters = \tao_models_classes_service_ServiceCallHelper::getInputValues($runtime, []);
|
|
$directoryIds = explode('|', $inputParameters['QtiTestCompilation']);
|
|
|
|
return $directoryIds;
|
|
}
|
|
|
|
/**
|
|
* @param $delivery
|
|
*
|
|
* @return tao_models_classes_service_StorageDirectory
|
|
* @throws common_exception_Error
|
|
*/
|
|
private function getPrivateDirectory($delivery)
|
|
{
|
|
$directoryIds = $this->getDirectoryIds($delivery);
|
|
$fileStorage = \tao_models_classes_service_FileStorage::singleton();
|
|
|
|
return $fileStorage->getDirectoryById($directoryIds[0]);
|
|
}
|
|
|
|
/**
|
|
* @return mixed
|
|
*/
|
|
public function getServiceLocator()
|
|
{
|
|
if (!$this->serviceLocator) {
|
|
$this->setServiceLocator(ServiceManager::getServiceManager());
|
|
}
|
|
|
|
return $this->serviceLocator;
|
|
}
|
|
|
|
/**
|
|
* @param core_kernel_classes_Resource $delivery
|
|
* @param array $filters
|
|
* @param array $storageOptions
|
|
*
|
|
* @return array
|
|
* @throws common_exception_Error
|
|
*/
|
|
protected function findResultsByDeliveryAndFilters($delivery, array $filters = [], array $storageOptions = [])
|
|
{
|
|
$results = [];
|
|
foreach ($this->getImplementation()->getResultByDelivery([$delivery->getUri()], $storageOptions) as $result) {
|
|
$results[] = $result['deliveryResultIdentifier'];
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
}
|