Moodle  2.2.1
http://www.collinsharper.com
C:/xampp/htdocs/moodle/question/engine/upgrade/upgradelib.php
Go to the documentation of this file.
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 
00028 defined('MOODLE_INTERNAL') || die();
00029 
00030 global $CFG;
00031 require_once($CFG->dirroot . '/question/engine/bank.php');
00032 require_once($CFG->dirroot . '/question/engine/upgrade/logger.php');
00033 require_once($CFG->dirroot . '/question/engine/upgrade/behaviourconverters.php');
00034 
00035 
00043 class question_engine_attempt_upgrader {
00045     protected $questionloader;
00047     protected $logger;
00049     protected $dotcounter = 0;
00051     protected $progressbar = null;
00053     protected $doingbackup = false;
00054 
00061     protected function print_progress($done, $outof, $quizid) {
00062         if (is_null($this->progressbar)) {
00063             $this->progressbar = new progress_bar('qe2upgrade');
00064             $this->progressbar->create();
00065         }
00066 
00067         gc_collect_cycles(); // This was really helpful in PHP 5.2. Perhaps remove.
00068         $a = new stdClass();
00069         $a->done = $done;
00070         $a->outof = $outof;
00071         $a->info = $quizid;
00072         $this->progressbar->update($done, $outof, get_string('upgradingquizattempts', 'quiz', $a));
00073     }
00074 
00075     protected function prevent_timeout() {
00076         set_time_limit(300);
00077         if ($this->doingbackup) {
00078             return;
00079         }
00080         echo '.';
00081         $this->dotcounter += 1;
00082         if ($this->dotcounter % 100 == 0) {
00083             echo '<br />';
00084         }
00085     }
00086 
00087     protected function get_quiz_ids() {
00088         global $CFG, $DB;
00089 
00090         // Look to see if the admin has set things up to only upgrade certain attempts.
00091         $partialupgradefile = $CFG->dirroot . '/' . $CFG->admin .
00092                 '/tool/qeupgradehelper/partialupgrade.php';
00093         $partialupgradefunction = 'tool_qeupgradehelper_get_quizzes_to_upgrade';
00094         if (is_readable($partialupgradefile)) {
00095             include_once($partialupgradefile);
00096             if (function_exists($partialupgradefunction)) {
00097                 $quizids = $partialupgradefunction();
00098 
00099                 // Ignore any quiz ids that do not acually exist.
00100                 if (empty($quizids)) {
00101                     return array();
00102                 }
00103                 list($test, $params) = $DB->get_in_or_equal($quizids);
00104                 return $DB->get_fieldset_sql("
00105                         SELECT id
00106                           FROM {quiz}
00107                          WHERE id $test
00108                       ORDER BY id", $params);
00109             }
00110         }
00111 
00112         // Otherwise, upgrade all attempts.
00113         return $DB->get_fieldset_sql('SELECT id FROM {quiz} ORDER BY id');
00114     }
00115 
00116     public function convert_all_quiz_attempts() {
00117         global $DB;
00118 
00119         $quizids = $this->get_quiz_ids();
00120         if (empty($quizids)) {
00121             return true;
00122         }
00123 
00124         $done = 0;
00125         $outof = count($quizids);
00126         $this->logger = new question_engine_assumption_logger();
00127 
00128         foreach ($quizids as $quizid) {
00129             $this->print_progress($done, $outof, $quizid);
00130 
00131             $quiz = $DB->get_record('quiz', array('id' => $quizid), '*', MUST_EXIST);
00132             $this->update_all_attempts_at_quiz($quiz);
00133 
00134             $done += 1;
00135         }
00136 
00137         $this->print_progress($outof, $outof, 'All done!');
00138         $this->logger = null;
00139     }
00140 
00141     public function get_attempts_extra_where() {
00142         return ' AND needsupgradetonewqe = 1';
00143     }
00144 
00145     public function update_all_attempts_at_quiz($quiz) {
00146         global $DB;
00147 
00148         // Wipe question loader cache.
00149         $this->questionloader = new question_engine_upgrade_question_loader($this->logger);
00150 
00151         $transaction = $DB->start_delegated_transaction();
00152 
00153         $params = array('quizid' => $quiz->id);
00154         $where = 'quiz = :quizid AND preview = 0' . $this->get_attempts_extra_where();
00155 
00156         $quizattemptsrs = $DB->get_recordset_select('quiz_attempts', $where, $params, 'uniqueid');
00157         $questionsessionsrs = $DB->get_recordset_sql("
00158                 SELECT s.*
00159                   FROM {question_sessions} s
00160                   JOIN {quiz_attempts} a ON (attemptid = uniqueid)
00161                  WHERE $where
00162               ORDER BY attemptid, questionid
00163         ", $params);
00164 
00165         $questionsstatesrs = $DB->get_recordset_sql("
00166                 SELECT s.*
00167                   FROM {question_states} s
00168                   JOIN {quiz_attempts} ON (s.attempt = uniqueid)
00169                  WHERE $where
00170               ORDER BY s.attempt, question, seq_number, s.id
00171         ", $params);
00172 
00173         $datatodo = $quizattemptsrs && $questionsessionsrs && $questionsstatesrs;
00174         while ($datatodo && $quizattemptsrs->valid()) {
00175             $attempt = $quizattemptsrs->current();
00176             $quizattemptsrs->next();
00177             $this->convert_quiz_attempt($quiz, $attempt, $questionsessionsrs, $questionsstatesrs);
00178         }
00179 
00180         $quizattemptsrs->close();
00181         $questionsessionsrs->close();
00182         $questionsstatesrs->close();
00183 
00184         $transaction->allow_commit();
00185     }
00186 
00187     protected function convert_quiz_attempt($quiz, $attempt, moodle_recordset $questionsessionsrs,
00188             moodle_recordset $questionsstatesrs) {
00189         $qas = array();
00190         $this->logger->set_current_attempt_id($attempt->id);
00191         while ($qsession = $this->get_next_question_session($attempt, $questionsessionsrs)) {
00192             $question = $this->load_question($qsession->questionid, $quiz->id);
00193             $qstates = $this->get_question_states($attempt, $question, $questionsstatesrs);
00194             try {
00195                 $qas[$qsession->questionid] = $this->convert_question_attempt(
00196                         $quiz, $attempt, $question, $qsession, $qstates);
00197             } catch (Exception $e) {
00198                 notify($e->getMessage());
00199             }
00200         }
00201         $this->logger->set_current_attempt_id(null);
00202 
00203         $questionorder = array();
00204         foreach (explode(',', $quiz->questions) as $questionid) {
00205             if ($questionid == 0) {
00206                 continue;
00207             }
00208             if (!array_key_exists($questionid, $qas)) {
00209                 $this->logger->log_assumption("Supplying minimal open state for
00210                         question {$questionid} in attempt {$attempt->id} at quiz
00211                         {$attempt->quiz}, since the session was missing.", $attempt->id);
00212                 try {
00213                     $question = $this->load_question($questionid, $quiz->id);
00214                     $qas[$questionid] = $this->supply_missing_question_attempt(
00215                             $quiz, $attempt, $question);
00216                 } catch (Exception $e) {
00217                     notify($e->getMessage());
00218                 }
00219             }
00220         }
00221 
00222         return $this->save_usage($quiz->preferredbehaviour, $attempt, $qas, $quiz->questions);
00223     }
00224 
00225     public function save_usage($preferredbehaviour, $attempt, $qas, $quizlayout) {
00226         $missing = array();
00227 
00228         $layout = explode(',', $attempt->layout);
00229         $questionkeys = array_combine(array_values($layout), array_keys($layout));
00230 
00231         $this->set_quba_preferred_behaviour($attempt->uniqueid, $preferredbehaviour);
00232 
00233         $i = 0;
00234         foreach (explode(',', $quizlayout) as $questionid) {
00235             if ($questionid == 0) {
00236                 continue;
00237             }
00238             $i++;
00239 
00240             if (!array_key_exists($questionid, $qas)) {
00241                 $missing[] = $questionid;
00242                 $layout[$questionkeys[$questionid]] = $questionid;
00243                 continue;
00244             }
00245 
00246             $qa = $qas[$questionid];
00247             $qa->questionusageid = $attempt->uniqueid;
00248             $qa->slot = $i;
00249             if (textlib::strlen($qa->questionsummary) > question_bank::MAX_SUMMARY_LENGTH) {
00250                 // It seems some people write very long quesions! MDL-30760
00251                 $qa->questionsummary = textlib::substr($qa->questionsummary,
00252                         0, question_bank::MAX_SUMMARY_LENGTH - 3) . '...';
00253             }
00254             $this->insert_record('question_attempts', $qa);
00255             $layout[$questionkeys[$questionid]] = $qa->slot;
00256 
00257             foreach ($qa->steps as $step) {
00258                 $step->questionattemptid = $qa->id;
00259                 $this->insert_record('question_attempt_steps', $step);
00260 
00261                 foreach ($step->data as $name => $value) {
00262                     $datum = new stdClass();
00263                     $datum->attemptstepid = $step->id;
00264                     $datum->name = $name;
00265                     $datum->value = $value;
00266                     $this->insert_record('question_attempt_step_data', $datum, false);
00267                 }
00268             }
00269         }
00270 
00271         $this->set_quiz_attempt_layout($attempt->uniqueid, implode(',', $layout));
00272 
00273         if ($missing) {
00274             notify("Question sessions for questions " .
00275                     implode(', ', $missing) .
00276                     " were missing when upgrading question usage {$attempt->uniqueid}.");
00277         }
00278     }
00279 
00280     protected function set_quba_preferred_behaviour($qubaid, $preferredbehaviour) {
00281         global $DB;
00282         $DB->set_field('question_usages', 'preferredbehaviour', $preferredbehaviour,
00283                 array('id' => $qubaid));
00284     }
00285 
00286     protected function set_quiz_attempt_layout($qubaid, $layout) {
00287         global $DB;
00288         $DB->set_field('quiz_attempts', 'layout', $layout, array('uniqueid' => $qubaid));
00289         $DB->set_field('quiz_attempts', 'needsupgradetonewqe', 0, array('uniqueid' => $qubaid));
00290     }
00291 
00292     protected function delete_quiz_attempt($qubaid) {
00293         global $DB;
00294         $DB->delete_records('quiz_attempts', array('uniqueid' => $qubaid));
00295         $DB->delete_records('question_attempts', array('id' => $qubaid));
00296     }
00297 
00298     protected function insert_record($table, $record, $saveid = true) {
00299         global $DB;
00300         $newid = $DB->insert_record($table, $record, $saveid);
00301         if ($saveid) {
00302             $record->id = $newid;
00303         }
00304         return $newid;
00305     }
00306 
00307     public function load_question($questionid, $quizid = null) {
00308         return $this->questionloader->get_question($questionid, $quizid);
00309     }
00310 
00311     public function load_dataset($questionid, $selecteditem) {
00312         return $this->questionloader->load_dataset($questionid, $selecteditem);
00313     }
00314 
00315     public function get_next_question_session($attempt, moodle_recordset $questionsessionsrs) {
00316         if (!$questionsessionsrs->valid()) {
00317             return false;
00318         }
00319 
00320         $qsession = $questionsessionsrs->current();
00321         if ($qsession->attemptid != $attempt->uniqueid) {
00322             // No more question sessions belonging to this attempt.
00323             return false;
00324         }
00325 
00326         // Session found, move the pointer in the RS and return the record.
00327         $questionsessionsrs->next();
00328         return $qsession;
00329     }
00330 
00331     public function get_question_states($attempt, $question, moodle_recordset $questionsstatesrs) {
00332         $qstates = array();
00333 
00334         while ($questionsstatesrs->valid()) {
00335             $state = $questionsstatesrs->current();
00336             if ($state->attempt != $attempt->uniqueid ||
00337                     $state->question != $question->id) {
00338                 // We have found all the states for this attempt. Stop.
00339                 break;
00340             }
00341 
00342             // Add the new state to the array, and advance.
00343             $qstates[] = $state;
00344             $questionsstatesrs->next();
00345         }
00346 
00347         return $qstates;
00348     }
00349 
00350     protected function get_converter_class_name($question, $quiz, $qsessionid) {
00351         global $DB;
00352         if ($question->qtype == 'deleted') {
00353             $where = '(question = :questionid OR '.$DB->sql_like('answer', ':randomid').') AND event = 7';
00354             $params = array('questionid'=>$question->id, 'randomid'=>"random{$question->id}-%");
00355             if ($DB->record_exists_select('question_states', $where, $params)) {
00356                 $this->logger->log_assumption("Assuming that deleted question {$question->id} was manually graded.");
00357                 return 'qbehaviour_manualgraded_converter';
00358             }
00359         }
00360         if ($question->qtype == 'essay') {
00361             return 'qbehaviour_manualgraded_converter';
00362         } else if ($question->qtype == 'description') {
00363             return 'qbehaviour_informationitem_converter';
00364         } else if ($quiz->preferredbehaviour == 'deferredfeedback') {
00365             return 'qbehaviour_deferredfeedback_converter';
00366         } else if ($quiz->preferredbehaviour == 'adaptive') {
00367             return 'qbehaviour_adaptive_converter';
00368         } else if ($quiz->preferredbehaviour == 'adaptivenopenalty') {
00369             return 'qbehaviour_adaptivenopenalty_converter';
00370         } else {
00371             throw new coding_exception("Question session {$qsessionid}
00372                     has an unexpected preferred behaviour {$quiz->preferredbehaviour}.");
00373         }
00374     }
00375 
00376     public function supply_missing_question_attempt($quiz, $attempt, $question) {
00377         if ($question->qtype == 'random') {
00378             throw new coding_exception("Cannot supply a missing qsession for question
00379                     {$question->id} in attempt {$attempt->id}.");
00380         }
00381 
00382         $converterclass = $this->get_converter_class_name($question, $quiz, 'missing');
00383 
00384         $qbehaviourupdater = new $converterclass($quiz, $attempt, $question,
00385                 null, null, $this->logger, $this);
00386         $qa = $qbehaviourupdater->supply_missing_qa();
00387         $qbehaviourupdater->discard();
00388         return $qa;
00389     }
00390 
00391     public function convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates) {
00392         $this->prevent_timeout();
00393 
00394         if ($question->qtype == 'random') {
00395             list($question, $qstates) = $this->decode_random_attempt($qstates, $question->maxmark);
00396             $qsession->questionid = $question->id;
00397         }
00398 
00399         $converterclass = $this->get_converter_class_name($question, $quiz, $qsession->id);
00400 
00401         $qbehaviourupdater = new $converterclass($quiz, $attempt, $question, $qsession,
00402                 $qstates, $this->logger, $this);
00403         $qa = $qbehaviourupdater->get_converted_qa();
00404         $qbehaviourupdater->discard();
00405         return $qa;
00406     }
00407 
00408     protected function decode_random_attempt($qstates, $maxmark) {
00409         $realquestionid = null;
00410         foreach ($qstates as $i => $state) {
00411             if (strpos($state->answer, '-') < 6) {
00412                 // Broken state, skip it.
00413                 $this->logger->log_assumption("Had to skip brokes state {$state->id}
00414                         for question {$state->question}.");
00415                 unset($qstates[$i]);
00416                 continue;
00417             }
00418             list($randombit, $realanswer) = explode('-', $state->answer, 2);
00419             $newquestionid = substr($randombit, 6);
00420             if ($realquestionid && $realquestionid != $newquestionid) {
00421                 throw new coding_exception("Question session {$this->qsession->id}
00422                         for random question points to two different real questions
00423                         {$realquestionid} and {$newquestionid}.");
00424             }
00425             $qstates[$i]->answer = $realanswer;
00426         }
00427 
00428         if (empty($newquestionid)) {
00429             // This attempt only had broken states. Set a fake $newquestionid to
00430             // prevent a null DB error later.
00431             $newquestionid = 0;
00432         }
00433 
00434         $newquestion = $this->load_question($newquestionid);
00435         $newquestion->maxmark = $maxmark;
00436         return array($newquestion, $qstates);
00437     }
00438 
00439     public function prepare_to_restore() {
00440         $this->doingbackup = true; // Prevent printing of dots to stop timeout on upgrade.
00441         $this->logger = new dummy_question_engine_assumption_logger();
00442         $this->questionloader = new question_engine_upgrade_question_loader($this->logger);
00443     }
00444 }
00445 
00446 
00454 class question_engine_upgrade_question_loader {
00455     private $cache = array();
00456     private $datasetcache = array();
00457 
00458     public function __construct($logger) {
00459         $this->logger = $logger;
00460     }
00461 
00462     protected function load_question($questionid, $quizid) {
00463         global $DB;
00464 
00465         if ($quizid) {
00466             $question = $DB->get_record_sql("
00467                 SELECT q.*, qqi.grade AS maxmark
00468                 FROM {question} q
00469                 JOIN {quiz_question_instances} qqi ON qqi.question = q.id
00470                 WHERE q.id = $questionid AND qqi.quiz = $quizid");
00471         } else {
00472             $question = $DB->get_record('question', array('id' => $questionid));
00473         }
00474 
00475         if (!$question) {
00476             return null;
00477         }
00478 
00479         if (empty($question->defaultmark)) {
00480             if (!empty($question->defaultgrade)) {
00481                 $question->defaultmark = $question->defaultgrade;
00482             } else {
00483                 $question->defaultmark = 0;
00484             }
00485             unset($question->defaultgrade);
00486         }
00487 
00488         $qtype = question_bank::get_qtype($question->qtype, false);
00489         if ($qtype->name() === 'missingtype') {
00490             $this->logger->log_assumption("Dealing with question id {$question->id}
00491                     that is of an unknown type {$question->qtype}.");
00492             $question->questiontext = '<p>' . get_string('warningmissingtype', 'quiz') .
00493                     '</p>' . $question->questiontext;
00494         }
00495 
00496         $qtype->get_question_options($question);
00497 
00498         return $question;
00499     }
00500 
00501     public function get_question($questionid, $quizid) {
00502         if (isset($this->cache[$questionid])) {
00503             return $this->cache[$questionid];
00504         }
00505 
00506         $question = $this->load_question($questionid, $quizid);
00507 
00508         if (!$question) {
00509             $this->logger->log_assumption("Dealing with question id {$questionid}
00510                     that was missing from the database.");
00511             $question = new stdClass();
00512             $question->id = $questionid;
00513             $question->qtype = 'deleted';
00514             $question->maxmark = 1; // Guess, but that is all we can do.
00515             $question->questiontext = get_string('deletedquestiontext', 'qtype_missingtype');
00516         }
00517 
00518         $this->cache[$questionid] = $question;
00519         return $this->cache[$questionid];
00520     }
00521 
00522     public function load_dataset($questionid, $selecteditem) {
00523         global $DB;
00524 
00525         if (isset($this->datasetcache[$questionid][$selecteditem])) {
00526             return $this->datasetcache[$questionid][$selecteditem];
00527         }
00528 
00529         $this->datasetcache[$questionid][$selecteditem] = $DB->get_records_sql_menu('
00530                 SELECT qdd.name, qdi.value
00531                   FROM {question_dataset_items} qdi
00532                   JOIN {question_dataset_definitions} qdd ON qdd.id = qdi.definition
00533                   JOIN {question_datasets} qd ON qdd.id = qd.datasetdefinition
00534                  WHERE qd.question = ?
00535                    AND qdi.itemnumber = ?
00536                 ', array($questionid, $selecteditem));
00537         return $this->datasetcache[$questionid][$selecteditem];
00538     }
00539 }
00540 
00541 
00549 abstract class question_qtype_attempt_updater {
00551     protected $question;
00553     protected $updater;
00555     protected $logger;
00557     protected $qeupdater;
00558 
00559     public function __construct($updater, $question, $logger, $qeupdater) {
00560         $this->updater = $updater;
00561         $this->question = $question;
00562         $this->logger = $logger;
00563         $this->qeupdater = $qeupdater;
00564     }
00565 
00566     public function discard() {
00567         // Help the garbage collector, which seems to be struggling.
00568         $this->updater = null;
00569         $this->question = null;
00570         $this->logger = null;
00571         $this->qeupdater = null;
00572     }
00573 
00574     protected function to_text($html) {
00575         return $this->updater->to_text($html);
00576     }
00577 
00578     public function question_summary() {
00579         return $this->to_text($this->question->questiontext);
00580     }
00581 
00582     public function compare_answers($answer1, $answer2) {
00583         return $answer1 == $answer2;
00584     }
00585 
00586     public function is_blank_answer($state) {
00587         return $state->answer == '';
00588     }
00589 
00590     public abstract function right_answer();
00591     public abstract function response_summary($state);
00592     public abstract function was_answered($state);
00593     public abstract function set_first_step_data_elements($state, &$data);
00594     public abstract function set_data_elements_for_step($state, &$data);
00595     public abstract function supply_missing_first_step_data(&$data);
00596 }
00597 
00598 
00599 class question_deleted_question_attempt_updater extends question_qtype_attempt_updater {
00600     public function right_answer() {
00601         return '';
00602     }
00603 
00604     public function response_summary($state) {
00605         return $state->answer;
00606     }
00607 
00608     public function was_answered($state) {
00609         return !empty($state->answer);
00610     }
00611 
00612     public function set_first_step_data_elements($state, &$data) {
00613         $data['upgradedfromdeletedquestion'] = $state->answer;
00614     }
00615 
00616     public function supply_missing_first_step_data(&$data) {
00617     }
00618 
00619     public function set_data_elements_for_step($state, &$data) {
00620         $data['upgradedfromdeletedquestion'] = $state->answer;
00621     }
00622 }
 All Data Structures Namespaces Files Functions Variables Enumerations