3 * This file contains dtabase upgrade code that is called from lib/db/upgrade.php,
4 * and also check methods that can be used for pre-install checks via
5 * admin/environment.php and lib/environmentlib.php.
7 * @copyright © 2007 The Open University
8 * @author T.J.Hunt@open.ac.uk
9 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
10 * @package questionbank
14 * This test is becuase the RQP question type was included in core
15 * up to and including Moodle 1.8, and was removed before Moodle 1.9.
17 * Therefore, we want to check whether any rqp questions exist in the database
18 * before doing the upgrade. However, the check is not relevant if that
19 * question type was never installed, or if the person has chosen to
20 * manually reinstall the rqp question type from contrib.
22 * @param $result the result object that can be modified.
23 * @return null if the test is irrelevant, or true or false depending on whether the test passes.
25 function question_check_no_rqp_questions($result) {
28 if (empty($CFG->qtype_rqp_version
) ||
is_dir($CFG->dirroot
. '/question/type/rqp')) {
31 $result->setStatus(count_records('question', 'qtype', 'rqp') == 0);
36 function question_remove_rqp_qtype() {
41 // Only remove the question type if the code is gone.
42 if (!is_dir($CFG->dirroot
. '/question/type/rqp')) {
43 $table = new XMLDBTable('question_rqp_states');
44 $result = $result && drop_table($table);
46 $table = new XMLDBTable('question_rqp');
47 $result = $result && drop_table($table);
49 $table = new XMLDBTable('question_rqp_types');
50 $result = $result && drop_table($table);
52 $table = new XMLDBTable('question_rqp_servers');
53 $result = $result && drop_table($table);
55 $result = $result && unset_config('qtype_rqp_version');
61 function question_remove_rqp_qtype_config_string() {
66 // An earlier, buggy version of the previous function missed out the unset_config call.
67 if (!empty($CFG->qtype_rqp_version
) && !is_dir($CFG->dirroot
. '/question/type/rqp')) {
68 $result = $result && unset_config('qtype_rqp_version');
75 * @param $result the result object that can be modified.
76 * @return null if the test is irrelevant, or true or false depending on whether the test passes.
78 function question_random_check($result){
80 if (!empty($CFG->running_installer
) //no test on first installation, no questions to test yet
81 ||
$CFG->version
>= 2007081000){//no test after upgrade seperates question cats into contexts.
84 if (!$toupdate = question_cwqpfs_to_update()){
85 $result->setStatus(true);//pass test
87 //set the feedback string here and not in xml file since we need something
88 //more complex than just a string picked from admin.php lang file
90 $a->reporturl
= "{$CFG->wwwroot}/{$CFG->admin}/report/question/";
91 $lang = str_replace('_utf8', '', current_language());
92 $a->docsurl
= "{$CFG->docroot}/$lang/admin/report/question/index";
93 $result->setFeedbackStr(array('questioncwqpfscheck', 'admin', $a));
94 $result->setStatus(false);//fail test
99 * Delete all 'random' questions that are not been used in a quiz.
101 function question_delete_unused_random(){
105 //delete all 'random' questions that are not been used in a quiz.
106 if ($qqis = get_records_sql("SELECT q.* FROM {$CFG->prefix}question q LEFT JOIN ".
107 "{$CFG->prefix}quiz_question_instances qqi ".
108 "ON q.id = qqi.question WHERE q.qtype='random' AND qqi.question IS NULL")){
109 $qqilist = join(array_keys($qqis), ',');
110 $result = $result && delete_records_select('question', "id IN ($qqilist)");
114 function question_cwqpfs_to_update($categories = null){
120 //any cats with questions picking from subcats?
121 if (!$cwqpfs = get_records_sql_menu("SELECT DISTINCT qc.id, 1 ".
122 "FROM {$CFG->prefix}question q, {$CFG->prefix}question_categories qc ".
123 "WHERE q.qtype='random' AND qc.id = q.category AND ".
124 sql_compare_text('q.questiontext'). " = '1'")){
127 if ($categories === null){
128 $categories = get_records('question_categories');
130 $categorychildparents = array();
131 foreach ($categories as $id => $category){
132 $categorychildparents[$category->course
][$id] = $category->parent
;
134 foreach ($categories as $id => $category){
135 if (FALSE !== array_key_exists($category->parent
, $categorychildparents[$category->course
])){
136 //this is not a top level cat
137 continue;//go to next category
139 $tofix +
= question_cwqpfs_check_children($id, $categories, $categorychildparents[$category->course
], $cwqpfs);
147 function question_cwqpfs_check_children($checkid, $categories, $categorychildparents, $cwqpfs){
149 if (array_key_exists($checkid, $cwqpfs)){//cwqpfs in this cat
150 $getchildren = array();
151 $getchildren[] = $checkid;
152 //search down tree and find all children
153 while ($nextid = array_shift($getchildren)){//repeat until $getchildren
155 $childids = array_keys($categorychildparents, $nextid);
156 foreach ($childids as $childid){
157 if ($categories[$childid]->publish
!= $categories[$checkid]->publish
){
158 $tofix[$childid] = $categories[$checkid]->publish
;
161 $getchildren = array_merge($getchildren, $childids);
163 } else { // check children for cwqpfs
164 $childrentocheck = array_keys($categorychildparents, $checkid);
165 foreach ($childrentocheck as $childtocheck){
166 $tofix +
= question_cwqpfs_check_children($childtocheck, $categories, $categorychildparents, $cwqpfs);
172 function question_category_next_parent_in($contextid, $question_categories, $id, $already_seen = array()){
173 // Recursively look for an ancestor category of the given category that
174 // belongs to context $contextid. (In a lot of cases, the parent will be
175 // the one.) If there is none, return 0, meaning the top level.
176 $already_seen[] = $id;
178 $nextparent = $question_categories[$id]->parent
;
179 if ($nextparent == 0) {
180 // Hit the top level, we are done.
182 } else if (!array_key_exists($nextparent, $question_categories)) {
183 // The category hierarchy must have been screwed up before, in that
184 // we have run out of categories to search, but without reaching the
185 // top level. Repair the situation by returning 0, meaning top level.
186 notify(get_string('upgradeproblemunknowncategory', 'question', $question_categories[$id]));
188 } else if (in_array($nextparent, $already_seen)) {
189 // The category hierarchy must have been screwed up before, in that
190 // we have just found a loop in the category 'tree'. That should,
191 // of course, be impossible, but it did acutally happen in at least once.
192 // Repair the situation by returning 0, meaning top level.
193 notify(get_string('upgradeproblemcategoryloop', 'question', implode(', ', $already_seen)));
195 } else if ($contextid == $question_categories[$nextparent]->contextid
) {
196 // Found a suitable category, we are done.
199 // The immediate parent is not in the same context, so look further up.
200 return question_category_next_parent_in($contextid, $question_categories, $nextparent, $already_seen);
205 * Check that either category parent is 0 or a category shared in the same context.
206 * Fix any categories to point to grand or grand grand parent etc in the same context or 0.
208 function question_category_checking($question_categories){
209 //make an array that is easier to search
210 $newparents = array();
211 foreach ($question_categories as $id => $category){
212 $newparents[$id] = question_category_next_parent_in($category->contextid
, $question_categories, $id);
214 foreach (array_keys($question_categories) as $id){
215 $question_categories[$id]->parent
= $newparents[$id];
217 return $question_categories;
220 function question_upgrade_context_etc(){
223 $result = $result && question_delete_unused_random();
225 $question_categories = get_records('question_categories');
226 if ($question_categories) {
227 //prepare content for new db structure
228 $tofix = question_cwqpfs_to_update($question_categories);
229 foreach ($tofix as $catid => $publish){
230 $question_categories[$catid]->publish
= $publish;
233 foreach ($question_categories as $id => $question_category){
234 $course = $question_categories[$id]->course
;
235 unset($question_categories[$id]->course
);
236 if ($question_categories[$id]->publish
){
237 $context = get_context_instance(CONTEXT_SYSTEM
);
238 //new name with old course name in brackets
239 $coursename = get_field('course', 'shortname', 'id', $course);
240 $question_categories[$id]->name
.= " ($coursename)";
242 $context = get_context_instance(CONTEXT_COURSE
, $course);
244 $question_categories[$id]->contextid
= $context->id
;
245 unset($question_categories[$id]->publish
);
248 $question_categories = question_category_checking($question_categories);
251 /// Define index course (not unique) to be dropped form question_categories
252 $table = new XMLDBTable('question_categories');
253 $index = new XMLDBIndex('course');
254 $index->setAttributes(XMLDB_INDEX_NOTUNIQUE
, array('course'));
256 /// Launch drop index course
257 $result = $result && drop_index($table, $index);
259 /// Define field course to be dropped from question_categories
260 $field = new XMLDBField('course');
262 /// Launch drop field course
263 $result = $result && drop_field($table, $field);
265 /// Define field context to be added to question_categories
266 $field = new XMLDBField('contextid');
267 $field->setAttributes(XMLDB_TYPE_INTEGER
, '10', XMLDB_UNSIGNED
, XMLDB_NOTNULL
, null, null, null, '0', 'name');
268 $field->comment
= 'context that this category is shared in';
270 /// Launch add field context
271 $result = $result && add_field($table, $field);
273 /// Define index context (not unique) to be added to question_categories
274 $index = new XMLDBIndex('contextid');
275 $index->setAttributes(XMLDB_INDEX_NOTUNIQUE
, array('contextid'));
276 $index->comment
= 'links to context table';
278 /// Launch add index context
279 $result = $result && add_index($table, $index);
281 $field = new XMLDBField('publish');
283 /// Launch drop field publish
284 $result = $result && drop_field($table, $field);
287 /// update table contents with previously calculated new contents.
288 if ($question_categories) {
289 foreach ($question_categories as $question_category) {
290 $question_category->name
= addslashes($question_category->name
);
291 $question_category->info
= addslashes($question_category->info
);
292 if (!$result = $result && update_record('question_categories', $question_category)){
293 notify(get_string('upgradeproblemcouldnotupdatecategory', 'question', $question_category));
298 /// Define field timecreated to be added to question
299 $table = new XMLDBTable('question');
300 $field = new XMLDBField('timecreated');
301 $field->setAttributes(XMLDB_TYPE_INTEGER
, '10', XMLDB_UNSIGNED
, XMLDB_NOTNULL
, null, null, null, '0', 'hidden');
303 /// Launch add field timecreated
304 $result = $result && add_field($table, $field);
306 /// Define field timemodified to be added to question
307 $table = new XMLDBTable('question');
308 $field = new XMLDBField('timemodified');
309 $field->setAttributes(XMLDB_TYPE_INTEGER
, '10', XMLDB_UNSIGNED
, XMLDB_NOTNULL
, null, null, null, '0', 'timecreated');
311 /// Launch add field timemodified
312 $result = $result && add_field($table, $field);
314 /// Define field createdby to be added to question
315 $table = new XMLDBTable('question');
316 $field = new XMLDBField('createdby');
317 $field->setAttributes(XMLDB_TYPE_INTEGER
, '10', XMLDB_UNSIGNED
, null, null, null, null, null, 'timemodified');
319 /// Launch add field createdby
320 $result = $result && add_field($table, $field);
322 /// Define field modifiedby to be added to question
323 $table = new XMLDBTable('question');
324 $field = new XMLDBField('modifiedby');
325 $field->setAttributes(XMLDB_TYPE_INTEGER
, '10', XMLDB_UNSIGNED
, null, null, null, null, null, 'createdby');
327 /// Launch add field modifiedby
328 $result = $result && add_field($table, $field);
330 /// Define key createdby (foreign) to be added to question
331 $table = new XMLDBTable('question');
332 $key = new XMLDBKey('createdby');
333 $key->setAttributes(XMLDB_KEY_FOREIGN
, array('createdby'), 'user', array('id'));
335 /// Launch add key createdby
336 $result = $result && add_key($table, $key);
338 /// Define key modifiedby (foreign) to be added to question
339 $table = new XMLDBTable('question');
340 $key = new XMLDBKey('modifiedby');
341 $key->setAttributes(XMLDB_KEY_FOREIGN
, array('modifiedby'), 'user', array('id'));
343 /// Launch add key modifiedby
344 $result = $result && add_key($table, $key);
350 * In Moodle, all random questions should have question.parent set to be the same
351 * as question.id. One effect of MDL-5482 is that this will not be true for questions that
352 * were backed up then restored. The probably does not cause many problems, except occasionally,
353 * if the bogus question.parent happens to point to a multianswer question type, or when you
354 * try to do a subsequent backup. Anyway, these question.parent values should be fixed, and
355 * that is what this update does.
357 function question_fix_random_question_parents() {
359 return execute_sql('UPDATE ' . $CFG->prefix
. 'question SET parent = id ' .
360 "WHERE qtype = 'random' AND parent <> id");