Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.15% covered (success)
91.15%
103 / 113
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
TestPreviewMapper
91.15% covered (success)
91.15%
103 / 113
50.00% covered (danger)
50.00%
3 / 6
31.67
0.00% covered (danger)
0.00%
0 / 1
 map
93.59% covered (success)
93.59%
73 / 78
0.00% covered (danger)
0.00%
0 / 1
15.06
 updateStats
80.00% covered (warning)
80.00%
16 / 20
0.00% covered (danger)
0.00%
0 / 1
7.39
 getRouteItemAssessmentItemRefs
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getItemLabel
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 getService
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 isItemInformational
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
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) 2020 (original work) Open Assessment Technologies SA ;
19 */
20
21declare(strict_types=1);
22
23namespace oat\taoQtiTestPreviewer\models\test\mapper;
24
25use oat\generis\model\OntologyAwareTrait;
26use oat\oatbox\service\ConfigurableService;
27use oat\taoQtiItem\model\qti\Service;
28use oat\taoQtiTestPreviewer\models\test\TestPreviewConfig;
29use oat\taoQtiTestPreviewer\models\test\TestPreviewMap;
30use qtism\data\AssessmentItemRef;
31use qtism\data\AssessmentTest;
32use qtism\data\ExtendedAssessmentItemRef;
33use qtism\data\NavigationMode;
34use qtism\runtime\tests\Route;
35use qtism\runtime\tests\RouteItem;
36
37class TestPreviewMapper extends ConfigurableService implements TestPreviewMapperInterface
38{
39    use OntologyAwareTrait;
40
41    /** @var Service */
42    private $service;
43
44    public function map(AssessmentTest $test, Route $route, TestPreviewConfig $config): TestPreviewMap
45    {
46        $map = [
47            'scope' => 'test',
48            'parts' => []
49        ];
50
51        $routeItems = $route->getAllRouteItems();
52        $checkForInformationalItem = $config->get(TestPreviewConfig::CHECK_INFORMATIONAL);
53        $forceInformationalTitles = $config->get(TestPreviewConfig::REVIEW_FORCE_INFORMATION_TITLE);
54        $displaySubsectionTitle = $config->get(TestPreviewConfig::REVIEW_DISPLAY_SUBSECTION_TITLE) ?? true;
55
56        $map['title'] = $test->getTitle();
57        $map['identifier'] = $test->getIdentifier();
58        $map['className'] = $test->getQtiClassName();
59        $map['toolName'] = $test->getToolName();
60        $map['exclusivelyLinear'] = $test->isExclusivelyLinear();
61        $map['hasTimeLimits'] = $test->hasTimeLimits();
62
63        $offset = 0;
64        $offsetSection = 0;
65        $lastSection = null;
66
67        /** @var RouteItem $routeItem */
68        foreach ($routeItems as $routeItem) {
69            foreach ($this->getRouteItemAssessmentItemRefs($routeItem) as $itemRef) {
70                $occurrence = $routeItem->getOccurence();
71
72                $testPart = $routeItem->getTestPart();
73                $partId = $testPart->getIdentifier();
74                $navigationMode = $testPart->getNavigationMode();
75
76                if ($displaySubsectionTitle) {
77                    $section = $routeItem->getAssessmentSection();
78                } else {
79                    $sections = $routeItem->getAssessmentSections()->getArrayCopy();
80                    $section = $sections[0];
81                }
82
83                $sectionId = $section->getIdentifier();
84                $itemId = $itemRef->getIdentifier();
85
86                if ($lastSection !== $sectionId) {
87                    $offsetSection = 0;
88                    $lastSection = $sectionId;
89                }
90
91                $itemUri = $itemRef->getHref();
92
93                $allowSkipping = true;
94                $sessionControl = $itemRef->getItemSessionControl();
95                if ($sessionControl !== null) {
96                    $allowSkipping = $sessionControl->doesAllowSkipping();
97                }
98
99                $itemInfos = [
100                    'id' => $itemId,
101                    'uri' => $itemUri,
102                    'label' => $this->getItemLabel($itemUri),
103                    'position' => $offset,
104                    'occurrence' => $occurrence,
105                    'remainingAttempts' => -1,
106                    'answered' => 0,
107                    'flagged' => false,
108                    'viewed' => false,
109                    'categories' => $itemRef->getCategories()->getArrayCopy(),
110                    'allowSkipping' => $allowSkipping
111                ];
112
113                $isItemInformational = true;
114
115                if ($checkForInformationalItem) {
116                    $isItemInformational = $this->isItemInformational($itemInfos['categories'], $itemRef);
117                    $itemInfos['informational'] = $isItemInformational;
118                }
119
120                if ($itemRef->hasTimeLimits()) {
121                    $itemInfos['timeConstraint'] = null; //@TODO Implement as feature
122                }
123
124                if (!isset($map['parts'][$partId])) {
125                    $map['parts'][$partId]['id'] = $partId;
126                    $map['parts'][$partId]['label'] = $partId;
127                    $map['parts'][$partId]['position'] = $offset;
128                    $map['parts'][$partId]['isLinear'] = $navigationMode == NavigationMode::LINEAR;
129
130                    if ($testPart->hasTimeLimits()) {
131                        $map['parts'][$partId]['timeConstraint'] = null; //@TODO Implement as feature
132                    }
133                }
134
135                if (!isset($map['parts'][$partId]['sections'][$sectionId])) {
136                    $map['parts'][$partId]['sections'][$sectionId]['id'] = $sectionId;
137                    $map['parts'][$partId]['sections'][$sectionId]['label'] = $section->getTitle();
138                    // @TODO Implement as feature
139                    $map['parts'][$partId]['sections'][$sectionId]['isCatAdaptive'] = false;
140                    $map['parts'][$partId]['sections'][$sectionId]['position'] = $offset;
141
142                    if ($section->hasTimeLimits()) {
143                        // @TODO Implement as feature
144                        $map['parts'][$partId]['sections'][$sectionId]['timeConstraint'] = null;
145                    }
146                }
147
148                $map['parts'][$partId]['sections'][$sectionId]['items'][$itemId] = $itemInfos;
149
150                $this->updateStats($map, $itemInfos);
151                $this->updateStats($map['parts'][$partId], $itemInfos);
152                $this->updateStats($map['parts'][$partId]['sections'][$sectionId], $itemInfos);
153
154                $offset++;
155
156                if (!$forceInformationalTitles || ($forceInformationalTitles && !$isItemInformational)) {
157                    $offsetSection++;
158                }
159            }
160        }
161
162        return new TestPreviewMap($map);
163    }
164
165    private function updateStats(array &$target, array $itemInfos): void
166    {
167        if (!isset($target['stats'])) {
168            $target['stats'] = [
169                'questions' => 0,
170                'answered' => 0,
171                'flagged' => 0,
172                'viewed' => 0,
173                'total' => 0,
174                'questionsViewed' => 0,
175            ];
176        }
177
178        if (empty($itemInfos['informational'])) {
179            $target['stats']['questions']++;
180
181            if (!empty($itemInfos['answered'])) {
182                $target['stats']['answered']++;
183            }
184
185            if (!empty($itemInfos['viewed'])) {
186                $target['stats']['questionsViewed']++;
187            }
188        }
189
190        if (!empty($itemInfos['flagged'])) {
191            $target['stats']['flagged']++;
192        }
193
194        if (!empty($itemInfos['viewed'])) {
195            $target['stats']['viewed']++;
196        }
197
198        $target['stats']['total']++;
199    }
200
201    /**
202     * @param RouteItem $routeItem
203     *
204     * @return AssessmentItemRef[]
205     */
206    private function getRouteItemAssessmentItemRefs(RouteItem $routeItem): array
207    {
208        return [
209            $routeItem->getAssessmentItemRef()
210        ];
211    }
212
213    /**
214     * @param string $itemUri
215     *
216     * @return string
217     */
218    private function getItemLabel(string $itemUri): string
219    {
220        $resource = $this->getResource($itemUri);
221        $item = $this->getService()->getDataItemByRdfItem($resource);
222
223        return $item !== null
224            ? $item->getAttributeValue('title')
225            : $resource->getLabel();
226    }
227
228    /**
229     * @return Service
230     */
231    private function getService(): Service
232    {
233        if (!isset($this->service)) {
234            $this->service = $this->getServiceLocator()->get(Service::class);
235        }
236
237        return $this->service;
238    }
239
240    /**
241     * @param $categories
242     * @param AssessmentItemRef|ExtendedAssessmentItemRef $itemRef
243     * @return bool
244     */
245    private function isItemInformational($categories, $itemRef): bool
246    {
247        $additionalCheck = false;
248
249        if (method_exists($itemRef, 'getResponseDeclarations') && !count($itemRef->getResponseDeclarations())) {
250            $additionalCheck = true;
251        }
252        return $additionalCheck || in_array('x-tao-itemusage-informational', $categories, true);
253    }
254}