| 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 |  * This defines the core classes of the Moodle question engine.
 | 
        
           |  |  | 19 |  *
 | 
        
           |  |  | 20 |  * @package    moodlecore
 | 
        
           |  |  | 21 |  * @subpackage questionengine
 | 
        
           |  |  | 22 |  * @copyright  2009 The Open University
 | 
        
           |  |  | 23 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 24 |  */
 | 
        
           |  |  | 25 |   | 
        
           |  |  | 26 |   | 
        
           |  |  | 27 | defined('MOODLE_INTERNAL') || die();
 | 
        
           |  |  | 28 |   | 
        
           |  |  | 29 | require_once($CFG->libdir . '/filelib.php');
 | 
        
           |  |  | 30 | require_once(__DIR__ . '/questionusage.php');
 | 
        
           |  |  | 31 | require_once(__DIR__ . '/questionattempt.php');
 | 
        
           |  |  | 32 | require_once(__DIR__ . '/questionattemptstep.php');
 | 
        
           |  |  | 33 | require_once(__DIR__ . '/states.php');
 | 
        
           |  |  | 34 | require_once(__DIR__ . '/datalib.php');
 | 
        
           |  |  | 35 | require_once(__DIR__ . '/renderer.php');
 | 
        
           |  |  | 36 | require_once(__DIR__ . '/bank.php');
 | 
        
           |  |  | 37 | require_once(__DIR__ . '/../type/questiontypebase.php');
 | 
        
           |  |  | 38 | require_once(__DIR__ . '/../type/questionbase.php');
 | 
        
           |  |  | 39 | require_once(__DIR__ . '/../type/rendererbase.php');
 | 
        
           |  |  | 40 | require_once(__DIR__ . '/../behaviour/behaviourtypebase.php');
 | 
        
           |  |  | 41 | require_once(__DIR__ . '/../behaviour/behaviourbase.php');
 | 
        
           |  |  | 42 | require_once(__DIR__ . '/../behaviour/rendererbase.php');
 | 
        
           |  |  | 43 | require_once($CFG->libdir . '/questionlib.php');
 | 
        
           |  |  | 44 |   | 
        
           |  |  | 45 |   | 
        
           |  |  | 46 | /**
 | 
        
           |  |  | 47 |  * This static class provides access to the other question engine classes.
 | 
        
           |  |  | 48 |  *
 | 
        
           |  |  | 49 |  * It provides functions for managing question behaviours), and for
 | 
        
           |  |  | 50 |  * creating, loading, saving and deleting {@link question_usage_by_activity}s,
 | 
        
           |  |  | 51 |  * which is the main class that is used by other code that wants to use questions.
 | 
        
           |  |  | 52 |  *
 | 
        
           |  |  | 53 |  * @copyright  2009 The Open University
 | 
        
           |  |  | 54 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 55 |  */
 | 
        
           |  |  | 56 | abstract class question_engine {
 | 
        
           |  |  | 57 |     /** @var array behaviour name => 1. Records which behaviours have been loaded. */
 | 
        
           |  |  | 58 |     private static $loadedbehaviours = array();
 | 
        
           |  |  | 59 |   | 
        
           |  |  | 60 |     /** @var array behaviour name => question_behaviour_type for this behaviour. */
 | 
        
           |  |  | 61 |     private static $behaviourtypes = array();
 | 
        
           |  |  | 62 |   | 
        
           |  |  | 63 |     /**
 | 
        
           |  |  | 64 |      * Create a new {@link question_usage_by_activity}. The usage is
 | 
        
           |  |  | 65 |      * created in memory. If you want it to persist, you will need to call
 | 
        
           |  |  | 66 |      * {@link save_questions_usage_by_activity()}.
 | 
        
           |  |  | 67 |      *
 | 
        
           |  |  | 68 |      * @param string $component the plugin creating this attempt. For example mod_quiz.
 | 
        
           |  |  | 69 |      * @param context $context the context this usage belongs to.
 | 
        
           |  |  | 70 |      * @return question_usage_by_activity the newly created object.
 | 
        
           |  |  | 71 |      */
 | 
        
           |  |  | 72 |     public static function make_questions_usage_by_activity($component, $context) {
 | 
        
           |  |  | 73 |         return new question_usage_by_activity($component, $context);
 | 
        
           |  |  | 74 |     }
 | 
        
           |  |  | 75 |   | 
        
           |  |  | 76 |     /**
 | 
        
           |  |  | 77 |      * Load a {@link question_usage_by_activity} from the database, based on its id.
 | 
        
           |  |  | 78 |      * @param int $qubaid the id of the usage to load.
 | 
        
           |  |  | 79 |      * @param moodle_database $db a database connectoin. Defaults to global $DB.
 | 
        
           |  |  | 80 |      * @return question_usage_by_activity loaded from the database.
 | 
        
           |  |  | 81 |      */
 | 
        
           | 1441 | ariadna | 82 |     public static function load_questions_usage_by_activity($qubaid, ?moodle_database $db = null) {
 | 
        
           | 1 | efrain | 83 |         $dm = new question_engine_data_mapper($db);
 | 
        
           |  |  | 84 |         return $dm->load_questions_usage_by_activity($qubaid);
 | 
        
           |  |  | 85 |     }
 | 
        
           |  |  | 86 |   | 
        
           |  |  | 87 |     /**
 | 
        
           |  |  | 88 |      * Save a {@link question_usage_by_activity} to the database. This works either
 | 
        
           |  |  | 89 |      * if the usage was newly created by {@link make_questions_usage_by_activity()}
 | 
        
           |  |  | 90 |      * or loaded from the database using {@link load_questions_usage_by_activity()}
 | 
        
           |  |  | 91 |      * @param question_usage_by_activity the usage to save.
 | 
        
           |  |  | 92 |      * @param moodle_database $db a database connectoin. Defaults to global $DB.
 | 
        
           |  |  | 93 |      */
 | 
        
           | 1441 | ariadna | 94 |     public static function save_questions_usage_by_activity(question_usage_by_activity $quba, ?moodle_database $db = null) {
 | 
        
           | 1 | efrain | 95 |         $dm = new question_engine_data_mapper($db);
 | 
        
           |  |  | 96 |         $observer = $quba->get_observer();
 | 
        
           |  |  | 97 |         if ($observer instanceof question_engine_unit_of_work) {
 | 
        
           |  |  | 98 |             $observer->save($dm);
 | 
        
           |  |  | 99 |         } else {
 | 
        
           |  |  | 100 |             $dm->insert_questions_usage_by_activity($quba);
 | 
        
           |  |  | 101 |         }
 | 
        
           |  |  | 102 |     }
 | 
        
           |  |  | 103 |   | 
        
           |  |  | 104 |     /**
 | 
        
           |  |  | 105 |      * Delete a {@link question_usage_by_activity} from the database, based on its id.
 | 
        
           |  |  | 106 |      * @param int $qubaid the id of the usage to delete.
 | 
        
           |  |  | 107 |      */
 | 
        
           |  |  | 108 |     public static function delete_questions_usage_by_activity($qubaid) {
 | 
        
           |  |  | 109 |         self::delete_questions_usage_by_activities(new qubaid_list(array($qubaid)));
 | 
        
           |  |  | 110 |     }
 | 
        
           |  |  | 111 |   | 
        
           |  |  | 112 |     /**
 | 
        
           |  |  | 113 |      * Delete {@link question_usage_by_activity}s from the database.
 | 
        
           |  |  | 114 |      * @param qubaid_condition $qubaids identifies which questions usages to delete.
 | 
        
           |  |  | 115 |      */
 | 
        
           |  |  | 116 |     public static function delete_questions_usage_by_activities(qubaid_condition $qubaids) {
 | 
        
           |  |  | 117 |         $dm = new question_engine_data_mapper();
 | 
        
           |  |  | 118 |         $dm->delete_questions_usage_by_activities($qubaids);
 | 
        
           |  |  | 119 |     }
 | 
        
           |  |  | 120 |   | 
        
           |  |  | 121 |     /**
 | 
        
           |  |  | 122 |      * Change the maxmark for the question_attempt with number in usage $slot
 | 
        
           |  |  | 123 |      * for all the specified question_attempts.
 | 
        
           |  |  | 124 |      * @param qubaid_condition $qubaids Selects which usages are updated.
 | 
        
           |  |  | 125 |      * @param int $slot the number is usage to affect.
 | 
        
           |  |  | 126 |      * @param number $newmaxmark the new max mark to set.
 | 
        
           |  |  | 127 |      */
 | 
        
           |  |  | 128 |     public static function set_max_mark_in_attempts(qubaid_condition $qubaids,
 | 
        
           |  |  | 129 |             $slot, $newmaxmark) {
 | 
        
           |  |  | 130 |         $dm = new question_engine_data_mapper();
 | 
        
           |  |  | 131 |         $dm->set_max_mark_in_attempts($qubaids, $slot, $newmaxmark);
 | 
        
           |  |  | 132 |     }
 | 
        
           |  |  | 133 |   | 
        
           |  |  | 134 |     /**
 | 
        
           |  |  | 135 |      * Validate that the manual grade submitted for a particular question is in range.
 | 
        
           |  |  | 136 |      * @param int $qubaid the question_usage id.
 | 
        
           |  |  | 137 |      * @param int $slot the slot number within the usage.
 | 
        
           |  |  | 138 |      * @return bool whether the submitted data is in range.
 | 
        
           |  |  | 139 |      */
 | 
        
           |  |  | 140 |     public static function is_manual_grade_in_range($qubaid, $slot) {
 | 
        
           |  |  | 141 |         $prefix = 'q' . $qubaid . ':' . $slot . '_';
 | 
        
           |  |  | 142 |         $mark = question_utils::optional_param_mark($prefix . '-mark');
 | 
        
           |  |  | 143 |         $maxmark = optional_param($prefix . '-maxmark', null, PARAM_FLOAT);
 | 
        
           |  |  | 144 |         $minfraction = optional_param($prefix . ':minfraction', null, PARAM_FLOAT);
 | 
        
           |  |  | 145 |         $maxfraction = optional_param($prefix . ':maxfraction', null, PARAM_FLOAT);
 | 
        
           |  |  | 146 |         return $mark === '' ||
 | 
        
           |  |  | 147 |                 ($mark !== null && $mark >= $minfraction * $maxmark && $mark <= $maxfraction * $maxmark) ||
 | 
        
           |  |  | 148 |                 ($mark === null && $maxmark === null);
 | 
        
           |  |  | 149 |     }
 | 
        
           |  |  | 150 |   | 
        
           |  |  | 151 |     /**
 | 
        
           |  |  | 152 |      * @param array $questionids of question ids.
 | 
        
           |  |  | 153 |      * @param qubaid_condition $qubaids ids of the usages to consider.
 | 
        
           |  |  | 154 |      * @return boolean whether any of these questions are being used by any of
 | 
        
           |  |  | 155 |      *      those usages.
 | 
        
           |  |  | 156 |      */
 | 
        
           | 1441 | ariadna | 157 |     public static function questions_in_use(array $questionids, ?qubaid_condition $qubaids = null) {
 | 
        
           | 1 | efrain | 158 |         if (is_null($qubaids)) {
 | 
        
           |  |  | 159 |             return false;
 | 
        
           |  |  | 160 |         }
 | 
        
           |  |  | 161 |         $dm = new question_engine_data_mapper();
 | 
        
           |  |  | 162 |         return $dm->questions_in_use($questionids, $qubaids);
 | 
        
           |  |  | 163 |     }
 | 
        
           |  |  | 164 |   | 
        
           |  |  | 165 |     /**
 | 
        
           |  |  | 166 |      * Get the number of times each variant has been used for each question in a list
 | 
        
           |  |  | 167 |      * in a set of usages.
 | 
        
           |  |  | 168 |      * @param array $questionids of question ids.
 | 
        
           |  |  | 169 |      * @param qubaid_condition $qubaids ids of the usages to consider.
 | 
        
           |  |  | 170 |      * @return array questionid => variant number => num uses.
 | 
        
           |  |  | 171 |      */
 | 
        
           |  |  | 172 |     public static function load_used_variants(array $questionids, qubaid_condition $qubaids) {
 | 
        
           |  |  | 173 |         $dm = new question_engine_data_mapper();
 | 
        
           |  |  | 174 |         return $dm->load_used_variants($questionids, $qubaids);
 | 
        
           |  |  | 175 |     }
 | 
        
           |  |  | 176 |   | 
        
           |  |  | 177 |     /**
 | 
        
           |  |  | 178 |      * Create an archetypal behaviour for a particular question attempt.
 | 
        
           |  |  | 179 |      * Used by {@link question_definition::make_behaviour()}.
 | 
        
           |  |  | 180 |      *
 | 
        
           |  |  | 181 |      * @param string $preferredbehaviour the type of model required.
 | 
        
           |  |  | 182 |      * @param question_attempt $qa the question attempt the model will process.
 | 
        
           |  |  | 183 |      * @return question_behaviour an instance of appropriate behaviour class.
 | 
        
           |  |  | 184 |      */
 | 
        
           |  |  | 185 |     public static function make_archetypal_behaviour($preferredbehaviour, question_attempt $qa) {
 | 
        
           |  |  | 186 |         if (!self::is_behaviour_archetypal($preferredbehaviour)) {
 | 
        
           |  |  | 187 |             throw new coding_exception('The requested behaviour is not actually ' .
 | 
        
           |  |  | 188 |                     'an archetypal one.');
 | 
        
           |  |  | 189 |         }
 | 
        
           |  |  | 190 |   | 
        
           |  |  | 191 |         self::load_behaviour_class($preferredbehaviour);
 | 
        
           |  |  | 192 |         $class = 'qbehaviour_' . $preferredbehaviour;
 | 
        
           |  |  | 193 |         return new $class($qa, $preferredbehaviour);
 | 
        
           |  |  | 194 |     }
 | 
        
           |  |  | 195 |   | 
        
           |  |  | 196 |     /**
 | 
        
           |  |  | 197 |      * @param string $behaviour the name of a behaviour.
 | 
        
           |  |  | 198 |      * @return array of {@link question_display_options} field names, that are
 | 
        
           |  |  | 199 |      * not relevant to this behaviour before a 'finish' action.
 | 
        
           |  |  | 200 |      */
 | 
        
           |  |  | 201 |     public static function get_behaviour_unused_display_options($behaviour) {
 | 
        
           |  |  | 202 |         return self::get_behaviour_type($behaviour)->get_unused_display_options();
 | 
        
           |  |  | 203 |     }
 | 
        
           |  |  | 204 |   | 
        
           |  |  | 205 |     /**
 | 
        
           |  |  | 206 |      * With this behaviour, is it possible that a question might finish as the student
 | 
        
           |  |  | 207 |      * interacts with it, without a call to the {@link question_attempt::finish()} method?
 | 
        
           |  |  | 208 |      * @param string $behaviour the name of a behaviour. E.g. 'deferredfeedback'.
 | 
        
           |  |  | 209 |      * @return bool whether with this behaviour, questions may finish naturally.
 | 
        
           |  |  | 210 |      */
 | 
        
           |  |  | 211 |     public static function can_questions_finish_during_the_attempt($behaviour) {
 | 
        
           |  |  | 212 |         return self::get_behaviour_type($behaviour)->can_questions_finish_during_the_attempt();
 | 
        
           |  |  | 213 |     }
 | 
        
           |  |  | 214 |   | 
        
           |  |  | 215 |     /**
 | 
        
           |  |  | 216 |      * Create a behaviour for a particular type. If that type cannot be
 | 
        
           |  |  | 217 |      * found, return an instance of qbehaviour_missing.
 | 
        
           |  |  | 218 |      *
 | 
        
           |  |  | 219 |      * Normally you should use {@link make_archetypal_behaviour()}, or
 | 
        
           |  |  | 220 |      * call the constructor of a particular model class directly. This method
 | 
        
           |  |  | 221 |      * is only intended for use by {@link question_attempt::load_from_records()}.
 | 
        
           |  |  | 222 |      *
 | 
        
           |  |  | 223 |      * @param string $behaviour the type of model to create.
 | 
        
           |  |  | 224 |      * @param question_attempt $qa the question attempt the model will process.
 | 
        
           |  |  | 225 |      * @param string $preferredbehaviour the preferred behaviour for the containing usage.
 | 
        
           |  |  | 226 |      * @return question_behaviour an instance of appropriate behaviour class.
 | 
        
           |  |  | 227 |      */
 | 
        
           |  |  | 228 |     public static function make_behaviour($behaviour, question_attempt $qa, $preferredbehaviour) {
 | 
        
           |  |  | 229 |         try {
 | 
        
           |  |  | 230 |             self::load_behaviour_class($behaviour);
 | 
        
           |  |  | 231 |         } catch (Exception $e) {
 | 
        
           |  |  | 232 |             self::load_behaviour_class('missing');
 | 
        
           |  |  | 233 |             return new qbehaviour_missing($qa, $preferredbehaviour);
 | 
        
           |  |  | 234 |         }
 | 
        
           |  |  | 235 |         $class = 'qbehaviour_' . $behaviour;
 | 
        
           |  |  | 236 |         return new $class($qa, $preferredbehaviour);
 | 
        
           |  |  | 237 |     }
 | 
        
           |  |  | 238 |   | 
        
           |  |  | 239 |     /**
 | 
        
           |  |  | 240 |      * Load the behaviour class(es) belonging to a particular model. That is,
 | 
        
           |  |  | 241 |      * include_once('/question/behaviour/' . $behaviour . '/behaviour.php'), with a bit
 | 
        
           |  |  | 242 |      * of checking.
 | 
        
           |  |  | 243 |      * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
 | 
        
           |  |  | 244 |      */
 | 
        
           |  |  | 245 |     public static function load_behaviour_class($behaviour) {
 | 
        
           |  |  | 246 |         global $CFG;
 | 
        
           |  |  | 247 |         if (isset(self::$loadedbehaviours[$behaviour])) {
 | 
        
           |  |  | 248 |             return;
 | 
        
           |  |  | 249 |         }
 | 
        
           |  |  | 250 |         $file = $CFG->dirroot . '/question/behaviour/' . $behaviour . '/behaviour.php';
 | 
        
           |  |  | 251 |         if (!is_readable($file)) {
 | 
        
           |  |  | 252 |             throw new coding_exception('Unknown question behaviour ' . $behaviour);
 | 
        
           |  |  | 253 |         }
 | 
        
           |  |  | 254 |         include_once($file);
 | 
        
           |  |  | 255 |   | 
        
           |  |  | 256 |         $class = 'qbehaviour_' . $behaviour;
 | 
        
           |  |  | 257 |         if (!class_exists($class)) {
 | 
        
           |  |  | 258 |             throw new coding_exception('Question behaviour ' . $behaviour .
 | 
        
           |  |  | 259 |                     ' does not define the required class ' . $class . '.');
 | 
        
           |  |  | 260 |         }
 | 
        
           |  |  | 261 |   | 
        
           |  |  | 262 |         self::$loadedbehaviours[$behaviour] = 1;
 | 
        
           |  |  | 263 |     }
 | 
        
           |  |  | 264 |   | 
        
           |  |  | 265 |     /**
 | 
        
           |  |  | 266 |      * Create a behaviour for a particular type. If that type cannot be
 | 
        
           |  |  | 267 |      * found, return an instance of qbehaviour_missing.
 | 
        
           |  |  | 268 |      *
 | 
        
           |  |  | 269 |      * Normally you should use {@link make_archetypal_behaviour()}, or
 | 
        
           |  |  | 270 |      * call the constructor of a particular model class directly. This method
 | 
        
           |  |  | 271 |      * is only intended for use by {@link question_attempt::load_from_records()}.
 | 
        
           |  |  | 272 |      *
 | 
        
           |  |  | 273 |      * @param string $behaviour the type of model to create.
 | 
        
           |  |  | 274 |      * @param question_attempt $qa the question attempt the model will process.
 | 
        
           |  |  | 275 |      * @param string $preferredbehaviour the preferred behaviour for the containing usage.
 | 
        
           |  |  | 276 |      * @return question_behaviour_type an instance of appropriate behaviour class.
 | 
        
           |  |  | 277 |      */
 | 
        
           |  |  | 278 |     public static function get_behaviour_type($behaviour) {
 | 
        
           |  |  | 279 |   | 
        
           |  |  | 280 |         if (array_key_exists($behaviour, self::$behaviourtypes)) {
 | 
        
           |  |  | 281 |             return self::$behaviourtypes[$behaviour];
 | 
        
           |  |  | 282 |         }
 | 
        
           |  |  | 283 |   | 
        
           |  |  | 284 |         self::load_behaviour_type_class($behaviour);
 | 
        
           |  |  | 285 |   | 
        
           |  |  | 286 |         $class = 'qbehaviour_' . $behaviour . '_type';
 | 
        
           |  |  | 287 |         if (class_exists($class)) {
 | 
        
           |  |  | 288 |             self::$behaviourtypes[$behaviour] = new $class();
 | 
        
           |  |  | 289 |         } else {
 | 
        
           |  |  | 290 |             debugging('Question behaviour ' . $behaviour .
 | 
        
           |  |  | 291 |                     ' does not define the required class ' . $class . '.', DEBUG_DEVELOPER);
 | 
        
           |  |  | 292 |             self::$behaviourtypes[$behaviour] = new question_behaviour_type_fallback($behaviour);
 | 
        
           |  |  | 293 |         }
 | 
        
           |  |  | 294 |   | 
        
           |  |  | 295 |         return self::$behaviourtypes[$behaviour];
 | 
        
           |  |  | 296 |     }
 | 
        
           |  |  | 297 |   | 
        
           |  |  | 298 |     /**
 | 
        
           |  |  | 299 |      * Load the behaviour type class for a particular behaviour. That is,
 | 
        
           |  |  | 300 |      * include_once('/question/behaviour/' . $behaviour . '/behaviourtype.php').
 | 
        
           |  |  | 301 |      * @param string $behaviour the behaviour name. For example 'interactive' or 'deferredfeedback'.
 | 
        
           |  |  | 302 |      */
 | 
        
           |  |  | 303 |     protected static function load_behaviour_type_class($behaviour) {
 | 
        
           |  |  | 304 |         global $CFG;
 | 
        
           |  |  | 305 |         if (isset(self::$behaviourtypes[$behaviour])) {
 | 
        
           |  |  | 306 |             return;
 | 
        
           |  |  | 307 |         }
 | 
        
           |  |  | 308 |         $file = $CFG->dirroot . '/question/behaviour/' . $behaviour . '/behaviourtype.php';
 | 
        
           |  |  | 309 |         if (!is_readable($file)) {
 | 
        
           |  |  | 310 |             debugging('Question behaviour ' . $behaviour .
 | 
        
           |  |  | 311 |                     ' is missing the behaviourtype.php file.', DEBUG_DEVELOPER);
 | 
        
           |  |  | 312 |         }
 | 
        
           |  |  | 313 |         include_once($file);
 | 
        
           |  |  | 314 |     }
 | 
        
           |  |  | 315 |   | 
        
           |  |  | 316 |     /**
 | 
        
           |  |  | 317 |      * Return an array where the keys are the internal names of the archetypal
 | 
        
           |  |  | 318 |      * behaviours, and the values are a human-readable name. An
 | 
        
           |  |  | 319 |      * archetypal behaviour is one that is suitable to pass the name of to
 | 
        
           |  |  | 320 |      * {@link question_usage_by_activity::set_preferred_behaviour()}.
 | 
        
           |  |  | 321 |      *
 | 
        
           |  |  | 322 |      * @return array model name => lang string for this behaviour name.
 | 
        
           |  |  | 323 |      */
 | 
        
           |  |  | 324 |     public static function get_archetypal_behaviours() {
 | 
        
           |  |  | 325 |         $archetypes = array();
 | 
        
           |  |  | 326 |         $behaviours = core_component::get_plugin_list('qbehaviour');
 | 
        
           |  |  | 327 |         foreach ($behaviours as $behaviour => $notused) {
 | 
        
           |  |  | 328 |             if (self::is_behaviour_archetypal($behaviour)) {
 | 
        
           |  |  | 329 |                 $archetypes[$behaviour] = self::get_behaviour_name($behaviour);
 | 
        
           |  |  | 330 |             }
 | 
        
           |  |  | 331 |         }
 | 
        
           |  |  | 332 |         asort($archetypes, SORT_LOCALE_STRING);
 | 
        
           |  |  | 333 |         return $archetypes;
 | 
        
           |  |  | 334 |     }
 | 
        
           |  |  | 335 |   | 
        
           |  |  | 336 |     /**
 | 
        
           |  |  | 337 |      * @param string $behaviour the name of a behaviour. E.g. 'deferredfeedback'.
 | 
        
           |  |  | 338 |      * @return bool whether this is an archetypal behaviour.
 | 
        
           |  |  | 339 |      */
 | 
        
           |  |  | 340 |     public static function is_behaviour_archetypal($behaviour) {
 | 
        
           |  |  | 341 |         return self::get_behaviour_type($behaviour)->is_archetypal();
 | 
        
           |  |  | 342 |     }
 | 
        
           |  |  | 343 |   | 
        
           |  |  | 344 |     /**
 | 
        
           |  |  | 345 |      * Return an array where the keys are the internal names of the behaviours
 | 
        
           |  |  | 346 |      * in preferred order and the values are a human-readable name.
 | 
        
           |  |  | 347 |      *
 | 
        
           |  |  | 348 |      * @param array $archetypes, array of behaviours
 | 
        
           |  |  | 349 |      * @param string $orderlist, a comma separated list of behaviour names
 | 
        
           |  |  | 350 |      * @param string $disabledlist, a comma separated list of behaviour names
 | 
        
           |  |  | 351 |      * @param string $current, current behaviour name
 | 
        
           |  |  | 352 |      * @return array model name => lang string for this behaviour name.
 | 
        
           |  |  | 353 |      */
 | 
        
           |  |  | 354 |     public static function sort_behaviours($archetypes, $orderlist, $disabledlist, $current=null) {
 | 
        
           |  |  | 355 |   | 
        
           |  |  | 356 |         // Get disabled behaviours
 | 
        
           |  |  | 357 |         if ($disabledlist) {
 | 
        
           |  |  | 358 |             $disabled = explode(',', $disabledlist);
 | 
        
           |  |  | 359 |         } else {
 | 
        
           |  |  | 360 |             $disabled = array();
 | 
        
           |  |  | 361 |         }
 | 
        
           |  |  | 362 |   | 
        
           |  |  | 363 |         if ($orderlist) {
 | 
        
           |  |  | 364 |             $order = explode(',', $orderlist);
 | 
        
           |  |  | 365 |         } else {
 | 
        
           |  |  | 366 |             $order = array();
 | 
        
           |  |  | 367 |         }
 | 
        
           |  |  | 368 |   | 
        
           |  |  | 369 |         foreach ($disabled as $behaviour) {
 | 
        
           |  |  | 370 |             if (array_key_exists($behaviour, $archetypes) && $behaviour != $current) {
 | 
        
           |  |  | 371 |                 unset($archetypes[$behaviour]);
 | 
        
           |  |  | 372 |             }
 | 
        
           |  |  | 373 |         }
 | 
        
           |  |  | 374 |   | 
        
           |  |  | 375 |         // Get behaviours in preferred order
 | 
        
           |  |  | 376 |         $behaviourorder = array();
 | 
        
           |  |  | 377 |         foreach ($order as $behaviour) {
 | 
        
           |  |  | 378 |             if (array_key_exists($behaviour, $archetypes)) {
 | 
        
           |  |  | 379 |                 $behaviourorder[$behaviour] = $archetypes[$behaviour];
 | 
        
           |  |  | 380 |             }
 | 
        
           |  |  | 381 |         }
 | 
        
           |  |  | 382 |         // Get the rest of behaviours and sort them alphabetically
 | 
        
           |  |  | 383 |         $leftover = array_diff_key($archetypes, $behaviourorder);
 | 
        
           |  |  | 384 |         asort($leftover, SORT_LOCALE_STRING);
 | 
        
           |  |  | 385 |   | 
        
           |  |  | 386 |         // Set up the final order to be displayed
 | 
        
           |  |  | 387 |         return $behaviourorder + $leftover;
 | 
        
           |  |  | 388 |     }
 | 
        
           |  |  | 389 |   | 
        
           |  |  | 390 |     /**
 | 
        
           |  |  | 391 |      * Return an array where the keys are the internal names of the behaviours
 | 
        
           |  |  | 392 |      * in preferred order and the values are a human-readable name.
 | 
        
           |  |  | 393 |      *
 | 
        
           |  |  | 394 |      * @param string $currentbehaviour
 | 
        
           |  |  | 395 |      * @return array model name => lang string for this behaviour name.
 | 
        
           |  |  | 396 |      */
 | 
        
           |  |  | 397 |     public static function get_behaviour_options($currentbehaviour) {
 | 
        
           |  |  | 398 |         $config = question_bank::get_config();
 | 
        
           |  |  | 399 |         $archetypes = self::get_archetypal_behaviours();
 | 
        
           |  |  | 400 |   | 
        
           |  |  | 401 |         // If no admin setting return all behavious
 | 
        
           |  |  | 402 |         if (empty($config->disabledbehaviours) && empty($config->behavioursortorder)) {
 | 
        
           |  |  | 403 |             return $archetypes;
 | 
        
           |  |  | 404 |         }
 | 
        
           |  |  | 405 |   | 
        
           |  |  | 406 |         if (empty($config->behavioursortorder)) {
 | 
        
           |  |  | 407 |             $order = '';
 | 
        
           |  |  | 408 |         } else {
 | 
        
           |  |  | 409 |             $order = $config->behavioursortorder;
 | 
        
           |  |  | 410 |         }
 | 
        
           |  |  | 411 |         if (empty($config->disabledbehaviours)) {
 | 
        
           |  |  | 412 |             $disabled = '';
 | 
        
           |  |  | 413 |         } else {
 | 
        
           |  |  | 414 |             $disabled = $config->disabledbehaviours;
 | 
        
           |  |  | 415 |         }
 | 
        
           |  |  | 416 |   | 
        
           |  |  | 417 |         return self::sort_behaviours($archetypes, $order, $disabled, $currentbehaviour);
 | 
        
           |  |  | 418 |     }
 | 
        
           |  |  | 419 |   | 
        
           |  |  | 420 |     /**
 | 
        
           |  |  | 421 |      * Get the translated name of a behaviour, for display in the UI.
 | 
        
           |  |  | 422 |      * @param string $behaviour the internal name of the model.
 | 
        
           |  |  | 423 |      * @return string name from the current language pack.
 | 
        
           |  |  | 424 |      */
 | 
        
           |  |  | 425 |     public static function get_behaviour_name($behaviour) {
 | 
        
           |  |  | 426 |         return get_string('pluginname', 'qbehaviour_' . $behaviour);
 | 
        
           |  |  | 427 |     }
 | 
        
           |  |  | 428 |   | 
        
           |  |  | 429 |     /**
 | 
        
           |  |  | 430 |      * @return array all the file area names that may contain response files.
 | 
        
           |  |  | 431 |      */
 | 
        
           |  |  | 432 |     public static function get_all_response_file_areas() {
 | 
        
           |  |  | 433 |         $variables = array();
 | 
        
           |  |  | 434 |         foreach (question_bank::get_all_qtypes() as $qtype) {
 | 
        
           |  |  | 435 |             $variables = array_merge($variables, $qtype->response_file_areas());
 | 
        
           |  |  | 436 |         }
 | 
        
           |  |  | 437 |   | 
        
           |  |  | 438 |         $areas = array();
 | 
        
           |  |  | 439 |         foreach (array_unique($variables) as $variable) {
 | 
        
           |  |  | 440 |             $areas[] = 'response_' . $variable;
 | 
        
           |  |  | 441 |         }
 | 
        
           |  |  | 442 |         return $areas;
 | 
        
           |  |  | 443 |     }
 | 
        
           |  |  | 444 |   | 
        
           |  |  | 445 |     /**
 | 
        
           |  |  | 446 |      * Returns the valid choices for the number of decimal places for showing
 | 
        
           |  |  | 447 |      * question marks. For use in the user interface.
 | 
        
           |  |  | 448 |      * @return array suitable for passing to {@link html_writer::select()} or similar.
 | 
        
           |  |  | 449 |      */
 | 
        
           |  |  | 450 |     public static function get_dp_options() {
 | 
        
           |  |  | 451 |         return question_display_options::get_dp_options();
 | 
        
           |  |  | 452 |     }
 | 
        
           |  |  | 453 |   | 
        
           |  |  | 454 |     /**
 | 
        
           |  |  | 455 |      * Initialise the JavaScript required on pages where questions will be displayed.
 | 
        
           |  |  | 456 |      *
 | 
        
           |  |  | 457 |      * @return string
 | 
        
           |  |  | 458 |      */
 | 
        
           |  |  | 459 |     public static function initialise_js() {
 | 
        
           |  |  | 460 |         return question_flags::initialise_js();
 | 
        
           |  |  | 461 |     }
 | 
        
           |  |  | 462 | }
 | 
        
           |  |  | 463 |   | 
        
           |  |  | 464 |   | 
        
           |  |  | 465 | /**
 | 
        
           |  |  | 466 |  * This class contains all the options that controls how a question is displayed.
 | 
        
           |  |  | 467 |  *
 | 
        
           |  |  | 468 |  * Normally, what will happen is that the calling code will set up some display
 | 
        
           |  |  | 469 |  * options to indicate what sort of question display it wants, and then before the
 | 
        
           |  |  | 470 |  * question is rendered, the behaviour will be given a chance to modify the
 | 
        
           |  |  | 471 |  * display options, so that, for example, A question that is finished will only
 | 
        
           |  |  | 472 |  * be shown read-only, and a question that has not been submitted will not have
 | 
        
           |  |  | 473 |  * any sort of feedback displayed.
 | 
        
           |  |  | 474 |  *
 | 
        
           |  |  | 475 |  * @copyright  2009 The Open University
 | 
        
           |  |  | 476 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 477 |  */
 | 
        
           |  |  | 478 | class question_display_options {
 | 
        
           |  |  | 479 |     /**#@+
 | 
        
           |  |  | 480 |      * @var int named constants for the values that most of the options take.
 | 
        
           |  |  | 481 |      */
 | 
        
           |  |  | 482 |     const SHOW_ALL = -1;
 | 
        
           |  |  | 483 |     const HIDDEN = 0;
 | 
        
           |  |  | 484 |     const VISIBLE = 1;
 | 
        
           |  |  | 485 |     const EDITABLE = 2;
 | 
        
           |  |  | 486 |     /**#@-*/
 | 
        
           |  |  | 487 |   | 
        
           |  |  | 488 |     /**#@+ @var int named constants for the {@see $marks} option. */
 | 
        
           |  |  | 489 |     const MAX_ONLY = 1;
 | 
        
           |  |  | 490 |     const MARK_AND_MAX = 2;
 | 
        
           |  |  | 491 |     /**#@-*/
 | 
        
           |  |  | 492 |   | 
        
           |  |  | 493 |     /**
 | 
        
           |  |  | 494 |      * @var int maximum value for the {@see $markpd} option. This is
 | 
        
           |  |  | 495 |      * effectively set by the database structure, which uses NUMBER(12,7) columns
 | 
        
           |  |  | 496 |      * for question marks/fractions.
 | 
        
           |  |  | 497 |      */
 | 
        
           |  |  | 498 |     const MAX_DP = 7;
 | 
        
           |  |  | 499 |   | 
        
           |  |  | 500 |     /**
 | 
        
           |  |  | 501 |      * @var boolean whether the question should be displayed as a read-only review,
 | 
        
           |  |  | 502 |      * or in an active state where you can change the answer.
 | 
        
           |  |  | 503 |      */
 | 
        
           |  |  | 504 |     public $readonly = false;
 | 
        
           |  |  | 505 |   | 
        
           |  |  | 506 |     /**
 | 
        
           |  |  | 507 |      * @var boolean whether the question type should output hidden form fields
 | 
        
           |  |  | 508 |      * to reset any incorrect parts of the resonse to blank.
 | 
        
           |  |  | 509 |      */
 | 
        
           |  |  | 510 |     public $clearwrong = false;
 | 
        
           |  |  | 511 |   | 
        
           |  |  | 512 |     /**
 | 
        
           |  |  | 513 |      * Should the student have what they got right and wrong clearly indicated.
 | 
        
           |  |  | 514 |      * This includes the green/red hilighting of the bits of their response,
 | 
        
           |  |  | 515 |      * whether the one-line summary of the current state of the question says
 | 
        
           |  |  | 516 |      * correct/incorrect or just answered.
 | 
        
           |  |  | 517 |      * @var int {@see question_display_options::HIDDEN} or
 | 
        
           |  |  | 518 |      * {@see question_display_options::VISIBLE}
 | 
        
           |  |  | 519 |      */
 | 
        
           |  |  | 520 |     public $correctness = self::VISIBLE;
 | 
        
           |  |  | 521 |   | 
        
           |  |  | 522 |     /**
 | 
        
           |  |  | 523 |      * The the mark and/or the maximum available mark for this question be visible?
 | 
        
           |  |  | 524 |      * @var int {@see question_display_options::HIDDEN},
 | 
        
           |  |  | 525 |      * {@see question_display_options::MAX_ONLY} or {@see question_display_options::MARK_AND_MAX}
 | 
        
           |  |  | 526 |      */
 | 
        
           |  |  | 527 |     public $marks = self::MARK_AND_MAX;
 | 
        
           |  |  | 528 |   | 
        
           |  |  | 529 |     /** @var int of decimal places to use when formatting marks for output. */
 | 
        
           |  |  | 530 |     public $markdp = 2;
 | 
        
           |  |  | 531 |   | 
        
           |  |  | 532 |     /**
 | 
        
           |  |  | 533 |      * Should the flag this question UI element be visible, and if so, should the
 | 
        
           |  |  | 534 |      * flag state be changeable?
 | 
        
           |  |  | 535 |      *
 | 
        
           |  |  | 536 |      * @var int {@see question_display_options::HIDDEN},
 | 
        
           |  |  | 537 |      * {@see question_display_options::VISIBLE} or {@see question_display_options::EDITABLE}
 | 
        
           |  |  | 538 |      */
 | 
        
           |  |  | 539 |     public $flags = self::VISIBLE;
 | 
        
           |  |  | 540 |   | 
        
           |  |  | 541 |     /**
 | 
        
           |  |  | 542 |      * Should the specific feedback be visible.
 | 
        
           |  |  | 543 |      *
 | 
        
           |  |  | 544 |      * Specific feedback is typically the part of the feedback that changes based on the
 | 
        
           |  |  | 545 |      * answer that the student gave. For example the feedback shown if a particular choice
 | 
        
           |  |  | 546 |      * has been chosen in a multi-choice question. It also includes the combined feedback
 | 
        
           |  |  | 547 |      * that a lost of question types have (e.g. feedback for any correct/incorrect response.)
 | 
        
           |  |  | 548 |      *
 | 
        
           |  |  | 549 |      * @var int {@see question_display_options::HIDDEN} or
 | 
        
           |  |  | 550 |      * {@see question_display_options::VISIBLE}
 | 
        
           |  |  | 551 |      */
 | 
        
           |  |  | 552 |     public $feedback = self::VISIBLE;
 | 
        
           |  |  | 553 |   | 
        
           |  |  | 554 |     /**
 | 
        
           |  |  | 555 |      * For questions with a number of sub-parts (like matching, or
 | 
        
           |  |  | 556 |      * multiple-choice, multiple-reponse) display the number of sub-parts that
 | 
        
           |  |  | 557 |      * were correct.
 | 
        
           |  |  | 558 |      * @var int {@see question_display_options::HIDDEN} or
 | 
        
           |  |  | 559 |      * {@see question_display_options::VISIBLE}
 | 
        
           |  |  | 560 |      */
 | 
        
           |  |  | 561 |     public $numpartscorrect = self::VISIBLE;
 | 
        
           |  |  | 562 |   | 
        
           |  |  | 563 |     /**
 | 
        
           |  |  | 564 |      * Should the general feedback be visible?
 | 
        
           |  |  | 565 |      *
 | 
        
           |  |  | 566 |      * This is typically feedback shown to all students after the question
 | 
        
           |  |  | 567 |      * is finished, irrespective of which answer they gave.
 | 
        
           |  |  | 568 |      *
 | 
        
           |  |  | 569 |      * @var int {@see question_display_options::HIDDEN} or
 | 
        
           |  |  | 570 |      * {@see question_display_options::VISIBLE}
 | 
        
           |  |  | 571 |      */
 | 
        
           |  |  | 572 |     public $generalfeedback = self::VISIBLE;
 | 
        
           |  |  | 573 |   | 
        
           |  |  | 574 |     /**
 | 
        
           |  |  | 575 |      * Should the automatically generated display of what the correct answer be visible?
 | 
        
           |  |  | 576 |      *
 | 
        
           |  |  | 577 |      * @var int {@see question_display_options::HIDDEN} or
 | 
        
           |  |  | 578 |      * {@see question_display_options::VISIBLE}
 | 
        
           |  |  | 579 |      */
 | 
        
           |  |  | 580 |     public $rightanswer = self::VISIBLE;
 | 
        
           |  |  | 581 |   | 
        
           |  |  | 582 |     /**
 | 
        
           |  |  | 583 |      * Should the manually added marker's comment be visible. Should the link for
 | 
        
           |  |  | 584 |      * adding/editing the comment be there.
 | 
        
           |  |  | 585 |      * @var int {@see question_display_options::HIDDEN},
 | 
        
           |  |  | 586 |      * {@see question_display_options::VISIBLE}, or {@see question_display_options::EDITABLE}.
 | 
        
           |  |  | 587 |      * Editable means that form fields are displayed inline.
 | 
        
           |  |  | 588 |      */
 | 
        
           |  |  | 589 |     public $manualcomment = self::VISIBLE;
 | 
        
           |  |  | 590 |   | 
        
           |  |  | 591 |     /**
 | 
        
           |  |  | 592 |      * Should we show a 'Make comment or override grade' link?
 | 
        
           |  |  | 593 |      * @var string base URL for the edit comment script, which will be shown if
 | 
        
           |  |  | 594 |      * $manualcomment = self::VISIBLE.
 | 
        
           |  |  | 595 |      */
 | 
        
           |  |  | 596 |     public $manualcommentlink = null;
 | 
        
           |  |  | 597 |   | 
        
           |  |  | 598 |     /**
 | 
        
           |  |  | 599 |      * Used in places like the question history table, to show a link to review
 | 
        
           |  |  | 600 |      * this question in a certain state. If blank, a link is not shown.
 | 
        
           |  |  | 601 |      * @var moodle_url base URL for a review question script.
 | 
        
           |  |  | 602 |      */
 | 
        
           |  |  | 603 |     public $questionreviewlink = null;
 | 
        
           |  |  | 604 |   | 
        
           |  |  | 605 |     /**
 | 
        
           |  |  | 606 |      * Should the history of previous question states table be visible?
 | 
        
           |  |  | 607 |      * @var int {@see question_display_options::HIDDEN} or
 | 
        
           |  |  | 608 |      * {@see question_display_options::VISIBLE}
 | 
        
           |  |  | 609 |      */
 | 
        
           |  |  | 610 |     public $history = self::HIDDEN;
 | 
        
           |  |  | 611 |   | 
        
           |  |  | 612 |     /**
 | 
        
           |  |  | 613 |      * @since 2.9
 | 
        
           |  |  | 614 |      * @var string extra HTML to include at the end of the outcome (feedback) box
 | 
        
           |  |  | 615 |      * of the question display.
 | 
        
           |  |  | 616 |      *
 | 
        
           |  |  | 617 |      * This field is now badly named. The place it included is was changed
 | 
        
           |  |  | 618 |      * (for the better) but the name was left unchanged for backwards compatibility.
 | 
        
           |  |  | 619 |      */
 | 
        
           |  |  | 620 |     public $extrainfocontent = '';
 | 
        
           |  |  | 621 |   | 
        
           |  |  | 622 |     /**
 | 
        
           |  |  | 623 |      * @since 2.9
 | 
        
           |  |  | 624 |      * @var string extra HTML to include in the history box of the question display,
 | 
        
           |  |  | 625 |      * if it is shown.
 | 
        
           |  |  | 626 |      */
 | 
        
           |  |  | 627 |     public $extrahistorycontent = '';
 | 
        
           |  |  | 628 |   | 
        
           |  |  | 629 |     /**
 | 
        
           |  |  | 630 |      * If not empty, then a link to edit the question will be included in
 | 
        
           |  |  | 631 |      * the info box for the question.
 | 
        
           |  |  | 632 |      *
 | 
        
           |  |  | 633 |      * If used, this array must contain an element courseid or cmid.
 | 
        
           |  |  | 634 |      *
 | 
        
           |  |  | 635 |      * It shoudl also contain a parameter returnurl => moodle_url giving a
 | 
        
           |  |  | 636 |      * sensible URL to go back to when the editing form is submitted or cancelled.
 | 
        
           |  |  | 637 |      *
 | 
        
           |  |  | 638 |      * @var array url parameter for the edit link. id => questiosnid will be
 | 
        
           |  |  | 639 |      * added automatically.
 | 
        
           |  |  | 640 |      */
 | 
        
           |  |  | 641 |     public $editquestionparams = array();
 | 
        
           |  |  | 642 |   | 
        
           |  |  | 643 |     /**
 | 
        
           |  |  | 644 |      * @var context the context the attempt being output belongs to.
 | 
        
           |  |  | 645 |      */
 | 
        
           |  |  | 646 |     public $context;
 | 
        
           |  |  | 647 |   | 
        
           |  |  | 648 |     /**
 | 
        
           |  |  | 649 |      * @var int The option to show the action author in the response history.
 | 
        
           |  |  | 650 |      */
 | 
        
           |  |  | 651 |     public $userinfoinhistory = self::HIDDEN;
 | 
        
           |  |  | 652 |   | 
        
           |  |  | 653 |     /**
 | 
        
           |  |  | 654 |      * This identifier should be added to the labels of all input fields in the question.
 | 
        
           |  |  | 655 |      *
 | 
        
           |  |  | 656 |      * This is so people using assistive technology can easily tell which input belong to
 | 
        
           |  |  | 657 |      * which question. The helper {@see self::add_question_identifier_to_label() makes this easier.
 | 
        
           |  |  | 658 |      *
 | 
        
           |  |  | 659 |      * If not set before the question is rendered, then it defaults to 'Question N'.
 | 
        
           |  |  | 660 |      * (lang string)
 | 
        
           |  |  | 661 |      *
 | 
        
           |  |  | 662 |      * @var string The identifier that the question being rendered is associated with.
 | 
        
           |  |  | 663 |      *              E.g. The question number when it is rendered on a quiz.
 | 
        
           |  |  | 664 |      */
 | 
        
           |  |  | 665 |     public $questionidentifier = null;
 | 
        
           |  |  | 666 |   | 
        
           |  |  | 667 |     /**
 | 
        
           |  |  | 668 |      * @var ?bool $versioninfo Should we display the version in the question info?
 | 
        
           |  |  | 669 |      */
 | 
        
           |  |  | 670 |     public ?bool $versioninfo = null;
 | 
        
           |  |  | 671 |   | 
        
           |  |  | 672 |     /**
 | 
        
           |  |  | 673 |      * Set all the feedback-related fields, feedback, numpartscorrect, generalfeedback,
 | 
        
           |  |  | 674 |      * rightanswer, manualcomment} and correctness to {@see question_display_options::HIDDEN}.
 | 
        
           |  |  | 675 |      */
 | 
        
           |  |  | 676 |     public function hide_all_feedback() {
 | 
        
           |  |  | 677 |         $this->feedback = self::HIDDEN;
 | 
        
           |  |  | 678 |         $this->numpartscorrect = self::HIDDEN;
 | 
        
           |  |  | 679 |         $this->generalfeedback = self::HIDDEN;
 | 
        
           |  |  | 680 |         $this->rightanswer = self::HIDDEN;
 | 
        
           |  |  | 681 |         $this->manualcomment = self::HIDDEN;
 | 
        
           |  |  | 682 |         $this->correctness = self::HIDDEN;
 | 
        
           |  |  | 683 |     }
 | 
        
           |  |  | 684 |   | 
        
           |  |  | 685 |     /**
 | 
        
           |  |  | 686 |      * Returns the valid choices for the number of decimal places for showing
 | 
        
           |  |  | 687 |      * question marks. For use in the user interface.
 | 
        
           |  |  | 688 |      *
 | 
        
           |  |  | 689 |      * Calling code should probably use {@see question_engine::get_dp_options()}
 | 
        
           |  |  | 690 |      * rather than calling this method directly.
 | 
        
           |  |  | 691 |      *
 | 
        
           |  |  | 692 |      * @return array suitable for passing to {@see html_writer::select()} or similar.
 | 
        
           |  |  | 693 |      */
 | 
        
           |  |  | 694 |     public static function get_dp_options() {
 | 
        
           |  |  | 695 |         $options = array();
 | 
        
           |  |  | 696 |         for ($i = 0; $i <= self::MAX_DP; $i += 1) {
 | 
        
           |  |  | 697 |             $options[$i] = $i;
 | 
        
           |  |  | 698 |         }
 | 
        
           |  |  | 699 |         return $options;
 | 
        
           |  |  | 700 |     }
 | 
        
           |  |  | 701 |   | 
        
           |  |  | 702 |     /**
 | 
        
           |  |  | 703 |      * Helper to add the question identify (if there is one) to the label of an input field in a question.
 | 
        
           |  |  | 704 |      *
 | 
        
           |  |  | 705 |      * @param string $label The plain field label. E.g. 'Answer 1'
 | 
        
           | 1441 | ariadna | 706 |      * @param bool $sridentifier If true, the question identifier, if added, will be wrapped in a visually-hidden. Default false.
 | 
        
           | 1 | efrain | 707 |      * @param bool $addbefore If true, the question identifier will be added before the label.
 | 
        
           |  |  | 708 |      * @return string The amended label. For example 'Answer 1, Question 1'.
 | 
        
           |  |  | 709 |      */
 | 
        
           |  |  | 710 |     public function add_question_identifier_to_label(string $label, bool $sridentifier = false, bool $addbefore = false): string {
 | 
        
           |  |  | 711 |         if (!$this->has_question_identifier()) {
 | 
        
           |  |  | 712 |             return $label;
 | 
        
           |  |  | 713 |         }
 | 
        
           |  |  | 714 |         $identifier = $this->questionidentifier;
 | 
        
           |  |  | 715 |         if ($sridentifier) {
 | 
        
           | 1441 | ariadna | 716 |             $identifier = html_writer::span($identifier, 'visually-hidden');
 | 
        
           | 1 | efrain | 717 |         }
 | 
        
           |  |  | 718 |         $fieldlang = 'fieldinquestion';
 | 
        
           |  |  | 719 |         if ($addbefore) {
 | 
        
           |  |  | 720 |             $fieldlang = 'fieldinquestionpre';
 | 
        
           |  |  | 721 |         }
 | 
        
           |  |  | 722 |         return get_string($fieldlang, 'question', (object)['fieldname' => $label, 'questionindentifier' => $identifier]);
 | 
        
           |  |  | 723 |     }
 | 
        
           |  |  | 724 |   | 
        
           |  |  | 725 |     /**
 | 
        
           |  |  | 726 |      * Whether a question number has been provided for the question that is being displayed.
 | 
        
           |  |  | 727 |      *
 | 
        
           |  |  | 728 |      * @return bool
 | 
        
           |  |  | 729 |      */
 | 
        
           |  |  | 730 |     public function has_question_identifier(): bool {
 | 
        
           |  |  | 731 |         return $this->questionidentifier !== null && trim($this->questionidentifier) !== '';
 | 
        
           |  |  | 732 |     }
 | 
        
           |  |  | 733 | }
 | 
        
           |  |  | 734 |   | 
        
           |  |  | 735 |   | 
        
           |  |  | 736 | /**
 | 
        
           |  |  | 737 |  * Contains the logic for handling question flags.
 | 
        
           |  |  | 738 |  *
 | 
        
           |  |  | 739 |  * @copyright  2010 The Open University
 | 
        
           |  |  | 740 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 741 |  */
 | 
        
           |  |  | 742 | abstract class question_flags {
 | 
        
           |  |  | 743 |     /**
 | 
        
           |  |  | 744 |      * Get the checksum that validates that a toggle request is valid.
 | 
        
           |  |  | 745 |      * @param int $qubaid the question usage id.
 | 
        
           |  |  | 746 |      * @param int $questionid the question id.
 | 
        
           |  |  | 747 |      * @param int $sessionid the question_attempt id.
 | 
        
           |  |  | 748 |      * @param object $user the user. If null, defaults to $USER.
 | 
        
           |  |  | 749 |      * @return string that needs to be sent to question/toggleflag.php for it to work.
 | 
        
           |  |  | 750 |      */
 | 
        
           |  |  | 751 |     protected static function get_toggle_checksum($qubaid, $questionid,
 | 
        
           |  |  | 752 |             $qaid, $slot, $user = null) {
 | 
        
           |  |  | 753 |         if (is_null($user)) {
 | 
        
           |  |  | 754 |             global $USER;
 | 
        
           |  |  | 755 |             $user = $USER;
 | 
        
           |  |  | 756 |         }
 | 
        
           |  |  | 757 |         return md5($qubaid . "_" . $user->secret . "_" . $questionid . "_" . $qaid . "_" . $slot);
 | 
        
           |  |  | 758 |     }
 | 
        
           |  |  | 759 |   | 
        
           |  |  | 760 |     /**
 | 
        
           |  |  | 761 |      * Get the postdata that needs to be sent to question/toggleflag.php to change the flag state.
 | 
        
           |  |  | 762 |      * You need to append &newstate=0/1 to this.
 | 
        
           |  |  | 763 |      * @return the post data to send.
 | 
        
           |  |  | 764 |      */
 | 
        
           |  |  | 765 |     public static function get_postdata(question_attempt $qa) {
 | 
        
           |  |  | 766 |         $qaid = $qa->get_database_id();
 | 
        
           |  |  | 767 |         $qubaid = $qa->get_usage_id();
 | 
        
           |  |  | 768 |         $qid = $qa->get_question_id();
 | 
        
           |  |  | 769 |         $slot = $qa->get_slot();
 | 
        
           |  |  | 770 |         $checksum = self::get_toggle_checksum($qubaid, $qid, $qaid, $slot);
 | 
        
           |  |  | 771 |         return "qaid={$qaid}&qubaid={$qubaid}&qid={$qid}&slot={$slot}&checksum={$checksum}&sesskey=" .
 | 
        
           |  |  | 772 |                 sesskey() . '&newstate=';
 | 
        
           |  |  | 773 |     }
 | 
        
           |  |  | 774 |   | 
        
           |  |  | 775 |     /**
 | 
        
           |  |  | 776 |      * If the request seems valid, update the flag state of a question attempt.
 | 
        
           |  |  | 777 |      * Throws exceptions if this is not a valid update request.
 | 
        
           |  |  | 778 |      * @param int $qubaid the question usage id.
 | 
        
           |  |  | 779 |      * @param int $questionid the question id.
 | 
        
           |  |  | 780 |      * @param int $sessionid the question_attempt id.
 | 
        
           |  |  | 781 |      * @param string $checksum checksum, as computed by {@link get_toggle_checksum()}
 | 
        
           |  |  | 782 |      *      corresponding to the last three arguments.
 | 
        
           |  |  | 783 |      * @param bool $newstate the new state of the flag. true = flagged.
 | 
        
           |  |  | 784 |      */
 | 
        
           |  |  | 785 |     public static function update_flag($qubaid, $questionid, $qaid, $slot, $checksum, $newstate) {
 | 
        
           |  |  | 786 |         // Check the checksum - it is very hard to know who a question session belongs
 | 
        
           |  |  | 787 |         // to, so we require that checksum parameter is matches an md5 hash of the
 | 
        
           |  |  | 788 |         // three ids and the users username. Since we are only updating a flag, that
 | 
        
           |  |  | 789 |         // probably makes it sufficiently difficult for malicious users to toggle
 | 
        
           |  |  | 790 |         // other users flags.
 | 
        
           |  |  | 791 |         if ($checksum != self::get_toggle_checksum($qubaid, $questionid, $qaid, $slot)) {
 | 
        
           |  |  | 792 |             throw new moodle_exception('errorsavingflags', 'question');
 | 
        
           |  |  | 793 |         }
 | 
        
           |  |  | 794 |   | 
        
           |  |  | 795 |         $dm = new question_engine_data_mapper();
 | 
        
           |  |  | 796 |         $dm->update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate);
 | 
        
           |  |  | 797 |     }
 | 
        
           |  |  | 798 |   | 
        
           |  |  | 799 |     public static function initialise_js() {
 | 
        
           |  |  | 800 |         global $CFG, $PAGE, $OUTPUT;
 | 
        
           |  |  | 801 |         static $done = false;
 | 
        
           |  |  | 802 |         if ($done) {
 | 
        
           |  |  | 803 |             return;
 | 
        
           |  |  | 804 |         }
 | 
        
           |  |  | 805 |         $module = array(
 | 
        
           |  |  | 806 |             'name' => 'core_question_flags',
 | 
        
           |  |  | 807 |             'fullpath' => '/question/flags.js',
 | 
        
           |  |  | 808 |             'requires' => array('base', 'dom', 'event-delegate', 'io-base'),
 | 
        
           |  |  | 809 |         );
 | 
        
           |  |  | 810 |         $actionurl = $CFG->wwwroot . '/question/toggleflag.php';
 | 
        
           |  |  | 811 |         $flagattributes = array(
 | 
        
           |  |  | 812 |   | 
        
           |  |  | 813 |                 'src' => $OUTPUT->image_url('i/unflagged') . '',
 | 
        
           |  |  | 814 |                 'title' => get_string('clicktoflag', 'question'),
 | 
        
           |  |  | 815 |                 'alt' => get_string('flagged', 'question'), // Label on toggle should not change.
 | 
        
           |  |  | 816 |                 'text' => get_string('clickflag', 'question'),
 | 
        
           |  |  | 817 |             ),
 | 
        
           |  |  | 818 |             1 => array(
 | 
        
           |  |  | 819 |                 'src' => $OUTPUT->image_url('i/flagged') . '',
 | 
        
           |  |  | 820 |                 'title' => get_string('clicktounflag', 'question'),
 | 
        
           |  |  | 821 |                 'alt' => get_string('flagged', 'question'),
 | 
        
           |  |  | 822 |                 'text' => get_string('clickunflag', 'question'),
 | 
        
           |  |  | 823 |             ),
 | 
        
           |  |  | 824 |         );
 | 
        
           |  |  | 825 |         $PAGE->requires->js_init_call('M.core_question_flags.init',
 | 
        
           |  |  | 826 |                 array($actionurl, $flagattributes), false, $module);
 | 
        
           |  |  | 827 |         $done = true;
 | 
        
           |  |  | 828 |     }
 | 
        
           |  |  | 829 | }
 | 
        
           |  |  | 830 |   | 
        
           |  |  | 831 |   | 
        
           |  |  | 832 | /**
 | 
        
           |  |  | 833 |  * Exception thrown when the system detects that a student has done something
 | 
        
           |  |  | 834 |  * out-of-order to a question. This can happen, for example, if they click
 | 
        
           |  |  | 835 |  * the browser's back button in a quiz, then try to submit a different response.
 | 
        
           |  |  | 836 |  *
 | 
        
           |  |  | 837 |  * @copyright  2010 The Open University
 | 
        
           |  |  | 838 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 839 |  */
 | 
        
           |  |  | 840 | class question_out_of_sequence_exception extends moodle_exception {
 | 
        
           |  |  | 841 |     public function __construct($qubaid, $slot, $postdata) {
 | 
        
           |  |  | 842 |         if ($postdata == null) {
 | 
        
           |  |  | 843 |             $postdata = data_submitted();
 | 
        
           |  |  | 844 |         }
 | 
        
           |  |  | 845 |         parent::__construct('submissionoutofsequence', 'question', '', null,
 | 
        
           |  |  | 846 |                 "QUBAid: {$qubaid}, slot: {$slot}, post data: " . print_r($postdata, true));
 | 
        
           |  |  | 847 |     }
 | 
        
           |  |  | 848 | }
 | 
        
           |  |  | 849 |   | 
        
           |  |  | 850 |   | 
        
           |  |  | 851 | /**
 | 
        
           |  |  | 852 |  * Useful functions for writing question types and behaviours.
 | 
        
           |  |  | 853 |  *
 | 
        
           |  |  | 854 |  * @copyright 2010 The Open University
 | 
        
           |  |  | 855 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 856 |  */
 | 
        
           |  |  | 857 | abstract class question_utils {
 | 
        
           |  |  | 858 |     /**
 | 
        
           |  |  | 859 |      * @var float tolerance to use when comparing question mark/fraction values.
 | 
        
           |  |  | 860 |      *
 | 
        
           |  |  | 861 |      * When comparing floating point numbers in a computer, the representation is not
 | 
        
           |  |  | 862 |      * necessarily exact. Therefore, we need to allow a tolerance.
 | 
        
           |  |  | 863 |      * Question marks are stored in the database as decimal numbers with 7 decimal places.
 | 
        
           |  |  | 864 |      * Therefore, this is the appropriate tolerance to use.
 | 
        
           |  |  | 865 |      */
 | 
        
           |  |  | 866 |     const MARK_TOLERANCE = 0.00000005;
 | 
        
           |  |  | 867 |   | 
        
           |  |  | 868 |     /**
 | 
        
           |  |  | 869 |      * Tests to see whether two arrays have the same keys, with the same values
 | 
        
           |  |  | 870 |      * (as compared by ===) for each key. However, the order of the arrays does
 | 
        
           |  |  | 871 |      * not have to be the same.
 | 
        
           |  |  | 872 |      * @param array $array1 the first array.
 | 
        
           |  |  | 873 |      * @param array $array2 the second array.
 | 
        
           |  |  | 874 |      * @return bool whether the two arrays have the same keys with the same
 | 
        
           |  |  | 875 |      *      corresponding values.
 | 
        
           |  |  | 876 |      */
 | 
        
           |  |  | 877 |     public static function arrays_have_same_keys_and_values(array $array1, array $array2) {
 | 
        
           |  |  | 878 |         if (count($array1) != count($array2)) {
 | 
        
           |  |  | 879 |             return false;
 | 
        
           |  |  | 880 |         }
 | 
        
           |  |  | 881 |         foreach ($array1 as $key => $value1) {
 | 
        
           |  |  | 882 |             if (!array_key_exists($key, $array2)) {
 | 
        
           |  |  | 883 |                 return false;
 | 
        
           |  |  | 884 |             }
 | 
        
           |  |  | 885 |             if (((string) $value1) !== ((string) $array2[$key])) {
 | 
        
           |  |  | 886 |                 return false;
 | 
        
           |  |  | 887 |             }
 | 
        
           |  |  | 888 |         }
 | 
        
           |  |  | 889 |         return true;
 | 
        
           |  |  | 890 |     }
 | 
        
           |  |  | 891 |   | 
        
           |  |  | 892 |     /**
 | 
        
           |  |  | 893 |      * Tests to see whether two arrays have the same value at a particular key.
 | 
        
           |  |  | 894 |      * This method will return true if:
 | 
        
           |  |  | 895 |      * 1. Neither array contains the key; or
 | 
        
           |  |  | 896 |      * 2. Both arrays contain the key, and the corresponding values compare
 | 
        
           |  |  | 897 |      *      identical when cast to strings and compared with ===.
 | 
        
           |  |  | 898 |      * @param array $array1 the first array.
 | 
        
           |  |  | 899 |      * @param array $array2 the second array.
 | 
        
           |  |  | 900 |      * @param string $key an array key.
 | 
        
           |  |  | 901 |      * @return bool whether the two arrays have the same value (or lack of
 | 
        
           |  |  | 902 |      *      one) for a given key.
 | 
        
           |  |  | 903 |      */
 | 
        
           |  |  | 904 |     public static function arrays_same_at_key(array $array1, array $array2, $key) {
 | 
        
           |  |  | 905 |         if (array_key_exists($key, $array1) && array_key_exists($key, $array2)) {
 | 
        
           |  |  | 906 |             return ((string) $array1[$key]) === ((string) $array2[$key]);
 | 
        
           |  |  | 907 |         }
 | 
        
           |  |  | 908 |         if (!array_key_exists($key, $array1) && !array_key_exists($key, $array2)) {
 | 
        
           |  |  | 909 |             return true;
 | 
        
           |  |  | 910 |         }
 | 
        
           |  |  | 911 |         return false;
 | 
        
           |  |  | 912 |     }
 | 
        
           |  |  | 913 |   | 
        
           |  |  | 914 |     /**
 | 
        
           |  |  | 915 |      * Tests to see whether two arrays have the same value at a particular key.
 | 
        
           |  |  | 916 |      * Missing values are replaced by '', and then the values are cast to
 | 
        
           |  |  | 917 |      * strings and compared with ===.
 | 
        
           |  |  | 918 |      * @param array $array1 the first array.
 | 
        
           |  |  | 919 |      * @param array $array2 the second array.
 | 
        
           |  |  | 920 |      * @param string $key an array key.
 | 
        
           |  |  | 921 |      * @return bool whether the two arrays have the same value (or lack of
 | 
        
           |  |  | 922 |      *      one) for a given key.
 | 
        
           |  |  | 923 |      */
 | 
        
           |  |  | 924 |     public static function arrays_same_at_key_missing_is_blank(
 | 
        
           |  |  | 925 |             array $array1, array $array2, $key) {
 | 
        
           |  |  | 926 |         if (array_key_exists($key, $array1)) {
 | 
        
           |  |  | 927 |             $value1 = $array1[$key];
 | 
        
           |  |  | 928 |         } else {
 | 
        
           |  |  | 929 |             $value1 = '';
 | 
        
           |  |  | 930 |         }
 | 
        
           |  |  | 931 |         if (array_key_exists($key, $array2)) {
 | 
        
           |  |  | 932 |             $value2 = $array2[$key];
 | 
        
           |  |  | 933 |         } else {
 | 
        
           |  |  | 934 |             $value2 = '';
 | 
        
           |  |  | 935 |         }
 | 
        
           |  |  | 936 |         return ((string) $value1) === ((string) $value2);
 | 
        
           |  |  | 937 |     }
 | 
        
           |  |  | 938 |   | 
        
           |  |  | 939 |     /**
 | 
        
           |  |  | 940 |      * Tests to see whether two arrays have the same value at a particular key.
 | 
        
           |  |  | 941 |      * Missing values are replaced by 0, and then the values are cast to
 | 
        
           |  |  | 942 |      * integers and compared with ===.
 | 
        
           |  |  | 943 |      * @param array $array1 the first array.
 | 
        
           |  |  | 944 |      * @param array $array2 the second array.
 | 
        
           |  |  | 945 |      * @param string $key an array key.
 | 
        
           |  |  | 946 |      * @return bool whether the two arrays have the same value (or lack of
 | 
        
           |  |  | 947 |      *      one) for a given key.
 | 
        
           |  |  | 948 |      */
 | 
        
           |  |  | 949 |     public static function arrays_same_at_key_integer(
 | 
        
           |  |  | 950 |             array $array1, array $array2, $key) {
 | 
        
           |  |  | 951 |         if (array_key_exists($key, $array1)) {
 | 
        
           |  |  | 952 |             $value1 = (int) $array1[$key];
 | 
        
           |  |  | 953 |         } else {
 | 
        
           |  |  | 954 |             $value1 = 0;
 | 
        
           |  |  | 955 |         }
 | 
        
           |  |  | 956 |         if (array_key_exists($key, $array2)) {
 | 
        
           |  |  | 957 |             $value2 = (int) $array2[$key];
 | 
        
           |  |  | 958 |         } else {
 | 
        
           |  |  | 959 |             $value2 = 0;
 | 
        
           |  |  | 960 |         }
 | 
        
           |  |  | 961 |         return $value1 === $value2;
 | 
        
           |  |  | 962 |     }
 | 
        
           |  |  | 963 |   | 
        
           |  |  | 964 |     private static $units     = array('', 'i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix');
 | 
        
           |  |  | 965 |     private static $tens      = array('', 'x', 'xx', 'xxx', 'xl', 'l', 'lx', 'lxx', 'lxxx', 'xc');
 | 
        
           |  |  | 966 |     private static $hundreds  = array('', 'c', 'cc', 'ccc', 'cd', 'd', 'dc', 'dcc', 'dccc', 'cm');
 | 
        
           |  |  | 967 |     private static $thousands = array('', 'm', 'mm', 'mmm');
 | 
        
           |  |  | 968 |   | 
        
           |  |  | 969 |     /**
 | 
        
           |  |  | 970 |      * Convert an integer to roman numerals.
 | 
        
           |  |  | 971 |      * @param int $number an integer between 1 and 3999 inclusive. Anything else
 | 
        
           |  |  | 972 |      *      will throw an exception.
 | 
        
           |  |  | 973 |      * @return string the number converted to lower case roman numerals.
 | 
        
           |  |  | 974 |      */
 | 
        
           |  |  | 975 |     public static function int_to_roman($number) {
 | 
        
           |  |  | 976 |         if (!is_integer($number) || $number < 1 || $number > 3999) {
 | 
        
           |  |  | 977 |             throw new coding_exception('Only integers between 0 and 3999 can be ' .
 | 
        
           |  |  | 978 |                     'converted to roman numerals.', $number);
 | 
        
           |  |  | 979 |         }
 | 
        
           |  |  | 980 |   | 
        
           |  |  | 981 |         return self::$thousands[floor($number / 1000) % 10] . self::$hundreds[floor($number / 100) % 10] .
 | 
        
           |  |  | 982 |                 self::$tens[floor($number / 10) % 10] . self::$units[$number % 10];
 | 
        
           |  |  | 983 |     }
 | 
        
           |  |  | 984 |   | 
        
           |  |  | 985 |     /**
 | 
        
           |  |  | 986 |      * Convert an integer to a letter of alphabet.
 | 
        
           |  |  | 987 |      * @param int $number an integer between 1 and 26 inclusive.
 | 
        
           |  |  | 988 |      * Anything else will throw an exception.
 | 
        
           |  |  | 989 |      * @return string the number converted to upper case letter of alphabet.
 | 
        
           |  |  | 990 |      */
 | 
        
           |  |  | 991 |     public static function int_to_letter($number) {
 | 
        
           |  |  | 992 |         $alphabet = [
 | 
        
           |  |  | 993 |                 '1' => 'A',
 | 
        
           |  |  | 994 |                 '2' => 'B',
 | 
        
           |  |  | 995 |                 '3' => 'C',
 | 
        
           |  |  | 996 |                 '4' => 'D',
 | 
        
           |  |  | 997 |                 '5' => 'E',
 | 
        
           |  |  | 998 |                 '6' => 'F',
 | 
        
           |  |  | 999 |                 '7' => 'G',
 | 
        
           |  |  | 1000 |                 '8' => 'H',
 | 
        
           |  |  | 1001 |                 '9' => 'I',
 | 
        
           |  |  | 1002 |                 '10' => 'J',
 | 
        
           |  |  | 1003 |                 '11' => 'K',
 | 
        
           |  |  | 1004 |                 '12' => 'L',
 | 
        
           |  |  | 1005 |                 '13' => 'M',
 | 
        
           |  |  | 1006 |                 '14' => 'N',
 | 
        
           |  |  | 1007 |                 '15' => 'O',
 | 
        
           |  |  | 1008 |                 '16' => 'P',
 | 
        
           |  |  | 1009 |                 '17' => 'Q',
 | 
        
           |  |  | 1010 |                 '18' => 'R',
 | 
        
           |  |  | 1011 |                 '19' => 'S',
 | 
        
           |  |  | 1012 |                 '20' => 'T',
 | 
        
           |  |  | 1013 |                 '21' => 'U',
 | 
        
           |  |  | 1014 |                 '22' => 'V',
 | 
        
           |  |  | 1015 |                 '23' => 'W',
 | 
        
           |  |  | 1016 |                 '24' => 'X',
 | 
        
           |  |  | 1017 |                 '25' => 'Y',
 | 
        
           |  |  | 1018 |                 '26' => 'Z'
 | 
        
           |  |  | 1019 |         ];
 | 
        
           |  |  | 1020 |         if (!is_integer($number) || $number < 1 || $number > count($alphabet)) {
 | 
        
           |  |  | 1021 |             throw new coding_exception('Only integers between 1 and 26 can be converted to letters.', $number);
 | 
        
           |  |  | 1022 |         }
 | 
        
           |  |  | 1023 |         return $alphabet[$number];
 | 
        
           |  |  | 1024 |     }
 | 
        
           |  |  | 1025 |   | 
        
           |  |  | 1026 |     /**
 | 
        
           |  |  | 1027 |      * Typically, $mark will have come from optional_param($name, null, PARAM_RAW_TRIMMED).
 | 
        
           |  |  | 1028 |      * This method copes with:
 | 
        
           |  |  | 1029 |      *  - keeping null or '' input unchanged - important to let teaches set a question back to requries grading.
 | 
        
           |  |  | 1030 |      *  - numbers that were typed as either 1.00 or 1,00 form.
 | 
        
           |  |  | 1031 |      *  - invalid things, which get turned into null.
 | 
        
           |  |  | 1032 |      *
 | 
        
           |  |  | 1033 |      * @param string|null $mark raw use input of a mark.
 | 
        
           |  |  | 1034 |      * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.
 | 
        
           |  |  | 1035 |      */
 | 
        
           |  |  | 1036 |     public static function clean_param_mark($mark) {
 | 
        
           |  |  | 1037 |         if ($mark === '' || is_null($mark)) {
 | 
        
           |  |  | 1038 |             return $mark;
 | 
        
           |  |  | 1039 |         }
 | 
        
           |  |  | 1040 |   | 
        
           |  |  | 1041 |         $mark = str_replace(',', '.', $mark);
 | 
        
           |  |  | 1042 |         // This regexp should match the one in validate_param.
 | 
        
           |  |  | 1043 |         if (!preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', $mark)) {
 | 
        
           |  |  | 1044 |             return null;
 | 
        
           |  |  | 1045 |         }
 | 
        
           |  |  | 1046 |   | 
        
           |  |  | 1047 |         return clean_param($mark, PARAM_FLOAT);
 | 
        
           |  |  | 1048 |     }
 | 
        
           |  |  | 1049 |   | 
        
           |  |  | 1050 |     /**
 | 
        
           |  |  | 1051 |      * Get a sumitted variable (from the GET or POST data) that is a mark.
 | 
        
           |  |  | 1052 |      * @param string $parname the submitted variable name.
 | 
        
           |  |  | 1053 |      * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.
 | 
        
           |  |  | 1054 |      */
 | 
        
           |  |  | 1055 |     public static function optional_param_mark($parname) {
 | 
        
           |  |  | 1056 |         return self::clean_param_mark(
 | 
        
           |  |  | 1057 |                 optional_param($parname, null, PARAM_RAW_TRIMMED));
 | 
        
           |  |  | 1058 |     }
 | 
        
           |  |  | 1059 |   | 
        
           |  |  | 1060 |     /**
 | 
        
           |  |  | 1061 |      * Convert part of some question content to plain text.
 | 
        
           |  |  | 1062 |      * @param string $text the text.
 | 
        
           |  |  | 1063 |      * @param int $format the text format.
 | 
        
           |  |  | 1064 |      * @param array $options formatting options. Passed to {@link format_text}.
 | 
        
           |  |  | 1065 |      * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.
 | 
        
           |  |  | 1066 |      */
 | 
        
           |  |  | 1067 |     public static function to_plain_text($text, $format, $options = array('noclean' => 'true')) {
 | 
        
           |  |  | 1068 |         // The following call to html_to_text uses the option that strips out
 | 
        
           |  |  | 1069 |         // all URLs, but format_text complains if it finds @@PLUGINFILE@@ tokens.
 | 
        
           |  |  | 1070 |         // So, we need to replace @@PLUGINFILE@@ with a real URL, but it doesn't
 | 
        
           |  |  | 1071 |         // matter what. We use http://example.com/.
 | 
        
           |  |  | 1072 |         $text = str_replace('@@PLUGINFILE@@/', 'http://example.com/', $text);
 | 
        
           |  |  | 1073 |         return html_to_text(format_text($text, $format, $options), 0, false);
 | 
        
           |  |  | 1074 |     }
 | 
        
           |  |  | 1075 |   | 
        
           |  |  | 1076 |     /**
 | 
        
           |  |  | 1077 |      * Get the options required to configure the filepicker for one of the editor
 | 
        
           |  |  | 1078 |      * toolbar buttons.
 | 
        
           |  |  | 1079 |      *
 | 
        
           |  |  | 1080 |      * @param mixed $acceptedtypes array of types of '*'.
 | 
        
           |  |  | 1081 |      * @param int $draftitemid the draft area item id.
 | 
        
           |  |  | 1082 |      * @param context $context the context.
 | 
        
           |  |  | 1083 |      * @return object the required options.
 | 
        
           |  |  | 1084 |      */
 | 
        
           |  |  | 1085 |     protected static function specific_filepicker_options($acceptedtypes, $draftitemid, $context) {
 | 
        
           |  |  | 1086 |         $filepickeroptions = new stdClass();
 | 
        
           |  |  | 1087 |         $filepickeroptions->accepted_types = $acceptedtypes;
 | 
        
           |  |  | 1088 |         $filepickeroptions->return_types = FILE_INTERNAL | FILE_EXTERNAL;
 | 
        
           |  |  | 1089 |         $filepickeroptions->context = $context;
 | 
        
           |  |  | 1090 |         $filepickeroptions->env = 'filepicker';
 | 
        
           |  |  | 1091 |   | 
        
           |  |  | 1092 |         $options = initialise_filepicker($filepickeroptions);
 | 
        
           |  |  | 1093 |         $options->context = $context;
 | 
        
           |  |  | 1094 |         $options->client_id = uniqid();
 | 
        
           |  |  | 1095 |         $options->env = 'editor';
 | 
        
           |  |  | 1096 |         $options->itemid = $draftitemid;
 | 
        
           |  |  | 1097 |   | 
        
           |  |  | 1098 |         return $options;
 | 
        
           |  |  | 1099 |     }
 | 
        
           |  |  | 1100 |   | 
        
           |  |  | 1101 |     /**
 | 
        
           |  |  | 1102 |      * Get filepicker options for question related text areas.
 | 
        
           |  |  | 1103 |      *
 | 
        
           |  |  | 1104 |      * @param context $context the context.
 | 
        
           |  |  | 1105 |      * @param int $draftitemid the draft area item id.
 | 
        
           |  |  | 1106 |      * @return array An array of options
 | 
        
           |  |  | 1107 |      */
 | 
        
           |  |  | 1108 |     public static function get_filepicker_options($context, $draftitemid) {
 | 
        
           |  |  | 1109 |         return [
 | 
        
           |  |  | 1110 |                 'image' => self::specific_filepicker_options(['image'], $draftitemid, $context),
 | 
        
           |  |  | 1111 |                 'media' => self::specific_filepicker_options(['video', 'audio'], $draftitemid, $context),
 | 
        
           |  |  | 1112 |                 'link'  => self::specific_filepicker_options('*', $draftitemid, $context),
 | 
        
           |  |  | 1113 |             ];
 | 
        
           |  |  | 1114 |     }
 | 
        
           |  |  | 1115 |   | 
        
           |  |  | 1116 |     /**
 | 
        
           |  |  | 1117 |      * Get editor options for question related text areas.
 | 
        
           |  |  | 1118 |      *
 | 
        
           |  |  | 1119 |      * @param context $context the context.
 | 
        
           |  |  | 1120 |      * @return array An array of options
 | 
        
           |  |  | 1121 |      */
 | 
        
           |  |  | 1122 |     public static function get_editor_options($context) {
 | 
        
           |  |  | 1123 |         global $CFG;
 | 
        
           |  |  | 1124 |   | 
        
           |  |  | 1125 |         $editoroptions = [
 | 
        
           |  |  | 1126 |                 'subdirs'  => 0,
 | 
        
           |  |  | 1127 |                 'context'  => $context,
 | 
        
           |  |  | 1128 |                 'maxfiles' => EDITOR_UNLIMITED_FILES,
 | 
        
           |  |  | 1129 |                 'maxbytes' => $CFG->maxbytes,
 | 
        
           |  |  | 1130 |                 'noclean' => 0,
 | 
        
           |  |  | 1131 |                 'trusttext' => 0,
 | 
        
           |  |  | 1132 |                 'autosave' => false
 | 
        
           |  |  | 1133 |         ];
 | 
        
           |  |  | 1134 |   | 
        
           |  |  | 1135 |         return $editoroptions;
 | 
        
           |  |  | 1136 |     }
 | 
        
           | 1441 | ariadna | 1137 |   | 
        
           |  |  | 1138 |     /**
 | 
        
           |  |  | 1139 |      * Format question fragment string and apply filtering,
 | 
        
           |  |  | 1140 |      *
 | 
        
           |  |  | 1141 |      * @param string $text current text that we want to be apply filters.
 | 
        
           |  |  | 1142 |      * @param context $context of the page question are in.
 | 
        
           |  |  | 1143 |      * @return string  result has been modified by filters.
 | 
        
           |  |  | 1144 |      */
 | 
        
           |  |  | 1145 |     public static function format_question_fragment(string $text, context $context): string {
 | 
        
           |  |  | 1146 |         global $PAGE;
 | 
        
           |  |  | 1147 |         $filtermanager = \filter_manager::instance();
 | 
        
           |  |  | 1148 |         $filtermanager->setup_page_for_filters($PAGE, $context);
 | 
        
           |  |  | 1149 |         return $filtermanager->filter_string($text, $context);
 | 
        
           |  |  | 1150 |     }
 | 
        
           | 1 | efrain | 1151 | }
 | 
        
           |  |  | 1152 |   | 
        
           |  |  | 1153 |   | 
        
           |  |  | 1154 | /**
 | 
        
           |  |  | 1155 |  * The interface for strategies for controlling which variant of each question is used.
 | 
        
           |  |  | 1156 |  *
 | 
        
           |  |  | 1157 |  * @copyright  2011 The Open University
 | 
        
           |  |  | 1158 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 1159 |  */
 | 
        
           |  |  | 1160 | interface question_variant_selection_strategy {
 | 
        
           |  |  | 1161 |     /**
 | 
        
           |  |  | 1162 |      * @param int $maxvariants the num
 | 
        
           |  |  | 1163 |      * @param string $seed data that can be used to controls how the variant is selected
 | 
        
           |  |  | 1164 |      *      in a semi-random way.
 | 
        
           |  |  | 1165 |      * @return int the variant to use, a number betweeb 1 and $maxvariants inclusive.
 | 
        
           |  |  | 1166 |      */
 | 
        
           |  |  | 1167 |     public function choose_variant($maxvariants, $seed);
 | 
        
           |  |  | 1168 | }
 | 
        
           |  |  | 1169 |   | 
        
           |  |  | 1170 |   | 
        
           |  |  | 1171 | /**
 | 
        
           |  |  | 1172 |  * A {@link question_variant_selection_strategy} that is completely random.
 | 
        
           |  |  | 1173 |  *
 | 
        
           |  |  | 1174 |  * @copyright  2011 The Open University
 | 
        
           |  |  | 1175 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 1176 |  */
 | 
        
           |  |  | 1177 | class question_variant_random_strategy implements question_variant_selection_strategy {
 | 
        
           |  |  | 1178 |     public function choose_variant($maxvariants, $seed) {
 | 
        
           |  |  | 1179 |         return rand(1, $maxvariants);
 | 
        
           |  |  | 1180 |     }
 | 
        
           |  |  | 1181 | }
 | 
        
           |  |  | 1182 |   | 
        
           |  |  | 1183 |   | 
        
           |  |  | 1184 | /**
 | 
        
           |  |  | 1185 |  * A {@link question_variant_selection_strategy} that is effectively random
 | 
        
           |  |  | 1186 |  * for the first attempt, and then after that cycles through the available
 | 
        
           |  |  | 1187 |  * variants so that the students will not get a repeated variant until they have
 | 
        
           |  |  | 1188 |  * seen them all.
 | 
        
           |  |  | 1189 |  *
 | 
        
           |  |  | 1190 |  * @copyright  2011 The Open University
 | 
        
           |  |  | 1191 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 1192 |  */
 | 
        
           |  |  | 1193 | class question_variant_pseudorandom_no_repeats_strategy
 | 
        
           |  |  | 1194 |         implements question_variant_selection_strategy {
 | 
        
           |  |  | 1195 |   | 
        
           |  |  | 1196 |     /** @var int the number of attempts this users has had, including the curent one. */
 | 
        
           |  |  | 1197 |     protected $attemptno;
 | 
        
           |  |  | 1198 |   | 
        
           |  |  | 1199 |     /** @var int the user id the attempt belongs to. */
 | 
        
           |  |  | 1200 |     protected $userid;
 | 
        
           |  |  | 1201 |   | 
        
           |  |  | 1202 |     /** @var string extra input fed into the pseudo-random code. */
 | 
        
           |  |  | 1203 |     protected $extrarandomness = '';
 | 
        
           |  |  | 1204 |   | 
        
           |  |  | 1205 |     /**
 | 
        
           |  |  | 1206 |      * Constructor.
 | 
        
           |  |  | 1207 |      * @param int $attemptno The attempt number.
 | 
        
           |  |  | 1208 |      * @param int $userid the user the attempt is for (defaults to $USER->id).
 | 
        
           |  |  | 1209 |      */
 | 
        
           |  |  | 1210 |     public function __construct($attemptno, $userid = null, $extrarandomness = '') {
 | 
        
           |  |  | 1211 |         $this->attemptno = $attemptno;
 | 
        
           |  |  | 1212 |         if (is_null($userid)) {
 | 
        
           |  |  | 1213 |             global $USER;
 | 
        
           |  |  | 1214 |             $this->userid = $USER->id;
 | 
        
           |  |  | 1215 |         } else {
 | 
        
           |  |  | 1216 |             $this->userid = $userid;
 | 
        
           |  |  | 1217 |         }
 | 
        
           |  |  | 1218 |   | 
        
           |  |  | 1219 |         if ($extrarandomness) {
 | 
        
           |  |  | 1220 |             $this->extrarandomness = '|' . $extrarandomness;
 | 
        
           |  |  | 1221 |         }
 | 
        
           |  |  | 1222 |     }
 | 
        
           |  |  | 1223 |   | 
        
           |  |  | 1224 |     public function choose_variant($maxvariants, $seed) {
 | 
        
           |  |  | 1225 |         if ($maxvariants == 1) {
 | 
        
           |  |  | 1226 |             return 1;
 | 
        
           |  |  | 1227 |         }
 | 
        
           |  |  | 1228 |   | 
        
           |  |  | 1229 |         $hash = sha1($seed . '|user' . $this->userid . $this->extrarandomness);
 | 
        
           |  |  | 1230 |         $randint = hexdec(substr($hash, 17, 7));
 | 
        
           |  |  | 1231 |   | 
        
           |  |  | 1232 |         return ($randint + $this->attemptno) % $maxvariants + 1;
 | 
        
           |  |  | 1233 |     }
 | 
        
           |  |  | 1234 | }
 | 
        
           |  |  | 1235 |   | 
        
           |  |  | 1236 | /**
 | 
        
           |  |  | 1237 |  * A {@link question_variant_selection_strategy} designed ONLY for testing.
 | 
        
           |  |  | 1238 |  * For selected questions it wil return a specific variants. For the other
 | 
        
           |  |  | 1239 |  * slots it will use a fallback strategy.
 | 
        
           |  |  | 1240 |  *
 | 
        
           |  |  | 1241 |  * @copyright  2013 The Open University
 | 
        
           |  |  | 1242 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 1243 |  */
 | 
        
           |  |  | 1244 | class question_variant_forced_choices_selection_strategy
 | 
        
           |  |  | 1245 |     implements question_variant_selection_strategy {
 | 
        
           |  |  | 1246 |   | 
        
           |  |  | 1247 |     /** @var array seed => variant to select. */
 | 
        
           |  |  | 1248 |     protected $forcedchoices;
 | 
        
           |  |  | 1249 |   | 
        
           |  |  | 1250 |     /** @var question_variant_selection_strategy strategy used to make the non-forced choices. */
 | 
        
           |  |  | 1251 |     protected $basestrategy;
 | 
        
           |  |  | 1252 |   | 
        
           |  |  | 1253 |     /**
 | 
        
           |  |  | 1254 |      * Constructor.
 | 
        
           |  |  | 1255 |      * @param array $forcedchoices array seed => variant to select.
 | 
        
           |  |  | 1256 |      * @param question_variant_selection_strategy $basestrategy strategy used
 | 
        
           |  |  | 1257 |      *      to make the non-forced choices.
 | 
        
           |  |  | 1258 |      */
 | 
        
           |  |  | 1259 |     public function __construct(array $forcedchoices, question_variant_selection_strategy $basestrategy) {
 | 
        
           |  |  | 1260 |         $this->forcedchoices = $forcedchoices;
 | 
        
           |  |  | 1261 |         $this->basestrategy  = $basestrategy;
 | 
        
           |  |  | 1262 |     }
 | 
        
           |  |  | 1263 |   | 
        
           |  |  | 1264 |     public function choose_variant($maxvariants, $seed) {
 | 
        
           |  |  | 1265 |         if (array_key_exists($seed, $this->forcedchoices)) {
 | 
        
           |  |  | 1266 |             if ($this->forcedchoices[$seed] > $maxvariants) {
 | 
        
           |  |  | 1267 |                 throw new coding_exception('Forced variant out of range.');
 | 
        
           |  |  | 1268 |             }
 | 
        
           |  |  | 1269 |             return $this->forcedchoices[$seed];
 | 
        
           |  |  | 1270 |         } else {
 | 
        
           |  |  | 1271 |             return $this->basestrategy->choose_variant($maxvariants, $seed);
 | 
        
           |  |  | 1272 |         }
 | 
        
           |  |  | 1273 |     }
 | 
        
           |  |  | 1274 |   | 
        
           |  |  | 1275 |     /**
 | 
        
           |  |  | 1276 |      * Helper method for preparing the $forcedchoices array.
 | 
        
           |  |  | 1277 |      * @param array                      $variantsbyslot slot number => variant to select.
 | 
        
           |  |  | 1278 |      * @param question_usage_by_activity $quba           the question usage we need a strategy for.
 | 
        
           |  |  | 1279 |      * @throws coding_exception when variant cannot be forced as doesn't work.
 | 
        
           |  |  | 1280 |      * @return array that can be passed to the constructor as $forcedchoices.
 | 
        
           |  |  | 1281 |      */
 | 
        
           |  |  | 1282 |     public static function prepare_forced_choices_array(array $variantsbyslot,
 | 
        
           |  |  | 1283 |                                                         question_usage_by_activity $quba) {
 | 
        
           |  |  | 1284 |   | 
        
           |  |  | 1285 |         $forcedchoices = array();
 | 
        
           |  |  | 1286 |   | 
        
           |  |  | 1287 |         foreach ($variantsbyslot as $slot => $varianttochoose) {
 | 
        
           |  |  | 1288 |             $question = $quba->get_question($slot);
 | 
        
           |  |  | 1289 |             $seed = $question->get_variants_selection_seed();
 | 
        
           |  |  | 1290 |             if (array_key_exists($seed, $forcedchoices) && $forcedchoices[$seed] != $varianttochoose) {
 | 
        
           |  |  | 1291 |                 throw new coding_exception('Inconsistent forced variant detected at slot ' . $slot);
 | 
        
           |  |  | 1292 |             }
 | 
        
           |  |  | 1293 |             if ($varianttochoose > $question->get_num_variants()) {
 | 
        
           |  |  | 1294 |                 throw new coding_exception('Forced variant out of range at slot ' . $slot);
 | 
        
           |  |  | 1295 |             }
 | 
        
           |  |  | 1296 |             $forcedchoices[$seed] = $varianttochoose;
 | 
        
           |  |  | 1297 |         }
 | 
        
           |  |  | 1298 |         return $forcedchoices;
 | 
        
           |  |  | 1299 |     }
 | 
        
           |  |  | 1300 | }
 |