Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** This defines the core classes of the Moodle question engine.** @package moodlecore* @subpackage questionengine* @copyright 2009 The Open University* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/defined('MOODLE_INTERNAL') || die();require_once($CFG->libdir . '/filelib.php');require_once(__DIR__ . '/questionusage.php');require_once(__DIR__ . '/questionattempt.php');require_once(__DIR__ . '/questionattemptstep.php');require_once(__DIR__ . '/states.php');require_once(__DIR__ . '/datalib.php');require_once(__DIR__ . '/renderer.php');require_once(__DIR__ . '/bank.php');require_once(__DIR__ . '/../type/questiontypebase.php');require_once(__DIR__ . '/../type/questionbase.php');require_once(__DIR__ . '/../type/rendererbase.php');require_once(__DIR__ . '/../behaviour/behaviourtypebase.php');require_once(__DIR__ . '/../behaviour/behaviourbase.php');require_once(__DIR__ . '/../behaviour/rendererbase.php');require_once($CFG->libdir . '/questionlib.php');/*** This static class provides access to the other question engine classes.** It provides functions for managing question behaviours), and for* creating, loading, saving and deleting {@link question_usage_by_activity}s,* which is the main class that is used by other code that wants to use questions.** @copyright 2009 The Open University* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/abstract class question_engine {/** @var array behaviour name => 1. Records which behaviours have been loaded. */private static $loadedbehaviours = array();/** @var array behaviour name => question_behaviour_type for this behaviour. */private static $behaviourtypes = array();/*** Create a new {@link question_usage_by_activity}. The usage is* created in memory. If you want it to persist, you will need to call* {@link save_questions_usage_by_activity()}.** @param string $component the plugin creating this attempt. For example mod_quiz.* @param context $context the context this usage belongs to.* @return question_usage_by_activity the newly created object.*/public static function make_questions_usage_by_activity($component, $context) {return new question_usage_by_activity($component, $context);}/*** Load a {@link question_usage_by_activity} from the database, based on its id.* @param int $qubaid the id of the usage to load.* @param moodle_database $db a database connectoin. Defaults to global $DB.* @return question_usage_by_activity loaded from the database.*/public static function load_questions_usage_by_activity($qubaid, moodle_database $db = null) {$dm = new question_engine_data_mapper($db);return $dm->load_questions_usage_by_activity($qubaid);}/*** Save a {@link question_usage_by_activity} to the database. This works either* if the usage was newly created by {@link make_questions_usage_by_activity()}* or loaded from the database using {@link load_questions_usage_by_activity()}* @param question_usage_by_activity the usage to save.* @param moodle_database $db a database connectoin. Defaults to global $DB.*/public static function save_questions_usage_by_activity(question_usage_by_activity $quba, moodle_database $db = null) {$dm = new question_engine_data_mapper($db);$observer = $quba->get_observer();if ($observer instanceof question_engine_unit_of_work) {$observer->save($dm);} else {$dm->insert_questions_usage_by_activity($quba);}}/*** Delete a {@link question_usage_by_activity} from the database, based on its id.* @param int $qubaid the id of the usage to delete.*/public static function delete_questions_usage_by_activity($qubaid) {self::delete_questions_usage_by_activities(new qubaid_list(array($qubaid)));}/*** Delete {@link question_usage_by_activity}s from the database.* @param qubaid_condition $qubaids identifies which questions usages to delete.*/public static function delete_questions_usage_by_activities(qubaid_condition $qubaids) {$dm = new question_engine_data_mapper();$dm->delete_questions_usage_by_activities($qubaids);}/*** Change the maxmark for the question_attempt with number in usage $slot* for all the specified question_attempts.* @param qubaid_condition $qubaids Selects which usages are updated.* @param int $slot the number is usage to affect.* @param number $newmaxmark the new max mark to set.*/public static function set_max_mark_in_attempts(qubaid_condition $qubaids,$slot, $newmaxmark) {$dm = new question_engine_data_mapper();$dm->set_max_mark_in_attempts($qubaids, $slot, $newmaxmark);}/*** Validate that the manual grade submitted for a particular question is in range.* @param int $qubaid the question_usage id.* @param int $slot the slot number within the usage.* @return bool whether the submitted data is in range.*/public static function is_manual_grade_in_range($qubaid, $slot) {$prefix = 'q' . $qubaid . ':' . $slot . '_';$mark = question_utils::optional_param_mark($prefix . '-mark');$maxmark = optional_param($prefix . '-maxmark', null, PARAM_FLOAT);$minfraction = optional_param($prefix . ':minfraction', null, PARAM_FLOAT);$maxfraction = optional_param($prefix . ':maxfraction', null, PARAM_FLOAT);return $mark === '' ||($mark !== null && $mark >= $minfraction * $maxmark && $mark <= $maxfraction * $maxmark) ||($mark === null && $maxmark === null);}/*** @param array $questionids of question ids.* @param qubaid_condition $qubaids ids of the usages to consider.* @return boolean whether any of these questions are being used by any of* those usages.*/public static function questions_in_use(array $questionids, qubaid_condition $qubaids = null) {if (is_null($qubaids)) {return false;}$dm = new question_engine_data_mapper();return $dm->questions_in_use($questionids, $qubaids);}/*** Get the number of times each variant has been used for each question in a list* in a set of usages.* @param array $questionids of question ids.* @param qubaid_condition $qubaids ids of the usages to consider.* @return array questionid => variant number => num uses.*/public static function load_used_variants(array $questionids, qubaid_condition $qubaids) {$dm = new question_engine_data_mapper();return $dm->load_used_variants($questionids, $qubaids);}/*** Create an archetypal behaviour for a particular question attempt.* Used by {@link question_definition::make_behaviour()}.** @param string $preferredbehaviour the type of model required.* @param question_attempt $qa the question attempt the model will process.* @return question_behaviour an instance of appropriate behaviour class.*/public static function make_archetypal_behaviour($preferredbehaviour, question_attempt $qa) {if (!self::is_behaviour_archetypal($preferredbehaviour)) {throw new coding_exception('The requested behaviour is not actually ' .'an archetypal one.');}self::load_behaviour_class($preferredbehaviour);$class = 'qbehaviour_' . $preferredbehaviour;return new $class($qa, $preferredbehaviour);}/*** @param string $behaviour the name of a behaviour.* @return array of {@link question_display_options} field names, that are* not relevant to this behaviour before a 'finish' action.*/public static function get_behaviour_unused_display_options($behaviour) {return self::get_behaviour_type($behaviour)->get_unused_display_options();}/*** With this behaviour, is it possible that a question might finish as the student* interacts with it, without a call to the {@link question_attempt::finish()} method?* @param string $behaviour the name of a behaviour. E.g. 'deferredfeedback'.* @return bool whether with this behaviour, questions may finish naturally.*/public static function can_questions_finish_during_the_attempt($behaviour) {return self::get_behaviour_type($behaviour)->can_questions_finish_during_the_attempt();}/*** Create a behaviour for a particular type. If that type cannot be* found, return an instance of qbehaviour_missing.** Normally you should use {@link make_archetypal_behaviour()}, or* call the constructor of a particular model class directly. This method* is only intended for use by {@link question_attempt::load_from_records()}.** @param string $behaviour the type of model to create.* @param question_attempt $qa the question attempt the model will process.* @param string $preferredbehaviour the preferred behaviour for the containing usage.* @return question_behaviour an instance of appropriate behaviour class.*/public static function make_behaviour($behaviour, question_attempt $qa, $preferredbehaviour) {try {self::load_behaviour_class($behaviour);} catch (Exception $e) {self::load_behaviour_class('missing');return new qbehaviour_missing($qa, $preferredbehaviour);}$class = 'qbehaviour_' . $behaviour;return new $class($qa, $preferredbehaviour);}/*** Load the behaviour class(es) belonging to a particular model. That is,* include_once('/question/behaviour/' . $behaviour . '/behaviour.php'), with a bit* of checking.* @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.*/public static function load_behaviour_class($behaviour) {global $CFG;if (isset(self::$loadedbehaviours[$behaviour])) {return;}$file = $CFG->dirroot . '/question/behaviour/' . $behaviour . '/behaviour.php';if (!is_readable($file)) {throw new coding_exception('Unknown question behaviour ' . $behaviour);}include_once($file);$class = 'qbehaviour_' . $behaviour;if (!class_exists($class)) {throw new coding_exception('Question behaviour ' . $behaviour .' does not define the required class ' . $class . '.');}self::$loadedbehaviours[$behaviour] = 1;}/*** Create a behaviour for a particular type. If that type cannot be* found, return an instance of qbehaviour_missing.** Normally you should use {@link make_archetypal_behaviour()}, or* call the constructor of a particular model class directly. This method* is only intended for use by {@link question_attempt::load_from_records()}.** @param string $behaviour the type of model to create.* @param question_attempt $qa the question attempt the model will process.* @param string $preferredbehaviour the preferred behaviour for the containing usage.* @return question_behaviour_type an instance of appropriate behaviour class.*/public static function get_behaviour_type($behaviour) {if (array_key_exists($behaviour, self::$behaviourtypes)) {return self::$behaviourtypes[$behaviour];}self::load_behaviour_type_class($behaviour);$class = 'qbehaviour_' . $behaviour . '_type';if (class_exists($class)) {self::$behaviourtypes[$behaviour] = new $class();} else {debugging('Question behaviour ' . $behaviour .' does not define the required class ' . $class . '.', DEBUG_DEVELOPER);self::$behaviourtypes[$behaviour] = new question_behaviour_type_fallback($behaviour);}return self::$behaviourtypes[$behaviour];}/*** Load the behaviour type class for a particular behaviour. That is,* include_once('/question/behaviour/' . $behaviour . '/behaviourtype.php').* @param string $behaviour the behaviour name. For example 'interactive' or 'deferredfeedback'.*/protected static function load_behaviour_type_class($behaviour) {global $CFG;if (isset(self::$behaviourtypes[$behaviour])) {return;}$file = $CFG->dirroot . '/question/behaviour/' . $behaviour . '/behaviourtype.php';if (!is_readable($file)) {debugging('Question behaviour ' . $behaviour .' is missing the behaviourtype.php file.', DEBUG_DEVELOPER);}include_once($file);}/*** Return an array where the keys are the internal names of the archetypal* behaviours, and the values are a human-readable name. An* archetypal behaviour is one that is suitable to pass the name of to* {@link question_usage_by_activity::set_preferred_behaviour()}.** @return array model name => lang string for this behaviour name.*/public static function get_archetypal_behaviours() {$archetypes = array();$behaviours = core_component::get_plugin_list('qbehaviour');foreach ($behaviours as $behaviour => $notused) {if (self::is_behaviour_archetypal($behaviour)) {$archetypes[$behaviour] = self::get_behaviour_name($behaviour);}}asort($archetypes, SORT_LOCALE_STRING);return $archetypes;}/*** @param string $behaviour the name of a behaviour. E.g. 'deferredfeedback'.* @return bool whether this is an archetypal behaviour.*/public static function is_behaviour_archetypal($behaviour) {return self::get_behaviour_type($behaviour)->is_archetypal();}/*** Return an array where the keys are the internal names of the behaviours* in preferred order and the values are a human-readable name.** @param array $archetypes, array of behaviours* @param string $orderlist, a comma separated list of behaviour names* @param string $disabledlist, a comma separated list of behaviour names* @param string $current, current behaviour name* @return array model name => lang string for this behaviour name.*/public static function sort_behaviours($archetypes, $orderlist, $disabledlist, $current=null) {// Get disabled behavioursif ($disabledlist) {$disabled = explode(',', $disabledlist);} else {$disabled = array();}if ($orderlist) {$order = explode(',', $orderlist);} else {$order = array();}foreach ($disabled as $behaviour) {if (array_key_exists($behaviour, $archetypes) && $behaviour != $current) {unset($archetypes[$behaviour]);}}// Get behaviours in preferred order$behaviourorder = array();foreach ($order as $behaviour) {if (array_key_exists($behaviour, $archetypes)) {$behaviourorder[$behaviour] = $archetypes[$behaviour];}}// Get the rest of behaviours and sort them alphabetically$leftover = array_diff_key($archetypes, $behaviourorder);asort($leftover, SORT_LOCALE_STRING);// Set up the final order to be displayedreturn $behaviourorder + $leftover;}/*** Return an array where the keys are the internal names of the behaviours* in preferred order and the values are a human-readable name.** @param string $currentbehaviour* @return array model name => lang string for this behaviour name.*/public static function get_behaviour_options($currentbehaviour) {$config = question_bank::get_config();$archetypes = self::get_archetypal_behaviours();// If no admin setting return all behaviousif (empty($config->disabledbehaviours) && empty($config->behavioursortorder)) {return $archetypes;}if (empty($config->behavioursortorder)) {$order = '';} else {$order = $config->behavioursortorder;}if (empty($config->disabledbehaviours)) {$disabled = '';} else {$disabled = $config->disabledbehaviours;}return self::sort_behaviours($archetypes, $order, $disabled, $currentbehaviour);}/*** Get the translated name of a behaviour, for display in the UI.* @param string $behaviour the internal name of the model.* @return string name from the current language pack.*/public static function get_behaviour_name($behaviour) {return get_string('pluginname', 'qbehaviour_' . $behaviour);}/*** @return array all the file area names that may contain response files.*/public static function get_all_response_file_areas() {$variables = array();foreach (question_bank::get_all_qtypes() as $qtype) {$variables = array_merge($variables, $qtype->response_file_areas());}$areas = array();foreach (array_unique($variables) as $variable) {$areas[] = 'response_' . $variable;}return $areas;}/*** Returns the valid choices for the number of decimal places for showing* question marks. For use in the user interface.* @return array suitable for passing to {@link html_writer::select()} or similar.*/public static function get_dp_options() {return question_display_options::get_dp_options();}/*** Initialise the JavaScript required on pages where questions will be displayed.** @return string*/public static function initialise_js() {return question_flags::initialise_js();}}/*** This class contains all the options that controls how a question is displayed.** Normally, what will happen is that the calling code will set up some display* options to indicate what sort of question display it wants, and then before the* question is rendered, the behaviour will be given a chance to modify the* display options, so that, for example, A question that is finished will only* be shown read-only, and a question that has not been submitted will not have* any sort of feedback displayed.** @copyright 2009 The Open University* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class question_display_options {/**#@+* @var int named constants for the values that most of the options take.*/const SHOW_ALL = -1;const HIDDEN = 0;const VISIBLE = 1;const EDITABLE = 2;/**#@-*//**#@+ @var int named constants for the {@see $marks} option. */const MAX_ONLY = 1;const MARK_AND_MAX = 2;/**#@-*//*** @var int maximum value for the {@see $markpd} option. This is* effectively set by the database structure, which uses NUMBER(12,7) columns* for question marks/fractions.*/const MAX_DP = 7;/*** @var boolean whether the question should be displayed as a read-only review,* or in an active state where you can change the answer.*/public $readonly = false;/*** @var boolean whether the question type should output hidden form fields* to reset any incorrect parts of the resonse to blank.*/public $clearwrong = false;/*** Should the student have what they got right and wrong clearly indicated.* This includes the green/red hilighting of the bits of their response,* whether the one-line summary of the current state of the question says* correct/incorrect or just answered.* @var int {@see question_display_options::HIDDEN} or* {@see question_display_options::VISIBLE}*/public $correctness = self::VISIBLE;/*** The the mark and/or the maximum available mark for this question be visible?* @var int {@see question_display_options::HIDDEN},* {@see question_display_options::MAX_ONLY} or {@see question_display_options::MARK_AND_MAX}*/public $marks = self::MARK_AND_MAX;/** @var int of decimal places to use when formatting marks for output. */public $markdp = 2;/*** Should the flag this question UI element be visible, and if so, should the* flag state be changeable?** @var int {@see question_display_options::HIDDEN},* {@see question_display_options::VISIBLE} or {@see question_display_options::EDITABLE}*/public $flags = self::VISIBLE;/*** Should the specific feedback be visible.** Specific feedback is typically the part of the feedback that changes based on the* answer that the student gave. For example the feedback shown if a particular choice* has been chosen in a multi-choice question. It also includes the combined feedback* that a lost of question types have (e.g. feedback for any correct/incorrect response.)** @var int {@see question_display_options::HIDDEN} or* {@see question_display_options::VISIBLE}*/public $feedback = self::VISIBLE;/*** For questions with a number of sub-parts (like matching, or* multiple-choice, multiple-reponse) display the number of sub-parts that* were correct.* @var int {@see question_display_options::HIDDEN} or* {@see question_display_options::VISIBLE}*/public $numpartscorrect = self::VISIBLE;/*** Should the general feedback be visible?** This is typically feedback shown to all students after the question* is finished, irrespective of which answer they gave.** @var int {@see question_display_options::HIDDEN} or* {@see question_display_options::VISIBLE}*/public $generalfeedback = self::VISIBLE;/*** Should the automatically generated display of what the correct answer be visible?** @var int {@see question_display_options::HIDDEN} or* {@see question_display_options::VISIBLE}*/public $rightanswer = self::VISIBLE;/*** Should the manually added marker's comment be visible. Should the link for* adding/editing the comment be there.* @var int {@see question_display_options::HIDDEN},* {@see question_display_options::VISIBLE}, or {@see question_display_options::EDITABLE}.* Editable means that form fields are displayed inline.*/public $manualcomment = self::VISIBLE;/*** Should we show a 'Make comment or override grade' link?* @var string base URL for the edit comment script, which will be shown if* $manualcomment = self::VISIBLE.*/public $manualcommentlink = null;/*** Used in places like the question history table, to show a link to review* this question in a certain state. If blank, a link is not shown.* @var moodle_url base URL for a review question script.*/public $questionreviewlink = null;/*** Should the history of previous question states table be visible?* @var int {@see question_display_options::HIDDEN} or* {@see question_display_options::VISIBLE}*/public $history = self::HIDDEN;/*** @since 2.9* @var string extra HTML to include at the end of the outcome (feedback) box* of the question display.** This field is now badly named. The place it included is was changed* (for the better) but the name was left unchanged for backwards compatibility.*/public $extrainfocontent = '';/*** @since 2.9* @var string extra HTML to include in the history box of the question display,* if it is shown.*/public $extrahistorycontent = '';/*** If not empty, then a link to edit the question will be included in* the info box for the question.** If used, this array must contain an element courseid or cmid.** It shoudl also contain a parameter returnurl => moodle_url giving a* sensible URL to go back to when the editing form is submitted or cancelled.** @var array url parameter for the edit link. id => questiosnid will be* added automatically.*/public $editquestionparams = array();/*** @var context the context the attempt being output belongs to.*/public $context;/*** @var int The option to show the action author in the response history.*/public $userinfoinhistory = self::HIDDEN;/*** This identifier should be added to the labels of all input fields in the question.** This is so people using assistive technology can easily tell which input belong to* which question. The helper {@see self::add_question_identifier_to_label() makes this easier.** If not set before the question is rendered, then it defaults to 'Question N'.* (lang string)** @var string The identifier that the question being rendered is associated with.* E.g. The question number when it is rendered on a quiz.*/public $questionidentifier = null;/*** @var ?bool $versioninfo Should we display the version in the question info?*/public ?bool $versioninfo = null;/*** Set all the feedback-related fields, feedback, numpartscorrect, generalfeedback,* rightanswer, manualcomment} and correctness to {@see question_display_options::HIDDEN}.*/public function hide_all_feedback() {$this->feedback = self::HIDDEN;$this->numpartscorrect = self::HIDDEN;$this->generalfeedback = self::HIDDEN;$this->rightanswer = self::HIDDEN;$this->manualcomment = self::HIDDEN;$this->correctness = self::HIDDEN;}/*** Returns the valid choices for the number of decimal places for showing* question marks. For use in the user interface.** Calling code should probably use {@see question_engine::get_dp_options()}* rather than calling this method directly.** @return array suitable for passing to {@see html_writer::select()} or similar.*/public static function get_dp_options() {$options = array();for ($i = 0; $i <= self::MAX_DP; $i += 1) {$options[$i] = $i;}return $options;}/*** Helper to add the question identify (if there is one) to the label of an input field in a question.** @param string $label The plain field label. E.g. 'Answer 1'* @param bool $sridentifier If true, the question identifier, if added, will be wrapped in a sr-only span. Default false.* @param bool $addbefore If true, the question identifier will be added before the label.* @return string The amended label. For example 'Answer 1, Question 1'.*/public function add_question_identifier_to_label(string $label, bool $sridentifier = false, bool $addbefore = false): string {if (!$this->has_question_identifier()) {return $label;}$identifier = $this->questionidentifier;if ($sridentifier) {$identifier = html_writer::span($identifier, 'sr-only');}$fieldlang = 'fieldinquestion';if ($addbefore) {$fieldlang = 'fieldinquestionpre';}return get_string($fieldlang, 'question', (object)['fieldname' => $label, 'questionindentifier' => $identifier]);}/*** Whether a question number has been provided for the question that is being displayed.** @return bool*/public function has_question_identifier(): bool {return $this->questionidentifier !== null && trim($this->questionidentifier) !== '';}}/*** Contains the logic for handling question flags.** @copyright 2010 The Open University* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/abstract class question_flags {/*** Get the checksum that validates that a toggle request is valid.* @param int $qubaid the question usage id.* @param int $questionid the question id.* @param int $sessionid the question_attempt id.* @param object $user the user. If null, defaults to $USER.* @return string that needs to be sent to question/toggleflag.php for it to work.*/protected static function get_toggle_checksum($qubaid, $questionid,$qaid, $slot, $user = null) {if (is_null($user)) {global $USER;$user = $USER;}return md5($qubaid . "_" . $user->secret . "_" . $questionid . "_" . $qaid . "_" . $slot);}/*** Get the postdata that needs to be sent to question/toggleflag.php to change the flag state.* You need to append &newstate=0/1 to this.* @return the post data to send.*/public static function get_postdata(question_attempt $qa) {$qaid = $qa->get_database_id();$qubaid = $qa->get_usage_id();$qid = $qa->get_question_id();$slot = $qa->get_slot();$checksum = self::get_toggle_checksum($qubaid, $qid, $qaid, $slot);return "qaid={$qaid}&qubaid={$qubaid}&qid={$qid}&slot={$slot}&checksum={$checksum}&sesskey=" .sesskey() . '&newstate=';}/*** If the request seems valid, update the flag state of a question attempt.* Throws exceptions if this is not a valid update request.* @param int $qubaid the question usage id.* @param int $questionid the question id.* @param int $sessionid the question_attempt id.* @param string $checksum checksum, as computed by {@link get_toggle_checksum()}* corresponding to the last three arguments.* @param bool $newstate the new state of the flag. true = flagged.*/public static function update_flag($qubaid, $questionid, $qaid, $slot, $checksum, $newstate) {// Check the checksum - it is very hard to know who a question session belongs// to, so we require that checksum parameter is matches an md5 hash of the// three ids and the users username. Since we are only updating a flag, that// probably makes it sufficiently difficult for malicious users to toggle// other users flags.if ($checksum != self::get_toggle_checksum($qubaid, $questionid, $qaid, $slot)) {throw new moodle_exception('errorsavingflags', 'question');}$dm = new question_engine_data_mapper();$dm->update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate);}public static function initialise_js() {global $CFG, $PAGE, $OUTPUT;static $done = false;if ($done) {return;}$module = array('name' => 'core_question_flags','fullpath' => '/question/flags.js','requires' => array('base', 'dom', 'event-delegate', 'io-base'),);$actionurl = $CFG->wwwroot . '/question/toggleflag.php';$flagattributes = array(0 => array('src' => $OUTPUT->image_url('i/unflagged') . '','title' => get_string('clicktoflag', 'question'),'alt' => get_string('flagged', 'question'), // Label on toggle should not change.'text' => get_string('clickflag', 'question'),),1 => array('src' => $OUTPUT->image_url('i/flagged') . '','title' => get_string('clicktounflag', 'question'),'alt' => get_string('flagged', 'question'),'text' => get_string('clickunflag', 'question'),),);$PAGE->requires->js_init_call('M.core_question_flags.init',array($actionurl, $flagattributes), false, $module);$done = true;}}/*** Exception thrown when the system detects that a student has done something* out-of-order to a question. This can happen, for example, if they click* the browser's back button in a quiz, then try to submit a different response.** @copyright 2010 The Open University* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class question_out_of_sequence_exception extends moodle_exception {public function __construct($qubaid, $slot, $postdata) {if ($postdata == null) {$postdata = data_submitted();}parent::__construct('submissionoutofsequence', 'question', '', null,"QUBAid: {$qubaid}, slot: {$slot}, post data: " . print_r($postdata, true));}}/*** Useful functions for writing question types and behaviours.** @copyright 2010 The Open University* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/abstract class question_utils {/*** @var float tolerance to use when comparing question mark/fraction values.** When comparing floating point numbers in a computer, the representation is not* necessarily exact. Therefore, we need to allow a tolerance.* Question marks are stored in the database as decimal numbers with 7 decimal places.* Therefore, this is the appropriate tolerance to use.*/const MARK_TOLERANCE = 0.00000005;/*** Tests to see whether two arrays have the same keys, with the same values* (as compared by ===) for each key. However, the order of the arrays does* not have to be the same.* @param array $array1 the first array.* @param array $array2 the second array.* @return bool whether the two arrays have the same keys with the same* corresponding values.*/public static function arrays_have_same_keys_and_values(array $array1, array $array2) {if (count($array1) != count($array2)) {return false;}foreach ($array1 as $key => $value1) {if (!array_key_exists($key, $array2)) {return false;}if (((string) $value1) !== ((string) $array2[$key])) {return false;}}return true;}/*** Tests to see whether two arrays have the same value at a particular key.* This method will return true if:* 1. Neither array contains the key; or* 2. Both arrays contain the key, and the corresponding values compare* identical when cast to strings and compared with ===.* @param array $array1 the first array.* @param array $array2 the second array.* @param string $key an array key.* @return bool whether the two arrays have the same value (or lack of* one) for a given key.*/public static function arrays_same_at_key(array $array1, array $array2, $key) {if (array_key_exists($key, $array1) && array_key_exists($key, $array2)) {return ((string) $array1[$key]) === ((string) $array2[$key]);}if (!array_key_exists($key, $array1) && !array_key_exists($key, $array2)) {return true;}return false;}/*** Tests to see whether two arrays have the same value at a particular key.* Missing values are replaced by '', and then the values are cast to* strings and compared with ===.* @param array $array1 the first array.* @param array $array2 the second array.* @param string $key an array key.* @return bool whether the two arrays have the same value (or lack of* one) for a given key.*/public static function arrays_same_at_key_missing_is_blank(array $array1, array $array2, $key) {if (array_key_exists($key, $array1)) {$value1 = $array1[$key];} else {$value1 = '';}if (array_key_exists($key, $array2)) {$value2 = $array2[$key];} else {$value2 = '';}return ((string) $value1) === ((string) $value2);}/*** Tests to see whether two arrays have the same value at a particular key.* Missing values are replaced by 0, and then the values are cast to* integers and compared with ===.* @param array $array1 the first array.* @param array $array2 the second array.* @param string $key an array key.* @return bool whether the two arrays have the same value (or lack of* one) for a given key.*/public static function arrays_same_at_key_integer(array $array1, array $array2, $key) {if (array_key_exists($key, $array1)) {$value1 = (int) $array1[$key];} else {$value1 = 0;}if (array_key_exists($key, $array2)) {$value2 = (int) $array2[$key];} else {$value2 = 0;}return $value1 === $value2;}private static $units = array('', 'i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix');private static $tens = array('', 'x', 'xx', 'xxx', 'xl', 'l', 'lx', 'lxx', 'lxxx', 'xc');private static $hundreds = array('', 'c', 'cc', 'ccc', 'cd', 'd', 'dc', 'dcc', 'dccc', 'cm');private static $thousands = array('', 'm', 'mm', 'mmm');/*** Convert an integer to roman numerals.* @param int $number an integer between 1 and 3999 inclusive. Anything else* will throw an exception.* @return string the number converted to lower case roman numerals.*/public static function int_to_roman($number) {if (!is_integer($number) || $number < 1 || $number > 3999) {throw new coding_exception('Only integers between 0 and 3999 can be ' .'converted to roman numerals.', $number);}return self::$thousands[floor($number / 1000) % 10] . self::$hundreds[floor($number / 100) % 10] .self::$tens[floor($number / 10) % 10] . self::$units[$number % 10];}/*** Convert an integer to a letter of alphabet.* @param int $number an integer between 1 and 26 inclusive.* Anything else will throw an exception.* @return string the number converted to upper case letter of alphabet.*/public static function int_to_letter($number) {$alphabet = ['1' => 'A','2' => 'B','3' => 'C','4' => 'D','5' => 'E','6' => 'F','7' => 'G','8' => 'H','9' => 'I','10' => 'J','11' => 'K','12' => 'L','13' => 'M','14' => 'N','15' => 'O','16' => 'P','17' => 'Q','18' => 'R','19' => 'S','20' => 'T','21' => 'U','22' => 'V','23' => 'W','24' => 'X','25' => 'Y','26' => 'Z'];if (!is_integer($number) || $number < 1 || $number > count($alphabet)) {throw new coding_exception('Only integers between 1 and 26 can be converted to letters.', $number);}return $alphabet[$number];}/*** Typically, $mark will have come from optional_param($name, null, PARAM_RAW_TRIMMED).* This method copes with:* - keeping null or '' input unchanged - important to let teaches set a question back to requries grading.* - numbers that were typed as either 1.00 or 1,00 form.* - invalid things, which get turned into null.** @param string|null $mark raw use input of a mark.* @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.*/public static function clean_param_mark($mark) {if ($mark === '' || is_null($mark)) {return $mark;}$mark = str_replace(',', '.', $mark);// This regexp should match the one in validate_param.if (!preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', $mark)) {return null;}return clean_param($mark, PARAM_FLOAT);}/*** Get a sumitted variable (from the GET or POST data) that is a mark.* @param string $parname the submitted variable name.* @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.*/public static function optional_param_mark($parname) {return self::clean_param_mark(optional_param($parname, null, PARAM_RAW_TRIMMED));}/*** Convert part of some question content to plain text.* @param string $text the text.* @param int $format the text format.* @param array $options formatting options. Passed to {@link format_text}.* @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.*/public static function to_plain_text($text, $format, $options = array('noclean' => 'true')) {// The following call to html_to_text uses the option that strips out// all URLs, but format_text complains if it finds @@PLUGINFILE@@ tokens.// So, we need to replace @@PLUGINFILE@@ with a real URL, but it doesn't// matter what. We use http://example.com/.$text = str_replace('@@PLUGINFILE@@/', 'http://example.com/', $text);return html_to_text(format_text($text, $format, $options), 0, false);}/*** Get the options required to configure the filepicker for one of the editor* toolbar buttons.** @param mixed $acceptedtypes array of types of '*'.* @param int $draftitemid the draft area item id.* @param context $context the context.* @return object the required options.*/protected static function specific_filepicker_options($acceptedtypes, $draftitemid, $context) {$filepickeroptions = new stdClass();$filepickeroptions->accepted_types = $acceptedtypes;$filepickeroptions->return_types = FILE_INTERNAL | FILE_EXTERNAL;$filepickeroptions->context = $context;$filepickeroptions->env = 'filepicker';$options = initialise_filepicker($filepickeroptions);$options->context = $context;$options->client_id = uniqid();$options->env = 'editor';$options->itemid = $draftitemid;return $options;}/*** Get filepicker options for question related text areas.** @param context $context the context.* @param int $draftitemid the draft area item id.* @return array An array of options*/public static function get_filepicker_options($context, $draftitemid) {return ['image' => self::specific_filepicker_options(['image'], $draftitemid, $context),'media' => self::specific_filepicker_options(['video', 'audio'], $draftitemid, $context),'link' => self::specific_filepicker_options('*', $draftitemid, $context),];}/*** Get editor options for question related text areas.** @param context $context the context.* @return array An array of options*/public static function get_editor_options($context) {global $CFG;$editoroptions = ['subdirs' => 0,'context' => $context,'maxfiles' => EDITOR_UNLIMITED_FILES,'maxbytes' => $CFG->maxbytes,'noclean' => 0,'trusttext' => 0,'autosave' => false];return $editoroptions;}}/*** The interface for strategies for controlling which variant of each question is used.** @copyright 2011 The Open University* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/interface question_variant_selection_strategy {/*** @param int $maxvariants the num* @param string $seed data that can be used to controls how the variant is selected* in a semi-random way.* @return int the variant to use, a number betweeb 1 and $maxvariants inclusive.*/public function choose_variant($maxvariants, $seed);}/*** A {@link question_variant_selection_strategy} that is completely random.** @copyright 2011 The Open University* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class question_variant_random_strategy implements question_variant_selection_strategy {public function choose_variant($maxvariants, $seed) {return rand(1, $maxvariants);}}/*** A {@link question_variant_selection_strategy} that is effectively random* for the first attempt, and then after that cycles through the available* variants so that the students will not get a repeated variant until they have* seen them all.** @copyright 2011 The Open University* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class question_variant_pseudorandom_no_repeats_strategyimplements question_variant_selection_strategy {/** @var int the number of attempts this users has had, including the curent one. */protected $attemptno;/** @var int the user id the attempt belongs to. */protected $userid;/** @var string extra input fed into the pseudo-random code. */protected $extrarandomness = '';/*** Constructor.* @param int $attemptno The attempt number.* @param int $userid the user the attempt is for (defaults to $USER->id).*/public function __construct($attemptno, $userid = null, $extrarandomness = '') {$this->attemptno = $attemptno;if (is_null($userid)) {global $USER;$this->userid = $USER->id;} else {$this->userid = $userid;}if ($extrarandomness) {$this->extrarandomness = '|' . $extrarandomness;}}public function choose_variant($maxvariants, $seed) {if ($maxvariants == 1) {return 1;}$hash = sha1($seed . '|user' . $this->userid . $this->extrarandomness);$randint = hexdec(substr($hash, 17, 7));return ($randint + $this->attemptno) % $maxvariants + 1;}}/*** A {@link question_variant_selection_strategy} designed ONLY for testing.* For selected questions it wil return a specific variants. For the other* slots it will use a fallback strategy.** @copyright 2013 The Open University* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class question_variant_forced_choices_selection_strategyimplements question_variant_selection_strategy {/** @var array seed => variant to select. */protected $forcedchoices;/** @var question_variant_selection_strategy strategy used to make the non-forced choices. */protected $basestrategy;/*** Constructor.* @param array $forcedchoices array seed => variant to select.* @param question_variant_selection_strategy $basestrategy strategy used* to make the non-forced choices.*/public function __construct(array $forcedchoices, question_variant_selection_strategy $basestrategy) {$this->forcedchoices = $forcedchoices;$this->basestrategy = $basestrategy;}public function choose_variant($maxvariants, $seed) {if (array_key_exists($seed, $this->forcedchoices)) {if ($this->forcedchoices[$seed] > $maxvariants) {throw new coding_exception('Forced variant out of range.');}return $this->forcedchoices[$seed];} else {return $this->basestrategy->choose_variant($maxvariants, $seed);}}/*** Helper method for preparing the $forcedchoices array.* @param array $variantsbyslot slot number => variant to select.* @param question_usage_by_activity $quba the question usage we need a strategy for.* @throws coding_exception when variant cannot be forced as doesn't work.* @return array that can be passed to the constructor as $forcedchoices.*/public static function prepare_forced_choices_array(array $variantsbyslot,question_usage_by_activity $quba) {$forcedchoices = array();foreach ($variantsbyslot as $slot => $varianttochoose) {$question = $quba->get_question($slot);$seed = $question->get_variants_selection_seed();if (array_key_exists($seed, $forcedchoices) && $forcedchoices[$seed] != $varianttochoose) {throw new coding_exception('Inconsistent forced variant detected at slot ' . $slot);}if ($varianttochoose > $question->get_num_variants()) {throw new coding_exception('Forced variant out of range at slot ' . $slot);}$forcedchoices[$seed] = $varianttochoose;}return $forcedchoices;}}