Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
2.86% covered (danger)
2.86%
4 / 140
0.00% covered (danger)
0.00%
0 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
SingleDeliveryResultsExporter
2.86% covered (danger)
2.86%
4 / 140
0.00% covered (danger)
0.00%
0 / 20
1506.74
0.00% covered (danger)
0.00%
0 / 1
 __construct
40.00% covered (danger)
40.00%
4 / 10
0.00% covered (danger)
0.00%
0 / 1
2.86
 getResultFormat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getResourceToExport
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setColumnsToExport
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getColumnsToExport
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 setVariableToExport
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 setFiltersToExport
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getFiltersToExport
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVariableToExport
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setStorageOptions
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getData
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 sortByStartDate
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 getCells
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 export
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
30
 getExporter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExportData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 saveToLocal
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getFileName
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 buildColumns
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
90
 getFileSystemService
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2017-2022 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
19 *
20 */
21
22declare(strict_types=1);
23
24namespace oat\taoOutcomeUi\model\export;
25
26use common_exception_InvalidArgumentType;
27use common_exception_NotFound;
28use core_kernel_classes_Resource;
29use oat\generis\model\OntologyAwareTrait;
30use oat\oatbox\filesystem\FileSystemService;
31use oat\tao\model\export\implementation\AbstractFileExporter;
32use oat\tao\model\export\implementation\CsvExporter;
33use oat\tao\model\taskQueue\Task\FilesystemAwareTrait;
34use oat\taoOutcomeUi\model\ResultsService;
35use oat\taoOutcomeUi\model\table\ContextTypePropertyColumn;
36use oat\taoOutcomeUi\model\table\VariableColumn;
37use oat\taoOutcomeUi\model\table\VariableDataProvider;
38use tao_models_classes_table_Column as TableColumn;
39use Zend\ServiceManager\ServiceLocatorAwareTrait;
40
41/**
42 * SingleDeliveryResultsExporter
43 *
44 * @author Gyula Szucs <gyula@taotesting.com>
45 */
46class SingleDeliveryResultsExporter implements ResultsExporterInterface
47{
48    use OntologyAwareTrait;
49    use ServiceLocatorAwareTrait;
50    use FilesystemAwareTrait;
51
52    public const RESULT_FORMAT = 'CSV';
53    private core_kernel_classes_Resource $delivery;
54    private ResultsService $resultsService;
55    private array $columnsToExport = [];
56
57    /**
58     * @var TableColumn[]
59     */
60    private array $builtColumns = [];
61
62    /**
63     * Which submitted variables are we exporting?
64     *
65     * Possible values:
66     *  - lastSubmitted (default)
67     *  - firstSubmitted
68     *
69     * @var string
70     */
71    private string $variableToExport = ResultsService::VARIABLES_FILTER_LAST_SUBMITTED;
72    private array $storageOptions = [];
73    private ColumnsProvider $columnsProvider;
74    private array $filters = [];
75    public const CHUNK_SIZE = 100;
76
77    /**
78     * @throws common_exception_NotFound
79     */
80    public function __construct(
81        core_kernel_classes_Resource $delivery,
82        ResultsService $resultsService,
83        ColumnsProvider $columnsProvider
84    ) {
85        $this->delivery = $this->getResource($delivery);
86
87        if (!$this->delivery->exists()) {
88            throw new common_exception_NotFound(
89                sprintf(
90                    'Results Exporter: delivery "%s" does not exist.',
91                    $this->delivery->getUri()
92                )
93            );
94        }
95
96        $this->resultsService = $resultsService;
97        $this->columnsProvider = $columnsProvider;
98    }
99
100    public function getResultFormat(): string
101    {
102        return static::RESULT_FORMAT;
103    }
104
105    /**
106     * @inheritdoc
107     */
108    public function getResourceToExport(): core_kernel_classes_Resource
109    {
110        return $this->delivery;
111    }
112
113    /**
114     * @inheritdoc
115     */
116    public function setColumnsToExport($columnsToExport)
117    {
118        $this->columnsToExport = $columnsToExport;
119
120        return $this;
121    }
122
123    /**
124     * @inheritdoc
125     */
126    public function getColumnsToExport(): array
127    {
128        if (empty($this->builtColumns)) {
129            if (!empty($this->columnsToExport)) {
130                $columns = $this->columnsToExport;
131            } else {
132                $variables = array_merge(
133                    $this->columnsProvider->getGradeColumns(),
134                    $this->columnsProvider->getResponseColumns()
135                );
136                $columns = array_merge(
137                    $this->columnsProvider->getTestTakerColumns(),
138                    $this->columnsProvider->getDeliveryColumns(),
139                    $variables,
140                    $this->columnsProvider->getDeliveryExecutionColumns()
141                );
142            }
143
144            // build column objects
145            $this->builtColumns = $this->buildColumns($columns);
146        }
147
148        return $this->builtColumns;
149    }
150
151    /**
152     * @inheritdoc
153     */
154    public function setVariableToExport($variableToExport)
155    {
156        $allowedFilters = [
157            ResultsService::VARIABLES_FILTER_ALL,
158            ResultsService::VARIABLES_FILTER_FIRST_SUBMITTED,
159            ResultsService::VARIABLES_FILTER_LAST_SUBMITTED,
160        ];
161        if (!in_array($variableToExport, $allowedFilters)) {
162            throw new \InvalidArgumentException(
163                sprintf(
164                    'Results Exporter: wrong submitted variable "%s"',
165                    $variableToExport
166                )
167            );
168        }
169
170        $this->variableToExport = $variableToExport;
171
172        return $this;
173    }
174
175    public function setFiltersToExport($filters)
176    {
177        $this->filters = $filters;
178        return $this;
179    }
180
181    public function getFiltersToExport()
182    {
183        return $this->filters;
184    }
185
186    /**
187     * @inheritdoc
188     */
189    public function getVariableToExport()
190    {
191        return $this->variableToExport;
192    }
193
194    /**
195     * @inheritdoc
196     */
197    public function setStorageOptions(array $storageOptions)
198    {
199        $this->storageOptions = $storageOptions;
200
201        return $this;
202    }
203
204    public function getData(): array
205    {
206        $results = $this->resultsService->getResultsByDelivery(
207            $this->getResourceToExport(),
208            $this->storageOptions,
209            $this->getFiltersToExport()
210        );
211
212        $cells = $this->resultsService->getCellsByResults(
213            $results,
214            $this->getColumnsToExport(),
215            $this->getVariableToExport(),
216            $this->getFiltersToExport(),
217            0,
218            PHP_INT_MAX
219        );
220
221        if ($cells === null) {
222            $cells = [];
223        }
224
225        // flattening data: only 'cell' is what we need
226        return array_map(function ($row) {
227            return $row['cell'];
228        }, $cells);
229    }
230
231    private function sortByStartDate(&$data)
232    {
233        usort($data, function ($a, $b) {
234            $bDate = $b[ColumnsProvider::LABEL_START_DELIVERY_EXECUTION] ?? null;
235            $aDate = $a[ColumnsProvider::LABEL_START_DELIVERY_EXECUTION] ?? null;
236            $startB = $bDate ? strtotime($bDate) : 0;
237            $startA = $aDate ? strtotime($aDate) : 0;
238            return $startB - $startA;
239        });
240        $data = array_reverse($data);
241    }
242
243    /**
244     * @param array $results
245     * @param int $offset
246     * @param null $limit
247     * @return array
248     * @throws \common_Exception
249     * @throws \common_exception_Error
250     */
251    private function getCells(array $results, int $offset = 0, $limit = null): ?array
252    {
253        $cells = $this->resultsService->getCellsByResults(
254            $results,
255            $this->getColumnsToExport(),
256            $this->getVariableToExport(),
257            $this->getFiltersToExport(),
258            $offset,
259            $limit
260        );
261
262        if ($cells === null) {
263            return null;
264        }
265        // flattening data: only 'cell' is what we need
266        return array_map(function ($row) {
267            return $row['cell'];
268        }, $cells);
269    }
270
271    /**
272     * @inheritdoc
273     */
274    public function export($destination = null): string
275    {
276        $columnNames = $this->resultsService->getColumnNames($this->getColumnsToExport());
277
278        $data = $this->resultsService->getResultsByDelivery(
279            $this->getResourceToExport(),
280            $this->storageOptions,
281            $this->getFiltersToExport()
282        );
283
284        $offset = 0;
285
286        $result = [];
287
288        // getCells() consumes much memory inside it, so let's collect cells iteratively
289        do {
290            $cells = $this->getCells($data, $offset, self::CHUNK_SIZE);
291            $offset += self::CHUNK_SIZE;
292            if ($cells === null) {
293                break;
294            }
295            foreach ($cells as $row) {
296                $rowResult = [];
297                foreach ($row as $rowKey => $rowVal) {
298                    $rowResult[$rowKey] = $rowVal[0];
299                }
300                $result[] = $rowResult;
301            }
302        } while ($cells !== null);
303
304        $this->sortByStartDate($result);
305
306        array_unshift($result, $columnNames);
307
308        $exporter = $this->getExporter($result);
309
310        unset($columnNames, $data, $result);
311
312        return is_null($destination)
313            ? $this->saveStringToStorage($this->getExportData($exporter), $this->getFileName())
314            : $this->saveToLocal($exporter, $destination);
315    }
316
317    protected function getExporter(array $result): AbstractFileExporter
318    {
319        return new CsvExporter($result);
320    }
321
322    /**
323     * @param CsvExporter $exporter
324     * @return string
325     * @throws common_exception_InvalidArgumentType
326     */
327    protected function getExportData(AbstractFileExporter $exporter): string
328    {
329        return $exporter->export(false, false, ',', '"', false);
330    }
331
332    /**
333     * @throws common_exception_InvalidArgumentType
334     */
335    private function saveToLocal(AbstractFileExporter $exporter, string $destination): string
336    {
337        $fullPath = realpath($destination) . DIRECTORY_SEPARATOR . $this->getFileName();
338
339        file_put_contents($fullPath, $this->getExportData($exporter));
340
341        return $fullPath;
342    }
343
344    private function getFileName(): string
345    {
346        return 'results_export_'
347            . strtolower(\tao_helpers_Display::textCleaner($this->delivery->getLabel(), '*'))
348            . '_'
349            . \tao_helpers_Uri::getUniqueId($this->delivery->getUri())
350            . '_'
351            . date('YmdHis') . rand(10, 99) //more unique name
352            . '.' . strtolower($this->getResultFormat());
353    }
354
355    /**
356     * Build the column objects from the provided array of decoded column values. For example:
357     *
358     * [
359     *  type = "oat\taoOutcomeUi\model\table\ContextTypePropertyColumn"
360     *  label = "Test Taker"
361     *  prop = "http://www.w3.org/2000/01/rdf-schema#label"
362     *  contextType = "test_taker"
363     * ]
364     * [
365     *  type = "oat\taoOutcomeUi\model\table\GradeColumn"
366     *  label = "Planets and moons-SCORE"
367     *  contextId = "http://taoplatform.loc/tao.rdf#i1499248290562399"
368     *  contextLabel = "Planets and moons"
369     *  variableIdentifier = "SCORE"
370     * ]
371     *
372     * @return TableColumn[]
373     */
374    private function buildColumns(array $columnsData): array
375    {
376        $columns = [];
377        $dataProvider = new VariableDataProvider();
378
379        foreach ($columnsData as $column) {
380            if (!isset($column['type']) || !is_subclass_of($column['type'], TableColumn::class)) {
381                throw new \RuntimeException('Column type not specified or wrong type provided');
382            }
383
384            $column = TableColumn::buildColumnFromArray($column);
385            if (!is_null($column)) {
386                if ($column instanceof VariableColumn) {
387                    $column->setDataProvider($dataProvider);
388                }
389
390                if ($column instanceof ContextTypePropertyColumn && $column->getProperty()->getUri() == RDFS_LABEL) {
391                    $column->label = $column->isTestTakerType() ? __('Test Taker') : __('Delivery');
392                }
393
394                $columns[] = $column;
395            }
396        }
397
398        return $columns;
399    }
400
401    /**
402     * @see FilesystemAwareTrait::getFileSystemService()
403     */
404    protected function getFileSystemService()
405    {
406        return $this->getServiceLocator()
407            ->get(FileSystemService::SERVICE_ID);
408    }
409}