| 1 | efrain | 1 | <?php
 | 
        
           |  |  | 2 | // This file is part of Moodle - http://moodle.org/
 | 
        
           |  |  | 3 | //
 | 
        
           |  |  | 4 | // Moodle is free software: you can redistribute it and/or modify
 | 
        
           |  |  | 5 | // it under the terms of the GNU General Public License as published by
 | 
        
           |  |  | 6 | // the Free Software Foundation, either version 3 of the License, or
 | 
        
           |  |  | 7 | // (at your option) any later version.
 | 
        
           |  |  | 8 | //
 | 
        
           |  |  | 9 | // Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | 
        
           |  |  | 16 |   | 
        
           |  |  | 17 | /**
 | 
        
           |  |  | 18 |  * Represents an ordering question.
 | 
        
           |  |  | 19 |  *
 | 
        
           |  |  | 20 |  * @package    qtype_ordering
 | 
        
           |  |  | 21 |  * @copyright  2013 Gordon Bateson (gordon.bateson@gmail.com)
 | 
        
           |  |  | 22 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 23 |  */
 | 
        
           |  |  | 24 | class qtype_ordering_question extends question_graded_automatically {
 | 
        
           |  |  | 25 |   | 
        
           |  |  | 26 |     /** Select all answers */
 | 
        
           |  |  | 27 |     const SELECT_ALL = 0;
 | 
        
           |  |  | 28 |     /** Select random set of answers */
 | 
        
           |  |  | 29 |     const SELECT_RANDOM = 1;
 | 
        
           |  |  | 30 |     /** Select contiguous subset of answers */
 | 
        
           |  |  | 31 |     const SELECT_CONTIGUOUS = 2;
 | 
        
           |  |  | 32 |   | 
        
           |  |  | 33 |     /** Show answers in vertical list */
 | 
        
           |  |  | 34 |     const LAYOUT_VERTICAL = 0;
 | 
        
           |  |  | 35 |     /** Show answers in one horizontal line */
 | 
        
           |  |  | 36 |     const LAYOUT_HORIZONTAL = 1;
 | 
        
           |  |  | 37 |   | 
        
           |  |  | 38 |     /** The minimum number of items to create a subset */
 | 
        
           |  |  | 39 |     const MIN_SUBSET_ITEMS = 2;
 | 
        
           |  |  | 40 |   | 
        
           |  |  | 41 |     /** Default value for numberingstyle */
 | 
        
           |  |  | 42 |     const NUMBERING_STYLE_DEFAULT = 'none';
 | 
        
           |  |  | 43 |   | 
        
           |  |  | 44 |     /** @var int Zero grade on any error */
 | 
        
           |  |  | 45 |     const GRADING_ALL_OR_NOTHING = -1;
 | 
        
           |  |  | 46 |     /** @var int Counts items, placed into right absolute place */
 | 
        
           |  |  | 47 |     const GRADING_ABSOLUTE_POSITION = 0;
 | 
        
           |  |  | 48 |     /** @var int Every sequential pair in right order is graded (last pair is excluded) */
 | 
        
           |  |  | 49 |     const GRADING_RELATIVE_NEXT_EXCLUDE_LAST = 1;
 | 
        
           |  |  | 50 |     /** @var int Every sequential pair in right order is graded (last pair is included) */
 | 
        
           |  |  | 51 |     const GRADING_RELATIVE_NEXT_INCLUDE_LAST = 2;
 | 
        
           |  |  | 52 |     /** @var int Single answers that are placed before and after each answer is graded if in right order*/
 | 
        
           |  |  | 53 |     const GRADING_RELATIVE_ONE_PREVIOUS_AND_NEXT = 3;
 | 
        
           |  |  | 54 |     /** @var int All answers that are placed before and after each answer is graded if in right order*/
 | 
        
           |  |  | 55 |     const GRADING_RELATIVE_ALL_PREVIOUS_AND_NEXT = 4;
 | 
        
           |  |  | 56 |     /** @var int Only longest ordered subset is graded */
 | 
        
           |  |  | 57 |     const GRADING_LONGEST_ORDERED_SUBSET = 5;
 | 
        
           |  |  | 58 |     /** @var int Only longest ordered and contiguous subset is graded */
 | 
        
           |  |  | 59 |     const GRADING_LONGEST_CONTIGUOUS_SUBSET = 6;
 | 
        
           |  |  | 60 |     /** @var int Items are graded relative to their position in the correct answer */
 | 
        
           |  |  | 61 |     const GRADING_RELATIVE_TO_CORRECT = 7;
 | 
        
           |  |  | 62 |   | 
        
           |  |  | 63 |     /** @var int {@see LAYOUT_VERTICAL} or {@see LAYOUT_HORIZONTAL}. */
 | 
        
           |  |  | 64 |     public $layouttype;
 | 
        
           |  |  | 65 |   | 
        
           |  |  | 66 |     /** @var int {@see SELECT_ALL}, {@see SELECT_RANDOM} or {@see SELECT_CONTIGUOUS}. */
 | 
        
           |  |  | 67 |     public $selecttype;
 | 
        
           |  |  | 68 |   | 
        
           |  |  | 69 |     /** @var int if {@see $selecttype} is not SELECT_ALL, then the number to select. */
 | 
        
           |  |  | 70 |     public $selectcount;
 | 
        
           |  |  | 71 |   | 
        
           |  |  | 72 |     /** @var int Which grading strategy to use. One of the GRADING_... constants. */
 | 
        
           |  |  | 73 |     public $gradingtype;
 | 
        
           |  |  | 74 |   | 
        
           |  |  | 75 |     /** @var bool Should details of the grading calculation be shown to students. */
 | 
        
           |  |  | 76 |     public $showgrading;
 | 
        
           |  |  | 77 |   | 
        
           |  |  | 78 |     /** @var string How to number the items. A key from the array returned by {@see get_numbering_styles()}. */
 | 
        
           |  |  | 79 |     public $numberingstyle;
 | 
        
           |  |  | 80 |   | 
        
           |  |  | 81 |     // Fields from "qtype_ordering_options" table.
 | 
        
           |  |  | 82 |     /** @var string */
 | 
        
           |  |  | 83 |     public $correctfeedback;
 | 
        
           |  |  | 84 |     /** @var int */
 | 
        
           |  |  | 85 |     public $correctfeedbackformat;
 | 
        
           |  |  | 86 |     /** @var string */
 | 
        
           |  |  | 87 |     public $incorrectfeedback;
 | 
        
           |  |  | 88 |     /** @var int */
 | 
        
           |  |  | 89 |     public $incorrectfeedbackformat;
 | 
        
           |  |  | 90 |     /** @var string */
 | 
        
           |  |  | 91 |     public $partiallycorrectfeedback;
 | 
        
           |  |  | 92 |     /** @var int */
 | 
        
           |  |  | 93 |     public $partiallycorrectfeedbackformat;
 | 
        
           |  |  | 94 |   | 
        
           |  |  | 95 |     /** @var array Records from "question_answers" table */
 | 
        
           |  |  | 96 |     public $answers;
 | 
        
           |  |  | 97 |   | 
        
           |  |  | 98 |     /** @var array of answerids in correct order */
 | 
        
           |  |  | 99 |     public $correctresponse;
 | 
        
           |  |  | 100 |   | 
        
           |  |  | 101 |     /** @var array contatining current order of answerids */
 | 
        
           |  |  | 102 |     public $currentresponse;
 | 
        
           |  |  | 103 |   | 
        
           |  |  | 104 |     /** @var array of scored for every item */
 | 
        
           |  |  | 105 |     protected $itemscores = [];
 | 
        
           |  |  | 106 |   | 
        
           |  |  | 107 |     public function start_attempt(question_attempt_step $step, $variant) {
 | 
        
           |  |  | 108 |         $countanswers = count($this->answers);
 | 
        
           |  |  | 109 |   | 
        
           |  |  | 110 |         // Sanitize "selecttype".
 | 
        
           |  |  | 111 |         $selecttype = $this->selecttype;
 | 
        
           |  |  | 112 |         $selecttype = max(0, $selecttype);
 | 
        
           |  |  | 113 |         $selecttype = min(2, $selecttype);
 | 
        
           |  |  | 114 |   | 
        
           |  |  | 115 |         // Sanitize "selectcount".
 | 
        
           |  |  | 116 |         $selectcount = $this->selectcount;
 | 
        
           |  |  | 117 |         $selectcount = max(self::MIN_SUBSET_ITEMS, $selectcount);
 | 
        
           |  |  | 118 |         $selectcount = min($countanswers, $selectcount);
 | 
        
           |  |  | 119 |   | 
        
           |  |  | 120 |         // Ensure consistency between "selecttype" and "selectcount".
 | 
        
           |  |  | 121 |         switch (true) {
 | 
        
           |  |  | 122 |             case ($selecttype == self::SELECT_ALL):
 | 
        
           |  |  | 123 |                 $selectcount = $countanswers;
 | 
        
           |  |  | 124 |                 break;
 | 
        
           |  |  | 125 |             case ($selectcount == $countanswers):
 | 
        
           |  |  | 126 |                 $selecttype = self::SELECT_ALL;
 | 
        
           |  |  | 127 |                 break;
 | 
        
           |  |  | 128 |         }
 | 
        
           |  |  | 129 |   | 
        
           |  |  | 130 |         // Extract answer ids.
 | 
        
           |  |  | 131 |         switch ($selecttype) {
 | 
        
           |  |  | 132 |             case self::SELECT_ALL:
 | 
        
           |  |  | 133 |                 $answerids = array_keys($this->answers);
 | 
        
           |  |  | 134 |                 break;
 | 
        
           |  |  | 135 |   | 
        
           |  |  | 136 |             case self::SELECT_RANDOM:
 | 
        
           |  |  | 137 |                 $answerids = array_rand($this->answers, $selectcount);
 | 
        
           |  |  | 138 |                 break;
 | 
        
           |  |  | 139 |   | 
        
           |  |  | 140 |             case self::SELECT_CONTIGUOUS:
 | 
        
           |  |  | 141 |                 $answerids = array_keys($this->answers);
 | 
        
           |  |  | 142 |                 $offset = mt_rand(0, $countanswers - $selectcount);
 | 
        
           |  |  | 143 |                 $answerids = array_slice($answerids, $offset, $selectcount);
 | 
        
           |  |  | 144 |                 break;
 | 
        
           |  |  | 145 |         }
 | 
        
           |  |  | 146 |   | 
        
           |  |  | 147 |         $this->correctresponse = $answerids;
 | 
        
           |  |  | 148 |         $step->set_qt_var('_correctresponse', implode(',', $this->correctresponse));
 | 
        
           |  |  | 149 |   | 
        
           |  |  | 150 |         shuffle($answerids);
 | 
        
           |  |  | 151 |         $this->currentresponse = $answerids;
 | 
        
           |  |  | 152 |         $step->set_qt_var('_currentresponse', implode(',', $this->currentresponse));
 | 
        
           |  |  | 153 |     }
 | 
        
           |  |  | 154 |   | 
        
           |  |  | 155 |     public function apply_attempt_state(question_attempt_step $step) {
 | 
        
           |  |  | 156 |         $this->currentresponse = array_filter(explode(',', $step->get_qt_var('_currentresponse')));
 | 
        
           |  |  | 157 |         $this->correctresponse = array_filter(explode(',', $step->get_qt_var('_correctresponse')));
 | 
        
           |  |  | 158 |     }
 | 
        
           |  |  | 159 |   | 
        
           |  |  | 160 |     public function validate_can_regrade_with_other_version(question_definition $otherversion): ?string {
 | 
        
           |  |  | 161 |         $basemessage = parent::validate_can_regrade_with_other_version($otherversion);
 | 
        
           |  |  | 162 |         if ($basemessage) {
 | 
        
           |  |  | 163 |             return $basemessage;
 | 
        
           |  |  | 164 |         }
 | 
        
           |  |  | 165 |   | 
        
           |  |  | 166 |         if (count($this->answers) != count($otherversion->answers)) {
 | 
        
           |  |  | 167 |             return get_string('regradeissuenumitemschanged', 'qtype_ordering');
 | 
        
           |  |  | 168 |         }
 | 
        
           |  |  | 169 |   | 
        
           |  |  | 170 |         return null;
 | 
        
           |  |  | 171 |     }
 | 
        
           |  |  | 172 |   | 
        
           |  |  | 173 |     public function update_attempt_state_data_for_new_version(
 | 
        
           |  |  | 174 |             question_attempt_step $oldstep, question_definition $oldquestion) {
 | 
        
           |  |  | 175 |         parent::update_attempt_state_data_for_new_version($oldstep, $oldquestion);
 | 
        
           |  |  | 176 |   | 
        
           |  |  | 177 |         $mapping = array_combine(array_keys($oldquestion->answers), array_keys($this->answers));
 | 
        
           |  |  | 178 |   | 
        
           |  |  | 179 |         $oldorder = explode(',', $oldstep->get_qt_var('_currentresponse'));
 | 
        
           |  |  | 180 |         $neworder = [];
 | 
        
           |  |  | 181 |         foreach ($oldorder as $oldid) {
 | 
        
           |  |  | 182 |             $neworder[] = $mapping[$oldid] ?? $oldid;
 | 
        
           |  |  | 183 |         }
 | 
        
           |  |  | 184 |   | 
        
           |  |  | 185 |         $oldcorrect = explode(',', $oldstep->get_qt_var('_correctresponse'));
 | 
        
           |  |  | 186 |         $newcorrect = [];
 | 
        
           |  |  | 187 |         foreach ($oldcorrect as $oldid) {
 | 
        
           |  |  | 188 |             $newcorrect[] = $mapping[$oldid] ?? $oldid;
 | 
        
           |  |  | 189 |         }
 | 
        
           |  |  | 190 |   | 
        
           |  |  | 191 |         return [
 | 
        
           |  |  | 192 |             '_currentresponse' => implode(',', $neworder),
 | 
        
           |  |  | 193 |             '_correctresponse' => implode(',', $newcorrect),
 | 
        
           |  |  | 194 |         ];
 | 
        
           |  |  | 195 |     }
 | 
        
           |  |  | 196 |   | 
        
           |  |  | 197 |     public function get_expected_data() {
 | 
        
           |  |  | 198 |         $name = $this->get_response_fieldname();
 | 
        
           |  |  | 199 |         return [$name => PARAM_TEXT];
 | 
        
           |  |  | 200 |     }
 | 
        
           |  |  | 201 |   | 
        
           |  |  | 202 |     public function get_correct_response() {
 | 
        
           |  |  | 203 |         $correctresponse = $this->correctresponse;
 | 
        
           |  |  | 204 |         foreach ($correctresponse as $position => $answerid) {
 | 
        
           |  |  | 205 |             $answer = $this->answers[$answerid];
 | 
        
           |  |  | 206 |             $correctresponse[$position] = $answer->md5key;
 | 
        
           |  |  | 207 |         }
 | 
        
           |  |  | 208 |         $name = $this->get_response_fieldname();
 | 
        
           |  |  | 209 |         return [$name => implode(',', $correctresponse)];
 | 
        
           |  |  | 210 |     }
 | 
        
           |  |  | 211 |   | 
        
           |  |  | 212 |     public function summarise_response(array $response) {
 | 
        
           |  |  | 213 |         $name = $this->get_response_fieldname();
 | 
        
           |  |  | 214 |         $items = [];
 | 
        
           |  |  | 215 |         if (array_key_exists($name, $response)) {
 | 
        
           |  |  | 216 |             $items = explode(',', $response[$name]);
 | 
        
           |  |  | 217 |         }
 | 
        
           |  |  | 218 |         $answerids = [];
 | 
        
           |  |  | 219 |         foreach ($this->answers as $answer) {
 | 
        
           |  |  | 220 |             $answerids[$answer->md5key] = $answer->id;
 | 
        
           |  |  | 221 |         }
 | 
        
           |  |  | 222 |         foreach ($items as $i => $item) {
 | 
        
           |  |  | 223 |             if (array_key_exists($item, $answerids)) {
 | 
        
           |  |  | 224 |                 $item = $this->answers[$answerids[$item]];
 | 
        
           |  |  | 225 |                 $item = $this->html_to_text($item->answer, $item->answerformat);
 | 
        
           |  |  | 226 |                 $item = shorten_text($item, 10, true); // Force truncate at 10 chars.
 | 
        
           |  |  | 227 |                 $items[$i] = $item;
 | 
        
           |  |  | 228 |             } else {
 | 
        
           |  |  | 229 |                 $items[$i] = ''; // Shouldn't happen!
 | 
        
           |  |  | 230 |             }
 | 
        
           |  |  | 231 |         }
 | 
        
           |  |  | 232 |         return implode('; ', array_filter($items));
 | 
        
           |  |  | 233 |     }
 | 
        
           |  |  | 234 |   | 
        
           |  |  | 235 |     public function classify_response(array $response) {
 | 
        
           |  |  | 236 |         $this->update_current_response($response);
 | 
        
           |  |  | 237 |         $fraction = 1 / count($this->correctresponse);
 | 
        
           |  |  | 238 |   | 
        
           |  |  | 239 |         $classifiedresponse = [];
 | 
        
           |  |  | 240 |         foreach ($this->correctresponse as $position => $answerid) {
 | 
        
           |  |  | 241 |             if (in_array($answerid, $this->currentresponse)) {
 | 
        
           |  |  | 242 |                 $currentposition = array_search($answerid, $this->currentresponse);
 | 
        
           |  |  | 243 |             }
 | 
        
           |  |  | 244 |   | 
        
           |  |  | 245 |             $answer = $this->answers[$answerid];
 | 
        
           |  |  | 246 |             $subqid = question_utils::to_plain_text($answer->answer, $answer->answerformat);
 | 
        
           |  |  | 247 |   | 
        
           |  |  | 248 |             // Truncate responses longer than 100 bytes because they cannot be stored in the database.
 | 
        
           |  |  | 249 |             // CAUTION: This will mess up answers which are not unique within the first 100 chars!
 | 
        
           |  |  | 250 |             $maxbytes = 100;
 | 
        
           |  |  | 251 |             if (strlen($subqid) > $maxbytes) {
 | 
        
           |  |  | 252 |                 // If the truncation point is in the middle of a multi-byte unicode char,
 | 
        
           |  |  | 253 |                 // we remove the incomplete part with a preg_match() that is unicode aware.
 | 
        
           |  |  | 254 |                 $subqid = substr($subqid, 0, $maxbytes);
 | 
        
           |  |  | 255 |                 if (preg_match('/^(.|\n)*/u', '', $subqid, $match)) {
 | 
        
           |  |  | 256 |                     $subqid = $match[0];
 | 
        
           |  |  | 257 |                 }
 | 
        
           |  |  | 258 |             }
 | 
        
           |  |  | 259 |   | 
        
           |  |  | 260 |             $classifiedresponse[$subqid] = new question_classified_response(
 | 
        
           |  |  | 261 |                 $currentposition + 1,
 | 
        
           |  |  | 262 |                 get_string('positionx', 'qtype_ordering', $currentposition + 1),
 | 
        
           |  |  | 263 |                 ($currentposition == $position) * $fraction
 | 
        
           |  |  | 264 |             );
 | 
        
           |  |  | 265 |         }
 | 
        
           |  |  | 266 |   | 
        
           |  |  | 267 |         return $classifiedresponse;
 | 
        
           |  |  | 268 |     }
 | 
        
           |  |  | 269 |   | 
        
           |  |  | 270 |     public function is_complete_response(array $response) {
 | 
        
           |  |  | 271 |         return true;
 | 
        
           |  |  | 272 |     }
 | 
        
           |  |  | 273 |   | 
        
           |  |  | 274 |     public function is_gradable_response(array $response) {
 | 
        
           |  |  | 275 |         return true;
 | 
        
           |  |  | 276 |     }
 | 
        
           |  |  | 277 |   | 
        
           |  |  | 278 |     public function get_validation_error(array $response) {
 | 
        
           |  |  | 279 |         return '';
 | 
        
           |  |  | 280 |     }
 | 
        
           |  |  | 281 |   | 
        
           |  |  | 282 |     public function is_same_response(array $old, array $new) {
 | 
        
           |  |  | 283 |         $name = $this->get_response_fieldname();
 | 
        
           |  |  | 284 |         return (isset($old[$name]) && isset($new[$name]) && $old[$name] == $new[$name]);
 | 
        
           |  |  | 285 |     }
 | 
        
           |  |  | 286 |   | 
        
           |  |  | 287 |     public function grade_response(array $response) {
 | 
        
           |  |  | 288 |         $this->update_current_response($response);
 | 
        
           |  |  | 289 |   | 
        
           |  |  | 290 |         $countcorrect = 0;
 | 
        
           |  |  | 291 |         $countanswers = 0;
 | 
        
           |  |  | 292 |   | 
        
           |  |  | 293 |         $gradingtype = $this->gradingtype;
 | 
        
           |  |  | 294 |         switch ($gradingtype) {
 | 
        
           |  |  | 295 |   | 
        
           |  |  | 296 |             case self::GRADING_ALL_OR_NOTHING:
 | 
        
           |  |  | 297 |             case self::GRADING_ABSOLUTE_POSITION:
 | 
        
           |  |  | 298 |                 $correctresponse = $this->correctresponse;
 | 
        
           |  |  | 299 |                 $currentresponse = $this->currentresponse;
 | 
        
           |  |  | 300 |                 foreach ($correctresponse as $position => $answerid) {
 | 
        
           |  |  | 301 |                     if (array_key_exists($position, $currentresponse)) {
 | 
        
           |  |  | 302 |                         if ($currentresponse[$position] == $answerid) {
 | 
        
           |  |  | 303 |                             $countcorrect++;
 | 
        
           |  |  | 304 |                         }
 | 
        
           |  |  | 305 |                     }
 | 
        
           |  |  | 306 |                     $countanswers++;
 | 
        
           |  |  | 307 |                 }
 | 
        
           |  |  | 308 |                 if ($gradingtype == self::GRADING_ALL_OR_NOTHING && $countcorrect < $countanswers) {
 | 
        
           |  |  | 309 |                     $countcorrect = 0;
 | 
        
           |  |  | 310 |                 }
 | 
        
           |  |  | 311 |                 break;
 | 
        
           |  |  | 312 |   | 
        
           |  |  | 313 |             case self::GRADING_RELATIVE_NEXT_EXCLUDE_LAST:
 | 
        
           |  |  | 314 |             case self::GRADING_RELATIVE_NEXT_INCLUDE_LAST:
 | 
        
           |  |  | 315 |                 $lastitem = ($gradingtype == self::GRADING_RELATIVE_NEXT_INCLUDE_LAST);
 | 
        
           |  |  | 316 |                 $currentresponse = $this->get_next_answerids($this->currentresponse, $lastitem);
 | 
        
           |  |  | 317 |                 $correctresponse = $this->get_next_answerids($this->correctresponse, $lastitem);
 | 
        
           |  |  | 318 |                 foreach ($correctresponse as $thisanswerid => $nextanswerid) {
 | 
        
           |  |  | 319 |                     if (array_key_exists($thisanswerid, $currentresponse)) {
 | 
        
           |  |  | 320 |                         if ($currentresponse[$thisanswerid] == $nextanswerid) {
 | 
        
           |  |  | 321 |                             $countcorrect++;
 | 
        
           |  |  | 322 |                         }
 | 
        
           |  |  | 323 |                     }
 | 
        
           |  |  | 324 |                     $countanswers++;
 | 
        
           |  |  | 325 |                 }
 | 
        
           |  |  | 326 |                 break;
 | 
        
           |  |  | 327 |   | 
        
           |  |  | 328 |             case self::GRADING_RELATIVE_ONE_PREVIOUS_AND_NEXT:
 | 
        
           |  |  | 329 |             case self::GRADING_RELATIVE_ALL_PREVIOUS_AND_NEXT:
 | 
        
           |  |  | 330 |                 $all = ($gradingtype == self::GRADING_RELATIVE_ALL_PREVIOUS_AND_NEXT);
 | 
        
           |  |  | 331 |                 $currentresponse = $this->get_previous_and_next_answerids($this->currentresponse, $all);
 | 
        
           |  |  | 332 |                 $correctresponse = $this->get_previous_and_next_answerids($this->correctresponse, $all);
 | 
        
           |  |  | 333 |                 foreach ($correctresponse as $thisanswerid => $answerids) {
 | 
        
           |  |  | 334 |                     if (array_key_exists($thisanswerid, $currentresponse)) {
 | 
        
           |  |  | 335 |                         $prev = $currentresponse[$thisanswerid]->prev;
 | 
        
           |  |  | 336 |                         $prev = array_intersect($prev, $answerids->prev);
 | 
        
           |  |  | 337 |                         $countcorrect += count($prev);
 | 
        
           |  |  | 338 |                         $next = $currentresponse[$thisanswerid]->next;
 | 
        
           |  |  | 339 |                         $next = array_intersect($next, $answerids->next);
 | 
        
           |  |  | 340 |                         $countcorrect += count($next);
 | 
        
           |  |  | 341 |                     }
 | 
        
           |  |  | 342 |                     $countanswers += count($answerids->prev);
 | 
        
           |  |  | 343 |                     $countanswers += count($answerids->next);
 | 
        
           |  |  | 344 |                 }
 | 
        
           |  |  | 345 |                 break;
 | 
        
           |  |  | 346 |   | 
        
           |  |  | 347 |             case self::GRADING_LONGEST_ORDERED_SUBSET:
 | 
        
           |  |  | 348 |             case self::GRADING_LONGEST_CONTIGUOUS_SUBSET:
 | 
        
           |  |  | 349 |                 $contiguous = ($gradingtype == self::GRADING_LONGEST_CONTIGUOUS_SUBSET);
 | 
        
           |  |  | 350 |                 $subset = $this->get_ordered_subset($contiguous);
 | 
        
           |  |  | 351 |                 $countcorrect = count($subset);
 | 
        
           |  |  | 352 |                 $countanswers = count($this->currentresponse);
 | 
        
           |  |  | 353 |                 break;
 | 
        
           |  |  | 354 |   | 
        
           |  |  | 355 |             case self::GRADING_RELATIVE_TO_CORRECT:
 | 
        
           |  |  | 356 |                 $correctresponse = $this->correctresponse;
 | 
        
           |  |  | 357 |                 $currentresponse = $this->currentresponse;
 | 
        
           |  |  | 358 |                 $count = (count($correctresponse) - 1);
 | 
        
           |  |  | 359 |                 foreach ($correctresponse as $position => $answerid) {
 | 
        
           |  |  | 360 |                     if (in_array($answerid, $currentresponse)) {
 | 
        
           |  |  | 361 |                         $currentposition = array_search($answerid, $currentresponse);
 | 
        
           |  |  | 362 |                         $currentscore = ($count - abs($position - $currentposition));
 | 
        
           |  |  | 363 |                         if ($currentscore > 0) {
 | 
        
           |  |  | 364 |                             $countcorrect += $currentscore;
 | 
        
           |  |  | 365 |                         }
 | 
        
           |  |  | 366 |                     }
 | 
        
           |  |  | 367 |                     $countanswers += $count;
 | 
        
           |  |  | 368 |                 }
 | 
        
           |  |  | 369 |                 break;
 | 
        
           |  |  | 370 |         }
 | 
        
           |  |  | 371 |         if ($countanswers == 0) {
 | 
        
           |  |  | 372 |             $fraction = 0;
 | 
        
           |  |  | 373 |         } else {
 | 
        
           |  |  | 374 |             $fraction = ($countcorrect / $countanswers);
 | 
        
           |  |  | 375 |         }
 | 
        
           |  |  | 376 |         return [
 | 
        
           |  |  | 377 |             $fraction,
 | 
        
           |  |  | 378 |             question_state::graded_state_for_fraction($fraction),
 | 
        
           |  |  | 379 |         ];
 | 
        
           |  |  | 380 |     }
 | 
        
           |  |  | 381 |   | 
        
           |  |  | 382 |     public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
 | 
        
           |  |  | 383 |         if ($component == 'question') {
 | 
        
           |  |  | 384 |             if ($filearea == 'answer') {
 | 
        
           |  |  | 385 |                 $answerid = reset($args); // Value of "itemid" is answer id.
 | 
        
           |  |  | 386 |                 return array_key_exists($answerid, $this->answers);
 | 
        
           |  |  | 387 |             }
 | 
        
           |  |  | 388 |             if (in_array($filearea, ['correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'])) {
 | 
        
           |  |  | 389 |                 return $this->check_combined_feedback_file_access($qa, $options, $filearea, $args);
 | 
        
           |  |  | 390 |             }
 | 
        
           |  |  | 391 |             if ($filearea == 'hint') {
 | 
        
           |  |  | 392 |                 return $this->check_hint_file_access($qa, $options, $args);
 | 
        
           |  |  | 393 |             }
 | 
        
           |  |  | 394 |         }
 | 
        
           |  |  | 395 |         return parent::check_file_access($qa, $options, $component, $filearea, $args, $forcedownload);
 | 
        
           |  |  | 396 |     }
 | 
        
           |  |  | 397 |   | 
        
           |  |  | 398 |     protected function check_combined_feedback_file_access($qa, $options, $filearea, $args = null) {
 | 
        
           |  |  | 399 |         $state = $qa->get_state();
 | 
        
           |  |  | 400 |         if (! $state->is_finished()) {
 | 
        
           |  |  | 401 |             $response = $qa->get_last_qt_data();
 | 
        
           |  |  | 402 |             if (! $this->is_gradable_response($response)) {
 | 
        
           |  |  | 403 |                 return false;
 | 
        
           |  |  | 404 |             }
 | 
        
           |  |  | 405 |             list($fraction, $state) = $this->grade_response($response);
 | 
        
           |  |  | 406 |         }
 | 
        
           |  |  | 407 |         if ($state->get_feedback_class().'feedback' == $filearea) {
 | 
        
           |  |  | 408 |             return ($this->id == reset($args));
 | 
        
           |  |  | 409 |         } else {
 | 
        
           |  |  | 410 |             return false;
 | 
        
           |  |  | 411 |         }
 | 
        
           |  |  | 412 |     }
 | 
        
           |  |  | 413 |   | 
        
           |  |  | 414 |     // Custom methods.
 | 
        
           |  |  | 415 |   | 
        
           |  |  | 416 |     /**
 | 
        
           |  |  | 417 |      * Returns response mform field name
 | 
        
           |  |  | 418 |      *
 | 
        
           |  |  | 419 |      * @return string
 | 
        
           |  |  | 420 |      */
 | 
        
           |  |  | 421 |     public function get_response_fieldname(): string {
 | 
        
           |  |  | 422 |         return 'response_' . $this->id;
 | 
        
           |  |  | 423 |     }
 | 
        
           |  |  | 424 |   | 
        
           |  |  | 425 |     /**
 | 
        
           |  |  | 426 |      * Unpack the students' response into an array which updates the question currentresponse.
 | 
        
           |  |  | 427 |      *
 | 
        
           |  |  | 428 |      * @param array $response Form data
 | 
        
           |  |  | 429 |      */
 | 
        
           |  |  | 430 |     public function update_current_response(array $response) {
 | 
        
           |  |  | 431 |         $name = $this->get_response_fieldname();
 | 
        
           |  |  | 432 |         if (array_key_exists($name, $response)) {
 | 
        
           |  |  | 433 |             $ids = explode(',', $response[$name]);
 | 
        
           |  |  | 434 |             foreach ($ids as $i => $id) {
 | 
        
           |  |  | 435 |                 foreach ($this->answers as $answer) {
 | 
        
           |  |  | 436 |                     if ($id == $answer->md5key) {
 | 
        
           |  |  | 437 |                         $ids[$i] = $answer->id;
 | 
        
           |  |  | 438 |                         break;
 | 
        
           |  |  | 439 |                     }
 | 
        
           |  |  | 440 |                 }
 | 
        
           |  |  | 441 |             }
 | 
        
           |  |  | 442 |             // Note: TH mentions that this is a bit of a hack.
 | 
        
           |  |  | 443 |             $this->currentresponse = $ids;
 | 
        
           |  |  | 444 |         }
 | 
        
           |  |  | 445 |     }
 | 
        
           |  |  | 446 |   | 
        
           |  |  | 447 |     /**
 | 
        
           |  |  | 448 |      * Returns layoutclass
 | 
        
           |  |  | 449 |      *
 | 
        
           |  |  | 450 |      * @return string
 | 
        
           |  |  | 451 |      */
 | 
        
           |  |  | 452 |     public function get_ordering_layoutclass(): string {
 | 
        
           |  |  | 453 |         switch ($this->layouttype) {
 | 
        
           |  |  | 454 |             case self::LAYOUT_VERTICAL:
 | 
        
           |  |  | 455 |                 return 'vertical';
 | 
        
           |  |  | 456 |             case self::LAYOUT_HORIZONTAL:
 | 
        
           |  |  | 457 |                 return 'horizontal';
 | 
        
           |  |  | 458 |             default:
 | 
        
           |  |  | 459 |                 return ''; // Shouldn't happen!
 | 
        
           |  |  | 460 |         }
 | 
        
           |  |  | 461 |     }
 | 
        
           |  |  | 462 |   | 
        
           |  |  | 463 |     /**
 | 
        
           |  |  | 464 |      * Returns array of next answers
 | 
        
           |  |  | 465 |      *
 | 
        
           |  |  | 466 |      * @param array $answerids array of answers id
 | 
        
           |  |  | 467 |      * @param bool $lastitem Include last item?
 | 
        
           |  |  | 468 |      * @return array of id of next answer
 | 
        
           |  |  | 469 |      */
 | 
        
           |  |  | 470 |     public function get_next_answerids(array $answerids, bool $lastitem = false): array {
 | 
        
           |  |  | 471 |         $nextanswerids = [];
 | 
        
           |  |  | 472 |         $imax = count($answerids);
 | 
        
           |  |  | 473 |         $imax--;
 | 
        
           |  |  | 474 |         if ($lastitem) {
 | 
        
           |  |  | 475 |             $nextanswerid = 0;
 | 
        
           |  |  | 476 |         } else {
 | 
        
           |  |  | 477 |             $nextanswerid = $answerids[$imax];
 | 
        
           |  |  | 478 |             $imax--;
 | 
        
           |  |  | 479 |         }
 | 
        
           |  |  | 480 |         for ($i = $imax; $i >= 0; $i--) {
 | 
        
           |  |  | 481 |             $thisanswerid = $answerids[$i];
 | 
        
           |  |  | 482 |             $nextanswerids[$thisanswerid] = $nextanswerid;
 | 
        
           |  |  | 483 |             $nextanswerid = $thisanswerid;
 | 
        
           |  |  | 484 |         }
 | 
        
           |  |  | 485 |         return $nextanswerids;
 | 
        
           |  |  | 486 |     }
 | 
        
           |  |  | 487 |   | 
        
           |  |  | 488 |     /**
 | 
        
           |  |  | 489 |      * Returns prev and next answers array
 | 
        
           |  |  | 490 |      *
 | 
        
           |  |  | 491 |      * @param array $answerids array of answers id
 | 
        
           |  |  | 492 |      * @param bool $all include all answers
 | 
        
           |  |  | 493 |      * @return array of array('prev' => previd, 'next' => nextid)
 | 
        
           |  |  | 494 |      */
 | 
        
           |  |  | 495 |     public function get_previous_and_next_answerids(array $answerids, bool $all = false): array {
 | 
        
           |  |  | 496 |         $prevnextanswerids = [];
 | 
        
           |  |  | 497 |         $next = $answerids;
 | 
        
           |  |  | 498 |         $prev = [];
 | 
        
           |  |  | 499 |         while ($answerid = array_shift($next)) {
 | 
        
           |  |  | 500 |             if ($all) {
 | 
        
           |  |  | 501 |                 $prevnextanswerids[$answerid] = (object) [
 | 
        
           |  |  | 502 |                     'prev' => $prev,
 | 
        
           |  |  | 503 |                     'next' => $next,
 | 
        
           |  |  | 504 |                 ];
 | 
        
           |  |  | 505 |             } else {
 | 
        
           |  |  | 506 |                 $prevnextanswerids[$answerid] = (object) [
 | 
        
           |  |  | 507 |                     'prev' => [empty($prev) ? 0 : $prev[0]],
 | 
        
           |  |  | 508 |                     'next' => [empty($next) ? 0 : $next[0]],
 | 
        
           |  |  | 509 |                 ];
 | 
        
           |  |  | 510 |             }
 | 
        
           |  |  | 511 |             array_unshift($prev, $answerid);
 | 
        
           |  |  | 512 |         }
 | 
        
           |  |  | 513 |         return $prevnextanswerids;
 | 
        
           |  |  | 514 |     }
 | 
        
           |  |  | 515 |   | 
        
           |  |  | 516 |     /**
 | 
        
           |  |  | 517 |      * Search for best ordered subset
 | 
        
           |  |  | 518 |      *
 | 
        
           |  |  | 519 |      * @param bool $contiguous A flag indicating whether only contiguous values should be considered for inclusion in the subset.
 | 
        
           |  |  | 520 |      * @return array
 | 
        
           |  |  | 521 |      */
 | 
        
           |  |  | 522 |     public function get_ordered_subset(bool $contiguous): array {
 | 
        
           |  |  | 523 |   | 
        
           |  |  | 524 |         $positions = $this->get_ordered_positions($this->correctresponse, $this->currentresponse);
 | 
        
           |  |  | 525 |         $subsets = $this->get_ordered_subsets($positions, $contiguous);
 | 
        
           |  |  | 526 |   | 
        
           |  |  | 527 |         // The best subset (longest and leftmost).
 | 
        
           |  |  | 528 |         $bestsubset = [];
 | 
        
           |  |  | 529 |   | 
        
           |  |  | 530 |         // The length of the best subset
 | 
        
           |  |  | 531 |         // initializing this to 1 means
 | 
        
           |  |  | 532 |         // we ignore single item subsets.
 | 
        
           |  |  | 533 |         $bestcount = 1;
 | 
        
           |  |  | 534 |   | 
        
           |  |  | 535 |         foreach ($subsets as $subset) {
 | 
        
           |  |  | 536 |             $count = count($subset);
 | 
        
           |  |  | 537 |             if ($count > $bestcount) {
 | 
        
           |  |  | 538 |                 $bestcount = $count;
 | 
        
           |  |  | 539 |                 $bestsubset = $subset;
 | 
        
           |  |  | 540 |             }
 | 
        
           |  |  | 541 |         }
 | 
        
           |  |  | 542 |         return $bestsubset;
 | 
        
           |  |  | 543 |     }
 | 
        
           |  |  | 544 |   | 
        
           |  |  | 545 |     /**
 | 
        
           |  |  | 546 |      * Get array of right answer positions for current response
 | 
        
           |  |  | 547 |      *
 | 
        
           |  |  | 548 |      * @param array $correctresponse
 | 
        
           |  |  | 549 |      * @param array $currentresponse
 | 
        
           |  |  | 550 |      * @return array
 | 
        
           |  |  | 551 |      */
 | 
        
           |  |  | 552 |     public function get_ordered_positions(array $correctresponse, array $currentresponse): array {
 | 
        
           |  |  | 553 |         $positions = [];
 | 
        
           |  |  | 554 |         foreach ($currentresponse as $answerid) {
 | 
        
           |  |  | 555 |             $positions[] = array_search($answerid, $correctresponse);
 | 
        
           |  |  | 556 |         }
 | 
        
           |  |  | 557 |         return $positions;
 | 
        
           |  |  | 558 |     }
 | 
        
           |  |  | 559 |   | 
        
           |  |  | 560 |     /**
 | 
        
           |  |  | 561 |      * Get all ordered subsets in the positions array
 | 
        
           |  |  | 562 |      *
 | 
        
           |  |  | 563 |      * @param array $positions maps an item's current position to its correct position
 | 
        
           |  |  | 564 |      * @param bool $contiguous TRUE if searching only for contiguous subsets; otherwise FALSE
 | 
        
           |  |  | 565 |      * @return array of ordered subsets from within the $positions array
 | 
        
           |  |  | 566 |      */
 | 
        
           |  |  | 567 |     public function get_ordered_subsets(array $positions, bool $contiguous): array {
 | 
        
           |  |  | 568 |   | 
        
           |  |  | 569 |         // Var $subsets is the collection of all subsets within $positions.
 | 
        
           |  |  | 570 |         $subsets = [];
 | 
        
           |  |  | 571 |   | 
        
           |  |  | 572 |         // Loop through the values at each position.
 | 
        
           |  |  | 573 |         foreach ($positions as $p => $value) {
 | 
        
           |  |  | 574 |   | 
        
           |  |  | 575 |             // Is $value a "new" value that cannot be added to any $subsets found so far?
 | 
        
           |  |  | 576 |             $isnew = true;
 | 
        
           |  |  | 577 |   | 
        
           |  |  | 578 |             // An array of new and saved subsets to be added to $subsets.
 | 
        
           |  |  | 579 |             $new = [];
 | 
        
           |  |  | 580 |   | 
        
           |  |  | 581 |             // Append the current value to any subsets to which it belongs
 | 
        
           |  |  | 582 |             // i.e. any subset whose end value is less than the current value.
 | 
        
           |  |  | 583 |             foreach ($subsets as $s => $subset) {
 | 
        
           |  |  | 584 |   | 
        
           |  |  | 585 |                 // Get value at end of $subset.
 | 
        
           |  |  | 586 |                 $end = $positions[end($subset)];
 | 
        
           |  |  | 587 |   | 
        
           |  |  | 588 |                 switch (true) {
 | 
        
           |  |  | 589 |   | 
        
           |  |  | 590 |                     case ($value == ($end + 1)):
 | 
        
           |  |  | 591 |                         // For a contiguous value, we simply append $p to the subset.
 | 
        
           |  |  | 592 |                         $isnew = false;
 | 
        
           |  |  | 593 |                         $subsets[$s][] = $p;
 | 
        
           |  |  | 594 |                         break;
 | 
        
           |  |  | 595 |   | 
        
           |  |  | 596 |                     case $contiguous:
 | 
        
           |  |  | 597 |                         // If the $contiguous flag is set, we ignore non-contiguous values.
 | 
        
           |  |  | 598 |                         break;
 | 
        
           |  |  | 599 |   | 
        
           |  |  | 600 |                     case ($value > $end):
 | 
        
           |  |  | 601 |                         // For a non-contiguous value, we save the subset so far,
 | 
        
           |  |  | 602 |                         // because a value between $end and $value may be found later,
 | 
        
           |  |  | 603 |                         // and then append $p to the subset.
 | 
        
           |  |  | 604 |                         $isnew = false;
 | 
        
           |  |  | 605 |                         $new[] = $subset;
 | 
        
           |  |  | 606 |                         $subsets[$s][] = $p;
 | 
        
           |  |  | 607 |                         break;
 | 
        
           |  |  | 608 |                 }
 | 
        
           |  |  | 609 |             }
 | 
        
           |  |  | 610 |   | 
        
           |  |  | 611 |             // If this is a "new" value, add it as a new subset.
 | 
        
           |  |  | 612 |             if ($isnew) {
 | 
        
           |  |  | 613 |                 $new[] = [$p];
 | 
        
           |  |  | 614 |             }
 | 
        
           |  |  | 615 |   | 
        
           |  |  | 616 |             // Append any "new" subsets that were found during this iteration.
 | 
        
           |  |  | 617 |             if (count($new)) {
 | 
        
           |  |  | 618 |                 $subsets = array_merge($subsets, $new);
 | 
        
           |  |  | 619 |             }
 | 
        
           |  |  | 620 |         }
 | 
        
           |  |  | 621 |   | 
        
           |  |  | 622 |         return $subsets;
 | 
        
           |  |  | 623 |     }
 | 
        
           |  |  | 624 |   | 
        
           |  |  | 625 |     /**
 | 
        
           |  |  | 626 |      * Helper function for get_select_types, get_layout_types, get_grading_types
 | 
        
           |  |  | 627 |      *
 | 
        
           |  |  | 628 |      * @param array $types
 | 
        
           |  |  | 629 |      * @param int $type
 | 
        
           |  |  | 630 |      * @return array|string array if $type is not specified and single string if $type is specified
 | 
        
           |  |  | 631 |      * @throws coding_exception
 | 
        
           |  |  | 632 |      * @codeCoverageIgnore
 | 
        
           |  |  | 633 |      */
 | 
        
           |  |  | 634 |     public static function get_types(array $types, $type): array|string {
 | 
        
           |  |  | 635 |         if ($type === null) {
 | 
        
           |  |  | 636 |             return $types; // Return all $types.
 | 
        
           |  |  | 637 |         }
 | 
        
           |  |  | 638 |         if (array_key_exists($type, $types)) {
 | 
        
           |  |  | 639 |             return $types[$type]; // One $type.
 | 
        
           |  |  | 640 |         }
 | 
        
           |  |  | 641 |   | 
        
           |  |  | 642 |         throw new coding_exception('Invalid type: ' . $type);
 | 
        
           |  |  | 643 |     }
 | 
        
           |  |  | 644 |   | 
        
           |  |  | 645 |     /**
 | 
        
           |  |  | 646 |      * Returns available values and descriptions for field "selecttype"
 | 
        
           |  |  | 647 |      *
 | 
        
           |  |  | 648 |      * @param int|null $type
 | 
        
           |  |  | 649 |      * @return array|string array if $type is not specified and single string if $type is specified
 | 
        
           |  |  | 650 |      * @codeCoverageIgnore
 | 
        
           |  |  | 651 |      */
 | 
        
           | 1441 | ariadna | 652 |     public static function get_select_types(?int $type = null): array|string {
 | 
        
           | 1 | efrain | 653 |         $plugin = 'qtype_ordering';
 | 
        
           |  |  | 654 |         $types = [
 | 
        
           |  |  | 655 |             self::SELECT_ALL => get_string('selectall', $plugin),
 | 
        
           |  |  | 656 |             self::SELECT_RANDOM => get_string('selectrandom', $plugin),
 | 
        
           |  |  | 657 |             self::SELECT_CONTIGUOUS => get_string('selectcontiguous', $plugin),
 | 
        
           |  |  | 658 |         ];
 | 
        
           |  |  | 659 |         return self::get_types($types, $type);
 | 
        
           |  |  | 660 |     }
 | 
        
           |  |  | 661 |   | 
        
           |  |  | 662 |     /**
 | 
        
           |  |  | 663 |      * Returns available values and descriptions for field "layouttype"
 | 
        
           |  |  | 664 |      *
 | 
        
           |  |  | 665 |      * @param int|null $type
 | 
        
           |  |  | 666 |      * @return array|string array if $type is not specified and single string if $type is specified
 | 
        
           |  |  | 667 |      * @codeCoverageIgnore
 | 
        
           |  |  | 668 |      */
 | 
        
           | 1441 | ariadna | 669 |     public static function get_layout_types(?int $type = null): array|string {
 | 
        
           | 1 | efrain | 670 |         $plugin = 'qtype_ordering';
 | 
        
           |  |  | 671 |         $types = [
 | 
        
           |  |  | 672 |             self::LAYOUT_VERTICAL   => get_string('vertical',   $plugin),
 | 
        
           |  |  | 673 |             self::LAYOUT_HORIZONTAL => get_string('horizontal', $plugin),
 | 
        
           |  |  | 674 |         ];
 | 
        
           |  |  | 675 |         return self::get_types($types, $type);
 | 
        
           |  |  | 676 |     }
 | 
        
           |  |  | 677 |   | 
        
           |  |  | 678 |     /**
 | 
        
           |  |  | 679 |      * Returns available values and descriptions for field "gradingtype"
 | 
        
           |  |  | 680 |      *
 | 
        
           |  |  | 681 |      * @param int|null $type
 | 
        
           |  |  | 682 |      * @return array|string array if $type is not specified and single string if $type is specified
 | 
        
           |  |  | 683 |      * @codeCoverageIgnore
 | 
        
           |  |  | 684 |      */
 | 
        
           | 1441 | ariadna | 685 |     public static function get_grading_types(?int $type = null): array|string {
 | 
        
           | 1 | efrain | 686 |         $plugin = 'qtype_ordering';
 | 
        
           |  |  | 687 |         $types = [
 | 
        
           |  |  | 688 |             self::GRADING_ALL_OR_NOTHING => get_string('allornothing', $plugin),
 | 
        
           |  |  | 689 |             self::GRADING_ABSOLUTE_POSITION => get_string('absoluteposition', $plugin),
 | 
        
           |  |  | 690 |             self::GRADING_RELATIVE_TO_CORRECT => get_string('relativetocorrect', $plugin),
 | 
        
           |  |  | 691 |             self::GRADING_RELATIVE_NEXT_EXCLUDE_LAST => get_string('relativenextexcludelast', $plugin),
 | 
        
           |  |  | 692 |             self::GRADING_RELATIVE_NEXT_INCLUDE_LAST => get_string('relativenextincludelast', $plugin),
 | 
        
           |  |  | 693 |             self::GRADING_RELATIVE_ONE_PREVIOUS_AND_NEXT => get_string('relativeonepreviousandnext', $plugin),
 | 
        
           |  |  | 694 |             self::GRADING_RELATIVE_ALL_PREVIOUS_AND_NEXT => get_string('relativeallpreviousandnext', $plugin),
 | 
        
           |  |  | 695 |             self::GRADING_LONGEST_ORDERED_SUBSET => get_string('longestorderedsubset', $plugin),
 | 
        
           |  |  | 696 |             self::GRADING_LONGEST_CONTIGUOUS_SUBSET => get_string('longestcontiguoussubset', $plugin),
 | 
        
           |  |  | 697 |         ];
 | 
        
           |  |  | 698 |         return self::get_types($types, $type);
 | 
        
           |  |  | 699 |     }
 | 
        
           |  |  | 700 |   | 
        
           |  |  | 701 |     /**
 | 
        
           |  |  | 702 |      * Get the numbering styles supported.
 | 
        
           |  |  | 703 |      *
 | 
        
           |  |  | 704 |      * For each style, there should be a corresponding lang string 'numberingstylexxx' in the qtype_ordering language file,
 | 
        
           |  |  | 705 |      * a case in the switch statement in number_in_style, and it should be listed in the definition of this column in install.xml.
 | 
        
           |  |  | 706 |      *
 | 
        
           |  |  | 707 |      * @param string|null $style The specific numbering style to retrieve.
 | 
        
           |  |  | 708 |      * @return array|string Numbering style(s).
 | 
        
           |  |  | 709 |      *                      The keys are style identifiers, and the values are the corresponding language strings.
 | 
        
           |  |  | 710 |      * @codeCoverageIgnore
 | 
        
           |  |  | 711 |      */
 | 
        
           | 1441 | ariadna | 712 |     public static function get_numbering_styles(?string $style = null): array|string {
 | 
        
           | 1 | efrain | 713 |         $plugin = 'qtype_ordering';
 | 
        
           |  |  | 714 |         $styles = [
 | 
        
           |  |  | 715 |             'none' => get_string('numberingstylenone', $plugin),
 | 
        
           |  |  | 716 |             'abc' => get_string('numberingstyleabc', $plugin),
 | 
        
           |  |  | 717 |             'ABCD' => get_string('numberingstyleABCD', $plugin),
 | 
        
           |  |  | 718 |             '123' => get_string('numberingstyle123', $plugin),
 | 
        
           |  |  | 719 |             'iii' => get_string('numberingstyleiii', $plugin),
 | 
        
           |  |  | 720 |             'IIII' => get_string('numberingstyleIIII', $plugin),
 | 
        
           |  |  | 721 |         ];
 | 
        
           |  |  | 722 |         return self::get_types($styles, $style);
 | 
        
           |  |  | 723 |     }
 | 
        
           |  |  | 724 |   | 
        
           |  |  | 725 |     /**
 | 
        
           |  |  | 726 |      * Return the number of subparts of this response that are correct|partial|incorrect.
 | 
        
           |  |  | 727 |      *
 | 
        
           |  |  | 728 |      * @param array $response A response.
 | 
        
           |  |  | 729 |      * @return array Array of three elements: the number of correct subparts,
 | 
        
           |  |  | 730 |      * the number of partial correct subparts and the number of incorrect subparts.
 | 
        
           |  |  | 731 |      */
 | 
        
           |  |  | 732 |     public function get_num_parts_right(array $response): array {
 | 
        
           |  |  | 733 |         $this->update_current_response($response);
 | 
        
           |  |  | 734 |         $gradingtype = $this->gradingtype;
 | 
        
           |  |  | 735 |   | 
        
           |  |  | 736 |         $numright = 0;
 | 
        
           |  |  | 737 |         $numpartial = 0;
 | 
        
           |  |  | 738 |         $numincorrect = 0;
 | 
        
           |  |  | 739 |         list($correctresponse, $currentresponse) = $this->get_response_depend_on_grading_type($gradingtype);
 | 
        
           |  |  | 740 |   | 
        
           |  |  | 741 |         foreach ($this->currentresponse as $position => $answerid) {
 | 
        
           |  |  | 742 |             [$fraction, $score, $maxscore] =
 | 
        
           |  |  | 743 |                 $this->get_fraction_maxscore_score_of_item($position, $answerid, $correctresponse, $currentresponse);
 | 
        
           |  |  | 744 |             if (is_null($fraction)) {
 | 
        
           |  |  | 745 |                 continue;
 | 
        
           |  |  | 746 |             }
 | 
        
           |  |  | 747 |   | 
        
           |  |  | 748 |             if ($fraction > 0.999999) {
 | 
        
           |  |  | 749 |                 $numright++;
 | 
        
           |  |  | 750 |             } else if ($fraction < 0.000001) {
 | 
        
           |  |  | 751 |                 $numincorrect++;
 | 
        
           |  |  | 752 |             } else {
 | 
        
           |  |  | 753 |                 $numpartial++;
 | 
        
           |  |  | 754 |             }
 | 
        
           |  |  | 755 |         }
 | 
        
           |  |  | 756 |   | 
        
           |  |  | 757 |         return [$numright, $numpartial, $numincorrect];
 | 
        
           |  |  | 758 |     }
 | 
        
           |  |  | 759 |   | 
        
           |  |  | 760 |     /**
 | 
        
           |  |  | 761 |      * Returns the grade for one item, base on the fraction scale.
 | 
        
           |  |  | 762 |      *
 | 
        
           |  |  | 763 |      * @param int $position The position of the current response.
 | 
        
           |  |  | 764 |      * @param int $answerid The answerid of the current response.
 | 
        
           |  |  | 765 |      * @param array $correctresponse The correct response list base on grading type.
 | 
        
           |  |  | 766 |      * @param array $currentresponse The current response list base on grading type.
 | 
        
           |  |  | 767 |      * @return array.
 | 
        
           |  |  | 768 |      */
 | 
        
           |  |  | 769 |     protected function get_fraction_maxscore_score_of_item(
 | 
        
           |  |  | 770 |         int $position,
 | 
        
           |  |  | 771 |         int $answerid,
 | 
        
           |  |  | 772 |         array $correctresponse,
 | 
        
           |  |  | 773 |         array $currentresponse
 | 
        
           |  |  | 774 |     ): array {
 | 
        
           |  |  | 775 |         $gradingtype = $this->gradingtype;
 | 
        
           |  |  | 776 |   | 
        
           |  |  | 777 |         $score    = 0;
 | 
        
           |  |  | 778 |         $maxscore = null;
 | 
        
           |  |  | 779 |   | 
        
           |  |  | 780 |         switch ($gradingtype) {
 | 
        
           |  |  | 781 |             case self::GRADING_ALL_OR_NOTHING:
 | 
        
           |  |  | 782 |             case self::GRADING_ABSOLUTE_POSITION:
 | 
        
           |  |  | 783 |                 if (isset($correctresponse[$position])) {
 | 
        
           |  |  | 784 |                     if ($correctresponse[$position] == $answerid) {
 | 
        
           |  |  | 785 |                         $score = 1;
 | 
        
           |  |  | 786 |                     }
 | 
        
           |  |  | 787 |                     $maxscore = 1;
 | 
        
           |  |  | 788 |                 }
 | 
        
           |  |  | 789 |                 break;
 | 
        
           |  |  | 790 |             case self::GRADING_RELATIVE_NEXT_EXCLUDE_LAST:
 | 
        
           |  |  | 791 |             case self::GRADING_RELATIVE_NEXT_INCLUDE_LAST:
 | 
        
           |  |  | 792 |                 if (isset($correctresponse[$answerid])) {
 | 
        
           |  |  | 793 |                     if (isset($currentresponse[$answerid]) && $currentresponse[$answerid] == $correctresponse[$answerid]) {
 | 
        
           |  |  | 794 |                         $score = 1;
 | 
        
           |  |  | 795 |                     }
 | 
        
           |  |  | 796 |                     $maxscore = 1;
 | 
        
           |  |  | 797 |                 }
 | 
        
           |  |  | 798 |                 break;
 | 
        
           |  |  | 799 |   | 
        
           |  |  | 800 |             case self::GRADING_RELATIVE_ONE_PREVIOUS_AND_NEXT:
 | 
        
           |  |  | 801 |             case self::GRADING_RELATIVE_ALL_PREVIOUS_AND_NEXT:
 | 
        
           |  |  | 802 |                 if (isset($correctresponse[$answerid])) {
 | 
        
           |  |  | 803 |                     $maxscore = 0;
 | 
        
           |  |  | 804 |                     $prev = $correctresponse[$answerid]->prev;
 | 
        
           |  |  | 805 |                     $maxscore += count($prev);
 | 
        
           |  |  | 806 |                     $prev = array_intersect($prev, $currentresponse[$answerid]->prev);
 | 
        
           |  |  | 807 |                     $score += count($prev);
 | 
        
           |  |  | 808 |                     $next = $correctresponse[$answerid]->next;
 | 
        
           |  |  | 809 |                     $maxscore += count($next);
 | 
        
           |  |  | 810 |                     $next = array_intersect($next, $currentresponse[$answerid]->next);
 | 
        
           |  |  | 811 |                     $score += count($next);
 | 
        
           |  |  | 812 |                 }
 | 
        
           |  |  | 813 |                 break;
 | 
        
           |  |  | 814 |   | 
        
           |  |  | 815 |             case self::GRADING_LONGEST_ORDERED_SUBSET:
 | 
        
           |  |  | 816 |             case self::GRADING_LONGEST_CONTIGUOUS_SUBSET:
 | 
        
           |  |  | 817 |                 if (isset($correctresponse[$position])) {
 | 
        
           |  |  | 818 |                     if (isset($currentresponse[$position])) {
 | 
        
           |  |  | 819 |                         $score = $currentresponse[$position];
 | 
        
           |  |  | 820 |                     }
 | 
        
           |  |  | 821 |                     $maxscore = 1;
 | 
        
           |  |  | 822 |                 }
 | 
        
           |  |  | 823 |                 break;
 | 
        
           |  |  | 824 |   | 
        
           |  |  | 825 |             case self::GRADING_RELATIVE_TO_CORRECT:
 | 
        
           |  |  | 826 |                 if (isset($correctresponse[$position])) {
 | 
        
           |  |  | 827 |                     $maxscore = (count($correctresponse) - 1);
 | 
        
           |  |  | 828 |                     $answerid = $currentresponse[$position];
 | 
        
           |  |  | 829 |                     $correctposition = array_search($answerid, $correctresponse);
 | 
        
           |  |  | 830 |                     $score = ($maxscore - abs($correctposition - $position));
 | 
        
           |  |  | 831 |                     if ($score < 0) {
 | 
        
           |  |  | 832 |                         $score = 0;
 | 
        
           |  |  | 833 |                     }
 | 
        
           |  |  | 834 |                 }
 | 
        
           |  |  | 835 |                 break;
 | 
        
           |  |  | 836 |         }
 | 
        
           |  |  | 837 |         $fraction = $maxscore ? $score / $maxscore : $maxscore;
 | 
        
           |  |  | 838 |   | 
        
           |  |  | 839 |         return [$fraction, $score, $maxscore];
 | 
        
           |  |  | 840 |     }
 | 
        
           |  |  | 841 |   | 
        
           |  |  | 842 |     /**
 | 
        
           |  |  | 843 |      * Get correcresponse and currentinfo depending on grading type.
 | 
        
           |  |  | 844 |      *
 | 
        
           |  |  | 845 |      * @param string $gradingtype The kind of grading.
 | 
        
           |  |  | 846 |      * @return array Correctresponse and currentresponsescore in one array.
 | 
        
           |  |  | 847 |      */
 | 
        
           |  |  | 848 |     protected function get_response_depend_on_grading_type(string $gradingtype): array {
 | 
        
           |  |  | 849 |   | 
        
           |  |  | 850 |         $correctresponse = [];
 | 
        
           |  |  | 851 |         $currentresponse = [];
 | 
        
           |  |  | 852 |         switch ($gradingtype) {
 | 
        
           |  |  | 853 |             case self::GRADING_ALL_OR_NOTHING:
 | 
        
           |  |  | 854 |             case self::GRADING_ABSOLUTE_POSITION:
 | 
        
           |  |  | 855 |             case self::GRADING_RELATIVE_TO_CORRECT:
 | 
        
           |  |  | 856 |                 $correctresponse = $this->correctresponse;
 | 
        
           |  |  | 857 |                 $currentresponse = $this->currentresponse;
 | 
        
           |  |  | 858 |                 break;
 | 
        
           |  |  | 859 |   | 
        
           |  |  | 860 |             case self::GRADING_RELATIVE_NEXT_EXCLUDE_LAST:
 | 
        
           |  |  | 861 |             case self::GRADING_RELATIVE_NEXT_INCLUDE_LAST:
 | 
        
           |  |  | 862 |                 $lastitem = ($gradingtype == self::GRADING_RELATIVE_NEXT_INCLUDE_LAST);
 | 
        
           |  |  | 863 |                 $correctresponse = $this->get_next_answerids($this->correctresponse, $lastitem);
 | 
        
           |  |  | 864 |                 $currentresponse = $this->get_next_answerids($this->currentresponse, $lastitem);
 | 
        
           |  |  | 865 |                 break;
 | 
        
           |  |  | 866 |   | 
        
           |  |  | 867 |             case self::GRADING_RELATIVE_ONE_PREVIOUS_AND_NEXT:
 | 
        
           |  |  | 868 |             case self::GRADING_RELATIVE_ALL_PREVIOUS_AND_NEXT:
 | 
        
           |  |  | 869 |                 $all = ($gradingtype == self::GRADING_RELATIVE_ALL_PREVIOUS_AND_NEXT);
 | 
        
           |  |  | 870 |                 $correctresponse = $this->get_previous_and_next_answerids($this->correctresponse, $all);
 | 
        
           |  |  | 871 |                 $currentresponse = $this->get_previous_and_next_answerids($this->currentresponse, $all);
 | 
        
           |  |  | 872 |                 break;
 | 
        
           |  |  | 873 |   | 
        
           |  |  | 874 |             case self::GRADING_LONGEST_ORDERED_SUBSET:
 | 
        
           |  |  | 875 |             case self::GRADING_LONGEST_CONTIGUOUS_SUBSET:
 | 
        
           |  |  | 876 |                 $correctresponse = $this->correctresponse;
 | 
        
           |  |  | 877 |                 $currentresponse = $this->currentresponse;
 | 
        
           |  |  | 878 |                 $contiguous = ($gradingtype == self::GRADING_LONGEST_CONTIGUOUS_SUBSET);
 | 
        
           |  |  | 879 |                 $subset = $this->get_ordered_subset($contiguous);
 | 
        
           |  |  | 880 |                 foreach ($currentresponse as $position => $answerid) {
 | 
        
           |  |  | 881 |                     if (array_search($position, $subset) === false) {
 | 
        
           |  |  | 882 |                         $currentresponse[$position] = 0;
 | 
        
           |  |  | 883 |                     } else {
 | 
        
           |  |  | 884 |                         $currentresponse[$position] = 1;
 | 
        
           |  |  | 885 |                     }
 | 
        
           |  |  | 886 |                 }
 | 
        
           |  |  | 887 |                 break;
 | 
        
           |  |  | 888 |         }
 | 
        
           |  |  | 889 |   | 
        
           |  |  | 890 |         return [$correctresponse, $currentresponse];
 | 
        
           |  |  | 891 |     }
 | 
        
           |  |  | 892 |   | 
        
           |  |  | 893 |     /**
 | 
        
           |  |  | 894 |      * Returns score for one item depending on correctness and question settings.
 | 
        
           |  |  | 895 |      *
 | 
        
           |  |  | 896 |      * @param question_definition $question question definition object
 | 
        
           |  |  | 897 |      * @param int $position The position of the current response.
 | 
        
           |  |  | 898 |      * @param int $answerid The answerid of the current response.
 | 
        
           |  |  | 899 |      * @return array (score, maxscore, fraction, percent, class)
 | 
        
           |  |  | 900 |      */
 | 
        
           |  |  | 901 |     public function get_ordering_item_score(question_definition $question, int $position, int $answerid): array {
 | 
        
           |  |  | 902 |   | 
        
           |  |  | 903 |         if (!isset($this->itemscores[$position])) {
 | 
        
           |  |  | 904 |   | 
        
           |  |  | 905 |             [$correctresponse, $currentresponse] = $this->get_response_depend_on_grading_type($this->gradingtype);
 | 
        
           |  |  | 906 |   | 
        
           |  |  | 907 |             $percent  = 0;    // 100 * $fraction.
 | 
        
           |  |  | 908 |             [$fraction, $score, $maxscore] =
 | 
        
           |  |  | 909 |                 $this->get_fraction_maxscore_score_of_item($position, $answerid, $correctresponse, $currentresponse);
 | 
        
           |  |  | 910 |   | 
        
           |  |  | 911 |             if ($maxscore === null) {
 | 
        
           |  |  | 912 |                 // An unscored item is either an illegal item
 | 
        
           |  |  | 913 |                 // or last item of RELATIVE_NEXT_EXCLUDE_LAST
 | 
        
           |  |  | 914 |                 // or an item in an incorrect ALL_OR_NOTHING
 | 
        
           |  |  | 915 |                 // or an item from an unrecognized grading type.
 | 
        
           |  |  | 916 |                 $class = 'unscored';
 | 
        
           |  |  | 917 |             } else {
 | 
        
           |  |  | 918 |                 if ($maxscore > 0) {
 | 
        
           |  |  | 919 |                     $percent = round(100 * $fraction, 0);
 | 
        
           |  |  | 920 |                 }
 | 
        
           |  |  | 921 |                 $class = match (true) {
 | 
        
           |  |  | 922 |                     $fraction > 0.999999 => 'correct',
 | 
        
           |  |  | 923 |                     $fraction < 0.000001 => 'incorrect',
 | 
        
           |  |  | 924 |                     $fraction >= 0.66 => 'partial66',
 | 
        
           |  |  | 925 |                     $fraction >= 0.33 => 'partial33',
 | 
        
           |  |  | 926 |                     default => 'partial00',
 | 
        
           |  |  | 927 |                 };
 | 
        
           |  |  | 928 |             }
 | 
        
           |  |  | 929 |   | 
        
           |  |  | 930 |             $itemscores = [
 | 
        
           |  |  | 931 |                 'score' => $score,
 | 
        
           |  |  | 932 |                 'maxscore' => $maxscore,
 | 
        
           |  |  | 933 |                 'fraction' => $fraction,
 | 
        
           |  |  | 934 |                 'percent' => $percent,
 | 
        
           |  |  | 935 |                 'class' => $class,
 | 
        
           |  |  | 936 |             ];
 | 
        
           |  |  | 937 |             $this->itemscores[$position] = $itemscores;
 | 
        
           |  |  | 938 |         }
 | 
        
           |  |  | 939 |   | 
        
           |  |  | 940 |         return $this->itemscores[$position];
 | 
        
           |  |  | 941 |     }
 | 
        
           |  |  | 942 |   | 
        
           |  |  | 943 | }
 |