From d98389ffedd1bb8b3a34aaba270a1734ad5bfb2f Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Fri, 6 May 2011 15:31:57 +0100 Subject: [PATCH] MDL-20636 Start of work on a local plugin to help with the question engine upgrade on large sites. --- local/qeupgradehelper/README.txt | 39 +++ local/qeupgradehelper/convertquiz.php | 73 +++++ local/qeupgradehelper/grabexample.php | 255 ++++++++++++++ local/qeupgradehelper/index.php | 82 +++++ .../lang/en/local_qeupgradehelper.php | 33 ++ local/qeupgradehelper/locallib.php | 310 ++++++++++++++++++ local/qeupgradehelper/makeupgradetest.php | 243 ++++++++++++++ local/qeupgradehelper/pretendupgrade.php | 224 +++++++++++++ local/qeupgradehelper/resetindex.php | 79 +++++ local/qeupgradehelper/resetquiz.php | 71 ++++ local/qeupgradehelper/settings.php | 32 ++ local/qeupgradehelper/styles.css | 2 + local/qeupgradehelper/version.php | 29 ++ 13 files changed, 1472 insertions(+) create mode 100755 local/qeupgradehelper/README.txt create mode 100755 local/qeupgradehelper/convertquiz.php create mode 100755 local/qeupgradehelper/grabexample.php create mode 100755 local/qeupgradehelper/index.php create mode 100755 local/qeupgradehelper/lang/en/local_qeupgradehelper.php create mode 100755 local/qeupgradehelper/locallib.php create mode 100755 local/qeupgradehelper/makeupgradetest.php create mode 100755 local/qeupgradehelper/pretendupgrade.php create mode 100755 local/qeupgradehelper/resetindex.php create mode 100755 local/qeupgradehelper/resetquiz.php create mode 100755 local/qeupgradehelper/settings.php create mode 100755 local/qeupgradehelper/styles.css create mode 100755 local/qeupgradehelper/version.php diff --git a/local/qeupgradehelper/README.txt b/local/qeupgradehelper/README.txt new file mode 100755 index 00000000000..80e457917d9 --- /dev/null +++ b/local/qeupgradehelper/README.txt @@ -0,0 +1,39 @@ +This plugin can help upgrade site with a large number of question attempts from +Moodle 2.0 to 2.1. + +With a lot of question attempts, doing the whole conversion on upgrade is very +slow. The plugin can help with that in various ways. + + +When installed in a Moodle 2.0 site: + +1. It provies a report of how much data there is to upgrade. + +2. It can extract test-cases from the database. This can help you report bugs +in the upgrade process to the developers. + +3. This plugin can also do a dry-run of the upgrade. (It loads the old data from +the database, transform it to the new form, but not write the transformed data +to the database.) + + +If this plugin is present during upgrade: + +4. then only a subset of attempts are upgraded. (You can edit a function in +this plugin to control which attempts are upgraded immediately.) + + +If this plugin is present in a Moodle 2.0 site after upgrade: + + +5. If not all attempts have been upgraded in a 2.1 site, then this plugin +displays a list of how many quizzes still need to be upgraded + +6. ... and can be used to complete the upgrade manually ... + +7. or this plugin has a cron script that can be used to finish the upgrade +automatically after the main upgrade has finished. + + +(Note that none of the above acutally works yet. It is just a statement of +intent. Lots of the code here is a partial implementation of the concepts.) \ No newline at end of file diff --git a/local/qeupgradehelper/convertquiz.php b/local/qeupgradehelper/convertquiz.php new file mode 100755 index 00000000000..e18e5abfa75 --- /dev/null +++ b/local/qeupgradehelper/convertquiz.php @@ -0,0 +1,73 @@ +. + + +/** + * Script to upgrade the attempts at a particular quiz, after confirmation. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(dirname(__FILE__) . '/../../../config.php'); +require_once(dirname(__FILE__) . '/locallib.php'); +require_once($CFG->libdir . '/adminlib.php'); + +$quizid = required_param('quizid', PARAM_INT); +$confirmed = optional_param('confirmed', false, PARAM_BOOL); + +require_login(); +require_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM)); + +$quizsummary = report_quizupgrade_get_quiz($quizid); +if (!$quizsummary) { + print_error('invalidquizid', 'report_quizupgrade', report_quizupgrade_url('index.php')); +} +$quizsummary->name = format_string($quizsummary->name); + +admin_externalpage_setup('reportquizupgrade'); + +if ($confirmed && data_submitted() && confirm_sesskey()) { + // Actually do the conversion. + admin_externalpage_print_header(); + print_heading(get_string('upgradingquizattempts', 'report_quizupgrade', $quizsummary)); + + $upgrader = new report_quizupgrade_attempt_upgrader($quizsummary->id, $quizsummary->numtoconvert); + $upgrader->convert_all_quiz_attempts(); + + print_heading(get_string('conversioncomplete', 'report_quizupgrade')); + echo '

' . get_string('gotoquizreport', 'report_quizupgrade') . '

'; + print_continue(report_quizupgrade_url('index.php')); + + admin_externalpage_print_footer(); + exit; +} + +// Print an are-you-sure page. +admin_externalpage_print_header(); +print_heading(get_string('areyousure', 'report_quizupgrade')); + +$message = get_string('areyousuremessage', 'report_quizupgrade', $quizsummary); +$params = array('quizid' => $quizsummary->id, 'confirmed' => 1, 'sesskey' => sesskey()); +notice_yesno($message, report_quizupgrade_url('convertquiz.php'), + report_quizupgrade_url('index.php'), $params, null, 'post', 'get'); + +admin_externalpage_print_footer(); \ No newline at end of file diff --git a/local/qeupgradehelper/grabexample.php b/local/qeupgradehelper/grabexample.php new file mode 100755 index 00000000000..8844bf80032 --- /dev/null +++ b/local/qeupgradehelper/grabexample.php @@ -0,0 +1,255 @@ +. + + +/** + * This script is a bit of a hack. It is like makeupgradetest.php but has been + * hacked so you can extract test cases from a different databse. + * + * To make this work, you need to fill in the details below, and add + * + * if (defined('NASTY_HACK_IGNORE_CONFIGPHP')) { + * return; + * } + * + * to the very top of your config.php file. + * + * @package moodlecore + * @subpackage questionengine + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +error_reporting(E_ALL); +ini_set('display_errors', 1); +define('NASTY_HACK_IGNORE_CONFIGPHP', true); + +// Clone config.php to point at the learnacct DB read-only. +unset($CFG); // Ignore this line +$CFG = new stdClass(); + +$CFG->debug = 6143; +$CFG->debugdisplay = 1; + +// The following block points this site at learnacct database, read-only. +$CFG->dbtype = 'postgres7'; +$CFG->dbhost = ''; // TODO to use this script, complete this section +$CFG->dbname = ''; // with details of the database you want to +$CFG->dbuser = ''; // connect to. +$CFG->dbpass = ''; +$CFG->prefix = ''; + +$CFG->wwwroot = ''; // TODO to use this script, complete this section +$CFG->dirroot = ''; // with data copied from this Moodle's config.php +$CFG->dataroot = ''; +$CFG->directorypermissions = 02777; + +$CFG->admin = 'admin'; + +require_once($CFG->dirroot . '/local/ouflags/ouflags.class.php'); +$OUFLAGS = new ouflags('vle','dev'); + +require_once($CFG->dirroot . '/lib/setup.php'); +require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->dirroot . '/question/engine/lib.php'); +require_once($CFG->dirroot . '/question/engine/upgradefromoldqe/upgrade.php'); + +$CFG->querylog = ''; + +// ============================================================= +// Settings form +class grab_settings_form extends moodleform { + public function definition() { + $mform = $this->_form; + + $behaviour = array( + 0 => 'Deferred feedback', + 1 => 'Interactive', + ); + + $qtypes = array( + 'ddwtos' => 'Drag-drop', + 'description' => 'Description', + 'essay' => 'Essay', + 'match' => 'Matching', + 'multichoice' => 'Multiple choice', + 'numerical' => 'Numerical', + 'opaque' => 'OpenMark', + 'oumultiresponse' => 'OU multiple-response', + 'random' => 'Random', + 'shortanswer' => 'Short-answer', + 'truefalse' => 'True/false', + ); + + $mform->addElement('header', 'h1', 'Either extract a specific question_session'); + $mform->addElement('text', 'qsid', 'Question session id', array('size' => '10')); + $mform->addElement('header', 'h2', 'Or find and extract an example by type'); + $mform->addElement('select', 'behaviour', 'Behaviour', $behaviour); + $mform->addElement('text', 'statehistory', 'State history', array('size' => '10')); + $mform->addElement('select', 'qtype', 'Question type', $qtypes); + $mform->addElement('text', 'extratests', 'Extra conditions', array('size' => '50')); + $this->add_action_buttons(false, 'Create test case'); + } +} + +class grabber_question_engine_attempt_upgrader extends question_engine_attempt_upgrader { + public function __construct() { + $this->questionloader = new question_engine_upgrade_question_loader(null); + } +} + +if ($sesskey = optional_param('sesskey', '', PARAM_RAW)) { + $USER->sesskey = $sesskey; +} + +print_header('Question engine upgrade test case extractor'); + +$mform = new grab_settings_form($CFG->wwwroot . '/question/engine/upgradefromoldqe/grabexample.php', null, 'get'); +if ($fromform = $mform->get_data()) { + if (!empty($fromform->qsid)) { + generate_unit_test($fromform->qsid, 'qsession' . $fromform->qsid); + } else { + notify('Searching ...', 'notifysuccess'); + flush(); + $qsid = find_test_case($fromform->behaviour, $fromform->statehistory, + $fromform->qtype, $fromform->extratests); + if ($qsid) { + generate_unit_test($qsid, 'history' . $fromform->statehistory); + } else { + notify('No suitable attempts found.'); + } + } +} + +$mform->display(); +print_footer('empty'); + +/** + * Identify the question session id of a question attempt matching certain + * requirements. + * @param integer $behaviour 0 = deferred feedback, 1 = interactive. + * @param string $statehistory of states, last first. E.g. 620. + * @param string $qtype question type. + * @return integer question_session.id. + */ +function find_test_case($behaviour, $statehistory, $qtype, $extratests) { + global $CFG; + $possibleids = get_records_sql_menu(" + SELECT + qsess.id, + 1 + + FROM {$CFG->prefix}question_sessions qsess + JOIN {$CFG->prefix}question_states qst ON qst.attempt = qsess.attemptid + AND qst.question = qsess.questionid + JOIN {$CFG->prefix}quiz_attempts quiza ON quiza.uniqueid = qsess.attemptid + JOIN {$CFG->prefix}quiz quiz ON quiz.id = quiza.quiz + JOIN {$CFG->prefix}question q ON q.id = qsess.questionid + + WHERE q.qtype = '{$qtype}' + AND quiz.optionflags = {$behaviour} + + GROUP BY + qsess.id + + HAVING SUM( + (CASE WHEN qst.event = 10 THEN 1 ELSE qst.event END) * + POWER(10, CAST(qst.seq_number AS NUMERIC(110,0))) + ) = {$statehistory} + {$extratests}", 0, 100); + + if (!$possibleids) { + return null; + } + + return array_rand($possibleids); +} + +/** + * Grab all the data that upgrade will need for upgrading one + * attempt at one question from the old DB. + */ +function generate_unit_test($questionsessionid, $namesuffix) { + $qsession = get_record('question_sessions', 'id', $questionsessionid); + $attempt = get_record('quiz_attempts', 'uniqueid', $qsession->attemptid); + $quiz = get_record('quiz', 'id', $attempt->quiz); + $qstates = get_records_select('question_states', + "attempt = {$qsession->attemptid} AND question = {$qsession->questionid}", + 'seq_number, id'); + + $upgrader = new grabber_question_engine_attempt_upgrader(); + + $question = $upgrader->load_question($qsession->questionid, $quiz->id); + + if ($quiz->optionflags) { + $quiz->preferredbehaviour = 'interactive'; + } else { + $quiz->preferredbehaviour = 'deferredfeedback'; + } + echo "
+    public function test_{$question->qtype}_{$quiz->preferredbehaviour}_{$namesuffix}() {
+";
+    $upgrader->display_convert_attempt_input($quiz, $attempt,
+            $question, $qsession, $qstates);
+
+    if ($question->qtype == 'random') {
+        list($randombit, $realanswer) = explode('-', reset($qstates)->answer, 2);
+        $newquestionid = substr($randombit, 6);
+        $newquestion = $upgrader->load_question($newquestionid);
+        $newquestion->maxmark = $question->maxmark;
+
+        echo $upgrader->format_var('$realquestion', $newquestion);
+        echo '        $this->loader->put_question_in_cache($realquestion);
+';
+    }
+
+    echo '
+        $qa = $this->updater->convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates);
+
+        $expectedqa = (object) array(';
+    echo "
+            'behaviour' => '{$quiz->preferredbehaviour}',
+            'questionid' => {$question->id},
+            'maxmark' => {$question->maxmark},
+            'minfraction' => 0,
+            'flagged' => 0,
+            'questionsummary' => '',
+            'rightanswer' => '',
+            'responsesummary' => '',
+            'timemodified' => 0,
+            'steps' => array(";
+    foreach ($qstates as $state) {
+        echo "
+                {$state->seq_number} => (object) array(
+                    'sequencenumber' => {$state->seq_number},
+                    'state' => '',
+                    'fraction' => null,
+                    'timecreated' => {$state->timestamp},
+                    'userid' => {$attempt->userid},
+                    'data' => array(),
+                ),";
+    }
+    echo '
+            ),
+        );
+
+        $this->assertEqual($expectedqa, $qa);
+    }
+
'; +} + diff --git a/local/qeupgradehelper/index.php b/local/qeupgradehelper/index.php new file mode 100755 index 00000000000..019783b5f6e --- /dev/null +++ b/local/qeupgradehelper/index.php @@ -0,0 +1,82 @@ +. + + +/** + * Ad-hoc quiz upgrade plugin. + * + * Allows the attempt data for quizzes that were not upgraded during the main + * upgrade to be upgraded at any time. + * + * This screen lists all the quizzes that still need to be upgraded. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(dirname(__FILE__) . '/../../../config.php'); +require_once(dirname(__FILE__) . '/locallib.php'); +require_once($CFG->libdir . '/adminlib.php'); + +require_login(); +require_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM)); + +// Start the page. +admin_externalpage_setup('reportquizupgrade'); +admin_externalpage_print_header(); + +$quizzes = report_quizupgrade_get_upgradable_quizzes(); + +if (empty($quizzes)) { + print_heading(get_string('alreadydone', 'report_quizupgrade')); + +} else { + print_heading(get_string('quizzeswithunconverted', 'report_quizupgrade')); + print_box(get_string('intro', 'report_quizupgrade')); + + $table = new stdClass; + $table->head = array( + get_string('quizid', 'report_quizupgrade'), + get_string('course'), + get_string('modulename', 'quiz'), + get_string('attemptstoconvert', 'report_quizupgrade'), + get_string('actions', 'report_quizupgrade'), + ); + + foreach ($quizzes as $quiz) { + $table->data[] = array( + $quiz->id, + '' . format_string($quiz->shortname) . '', + '' . format_string($quiz->name) . '', + $quiz->numtoconvert, + '' . get_string('convertattempts', 'report_quizupgrade') . '', + ); + } + + print_table($table); +} + +echo '

