Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
96 / 96
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
TestCategoryRulesUtils
100.00% covered (success)
100.00%
96 / 96
100.00% covered (success)
100.00%
10 / 10
22
100.00% covered (success)
100.00%
1 / 1
 extractCategories
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 appendNumberOfItemsVariable
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 appendNumberCorrectVariable
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 appendTotalScoreVariable
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 appendNumberCorrectOutcomeProcessing
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 appendTotalScoreOutcomeProcessing
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
2
 appendOutcomeDeclarationToTest
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
 countNumberOfItemsWithCategory
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 isVariableSetOutcomeValueTarget
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 appendOutcomeRule
100.00% covered (success)
100.00%
12 / 12
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) 2016 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
19 *
20 */
21
22namespace oat\taoQtiTest\models;
23
24use qtism\common\enums\BaseType;
25use qtism\common\enums\Cardinality;
26use qtism\common\collections\IdentifierCollection;
27use qtism\data\AssessmentTest;
28use qtism\data\state\OutcomeDeclaration;
29use qtism\data\state\DefaultValue;
30use qtism\data\state\ValueCollection;
31use qtism\data\state\Value;
32use qtism\data\rules\SetOutcomeValue;
33use qtism\data\rules\OutcomeRuleCollection;
34use qtism\data\rules\OutcomeRule;
35use qtism\data\processing\OutcomeProcessing;
36use qtism\data\expressions\ExpressionCollection;
37use qtism\data\expressions\NumberCorrect;
38use qtism\data\expressions\TestVariables;
39use qtism\data\expressions\operators\Sum;
40
41/**
42 * Utility class for Test Category Rules Generation.
43 *
44 * Provides utility methods supporting Test Category Rules Generation process.
45 */
46class TestCategoryRulesUtils
47{
48    public const NUMBER_ITEMS_SUFFIX = '_CATEGORY_NUMBER_ITEMS';
49    public const NUMBER_CORRECT_SUFFIX = '_CATEGORY_NUMBER_CORRECT';
50    public const TOTAL_SCORE_SUFFIX = '_CATEGORY_TOTAL_SCORE';
51
52    /**
53     * Extract all categories from a given QTI-SDK AssessmentTest object.
54     *
55     * This method will extract all category identifiers found as assessmentItemRef
56     * category identifiers from a given $test object.
57     *
58     * An optional $exclusion array can be given as a second parameter. Each of the Pearl
59     * Compatible Regular Expression will be applied on the categories to be extracted. If
60     * one of them matches a category, it will be excluded from the extraction process.
61     *
62     * Identifiers returned will be unique.
63     *
64     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
65     * @param array $exclusion (optional) An optional array of Pearl Compatible Regular Expressions.
66     * @return array An array of QTI category identifiers.
67     */
68    public static function extractCategories(AssessmentTest $test, array $exclusion = [])
69    {
70        $categories = [];
71
72        $assessmentItemRefs = $test->getComponentsByClassName('assessmentItemRef');
73        foreach ($assessmentItemRefs as $assessmentItemRef) {
74            $assessmentItemRefCategories = $assessmentItemRef->getCategories()->getArrayCopy();
75
76            $count = count($assessmentItemRefCategories);
77            for ($i = 0; $i < $count; $i++) {
78                foreach ($exclusion as $excl) {
79                    if (@preg_match($excl, $assessmentItemRefCategories[$i]) === 1) {
80                        unset($assessmentItemRefCategories[$i]);
81                        break;
82                    }
83                }
84            }
85
86            $categories = array_merge($categories, $assessmentItemRefCategories);
87        }
88
89        return array_unique($categories);
90    }
91
92    /**
93     * Append a variable dedicated to counting number of items related to a given category.
94     *
95     * This method will append a QTI outcome variable dedicated to count the number of items
96     * related to a given QTI $category, to a given QTI $test.
97     *
98     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
99     * @param string $category A QTI category identifier.
100     * @return string the identifier of the created QTI outcome variable.
101     */
102    public static function appendNumberOfItemsVariable(AssessmentTest $test, $category)
103    {
104        $varName = strtoupper($category) . self::NUMBER_ITEMS_SUFFIX;
105        self::appendOutcomeDeclarationToTest(
106            $test,
107            $varName,
108            BaseType::INTEGER,
109            self::countNumberOfItemsWithCategory($test, $category)
110        );
111
112        return $varName;
113    }
114
115    /**
116     * Append a variable dedicated to counting number of correctly responded items related to a given category.
117     *
118     * This method will append a QTI outcome variable dedicated to count the number of items that are correctly
119     * responded related to a given QTI $category, to a given QTI $test.
120     *
121     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
122     * @param string $category A QTI category identifier.
123     * @return string the identifier of the created QTI outcome variable.
124     */
125    public static function appendNumberCorrectVariable(AssessmentTest $test, $category)
126    {
127        $varName = strtoupper($category) . self::NUMBER_CORRECT_SUFFIX;
128        self::appendOutcomeDeclarationToTest($test, $varName, BaseType::INTEGER);
129
130        return $varName;
131    }
132
133    /**
134     * Append a variable dedicated to store the total score items related to a given category.
135     *
136     * This method will append a QTI outcome variable dedicated to store the total score of items
137     * related to a given QTI $category, to a given QTI $test.
138     *
139     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
140     * @param string $category A QTI category identifier.
141     * @return string the identifier of the created QTI outcome variable.
142     */
143    public static function appendTotalScoreVariable(AssessmentTest $test, $category)
144    {
145        $varName = strtoupper($category) . self::TOTAL_SCORE_SUFFIX;
146        self::appendOutcomeDeclarationToTest($test, $varName, BaseType::FLOAT);
147
148        return $varName;
149    }
150
151    /**
152     * Append the outcome processing rules to populate an outcome variable with the number of items correctly responded
153     * related to a given category.
154     *
155     * This method will append a QTI outcome processing to a given QTI-SDK AssessmentTest $test, dedicated to count
156     * the number of correctly responded items related to a given QTI $category.
157     *
158     * In case of an outcome processing rule targetting a variable name $varName already exists in the test, the outcome
159     * processing rule is not appended to the test.
160     *
161     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
162     * @param string $category A QTI category identifier.
163     * @param string $varName The QTI identifier of the variable to be populated by the outcome processing rule.
164     */
165    public static function appendNumberCorrectOutcomeProcessing(AssessmentTest $test, $category, $varName)
166    {
167        if (self::isVariableSetOutcomeValueTarget($test, $varName) === false) {
168            $numberCorrectExpression = new NumberCorrect();
169            $numberCorrectExpression->setIncludeCategories(
170                new IdentifierCollection(
171                    [$category]
172                )
173            );
174
175            $setOutcomeValue = new SetOutcomeValue(
176                $varName,
177                $numberCorrectExpression
178            );
179
180            self::appendOutcomeRule($test, $setOutcomeValue);
181        }
182    }
183
184    /**
185     * Append the outcome processing rules to populate an outcome variable with total score of items related to
186     * a given category.
187     *
188     * This method will append a QTI outcome processing to a given QTI-SDK AssessmentTest $test, dedicated to store
189     * the total score of items related to a given QTI $category.
190     *
191     * In case of the $weightIdentifier argument is given, the score will consider weights defined at the
192     * assessmentItemRef level identified by $weightIdentifier. Otherwise, no weights are taken into account while
193     * computing total scores.
194     *
195     * In case of an outcome processing rule targetting a variable name $varName already exists in the test, the outcome
196     * processing rule is not appended to the test.
197     *
198     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
199     * @param string $category A QTI category identifier.
200     * @param string $varName The QTI identifier of the variable to be populated by the outcome processing rule.
201     * @param string $scoreIdentifier (optional) An optional QTI identifier to be used as items' score variable
202     *                                (defaults to "SCORE").
203     * @param string $weightIdentifier (optional) An optional QTI identifier to be used as items' weight to be
204     *                                 considered for total score. (defaults to empty string).
205     */
206    public static function appendTotalScoreOutcomeProcessing(
207        AssessmentTest $test,
208        $category,
209        $varName,
210        $scoreVariableIdentifier = 'SCORE',
211        $weightIdentifier = ''
212    ) {
213        if (self::isVariableSetOutcomeValueTarget($test, $varName) === false) {
214            $testVariablesExpression = new TestVariables($scoreVariableIdentifier, BaseType::FLOAT);
215            $testVariablesExpression->setWeightIdentifier($weightIdentifier);
216            $testVariablesExpression->setIncludeCategories(
217                new IdentifierCollection(
218                    [$category]
219                )
220            );
221
222            $setOutcomeValue = new SetOutcomeValue(
223                $varName,
224                new Sum(
225                    new ExpressionCollection(
226                        [
227                            $testVariablesExpression
228                        ]
229                    )
230                )
231            );
232
233            self::appendOutcomeRule($test, $setOutcomeValue);
234        }
235    }
236
237    /**
238     * Append an outcome declaration to a test.
239     *
240     * This method will append an outcome declaration with identifier $varName, single cardinality
241     * to a given QTI-SDK AssessmentTest $test object.
242     *
243     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
244     * @param string $varName The variable name to be used for the outcome declaration.
245     * @param integer $baseType A QTI-SDK Base Type.
246     * @param mixed (optional) A default value for the variable.
247     */
248    public static function appendOutcomeDeclarationToTest(
249        AssessmentTest $test,
250        $varName,
251        $baseType,
252        $defaultValue = null
253    ) {
254        $outcomeDeclarations = $test->getOutcomeDeclarations();
255        $outcome = new OutcomeDeclaration($varName, $baseType, Cardinality::SINGLE);
256
257        if ($defaultValue !== null) {
258            $outcome->setDefaultValue(
259                new DefaultValue(
260                    new ValueCollection(
261                        [
262                            new Value(
263                                $defaultValue,
264                                $baseType
265                            )
266                        ]
267                    )
268                )
269            );
270        }
271
272        $outcomeDeclarations->attach($outcome);
273    }
274
275    /**
276     * Count the number of items in a test that belong to a given category.
277     *
278     * This method will count the number of items in a test that belong to a given category.
279     *
280     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
281     * @param string $category a QTI category identifier.
282     * @return integer The number of items that belong to $category.
283     */
284    public static function countNumberOfItemsWithCategory(AssessmentTest $test, $category)
285    {
286        $count = 0;
287
288        $assessmentItemRefs = $test->getComponentsByClassName('assessmentItemRef');
289        foreach ($assessmentItemRefs as $assessmentItemRef) {
290            if (in_array($category, $assessmentItemRef->getCategories()->getArrayCopy()) === true) {
291                $count++;
292            }
293        }
294
295        return $count;
296    }
297
298    /**
299     * Know whether or not a variable is the target of an existing setOutcomeValue QTI rule.
300     *
301     * This method enables the client code to know whether or not a variable with identifier
302     * $varName is the target of an existing setOutcomeValue QTI rule with a given
303     * AssessmentTest $test object.
304     *
305     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
306     * @param string $varName A QTI variable identifier.
307     * @return boolean
308     */
309    public static function isVariableSetOutcomeValueTarget(AssessmentTest $test, $varName)
310    {
311        $setOutcomeValues = $test->getComponentsByClassName('setOutcomeValue');
312        foreach ($setOutcomeValues as $setOutcomeValue) {
313            if ($setOutcomeValue->getIdentifier() === $varName) {
314                return true;
315            }
316        }
317
318        return false;
319    }
320
321    /**
322     * Append a QTI-SDK OutcomeRule object in an AssessmentTest's OutcomeProcessing.
323     *
324     * In case of no OutcomeProcessing is set yet for the AssessmentTest $test object,
325     * it will be automatically created, with the OutcomeRule $rule as its first
326     * rule. Otherwise, the OutcomeRule $rule is simply appended to the existing
327     * OutcomeProcessing object.
328     *
329     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
330     * @param qtism\data\rules\OutcomeRule A QTI-SDK OutcomeRule object.
331     */
332    private static function appendOutcomeRule(AssessmentTest $test, OutcomeRule $rule)
333    {
334        $outcomeProcessing = $test->getOutcomeProcessing();
335        if ($outcomeProcessing === null) {
336            $test->setOutcomeProcessing(
337                new OutcomeProcessing(
338                    new OutcomeRuleCollection(
339                        [
340                            $rule
341                        ]
342                    )
343                )
344            );
345        } else {
346            $outcomeProcessing->getOutcomeRules()[] = $rule;
347        }
348    }
349}