Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.77% covered (success)
90.77%
59 / 65
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ScalePreprocessor
90.77% covered (success)
90.77%
59 / 65
57.14% covered (warning)
57.14%
4 / 7
26.53
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 includeScaleObject
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
5
 getScaleRemoteList
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 findScaleByInterpretation
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 addCustomProperty
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
2.00
 isRemoteListScaleValid
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
5.05
 hasRequiredFields
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
8
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) 2025 (original work) Open Assessment Technologies SA;
19 */
20
21declare(strict_types=1);
22
23namespace oat\taoQtiItem\model\qti\metadata\exporter\scale;
24
25use DOMDocument;
26use DOMNodeList;
27use Monolog\Logger;
28use oat\oatbox\log\LoggerService;
29use oat\tao\model\Lists\Business\Domain\RemoteSourceContext;
30use oat\tao\model\Lists\Business\Service\RemoteSource;
31use oat\taoQtiItem\model\qti\metadata\exporter\CustomPropertiesManifestScanner;
32use tao_helpers_Uri;
33use Throwable;
34
35class ScalePreprocessor
36{
37    private RemoteSource $remoteSource;
38    private CustomPropertiesManifestScanner $manifestScanner;
39    private ?string $remoteListScale;
40    private LoggerService $loggerService;
41    private array $scaleCollection;
42
43    public function __construct(
44        RemoteSource $remoteSource,
45        CustomPropertiesManifestScanner $manifestScanner,
46        LoggerService $loggerService,
47        ?string $remoteListScale
48    ) {
49        $this->remoteSource = $remoteSource;
50        $this->manifestScanner = $manifestScanner;
51        $this->remoteListScale = $remoteListScale;
52        $this->loggerService = $loggerService;
53    }
54
55    public function includeScaleObject(DomDocument $manifest, DOMDocument $testDoc): void
56    {
57        if (!$this->isRemoteListScaleValid()) {
58            return;
59        }
60
61        $customProperties = $this->manifestScanner->getCustomProperties($manifest);
62        $outcomeDeclarations = $testDoc->getElementsByTagName('outcomeDeclaration');
63        foreach ($outcomeDeclarations as $outcomeDeclaration) {
64            $interpretation = tao_helpers_Uri::decode($outcomeDeclaration->getAttribute('interpretation'));
65            if ($this->findScaleByInterpretation($interpretation, $this->scaleCollection)) {
66                if ($this->manifestScanner->getCustomPropertyByUri($manifest, $interpretation)->length === 0) {
67                    $this->addCustomProperty(
68                        $manifest,
69                        $customProperties,
70                        array_filter($this->scaleCollection, function ($scale) use ($interpretation) {
71                            return $scale['uri'] === $interpretation;
72                        })
73                    );
74                }
75            }
76        }
77    }
78
79    public function getScaleRemoteList(): array
80    {
81        if (!$this->isRemoteListScaleValid()) {
82            return [];
83        }
84
85        return $this->scaleCollection;
86    }
87
88    private function findScaleByInterpretation(string $interpretation, array $scales): ?array
89    {
90        foreach ($scales as $scale) {
91            if ($scale['uri'] === $interpretation) {
92                return $scale;
93            }
94        }
95
96        return null;
97    }
98
99    private function addCustomProperty(DOMDocument $manifest, DOMNodeList $customProperties, array $scale): void
100    {
101        if (empty($scale)) {
102            return;
103        }
104
105        $scale = reset($scale);
106        $customProperties = $customProperties->item(0);
107        $newProperty = $manifest->createElement('property');
108        // Create and append child elements to the property
109        $uriElement = $manifest->createElement('uri', $scale['uri']);
110        $labelElement = $manifest->createElement('label', $scale['label']);
111        $domainElement = $manifest->createElement('domain', 'http://www.tao.lu/Ontologies/TAO.rdf#Scale');
112        $scaleElement = $manifest->createElement('scale', json_encode($scale['values']));
113        // Append all child elements to the new property
114        $newProperty->appendChild($uriElement);
115        $newProperty->appendChild($labelElement);
116        $newProperty->appendChild($domainElement);
117        $newProperty->appendChild($scaleElement);
118
119        $customProperties->appendChild($newProperty);
120    }
121
122    private function isRemoteListScaleValid(): bool
123    {
124        if (!$this->remoteListScale) {
125            $this->loggerService->debug('Environment variable REMOTE_LIST_SCALE is not defined');
126            return false;
127        }
128
129        $scaleCollection = iterator_to_array($this->remoteSource->fetchByContext(
130            new RemoteSourceContext([
131                RemoteSourceContext::PARAM_SOURCE_URL => $this->remoteListScale,
132                RemoteSourceContext::PARAM_PARSER => 'scale',
133            ])
134        ));
135
136        if (
137            is_array($scaleCollection)
138            && count($scaleCollection) > 0
139            && $this->hasRequiredFields($scaleCollection)
140        ) {
141            $this->scaleCollection = $scaleCollection;
142            return true;
143        }
144
145        $this->loggerService->warning('Remote list for scale is malformed');
146        return false;
147    }
148
149    private function hasRequiredFields(array $scaleCollection): bool
150    {
151        foreach ($scaleCollection as $scale) {
152            if (!isset($scale['uri'], $scale['label'], $scale['values'])) {
153                return false;
154            }
155            if (!is_array($scale['values'])) {
156                return false;
157            }
158
159            //in values array key and value are strings
160            foreach ($scale['values'] as $key => $value) {
161                if (!(is_string($key) || is_int($key)) || !is_string($value)) {
162                    return false;
163                }
164            }
165        }
166        return true;
167    }
168}