' . + get_string('gotoresetlink', 'report_quizupgrade') . '

'; + +admin_externalpage_print_footer(); diff --git a/local/qeupgradehelper/lang/en/local_qeupgradehelper.php b/local/qeupgradehelper/lang/en/local_qeupgradehelper.php new file mode 100755 index 00000000000..e2fb3fbd9c8 --- /dev/null +++ b/local/qeupgradehelper/lang/en/local_qeupgradehelper.php @@ -0,0 +1,33 @@ +numtoconvert} attempts at quiz \'{$a->name}\' in course {$a->shortname}?'; +$string['areyousureresetmessage'] = 'Quiz \'{$a->name}\' in course {$a->shortname} has {$a->totalattempts} attempts, of which {$a->convertedattempts} were upgraded from the old system. Of those, {$a->resettableattempts} can be reset, for later re-conversion. Do you want to proceed with this?'; +$string['attemptstoconvert'] = 'Attempts to convert'; +$string['conversioncomplete'] = 'Conversion complete'; +$string['convertattempts'] = 'Convert attempts...'; +$string['convertedattempts'] = 'Converted attempts'; +$string['gotoindex'] = 'Back to the list of quizzes that can be upgraded'; +$string['gotoquizreport'] = 'Go to the reports for this quiz, to check the upgrade'; +$string['gotoresetlink'] = 'Go to the list of quizzes that can be reset'; +$string['intro'] = 'Please do not convert any attempts unless you have discussed it with Phil Butcher or Tim Hunt.'; +$string['invalidquizid'] = 'Invaid quiz id. Either the quiz does not exist, or it has no attempts to convert.'; +$string['pluginname'] = 'Question engine upgrade helper'; +$string['quizid'] = 'Quiz id'; +$string['quizupgrade'] = 'Quiz upgrade status'; +$string['quizzeswithunconverted'] = 'The following quizzes have attempts that need to be converted'; +$string['quizzesthatcanbereset'] = 'The following quizzes have converted attempts that you may be able to reset'; +$string['resetattempts'] = 'Reset attempts...'; +$string['resetcomplete'] = 'Reset complete'; +$string['resettingquizattempts'] = 'Resetting quiz attempts'; +$string['upgradingquizattempts'] = 'Upgrading the attempts for quiz \'{$a->name}\' in course {$a->shortname}'; diff --git a/local/qeupgradehelper/locallib.php b/local/qeupgradehelper/locallib.php new file mode 100755 index 00000000000..2865c0d585c --- /dev/null +++ b/local/qeupgradehelper/locallib.php @@ -0,0 +1,310 @@ +. + + +/** + * Library code for the Quiz upgrade status report. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once($CFG->dirroot . '/question/engine/upgradefromoldqe/upgrade.php'); + +/** + * @param string $localurl part to go at the end of the URL. + * @return string the full URL of that page within this report. + */ +function report_quizupgrade_url($localurl) { + global $CFG; + return $CFG->wwwroot . '/' . $CFG->admin . '/report/quizupgrade/' . $localurl; +} + +/** + * Get the information about a quizzes that can be upgraded. + * @return array of objects with information about the quizzes that need upgrading. + * has fields quiz id, quiz name, course shortname, couresid and number of + * attempts that need converting. + */ +function report_quizupgrade_get_upgradable_quizzes() { + global $CFG; + return get_records_sql(" + SELECT + quiz.id, + quiz.name, + c.shortname, + c.id AS courseid, + COUNT(1) AS numtoconvert + + FROM {$CFG->prefix}quiz_attempts quiza + JOIN {$CFG->prefix}quiz quiz ON quiz.id = quiza.quiz + JOIN {$CFG->prefix}course c ON c.id = quiz.course + + WHERE quiza.preview = 0 + AND quiza.needsupgradetonewqe = 1 + + GROUP BY quiz.id, quiz.name, c.shortname, c.id + + ORDER BY c.shortname, quiz.name, quiz.id"); +} + +/** + * Get the information about a quiz to be upgraded. + * @param integer $quizid the quiz id. + * @return object the information about that quiz, as for + * {@link report_quizupgrade_get_upgradable_quizzes()}. + */ +function report_quizupgrade_get_quiz($quizid) { + global $CFG; + return get_record_sql(" + SELECT + quiz.id, + quiz.name, + c.shortname, + c.id AS courseid, + COUNT(1) AS numtoconvert + + FROM {$CFG->prefix}quiz_attempts quiza + JOIN {$CFG->prefix}quiz quiz ON quiz.id = quiza.quiz + JOIN {$CFG->prefix}course c ON c.id = quiz.course + + WHERE quiza.preview = 0 + AND quiza.needsupgradetonewqe = 1 + AND quiz.id = {$quizid} + + GROUP BY quiz.id, quiz.name, c.shortname, c.id + + ORDER BY c.shortname, quiz.name, quiz.id"); +} + +/** + * Get the information about quizzes that can be reset. + * @return array of objects with information about the quizzes that need upgrading. + * has fields quiz id, quiz name, course shortname, couresid and number of + * converted attempts. + */ +function report_quizupgrade_get_resettable_quizzes() { + global $CFG; + return get_records_sql(" + SELECT + quiz.id, + quiz.name, + c.shortname, + c.id AS courseid, + COUNT(1) AS convertedattempts + + FROM {$CFG->prefix}quiz_attempts quiza + JOIN {$CFG->prefix}quiz quiz ON quiz.id = quiza.quiz + JOIN {$CFG->prefix}course c ON c.id = quiz.course + + WHERE quiza.preview = 0 + AND quiza.needsupgradetonewqe = 0 + AND EXISTS(SELECT 1 FROM {$CFG->prefix}question_states + WHERE attempt = quiza.uniqueid) + + GROUP BY quiz.id, quiz.name, c.shortname, c.id + ORDER BY c.shortname, quiz.name, quiz.id"); +} + +/** + * Get the information about a quiz to be upgraded. + * @param integer $quizid the quiz id. + * @return object the information about that quiz, as for + * {@link report_quizupgrade_get_resettable_quizzes()}, but with extra fields + * totalattempts and resettableattempts. + */ +function report_quizupgrade_get_resettable_quiz($quizid) { + global $CFG; + return get_record_sql(" + SELECT + quiz.id, + quiz.name, + c.shortname, + c.id AS courseid, + COUNT(1) AS totalattempts, + SUM(CASE WHEN quiza.needsupgradetonewqe = 0 AND + oldtimemodified.time IS NOT NULL THEN 1 ELSE 0 END) AS convertedattempts, + SUM(CASE WHEN quiza.needsupgradetonewqe = 0 AND + oldtimemodified.time >= newtimemodified.time THEN 1 ELSE 0 END) AS resettableattempts + + FROM {$CFG->prefix}quiz_attempts quiza + JOIN {$CFG->prefix}quiz quiz ON quiz.id = quiza.quiz + JOIN {$CFG->prefix}course c ON c.id = quiz.course + LEFT JOIN ( + SELECT attempt, MAX(timestamp) AS time + FROM {$CFG->prefix}question_states + GROUP BY attempt + ) AS oldtimemodified ON oldtimemodified.attempt = quiza.uniqueid + LEFT JOIN ( + SELECT qa.questionusageid, MAX(qas.timecreated) AS time + FROM {$CFG->prefix}question_attempts qa + JOIN {$CFG->prefix}question_attempt_steps qas ON qas.questionattemptid = qa.id + GROUP BY qa.questionusageid + ) AS newtimemodified ON newtimemodified.questionusageid = quiza.uniqueid + + WHERE quiza.preview = 0 + AND quiz.id = {$quizid} + + GROUP BY quiz.id, quiz.name, c.shortname, c.id"); +} + +class report_quizupgrade_attempt_upgrader extends question_engine_attempt_upgrader { + public $quizid; + public $attemptsdone = 0; + public $attemptstodo; + + public function __construct($quizid, $attemptstodo) { + $this->quizid = $quizid; + $this->attemptstodo = $attemptstodo; + } + + protected function get_quiz_ids() { + return array($this->quizid => 1); + } + + protected function print_progress($done, $outof, $quizid) { + } + + protected function convert_quiz_attempt($quiz, $attempt, $questionsessionsrs, $questionsstatesrs) { + $this->attemptsdone += 1; + print_progress($this->attemptsdone, $this->attemptstodo); + return parent::convert_quiz_attempt($quiz, $attempt, $questionsessionsrs, $questionsstatesrs); + } + + protected function get_resettable_attempts($quiz) { + global $CFG; + return get_records_sql(" + SELECT + quiza.* + + FROM {$CFG->prefix}quiz_attempts quiza + LEFT JOIN ( + SELECT attempt, MAX(timestamp) AS time + FROM {$CFG->prefix}question_states + GROUP BY attempt + ) AS oldtimemodified ON oldtimemodified.attempt = quiza.uniqueid + LEFT JOIN ( + SELECT qa.questionusageid, MAX(qas.timecreated) AS time + FROM {$CFG->prefix}question_attempts qa + JOIN {$CFG->prefix}question_attempt_steps qas ON qas.questionattemptid = qa.id + GROUP BY qa.questionusageid + ) AS newtimemodified ON newtimemodified.questionusageid = quiza.uniqueid + + WHERE quiza.preview = 0 + AND quiza.needsupgradetonewqe = 0 + AND oldtimemodified.time >= newtimemodified.time + AND quiza.quiz = {$quiz->id}"); + } + + public function reset_all_resettable_attempts() { + begin_sql(); + $quiz = get_record('quiz', 'id', $this->quizid); + $attempts = $this->get_resettable_attempts($quiz); + foreach ($attempts as $attempt) { + $this->reset_attempt($quiz, $attempt); + } + commit_sql(); + } + + protected function reset_attempt($quiz, $attempt) { + global $CFG; + + $this->attemptsdone += 1; + print_progress($this->attemptsdone, $this->attemptstodo); + + $questionids = explode(',', $quiz->questions); + $slottoquestionid = array(0 => 0); + foreach ($questionids as $questionid) { + if ($questionid) { + $slottoquestionid[] = $questionid; + } + } + + $slotlayout = explode(',', $attempt->layout); + $oldlayout = array(); + $ok = true; + foreach ($slotlayout as $slot) { + if (array_key_exists($slot, $slottoquestionid)) { + $oldlayout[] = $slottoquestionid[$slot]; + } else { + $ok = false; + break; + } + } + + if ($ok) { + $layout = implode(',', $oldlayout); + } else { + $layout = $attempt->layout; + } + + delete_records_select('question_attempt_step_data', "attemptstepid IN ( + SELECT qas.id + FROM {$CFG->prefix}question_attempts qa + JOIN {$CFG->prefix}question_attempt_steps qas ON qas.questionattemptid = qa.id + WHERE questionusageid = {$attempt->uniqueid})"); + delete_records_select('question_attempt_steps', "questionattemptid IN ( + SELECT qa.id + FROM {$CFG->prefix}question_attempts qa + WHERE questionusageid = {$attempt->uniqueid})"); + delete_records('question_attempts', 'questionusageid', $attempt->uniqueid); + + set_field('question_usages', 'preferredbehaviour', 'to_be_set_later', + 'id', $attempt->uniqueid); + set_field('quiz_attempts', 'layout', $layout, + 'uniqueid', $attempt->uniqueid); + set_field('quiz_attempts', 'needsupgradetonewqe', 1, + 'uniqueid', $attempt->uniqueid); + } +} + +class grabber_question_engine_attempt_upgrader extends question_engine_attempt_upgrader { + public function __construct() { + $this->questionloader = new question_engine_upgrade_question_loader(null); + } + + public function format_var($name, $var) { + $out = var_export($var, true); + $out = str_replace('<', '<', $out); + $out = str_replace('ADOFetchObj::__set_state(array(', '(object) array(', $out); + $out = str_replace('stdClass::__set_state(array(', '(object) array(', $out); + $out = str_replace('array (', 'array(', $out); + $out = preg_replace('/=> \n\s*/', '=> ', $out); + $out = str_replace(')),', '),', $out); + $out = str_replace('))', ')', $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n(?! )/', "\n ", $out); + return " $name = $out;\n"; + } + + public function display_convert_attempt_input($quiz, $attempt, $question, $qsession, $qstates) { + echo $this->format_var('$quiz', $quiz); + echo $this->format_var('$attempt', $attempt); + echo $this->format_var('$question', $question); + echo $this->format_var('$qsession', $qsession); + echo $this->format_var('$qstates', $qstates); + } +} diff --git a/local/qeupgradehelper/makeupgradetest.php b/local/qeupgradehelper/makeupgradetest.php new file mode 100755 index 00000000000..4e5f7a0a563 --- /dev/null +++ b/local/qeupgradehelper/makeupgradetest.php @@ -0,0 +1,243 @@ +. + + +/** + * This is a simple script to facilitate testing. You need to run it before + * you go to /admin/ to upgrade your database. It extracts all the data for + * one particular attempt at one question, in a form that makes it easy to + * write a unit test for upgrade logic for that particular case. + * + * (The theory is that if the upgrade dies with an error, you can restore the + * database from backup, and then use this script to extract the problem case + * as a unit test. Then you can fix that unit tests. Then you can repeat the upgrade.) + * + * @package moodlecore + * @subpackage questionengine + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(dirname(__FILE__) . '/../../../config.php'); +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->libdir . '/formslib.php'); + +// ============================================================= +// Settings form +class grab_settings_form extends moodleform { + public function definition() { + $mform = $this->_form; + + $behaviour = array( + 0 => 'Deferred feedback', + 1 => 'Interactive', + ); + + $qtypes = array( + 'ddwtos' => 'Drag-drop', + 'description' => 'Description', + 'essay' => 'Essay', + 'match' => 'Matching', + 'multichoice' => 'Multiple choice', + 'numerical' => 'Numerical', + 'opaque' => 'OpenMark', + 'oumultiresponse' => 'OU multiple-response', + 'random' => 'Random', + 'shortanswer' => 'Short-answer', + 'truefalse' => 'True/false', + ); + + $mform->addElement('header', 'h1', 'Either extract a specific question_session'); + $mform->addElement('text', 'qsid', 'Question session id', array('size' => '10')); + $mform->addElement('header', 'h2', 'Or find and extract an example by type'); + $mform->addElement('select', 'behaviour', 'Behaviour', $behaviour); + $mform->addElement('text', 'statehistory', 'State history', array('size' => '10')); + $mform->addElement('select', 'qtype', 'Question type', $qtypes); + $mform->addElement('text', 'extratests', 'Extra conditions', array('size' => '50')); + $this->add_action_buttons(false, 'Create test case'); + } +} + +print_header('Question engine upgrade test case extractor'); + +$mform = new grab_settings_form($CFG->wwwroot . '/question/engine/upgradefromoldqe/grabexample.php', null, 'get'); +if ($fromform = $mform->get_data()) { + if (!empty($fromform->qsid)) { + generate_unit_test($fromform->qsid, 'qsession' . $fromform->qsid); + } else { + notify('Searching ...', 'notifysuccess'); + flush(); + $qsid = find_test_case($fromform->behaviour, $fromform->statehistory, + $fromform->qtype, $fromform->extratests); + if ($qsid) { + generate_unit_test($qsid, 'history' . $fromform->statehistory); + } else { + notify('No suitable attempts found.'); + } + } +} + +$mform->display(); +print_footer('empty'); + +/** + * Identify the question session id of a question attempt matching certain + * requirements. + * @param integer $behaviour 0 = deferred feedback, 1 = interactive. + * @param string $statehistory of states, last first. E.g. 620. + * @param string $qtype question type. + * @return integer question_session.id. + */ +function find_test_case($behaviour, $statehistory, $qtype, $extratests) { + global $CFG; + $possibleids = get_records_sql_menu(" + SELECT + qsess.id, + 1 + + FROM {$CFG->prefix}question_sessions qsess + JOIN {$CFG->prefix}question_states qst ON qst.attempt = qsess.attemptid + AND qst.question = qsess.questionid + JOIN {$CFG->prefix}quiz_attempts quiza ON quiza.uniqueid = qsess.attemptid + JOIN {$CFG->prefix}quiz quiz ON quiz.id = quiza.quiz + JOIN {$CFG->prefix}question q ON q.id = qsess.questionid + + WHERE q.qtype = '{$qtype}' + AND quiz.optionflags = {$behaviour} + + GROUP BY + qsess.id + + HAVING SUM( + (CASE WHEN qst.event = 10 THEN 1 ELSE qst.event END) * + POWER(10, CAST(qst.seq_number AS NUMERIC(110,0))) + ) = {$statehistory} + {$extratests}", 0, 100); + + if (!$possibleids) { + return null; + } + + return array_rand($possibleids); +} + +/** + * Grab all the data that upgrade will need for upgrading one + * attempt at one question from the old DB. + */ +function generate_unit_test($questionsessionid, $namesuffix) { + $qsession = get_record('question_sessions', 'id', $questionsessionid); + $attempt = get_record('quiz_attempts', 'uniqueid', $qsession->attemptid); + $quiz = get_record('quiz', 'id', $attempt->quiz); + $qstates = get_records_select('question_states', + "attempt = {$qsession->attemptid} AND question = {$qsession->questionid}", + 'seq_number, id'); + + $question = load_question($qsession->questionid, $quiz->id); + + if ($quiz->optionflags) { + $quiz->preferredbehaviour = 'interactive'; + } else { + $quiz->preferredbehaviour = 'deferredfeedback'; + } + echo "
+    public function test_{$question->qtype}_{$quiz->preferredbehaviour}_{$namesuffix}() {
+";
+    $upgrader->display_convert_attempt_input($quiz, $attempt,
+            $question, $qsession, $qstates);
+    echo '
+        $qa = $this->updater->convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates);
+
+        $expectedqa = (object) array(';
+    echo "
+            'behaviour' => '{$quiz->preferredbehaviour}',
+            'questionid' => {$question->id},
+            'maxmark' => {$question->maxmark},
+            'minfraction' => 0,
+            'flagged' => 0,
+            'questionsummary' => '',
+            'rightanswer' => '',
+            'responsesummary' => '',
+            'timemodified' => 0,
+            'steps' => array(";
+    foreach ($qstates as $state) {
+        echo "
+                {$state->seq_number} => (object) array(
+                    'sequencenumber' => {$state->seq_number},
+                    'state' => '',
+                    'fraction' => null,
+                    'timecreated' => {$state->timestamp},
+                    'userid' => {$attempt->userid},
+                    'data' => array(),
+                ),";
+    }
+    echo '
+            ),
+        );
+
+        $this->assertEqual($expectedqa, $qa);
+    }
+
'; +} + +function format_var($name, $var) { + $out = var_export($var, true); + $out = str_replace('<', '<', $out); + $out = str_replace('ADOFetchObj::__set_state(array(', '(object) array(', $out); + $out = str_replace('stdClass::__set_state(array(', '(object) array(', $out); + $out = str_replace('array (', 'array(', $out); + $out = preg_replace('/=> \n\s*/', '=> ', $out); + $out = str_replace(')),', '),', $out); + $out = str_replace('))', ')', $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n(?! )/', "\n ", $out); + return " $name = $out;\n"; +} + +function display_convert_attempt_input($quiz, $attempt, $question, $qsession, $qstates) { + echo format_var('$quiz', $quiz); + echo format_var('$attempt', $attempt); + echo format_var('$question', $question); + echo format_var('$qsession', $qsession); + echo format_var('$qstates', $qstates); +} + +function load_question($questionid, $quizid) { + global $CFG, $QTYPES; + + $question = get_record_sql(" + SELECT q.*, qqi.grade AS maxmark + FROM {$CFG->prefix}question q + JOIN {$CFG->prefix}quiz_question_instances qqi ON qqi.question = q.id + WHERE q.id = $questionid AND qqi.quiz = $quizid"); + + if (!array_key_exists($question->qtype, $QTYPES)) { + $question->qtype = 'missingtype'; + $question->questiontext = '

' . get_string('warningmissingtype', 'quiz') . '

' . $question->questiontext; + } + + $QTYPES[$question->qtype]->get_question_options($question); + + return$question; +} diff --git a/local/qeupgradehelper/pretendupgrade.php b/local/qeupgradehelper/pretendupgrade.php new file mode 100755 index 00000000000..651e17ac161 --- /dev/null +++ b/local/qeupgradehelper/pretendupgrade.php @@ -0,0 +1,224 @@ +. + + +/** + * This script is a bit of a hack. It connects to another database and tries to + * run most of the question engine upgrade. That is, it loads the old data, and + * tries to convert it to the new structure, but it does not try to output it + * at all. + * + * The idea is that this should find most of the logic errors, since the code to + * save the new data to the DB is quite simple. + * + * To make this work, you need to fill in the details below, and add + * + * if (defined('NASTY_HACK_IGNORE_CONFIGPHP')) { + * return; + * } + * + * to the very top of your config.php file. + * + * @package moodlecore + * @subpackage questionengine + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +error_reporting(E_ALL); +ini_set('display_errors', 1); +define('NASTY_HACK_IGNORE_CONFIGPHP', true); + +// Clone config.php to point at the learnacct DB read-only. +unset($CFG); // Ignore this line +$CFG = new stdClass(); + +$CFG->debug = 6143; +$CFG->debugdisplay = 1; + +// The following block points this site at learnacct database, read-only. +$CFG->dbtype = 'postgres7'; +$CFG->dbhost = ''; // TODO to use this script, complete this section +$CFG->dbname = ''; // with details of the database you want to +$CFG->dbuser = ''; // connect to. +$CFG->dbpass = ''; +$CFG->prefix = ''; + +$CFG->wwwroot = ''; // TODO to use this script, complete this section +$CFG->dirroot = ''; // with data copied from this Moodle's config.php +$CFG->dataroot = ''; +$CFG->directorypermissions = 02777; + +$CFG->admin = 'admin'; + +require_once($CFG->dirroot . '/local/ouflags/ouflags.class.php'); +$OUFLAGS = new ouflags('vle','dev'); + +require_once($CFG->dirroot . '/lib/setup.php'); +require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->dirroot . '/question/engine/lib.php'); +require_once($CFG->dirroot . '/question/engine/upgradefromoldqe/upgrade.php'); + +$CFG->querylog = ''; +$CFG->rcache = false; +raise_memory_limit('1024M'); + +// ============================================================= + +class pretend_question_engine_attempt_upgrader extends question_engine_attempt_upgrader { + public $fromquiz = 0; + public $toquiz = 1000000; + public $qsdone = 0; + + protected function get_quiz_ids() { + return get_records_select_menu('quiz', + "id >= {$this->fromquiz} AND id < {$this->toquiz}", 'id', 'id,1'); + } + + public function get_attemtps_where($quizid) { + return "quiz = {$quizid} AND preview = 0"; + } + + protected function set_quba_preferred_behaviour($qubaid, $preferredbehaviour) { + return true; + } + + protected function set_quiz_attempt_layout($qubaid, $layout) { + return true; + } + + protected function delete_quiz_attempt($qubaid) { + return true; + } + + protected function insert_record($table, $record, $saveid = true) { + if ($table == 'question_attempts') { + if ($this->toquiz - $this->fromquiz <= 10) { + echo "saving qa from {$record->_fromqsession} "; + } else { + echo 'S'; + } + } + $this->escape_fields($record); + if ($table == 'question_attempt_steps' && is_null($record->sequencenumber)) { + notify('Null sequencenumber found.'); + } + if ($saveid) { + $record->id = 666; + } + return true; + } + + protected function convert_quiz_attempt($quiz, $attempt, $questionsessionsrs, $questionsstatesrs) { + if (empty($quiz->preferredbehaviour)) { + if ($quiz->optionflags == 0) { + $quiz->preferredbehaviour = 'deferredfeedback'; + } else { + $quiz->preferredbehaviour = 'interactive'; + } + } + return parent::convert_quiz_attempt($quiz, $attempt, $questionsessionsrs, $questionsstatesrs); + } + + public function convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates) { + if ($this->toquiz - $this->fromquiz <= 10) { + if ($this->qsdone % 10 == 0) { + echo '
'; + } + echo "qs {$qsession->id} "; + } else { + if ($this->qsdone % 100 == 0) { + echo '
'; + } + echo "C"; + } + $qa = parent::convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates); + $qa->_fromqsession = $qsession->id; + $this->qsdone++; + return $qa; + } + + public function supply_missing_question_attempt($quiz, $attempt, $question) { + if ($this->toquiz - $this->fromquiz <= 10) { + if ($this->qsdone % 10 == 0) { + echo '
'; + } + echo "missing {$question->id} "; + } else { + if ($this->qsdone % 100 == 0) { + echo '
'; + } + echo "M"; + } + $qa = parent::supply_missing_question_attempt($quiz, $attempt, $question); + $qa->_fromqsession = 'missing'; + return $qa; + } + + protected function print_progress($done, $outof, $quizid) { + echo "\n\n

Quiz {$done}/{$outof} ({$quizid})

\n\n
"; + gc_collect_cycles(); + echo '

Current memory usage: ' . memory_get_usage() . '/' . memory_get_peak_usage() . '

'; + } +} + +// ============================================================= + +$fromquiz = required_param('from', PARAM_INT); +$toquiz = optional_param('to', $fromquiz + 1, PARAM_INT); + +print_header('Question engine upgrade tester'); +echo "\n\n

Starting pretend upgrade of database '$CFG->dbhost', prefix '$CFG->prefix' on host '$CFG->dbhost'.

\n\n
"; + +$timestart = time(); +$qsconverted = do_pretend_upgrade($fromquiz, $toquiz); +$totaltime = time() - $timestart; +echo "
\n\n

{$qsconverted} question sessions converted in {$totaltime} seconds.

\n\n"; + +if ($qsconverted > 0) { + echo "

Estimate for 5 million: " . format_time(5000000 / $qsconverted * $totaltime) . "

\n\n"; +} + +$number = $toquiz - $fromquiz; +echo "
\n\n

"; +if (record_exists_select('quiz', "id < {$fromquiz}")) { + $newfrom = $fromquiz - $number; + echo "Previous {$number} quizzes "; +} +if (record_exists_select('quiz', "id >= {$toquiz}")) { + $newto = $toquiz + $number; + echo "Next {$number} quizzes "; +} +echo "

\n\n"; + +print_footer('empty'); + +function do_pretend_upgrade($fromquiz, $toquiz) { + $upgrader = new pretend_question_engine_attempt_upgrader(); + $upgrader->fromquiz = $fromquiz; + $upgrader->toquiz = $toquiz; + +// xhprof_enable(XHPROF_FLAGS_MEMORY + XHPROF_FLAGS_NO_BUILTINS); + + $upgrader->convert_all_quiz_attempts(); + +// $xhprof_data = xhprof_disable(); +// print_object($xhprof_data); + + return $upgrader->qsdone; +} diff --git a/local/qeupgradehelper/resetindex.php b/local/qeupgradehelper/resetindex.php new file mode 100755 index 00000000000..068ac21b50a --- /dev/null +++ b/local/qeupgradehelper/resetindex.php @@ -0,0 +1,79 @@ +. + + +/** + * Ad-hoc quiz upgrade plugin. This screen lists quizzes with attempts that can + * be reset. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(dirname(__FILE__) . '/../../../config.php'); +require_once(dirname(__FILE__) . '/locallib.php'); +require_once($CFG->libdir . '/adminlib.php'); + +require_login(); +require_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM)); + +// Start the page. +admin_externalpage_setup('reportquizupgrade'); +admin_externalpage_print_header(); + +$quizzes = report_quizupgrade_get_resettable_quizzes(); + +if (empty($quizzes)) { + print_heading(get_string('none')); + +} else { + print_heading(get_string('quizzesthatcanbereset', 'report_quizupgrade')); + print_box(get_string('intro', 'report_quizupgrade')); + + $table = new stdClass; + $table->head = array( + get_string('quizid', 'report_quizupgrade'), + get_string('course'), + get_string('modulename', 'quiz'), + get_string('convertedattempts', 'report_quizupgrade'), + get_string('actions', 'report_quizupgrade'), + ); + + foreach ($quizzes as $quiz) { + $table->data[] = array( + $quiz->id, + '' . format_string($quiz->shortname) . '', + '' . format_string($quiz->name) . '', + $quiz->convertedattempts, + '' . get_string('resetattempts', 'report_quizupgrade') . '', + ); + } + + print_table($table); +} + +echo '

' . + get_string('gotoindex', 'report_quizupgrade') . '

'; + +admin_externalpage_print_footer(); diff --git a/local/qeupgradehelper/resetquiz.php b/local/qeupgradehelper/resetquiz.php new file mode 100755 index 00000000000..a7622a8991b --- /dev/null +++ b/local/qeupgradehelper/resetquiz.php @@ -0,0 +1,71 @@ +. + + +/** + * Script to reset the attempts at a particular quiz, after confirmation. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(dirname(__FILE__) . '/../../../config.php'); +require_once(dirname(__FILE__) . '/locallib.php'); +require_once($CFG->libdir . '/adminlib.php'); + +$quizid = required_param('quizid', PARAM_INT); +$confirmed = optional_param('confirmed', false, PARAM_BOOL); + +require_login(); +require_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM)); + +$quizsummary = report_quizupgrade_get_resettable_quiz($quizid); +if (!$quizsummary) { + print_error('invalidquizid', 'report_quizupgrade', report_quizupgrade_url('index.php')); +} +$quizsummary->name = format_string($quizsummary->name); + +admin_externalpage_setup('reportquizupgrade'); + +if ($confirmed && data_submitted() && confirm_sesskey()) { + // Actually do the conversion. + admin_externalpage_print_header(); + print_heading(get_string('resettingquizattempts', 'report_quizupgrade', $quizsummary)); + + $upgrader = new report_quizupgrade_attempt_upgrader($quizsummary->id, $quizsummary->resettableattempts); + $upgrader->reset_all_resettable_attempts(); + + print_heading(get_string('resetcomplete', 'report_quizupgrade')); + print_continue(report_quizupgrade_url('resetindex.php')); + + admin_externalpage_print_footer(); + exit; +} + +// Print an are-you-sure page. +admin_externalpage_print_header(); +print_heading(get_string('areyousure', 'report_quizupgrade')); + +$message = get_string('areyousureresetmessage', 'report_quizupgrade', $quizsummary); +$params = array('quizid' => $quizsummary->id, 'confirmed' => 1, 'sesskey' => sesskey()); +notice_yesno($message, report_quizupgrade_url('resetquiz.php'), + report_quizupgrade_url('resetindex.php'), $params, null, 'post', 'get'); + +admin_externalpage_print_footer(); \ No newline at end of file diff --git a/local/qeupgradehelper/settings.php b/local/qeupgradehelper/settings.php new file mode 100755 index 00000000000..e00657ffdb6 --- /dev/null +++ b/local/qeupgradehelper/settings.php @@ -0,0 +1,32 @@ +. + +/** + * Add page to admin menu. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($hassiteconfig) { // needs this condition or there is error on login page + $ADMIN->add('root', new admin_externalpage('qeupgradehelper', + get_string('pluginname', 'local_qeupgradehelper'), + new moodle_url('/local/qeupgradehelper/'))); +} diff --git a/local/qeupgradehelper/styles.css b/local/qeupgradehelper/styles.css new file mode 100755 index 00000000000..5ec4f0c2d44 --- /dev/null +++ b/local/qeupgradehelper/styles.css @@ -0,0 +1,2 @@ +#page-local-qeupgradehelper-index { +} diff --git a/local/qeupgradehelper/version.php b/local/qeupgradehelper/version.php new file mode 100755 index 00000000000..606099e0741 --- /dev/null +++ b/local/qeupgradehelper/version.php @@ -0,0 +1,29 @@ +. + +/** + * Version details. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$plugin->version = 2011040400; +$plugin->requires = 2010080300;