|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 // This file is part of Moodle - http://moodle.org/ 00003 // 00004 // Moodle is free software: you can redistribute it and/or modify 00005 // it under the terms of the GNU General Public License as published by 00006 // the Free Software Foundation, either version 3 of the License, or 00007 // (at your option) any later version. 00008 // 00009 // Moodle is distributed in the hope that it will be useful, 00010 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00011 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00012 // GNU General Public License for more details. 00013 // 00014 // You should have received a copy of the GNU General Public License 00015 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00016 00027 defined('MOODLE_INTERNAL') || die(); 00028 00029 require_once(dirname(__FILE__) . '/questionusage.php'); 00030 require_once(dirname(__FILE__) . '/questionattempt.php'); 00031 require_once(dirname(__FILE__) . '/questionattemptstep.php'); 00032 require_once(dirname(__FILE__) . '/states.php'); 00033 require_once(dirname(__FILE__) . '/datalib.php'); 00034 require_once(dirname(__FILE__) . '/renderer.php'); 00035 require_once(dirname(__FILE__) . '/bank.php'); 00036 require_once(dirname(__FILE__) . '/../type/questiontypebase.php'); 00037 require_once(dirname(__FILE__) . '/../type/questionbase.php'); 00038 require_once(dirname(__FILE__) . '/../type/rendererbase.php'); 00039 require_once(dirname(__FILE__) . '/../behaviour/behaviourbase.php'); 00040 require_once(dirname(__FILE__) . '/../behaviour/rendererbase.php'); 00041 require_once($CFG->libdir . '/questionlib.php'); 00042 00043 00054 abstract class question_engine { 00056 private static $loadedbehaviours = array(); 00057 00067 public static function make_questions_usage_by_activity($component, $context) { 00068 return new question_usage_by_activity($component, $context); 00069 } 00070 00076 public static function load_questions_usage_by_activity($qubaid) { 00077 $dm = new question_engine_data_mapper(); 00078 return $dm->load_questions_usage_by_activity($qubaid); 00079 } 00080 00087 public static function save_questions_usage_by_activity(question_usage_by_activity $quba) { 00088 $dm = new question_engine_data_mapper(); 00089 $observer = $quba->get_observer(); 00090 if ($observer instanceof question_engine_unit_of_work) { 00091 $observer->save($dm); 00092 } else { 00093 $dm->insert_questions_usage_by_activity($quba); 00094 } 00095 } 00096 00101 public static function delete_questions_usage_by_activity($qubaid) { 00102 self::delete_questions_usage_by_activities(new qubaid_list(array($qubaid))); 00103 } 00104 00109 public static function delete_questions_usage_by_activities(qubaid_condition $qubaids) { 00110 $dm = new question_engine_data_mapper(); 00111 $dm->delete_questions_usage_by_activities($qubaids); 00112 } 00113 00121 public static function set_max_mark_in_attempts(qubaid_condition $qubaids, 00122 $slot, $newmaxmark) { 00123 $dm = new question_engine_data_mapper(); 00124 $dm->set_max_mark_in_attempts($qubaids, $slot, $newmaxmark); 00125 } 00126 00133 public static function questions_in_use(array $questionids, qubaid_condition $qubaids = null) { 00134 if (is_null($qubaids)) { 00135 return false; 00136 } 00137 $dm = new question_engine_data_mapper(); 00138 return $dm->questions_in_use($questionids, $qubaids); 00139 } 00140 00149 public static function make_archetypal_behaviour($preferredbehaviour, question_attempt $qa) { 00150 self::load_behaviour_class($preferredbehaviour); 00151 $class = 'qbehaviour_' . $preferredbehaviour; 00152 if (!constant($class . '::IS_ARCHETYPAL')) { 00153 throw new coding_exception('The requested behaviour is not actually ' . 00154 'an archetypal one.'); 00155 } 00156 return new $class($qa, $preferredbehaviour); 00157 } 00158 00164 public static function get_behaviour_unused_display_options($behaviour) { 00165 self::load_behaviour_class($behaviour); 00166 $class = 'qbehaviour_' . $behaviour; 00167 if (!method_exists($class, 'get_unused_display_options')) { 00168 return question_behaviour::get_unused_display_options(); 00169 } 00170 return call_user_func(array($class, 'get_unused_display_options')); 00171 } 00172 00186 public static function make_behaviour($behaviour, question_attempt $qa, $preferredbehaviour) { 00187 try { 00188 self::load_behaviour_class($behaviour); 00189 } catch (Exception $e) { 00190 self::load_behaviour_class('missing'); 00191 return new qbehaviour_missing($qa, $preferredbehaviour); 00192 } 00193 $class = 'qbehaviour_' . $behaviour; 00194 return new $class($qa, $preferredbehaviour); 00195 } 00196 00203 public static function load_behaviour_class($behaviour) { 00204 global $CFG; 00205 if (isset(self::$loadedbehaviours[$behaviour])) { 00206 return; 00207 } 00208 $file = $CFG->dirroot . '/question/behaviour/' . $behaviour . '/behaviour.php'; 00209 if (!is_readable($file)) { 00210 throw new coding_exception('Unknown question behaviour ' . $behaviour); 00211 } 00212 include_once($file); 00213 self::$loadedbehaviours[$behaviour] = 1; 00214 } 00215 00224 public static function get_archetypal_behaviours() { 00225 $archetypes = array(); 00226 $behaviours = get_plugin_list('qbehaviour'); 00227 foreach ($behaviours as $behaviour => $notused) { 00228 if (self::is_behaviour_archetypal($behaviour)) { 00229 $archetypes[$behaviour] = self::get_behaviour_name($behaviour); 00230 } 00231 } 00232 asort($archetypes, SORT_LOCALE_STRING); 00233 return $archetypes; 00234 } 00235 00240 public static function is_behaviour_archetypal($behaviour) { 00241 self::load_behaviour_class($behaviour); 00242 $plugin = 'qbehaviour_' . $behaviour; 00243 return constant($plugin . '::IS_ARCHETYPAL'); 00244 } 00245 00256 public static function sort_behaviours($archetypes, $orderlist, $disabledlist, $current=null) { 00257 00258 // Get disabled behaviours 00259 if ($disabledlist) { 00260 $disabled = explode(',', $disabledlist); 00261 } else { 00262 $disabled = array(); 00263 } 00264 00265 if ($orderlist) { 00266 $order = explode(',', $orderlist); 00267 } else { 00268 $order = array(); 00269 } 00270 00271 foreach ($disabled as $behaviour) { 00272 if (array_key_exists($behaviour, $archetypes) && $behaviour != $current) { 00273 unset($archetypes[$behaviour]); 00274 } 00275 } 00276 00277 // Get behaviours in preferred order 00278 $behaviourorder = array(); 00279 foreach ($order as $behaviour) { 00280 if (array_key_exists($behaviour, $archetypes)) { 00281 $behaviourorder[$behaviour] = $archetypes[$behaviour]; 00282 } 00283 } 00284 // Get the rest of behaviours and sort them alphabetically 00285 $leftover = array_diff_key($archetypes, $behaviourorder); 00286 asort($leftover, SORT_LOCALE_STRING); 00287 00288 // Set up the final order to be displayed 00289 return $behaviourorder + $leftover; 00290 } 00291 00299 public static function get_behaviour_options($currentbehaviour) { 00300 $config = question_bank::get_config(); 00301 $archetypes = self::get_archetypal_behaviours(); 00302 00303 // If no admin setting return all behavious 00304 if (empty($config->disabledbehaviours) && empty($config->behavioursortorder)) { 00305 return $archetypes; 00306 } 00307 00308 if (empty($config->behavioursortorder)) { 00309 $order = ''; 00310 } else { 00311 $order = $config->behavioursortorder; 00312 } 00313 if (empty($config->disabledbehaviours)) { 00314 $disabled = ''; 00315 } else { 00316 $disabled = $config->disabledbehaviours; 00317 } 00318 00319 return self::sort_behaviours($archetypes, $order, $disabled, $currentbehaviour); 00320 } 00321 00327 public static function get_behaviour_name($behaviour) { 00328 return get_string('pluginname', 'qbehaviour_' . $behaviour); 00329 } 00330 00334 public static function get_all_response_file_areas() { 00335 $variables = array(); 00336 foreach (question_bank::get_all_qtypes() as $qtype) { 00337 $variables += $qtype->response_file_areas(); 00338 } 00339 00340 $areas = array(); 00341 foreach (array_unique($variables) as $variable) { 00342 $areas[] = 'response_' . $variable; 00343 } 00344 return $areas; 00345 } 00346 00352 public static function get_dp_options() { 00353 return question_display_options::get_dp_options(); 00354 } 00355 00359 public static function initialise_js() { 00360 return question_flags::initialise_js(); 00361 } 00362 } 00363 00364 00378 class question_display_options { 00380 const HIDDEN = 0; 00381 const VISIBLE = 1; 00382 const EDITABLE = 2; 00386 const MAX_ONLY = 1; 00387 const MARK_AND_MAX = 2; 00395 const MAX_DP = 7; 00396 00401 public $readonly = false; 00402 00407 public $clearwrong = false; 00408 00417 public $correctness = self::VISIBLE; 00418 00424 public $marks = self::MARK_AND_MAX; 00425 00427 public $markdp = 2; 00428 00435 public $flags = self::VISIBLE; 00436 00442 public $feedback = self::VISIBLE; 00443 00451 public $numpartscorrect = self::VISIBLE; 00452 00458 public $generalfeedback = self::VISIBLE; 00459 00466 public $rightanswer = self::VISIBLE; 00467 00475 public $manualcomment = self::VISIBLE; 00476 00482 public $manualcommentlink = null; 00483 00489 public $questionreviewlink = null; 00490 00496 public $history = self::HIDDEN; 00497 00510 public $editquestionparams = array(); 00511 00515 public $context; 00516 00522 public function hide_all_feedback() { 00523 $this->feedback = self::HIDDEN; 00524 $this->numpartscorrect = self::HIDDEN; 00525 $this->generalfeedback = self::HIDDEN; 00526 $this->rightanswer = self::HIDDEN; 00527 $this->manualcomment = self::HIDDEN; 00528 $this->correctness = self::HIDDEN; 00529 } 00530 00540 public static function get_dp_options() { 00541 $options = array(); 00542 for ($i = 0; $i <= self::MAX_DP; $i += 1) { 00543 $options[$i] = $i; 00544 } 00545 return $options; 00546 } 00547 } 00548 00549 00556 abstract class question_flags { 00565 protected static function get_toggle_checksum($qubaid, $questionid, 00566 $qaid, $slot, $user = null) { 00567 if (is_null($user)) { 00568 global $USER; 00569 $user = $USER; 00570 } 00571 return md5($qubaid . "_" . $user->secret . "_" . $questionid . "_" . $qaid . "_" . $slot); 00572 } 00573 00579 public static function get_postdata(question_attempt $qa) { 00580 $qaid = $qa->get_database_id(); 00581 $qubaid = $qa->get_usage_id(); 00582 $qid = $qa->get_question()->id; 00583 $slot = $qa->get_slot(); 00584 $checksum = self::get_toggle_checksum($qubaid, $qid, $qaid, $slot); 00585 return "qaid=$qaid&qubaid=$qubaid&qid=$qid&slot=$slot&checksum=$checksum&sesskey=" . 00586 sesskey() . '&newstate='; 00587 } 00588 00599 public static function update_flag($qubaid, $questionid, $qaid, $slot, $checksum, $newstate) { 00600 // Check the checksum - it is very hard to know who a question session belongs 00601 // to, so we require that checksum parameter is matches an md5 hash of the 00602 // three ids and the users username. Since we are only updating a flag, that 00603 // probably makes it sufficiently difficult for malicious users to toggle 00604 // other users flags. 00605 if ($checksum != self::get_toggle_checksum($qubaid, $questionid, $qaid, $slot)) { 00606 throw new moodle_exception('errorsavingflags', 'question'); 00607 } 00608 00609 $dm = new question_engine_data_mapper(); 00610 $dm->update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate); 00611 } 00612 00613 public static function initialise_js() { 00614 global $CFG, $PAGE, $OUTPUT; 00615 static $done = false; 00616 if ($done) { 00617 return; 00618 } 00619 $module = array( 00620 'name' => 'core_question_flags', 00621 'fullpath' => '/question/flags.js', 00622 'requires' => array('base', 'dom', 'event-delegate', 'io-base'), 00623 ); 00624 $actionurl = $CFG->wwwroot . '/question/toggleflag.php'; 00625 $flagtext = array( 00626 0 => get_string('clickflag', 'question'), 00627 1 => get_string('clickunflag', 'question') 00628 ); 00629 $flagattributes = array( 00630 0 => array( 00631 'src' => $OUTPUT->pix_url('i/unflagged') . '', 00632 'title' => get_string('clicktoflag', 'question'), 00633 'alt' => get_string('notflagged', 'question'), 00634 // 'text' => get_string('clickflag', 'question'), 00635 ), 00636 1 => array( 00637 'src' => $OUTPUT->pix_url('i/flagged') . '', 00638 'title' => get_string('clicktounflag', 'question'), 00639 'alt' => get_string('flagged', 'question'), 00640 // 'text' => get_string('clickunflag', 'question'), 00641 ), 00642 ); 00643 $PAGE->requires->js_init_call('M.core_question_flags.init', 00644 array($actionurl, $flagattributes, $flagtext), false, $module); 00645 $done = true; 00646 } 00647 } 00648 00649 00658 class question_out_of_sequence_exception extends moodle_exception { 00659 public function __construct($qubaid, $slot, $postdata) { 00660 if ($postdata == null) { 00661 $postdata = data_submitted(); 00662 } 00663 parent::__construct('submissionoutofsequence', 'question', '', null, 00664 "QUBAid: $qubaid, slot: $slot, post data: " . print_r($postdata, true)); 00665 } 00666 } 00667 00668 00675 abstract class question_utils { 00685 public static function arrays_have_same_keys_and_values(array $array1, array $array2) { 00686 if (count($array1) != count($array2)) { 00687 return false; 00688 } 00689 foreach ($array1 as $key => $value1) { 00690 if (!array_key_exists($key, $array2)) { 00691 return false; 00692 } 00693 if (((string) $value1) !== ((string) $array2[$key])) { 00694 return false; 00695 } 00696 } 00697 return true; 00698 } 00699 00712 public static function arrays_same_at_key(array $array1, array $array2, $key) { 00713 if (array_key_exists($key, $array1) && array_key_exists($key, $array2)) { 00714 return ((string) $array1[$key]) === ((string) $array2[$key]); 00715 } 00716 if (!array_key_exists($key, $array1) && !array_key_exists($key, $array2)) { 00717 return true; 00718 } 00719 return false; 00720 } 00721 00732 public static function arrays_same_at_key_missing_is_blank( 00733 array $array1, array $array2, $key) { 00734 if (array_key_exists($key, $array1)) { 00735 $value1 = $array1[$key]; 00736 } else { 00737 $value1 = ''; 00738 } 00739 if (array_key_exists($key, $array2)) { 00740 $value2 = $array2[$key]; 00741 } else { 00742 $value2 = ''; 00743 } 00744 return ((string) $value1) === ((string) $value2); 00745 } 00746 00757 public static function arrays_same_at_key_integer( 00758 array $array1, array $array2, $key) { 00759 if (array_key_exists($key, $array1)) { 00760 $value1 = $array1[$key]; 00761 } else { 00762 $value1 = 0; 00763 } 00764 if (array_key_exists($key, $array2)) { 00765 $value2 = $array2[$key]; 00766 } else { 00767 $value2 = 0; 00768 } 00769 return ((integer) $value1) === ((integer) $value2); 00770 } 00771 00772 private static $units = array('', 'i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix'); 00773 private static $tens = array('', 'x', 'xx', 'xxx', 'xl', 'l', 'lx', 'lxx', 'lxxx', 'xc'); 00774 private static $hundreds = array('', 'c', 'cc', 'ccc', 'cd', 'd', 'dc', 'dcc', 'dccc', 'cm'); 00775 private static $thousands = array('', 'm', 'mm', 'mmm'); 00776 00783 public static function int_to_roman($number) { 00784 if (!is_integer($number) || $number < 1 || $number > 3999) { 00785 throw new coding_exception('Only integers between 0 and 3999 can be ' . 00786 'converted to roman numerals.', $number); 00787 } 00788 00789 return self::$thousands[$number / 1000 % 10] . self::$hundreds[$number / 100 % 10] . 00790 self::$tens[$number / 10 % 10] . self::$units[$number % 10]; 00791 } 00792 } 00793 00794 00801 interface question_variant_selection_strategy { 00808 public function choose_variant($maxvariants, $seed); 00809 } 00810 00811 00818 class question_variant_random_strategy implements question_variant_selection_strategy { 00819 public function choose_variant($maxvariants, $seed) { 00820 return rand(1, $maxvariants); 00821 } 00822 } 00823 00824 00834 class question_variant_pseudorandom_no_repeats_strategy 00835 implements question_variant_selection_strategy { 00836 00838 protected $attemptno; 00839 00841 protected $userid; 00842 00848 public function __construct($attemptno, $userid = null) { 00849 $this->attemptno = $attemptno; 00850 if (is_null($userid)) { 00851 global $USER; 00852 $this->userid = $USER->id; 00853 } else { 00854 $this->userid = $userid; 00855 } 00856 } 00857 00858 public function choose_variant($maxvariants, $seed) { 00859 if ($maxvariants == 1) { 00860 return 1; 00861 } 00862 00863 $hash = sha1($seed . '|user' . $this->userid); 00864 $randint = hexdec(substr($hash, 17, 7)); 00865 00866 return ($randint + $this->attemptno) % $maxvariants + 1; 00867 } 00868 }