tao-test/app/taoOutcomeUi/model/export/SingleDeliveryResultsExporter.php

460 lines
13 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) 2017 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
*
*/
namespace oat\taoOutcomeUi\model\export;
use oat\generis\model\OntologyAwareTrait;
use oat\oatbox\filesystem\FileSystemService;
use oat\tao\model\export\implementation\CsvExporter;
use oat\tao\model\taskQueue\Task\FilesystemAwareTrait;
use oat\taoOutcomeUi\model\ResultsService;
use oat\taoOutcomeUi\model\table\ContextTypePropertyColumn;
use oat\taoOutcomeUi\model\table\VariableColumn;
use oat\taoOutcomeUi\model\table\VariableDataProvider;
use tao_models_classes_table_Column;
use Zend\ServiceManager\ServiceLocatorAwareTrait;
/**
* SingleDeliveryResultsExporter
*
* @author Gyula Szucs <gyula@taotesting.com>
*/
class SingleDeliveryResultsExporter implements ResultsExporterInterface
{
use OntologyAwareTrait;
use ServiceLocatorAwareTrait;
use FilesystemAwareTrait;
public const RESULT_FORMAT = 'CSV';
/**
* @var \core_kernel_classes_Resource
*/
private $delivery;
/**
* @var ResultsService
*/
private $resultsService;
/**
* Metadata columns to be exported.
*
* @var array
*/
private $columnsToExport = [];
/**
* @var tao_models_classes_table_Column[]
*/
private $builtColumns = [];
/**
* Which submitted variables are we exporting?
*
* Possible values:
* - lastSubmitted (default)
* - firstSubmitted
*
* @var string
*/
private $variableToExport = ResultsService::VARIABLES_FILTER_LAST_SUBMITTED;
/**
* @var array
*/
private $storageOptions = [];
/**
* @var ColumnsProvider
*/
private $columnsProvider;
/**
* @var array
*/
private $filters = [];
const CHUNK_SIZE = 100;
/**
* @param string|\core_kernel_classes_Resource $delivery
* @param ResultsService $resultsService
* @param ColumnsProvider $columnsProvider
* @throws \common_exception_NotFound
*/
public function __construct($delivery, ResultsService $resultsService, ColumnsProvider $columnsProvider)
{
$this->delivery = $this->getResource($delivery);
if (!$this->delivery->exists()) {
throw new \common_exception_NotFound('Results Exporter: delivery "' . $this->delivery->getUri() . '" does not exist.');
}
$this->resultsService = $resultsService;
$this->columnsProvider = $columnsProvider;
}
/**
* @return string
*/
public function getResultFormat()
{
return static::RESULT_FORMAT;
}
/**
* @inheritdoc
*/
public function getResourceToExport()
{
return $this->delivery;
}
/**
* @inheritdoc
*/
public function setColumnsToExport($columnsToExport)
{
if (is_string($columnsToExport)) {
$columnsToExport = $this->decodeColumns($columnsToExport);
}
$this->columnsToExport = (array) $columnsToExport;
return $this;
}
/**
* @inheritdoc
*/
public function getColumnsToExport()
{
if (empty($this->builtColumns)) {
if (!empty($this->columnsToExport)) {
$columns = $this->columnsToExport;
} else {
$variables = array_merge($this->columnsProvider->getGradeColumns(), $this->columnsProvider->getResponseColumns());
usort($variables, function ($a, $b) {
return strcmp($a["label"], $b["label"]);
});
$columns = array_merge(
$this->columnsProvider->getTestTakerColumns(),
$this->columnsProvider->getDeliveryColumns(),
$variables
);
}
// Needed by the filter to filter by start and end date
// filtering will be done as a post-processing
$columns = array_merge($columns, $this->columnsProvider->getDeliveryExecutionColumns());
// build column objects
$this->builtColumns = $this->buildColumns($columns);
}
return $this->builtColumns;
}
/**
* @inheritdoc
*/
public function setVariableToExport($variableToExport)
{
$allowedFilters = [
ResultsService::VARIABLES_FILTER_ALL,
ResultsService::VARIABLES_FILTER_FIRST_SUBMITTED,
ResultsService::VARIABLES_FILTER_LAST_SUBMITTED,
];
if (!in_array($variableToExport, $allowedFilters)) {
throw new \InvalidArgumentException('Results Exporter: wrong submitted variable "' . $variableToExport . '"');
}
$this->variableToExport = $variableToExport;
return $this;
}
public function setFiltersToExport($filters)
{
$this->filters = $filters;
return $this;
}
public function getFiltersToExport()
{
return $this->filters;
}
/**
* @inheritdoc
*/
public function getVariableToExport()
{
return $this->variableToExport;
}
/**
* @inheritdoc
*/
public function setStorageOptions(array $storageOptions)
{
$this->storageOptions = $storageOptions;
return $this;
}
/**
* @return array
*/
public function getData()
{
$results = $this->resultsService->getResultsByDelivery(
$this->getResourceToExport(),
$this->storageOptions,
$this->getFiltersToExport()
);
$cells = $this->resultsService->getCellsByResults(
$results,
$this->getColumnsToExport(),
$this->getVariableToExport(),
$this->getFiltersToExport(),
0,
PHP_INT_MAX
);
if ($cells === null) {
$cells = [];
}
// flattening data: only 'cell' is what we need
return array_map(function ($row) {
return $row['cell'];
}, $cells);
}
private function sortByStartDate(&$data)
{
usort($data, function ($a, $b) {
$bDate = $b[ColumnsProvider::LABEL_START_DELIVERY_EXECUTION];
$aDate = $a[ColumnsProvider::LABEL_START_DELIVERY_EXECUTION];
$startB = $bDate ? strtotime($bDate) : 0;
$startA = $aDate ? strtotime($aDate) : 0;
return $startB - $startA;
});
$data = array_reverse($data);
}
/**
* @param array $results
* @param int $offset
* @param null $limit
* @return array
* @throws \common_Exception
* @throws \common_exception_Error
*/
private function getCells($results, $offset = 0, $limit = null)
{
$cells = $this->resultsService->getCellsByResults(
$results,
$this->getColumnsToExport(),
$this->getVariableToExport(),
$this->getFiltersToExport(),
$offset,
$limit
);
if ($cells === null) {
return null;
}
// flattening data: only 'cell' is what we need
return array_map(function ($row) {
return $row['cell'];
}, $cells);
}
/**
* @inheritdoc
*/
public function export($destination = null)
{
$columnNames = $this->resultsService->getColumnNames($this->getColumnsToExport());
$data = $this->resultsService->getResultsByDelivery(
$this->getResourceToExport(),
$this->storageOptions,
$this->getFiltersToExport()
);
$offset = 0;
$result = [];
// getCells() consumes much memory inside it, so let's collect cells iteratively
do {
$cells = $this->getCells($data, $offset, self::CHUNK_SIZE);
$offset += self::CHUNK_SIZE;
if ($cells === null) {
break;
}
foreach ($cells as $row) {
$rowResult = [];
foreach ($row as $rowKey => $rowVal) {
$rowResult[$columnNames[$rowKey]] = $rowVal[0];
}
$result[] = $rowResult;
}
} while ($cells !== null);
$this->sortByStartDate($result);
//If there are no executions yet, the file is exported but contains only the header
if (empty($result)) {
$result = [array_fill_keys($columnNames, '')];
}
$exporter = $this->getExporter($result);
unset($columnNames, $data, $result);
return is_null($destination)
? $this->saveStringToStorage($this->getExportData($exporter), $this->getFileName())
: $this->saveToLocal($exporter, $destination);
}
/**
* @param array $result
* @return CsvExporter
*/
protected function getExporter(array $result)
{
return new CsvExporter($result);
}
/**
* @param CsvExporter $exporter
* @return string
* @throws \common_exception_InvalidArgumentType
*/
protected function getExportData($exporter)
{
return $exporter->export(true, false);
}
/**
* @param CsvExporter $exporter
* @param string $destination
* @return string
* @throws \common_exception_InvalidArgumentType
*/
private function saveToLocal($exporter, $destination)
{
$fullPath = realpath($destination) . DIRECTORY_SEPARATOR . $this->getFileName();
file_put_contents($fullPath, $this->getExportData($exporter));
return $fullPath;
}
/**
* @return string
*/
private function getFileName()
{
return 'results_export_'
. strtolower(\tao_helpers_Display::textCleaner($this->delivery->getLabel(), '*'))
. '_'
. \tao_helpers_Uri::getUniqueId($this->delivery->getUri())
. '_'
. date('YmdHis') . rand(10, 99) //more unique name
. '.' . strtolower($this->getResultFormat());
}
/**
* Decode the JSON encoded columns.
*
* @param string $columnsJson
* @return array
*/
private function decodeColumns($columnsJson)
{
return ($columnsData = json_decode($columnsJson, true)) !== null && json_last_error() === JSON_ERROR_NONE
? $columnsData
: [];
}
/**
* Build the column objects from the provided array of decoded column values. For example:
*
* [
* type = "oat\taoOutcomeUi\model\table\ContextTypePropertyColumn"
* label = "Test Taker"
* prop = "http://www.w3.org/2000/01/rdf-schema#label"
* contextType = "test_taker"
* ]
* [
* type = "oat\taoOutcomeUi\model\table\GradeColumn"
* label = "Planets and moons-SCORE"
* contextId = "http://taoplatform.loc/tao.rdf#i1499248290562399"
* contextLabel = "Planets and moons"
* variableIdentifier = "SCORE"
* ]
*
* @param array $columnsData
* @return tao_models_classes_table_Column[]
*/
private function buildColumns($columnsData)
{
$columns = [];
$dataProvider = new VariableDataProvider();
foreach ($columnsData as $column) {
if (!isset($column['type']) || !is_subclass_of($column['type'], tao_models_classes_table_Column::class)) {
throw new \RuntimeException('Column type not specified or wrong type provided');
}
$column = tao_models_classes_table_Column::buildColumnFromArray($column);
if (!is_null($column)) {
if ($column instanceof VariableColumn) {
$column->setDataProvider($dataProvider);
}
if ($column instanceof ContextTypePropertyColumn && $column->getProperty()->getUri() == RDFS_LABEL) {
$column->label = $column->isTestTakerType() ? __('Test Taker') : __('Delivery');
}
$columns[] = $column;
}
}
return $columns;
}
/**
* @see FilesystemAwareTrait::getFileSystemService()
*/
protected function getFileSystemService()
{
return $this->getServiceLocator()
->get(FileSystemService::SERVICE_ID);
}
}