Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.64% covered (success)
96.64%
115 / 119
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ResultMapper
96.64% covered (success)
96.64%
115 / 119
70.00% covered (warning)
70.00%
7 / 10
41
0.00% covered (danger)
0.00%
0 / 1
 loadSource
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getContext
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
 getTestVariables
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 getItemVariables
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
4.06
 createVariables
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
7.01
 createVariableFromItemVariable
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 createOutcomeVariable
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
8
 createResponseVariable
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
 serializeValueCollection
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
6
 assertIsLoaded
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 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) 2019-2024 (original work) Open Assessment Technologies SA;
19 */
20
21namespace oat\taoResultServer\models\Mapper;
22
23use common_exception_InvalidArgumentType;
24use common_exception_NotImplemented;
25use DateTimeInterface;
26use LogicException;
27use oat\dtms\DateTime;
28use oat\oatbox\service\ConfigurableService;
29use qtism\common\enums\BaseType;
30use qtism\common\enums\Cardinality;
31use qtism\data\results\AssessmentResult;
32use qtism\data\results\ItemResult;
33use qtism\data\results\ItemVariable;
34use qtism\data\results\ItemVariableCollection;
35use qtism\data\results\ResultOutcomeVariable;
36use qtism\data\results\ResultResponseVariable;
37use qtism\data\results\ResultTemplateVariable;
38use qtism\data\results\SessionIdentifier;
39use qtism\data\state\Value;
40use qtism\data\state\ValueCollection;
41use taoResultServer_models_classes_OutcomeVariable;
42use taoResultServer_models_classes_ResponseVariable;
43use taoResultServer_models_classes_Variable;
44
45class ResultMapper extends ConfigurableService
46{
47    /** @var AssessmentResult */
48    protected $assessmentResult;
49
50    /**
51     * Initialize Mapper with qti-sqk AssessmentResult
52     *
53     * @param AssessmentResult $assessmentResult
54     * @return ResultMapper
55     */
56    public function loadSource(AssessmentResult $assessmentResult)
57    {
58        $this->assessmentResult = $assessmentResult;
59        return $this;
60    }
61
62    /**
63     * Get a formatted array of AssessmentContext object
64     *
65     * @return array
66     * @throws LogicException If AssessmentResult is not loaded
67     */
68    public function getContext()
69    {
70        $this->assertIsLoaded();
71
72        $context = $this->assessmentResult->getContext();
73
74        $sessionIdentifiers = [];
75        if ($context->hasSessionIdentifiers()) {
76            $contextSessionIdentifiers = iterator_to_array($context->getSessionIdentifiers());
77            /** @var SessionIdentifier $sessionIdentifier */
78            foreach ($contextSessionIdentifiers as $sessionIdentifier) {
79                $sessionIdentifiers[$sessionIdentifier->getIdentifier()->getValue()] = $sessionIdentifier
80                    ->getSourceID()
81                    ->getValue();
82            }
83        }
84
85        $sourcedId = '';
86        if ($context->hasSourcedId()) {
87            $sourcedId = $context->getSourcedId()->getValue();
88        }
89
90        return [
91            'sourcedId' => $sourcedId,
92            'sessionIdentifiers' => $sessionIdentifiers
93        ];
94    }
95
96    /**
97     * Get test variables of result
98     * - Loop on all test itemVariables
99     * - Build tao outcome/response variables from qti sk variables
100     *
101     * @return array
102     * @throws common_exception_NotImplemented
103     * @throws common_exception_InvalidArgumentType
104     * @throws LogicException If AssessmentResult is not loaded
105     */
106    public function getTestVariables()
107    {
108        $this->assertIsLoaded();
109
110        if (!$this->assessmentResult->hasTestResult()) {
111            return [];
112        }
113
114        $testResult = $this->assessmentResult->getTestResult();
115
116        if (!$testResult->hasItemVariables()) {
117            return [];
118        }
119
120        return [
121            $testResult->getIdentifier()->getValue() => $this->createVariables(
122                $testResult->getItemVariables(),
123                $testResult->getDatestamp()
124            )
125        ];
126    }
127
128    /**
129     * Get item variables of result
130     * - Loop on all itemResult itemVariables
131     * - Build tao outcome/response variables from qti sk variables
132     *
133     * @return array
134     * @throws common_exception_NotImplemented
135     * @throws common_exception_InvalidArgumentType
136     * @throws LogicException If AssessmentResult is not loaded
137     */
138    public function getItemVariables()
139    {
140        $this->assertIsLoaded();
141
142        if (!$this->assessmentResult->hasItemResults()) {
143            return [];
144        }
145
146        $itemResults = $this->assessmentResult->getItemResults();
147        $itemVariables = [];
148
149        /** @var ItemResult $itemResult */
150        foreach ($itemResults as $itemResult) {
151            if (!$itemResult->hasItemVariables()) {
152                continue;
153            }
154            $itemVariables[$itemResult->getIdentifier()->getValue()] = $this->createVariables(
155                $itemResult->getItemVariables(),
156                $itemResult->getDatestamp()
157            );
158        }
159
160        return $itemVariables;
161    }
162
163    /**
164     * Create tao variables from ItemVariableCollection
165     * - Based on itemVariable class, create associated tao variable
166     * - Set variable epoch with itemResult datetime
167     *
168     * @param ItemVariableCollection $itemVariables
169     * @param DateTime $datetime
170     * @return taoResultServer_models_classes_Variable[]
171     * @throws common_exception_NotImplemented If itemVariable is not outcome|response (e.g. template)
172     * @throws common_exception_InvalidArgumentType
173     */
174    protected function createVariables(ItemVariableCollection $itemVariables, DateTimeInterface $datetime)
175    {
176        if (!$datetime instanceof DateTime) {
177            $datetime = new DateTime($datetime->format(DateTime::ISO8601));
178        }
179
180        $variables = [];
181        $i = 0;
182        foreach ($itemVariables as $itemVariable) {
183            $i++;
184            switch (get_class($itemVariable)) {
185                case ResultOutcomeVariable::class:
186                    $variable = $this->createOutcomeVariable($itemVariable);
187                    break;
188
189                case ResultResponseVariable::class:
190                    $variable = $this->createResponseVariable($itemVariable);
191                    break;
192
193                case ResultTemplateVariable::class:
194                default:
195                    throw new common_exception_NotImplemented(
196                        'Qti Result parser cannot deals with "' . get_class($itemVariable) . '".'
197                    );
198            }
199
200            $datetime->modify('+' . $i . ' microsecond');
201            $variable->setEpoch(number_format($datetime->getMicroseconds(true), 8) . ' ' . $datetime->format('U'));
202            $variables[] = $variable;
203        }
204
205        return $variables;
206    }
207
208    /**
209     * Initialize a taoResultServer_models_classes_Variable based on ItemVariable
210     * - including identifier, cardinality, baseType
211     *
212     * @param ItemVariable $itemVariable
213     * @param taoResultServer_models_classes_Variable $variable
214     * @return taoResultServer_models_classes_Variable
215     * @throws common_exception_InvalidArgumentType
216     */
217    protected function createVariableFromItemVariable(
218        ItemVariable $itemVariable,
219        taoResultServer_models_classes_Variable $variable
220    ) {
221        $variable->setIdentifier((string) $itemVariable->getIdentifier());
222        $variable->setCardinality(Cardinality::getNameByConstant($itemVariable->getCardinality()));
223
224        if (null !== $itemVariable->getBaseType()) {
225            $variable->setBaseType(BaseType::getNameByConstant($itemVariable->getBaseType()));
226        }
227
228        return $variable;
229    }
230
231    /**
232     * Transfer all attributes of a ResultOutcomeVariable to taoResultServer_models_classes_OutcomeVariable
233     *
234     * @param ResultOutcomeVariable $itemVariable
235     * @return taoResultServer_models_classes_OutcomeVariable
236     * @throws common_exception_InvalidArgumentType
237     * @todo Implements Long Interpretation
238     * @todo Implements Mastery Value
239     * @todo Implements multiple Values
240     * @todo Implements View
241     * @todo Implements Interpretation
242     */
243    protected function createOutcomeVariable(ResultOutcomeVariable $itemVariable)
244    {
245        /** @var taoResultServer_models_classes_OutcomeVariable $variable */
246        $variable = $this->createVariableFromItemVariable(
247            $itemVariable,
248            new taoResultServer_models_classes_OutcomeVariable()
249        );
250
251        if ($itemVariable->hasValues()) {
252            $variable->setValue($this->serializeValueCollection($itemVariable->getValues()));
253        }
254
255        if ($itemVariable->hasNormalMaximum()) {
256            $variable->setNormalMaximum((string)  $itemVariable->getNormalMaximum()->getValue());
257        }
258
259        if ($itemVariable->hasNormalMinimum()) {
260            $variable->setNormalMinimum((string)  $itemVariable->getNormalMinimum()->getValue());
261        }
262
263        if ($itemVariable->hasView()) {
264            $this->logInfo('Qti Result Parser does not handle Outcome View');
265        }
266
267        if ($itemVariable->hasInterpretation()) {
268            $this->logInfo('Qti Result Parser does not handle Outcome Interpretation');
269        }
270
271        if ($itemVariable->hasLongInterpretation()) {
272            $this->logInfo('Qti Result Parser does not handle Outcome Long Interpretation');
273        }
274
275        if ($itemVariable->hasMasteryValue()) {
276            $this->logInfo('Qti Result Parser does not handle Outcome Mastery Value');
277        }
278
279        return $variable;
280    }
281
282    /**
283     * Transfer all attributes of a ResultResponseVariable to taoResultServer_models_classes_ResponseVariable
284     *
285     * @param ResultResponseVariable $itemVariable
286     * @return taoResultServer_models_classes_ResponseVariable
287     * @throws common_exception_InvalidArgumentType
288     * @todo Deals with multiple CandidateResponse Values
289     * @todo Deals with multiple CorrectResponse Values
290     * @todo Implements Choice Sequence
291     */
292    protected function createResponseVariable(ResultResponseVariable $itemVariable)
293    {
294        /** @var taoResultServer_models_classes_ResponseVariable $variable */
295        $variable = $this->createVariableFromItemVariable(
296            $itemVariable,
297            new taoResultServer_models_classes_ResponseVariable()
298        );
299
300        if ($itemVariable->getCandidateResponse()->hasValues()) {
301            $variable->setCandidateResponse(
302                $this->serializeValueCollection($itemVariable->getCandidateResponse()->getValues())
303            );
304        }
305
306        if ($itemVariable->hasCorrectResponse()) {
307            $variable->setCorrectResponse(
308                $this->serializeValueCollection($itemVariable->getCorrectResponse()->getValues())
309            );
310        }
311
312        if ($itemVariable->hasChoiceSequence()) {
313            $this->logInfo('Qti Result Parser does not handle Response ChoiceSequence');
314        }
315
316        return $variable;
317    }
318
319    /**
320     * Helper to serialize qti valueCollection to string
321     *
322     * @param ValueCollection $valueCollection
323     * @return string
324     */
325    protected function serializeValueCollection(ValueCollection $valueCollection)
326    {
327        $isRecord = false;
328        $values = [];
329        /** @var Value $value */
330        foreach ($valueCollection as $value) {
331            $fieldIdentifier = $value->getFieldIdentifier();
332            $baseType = $value->getBaseType();
333            $isRecord = $isRecord || $fieldIdentifier && $baseType !== -1;
334            $values[] = $isRecord
335                ? [
336                    'name' => $fieldIdentifier,
337                    'base' => [
338                        BaseType::getNameByConstant($baseType) => $value->getValue(),
339                    ]
340                ]
341                : $value->getValue();
342        }
343
344        return $isRecord
345            ? json_encode(['record' => $values])
346            : implode(';', $values);
347    }
348
349    /**
350     * @throws LogicException If assessmentResult is not loaded
351     */
352    protected function assertIsLoaded()
353    {
354        if (!$this->assessmentResult) {
355            throw new LogicException('Result parser is not loaded and cannot parse QTI XML result.');
356        }
357    }
358}