2 //This php script contains all the stuff to backup quizzes
4 //This is the "graphical" structure of the quiz mod:
5 //To see, put your terminal to 160cc
11 // -------------------------------------------------------------------
13 // | quiz_grades | quiz_question_versions |
14 // | (UL,pk->id,fk->quiz) | (CL,pk->id,fk->quiz) |
16 // quiz_attempts quiz_question_instances quiz_feedback
17 // (UL,pk->id,fk->quiz) (CL,pk->id,fk->quiz,question) (CL,pk->id,fk->quiz)
19 // Meaning: pk->primary key field of the table
20 // fk->foreign key to link with parent
21 // nt->nested field (recursive data)
22 // SL->site level info
23 // CL->course level info
24 // UL->user level info
25 // files->table may have files
27 //-----------------------------------------------------------
29 // When we backup a quiz we also need to backup the questions and possibly
30 // the data about student interaction with the questions. The functions to do
31 // that are included with the following library
32 require_once("$CFG->dirroot/question/backuplib.php");
35 * Insert necessary category ids to backup_ids table. Called during backup_check.html
37 function insert_category_and_question_ids($course, $backup_unique_code, $instances = null) {
40 // Create missing categories and reasign orphaned questions.
41 fix_orphaned_questions($course);
42 // First, all categories from this course's context.
43 $coursecontext = get_context_instance(CONTEXT_COURSE
, $course);
44 $status = execute_sql("INSERT INTO {$CFG->prefix}backup_ids
45 (backup_code, table_name, old_id, info)
46 SELECT '$backup_unique_code', 'question_categories', qc.id, ''
47 FROM {$CFG->prefix}question_categories qc
48 WHERE qc.contextid = {$coursecontext->id}", false);
51 // then, all categories from this course's modules' contexts.
52 // using 'dummykeyname' in sql because otherwise get_records_sql_menu returns an error
53 // if two key names are the same.
54 $cmcontexts = get_records_sql_menu("SELECT c.id, c.id AS dummykeyname FROM `{$CFG->prefix}modules` `mod`,
55 `{$CFG->prefix}course_modules` `cm`,
56 `{$CFG->prefix}context` `c`
57 WHERE mod.name = 'quiz' AND mod.id = cm.module AND cm.id = c.instanceid
58 AND c.contextlevel = ".CONTEXT_MODULE
." AND cm.course = $course");
60 $status = $status && execute_sql("INSERT INTO {$CFG->prefix}backup_ids
61 (backup_code, table_name, old_id, info)
62 SELECT '$backup_unique_code', 'question_categories', qc.id, ''
63 FROM {$CFG->prefix}question_categories qc
64 WHERE qc.contextid IN (".join(array_keys($cmcontexts), ', ').")", false);
66 //put the ids of the questions from all these categories into the db.
67 $status = $status && execute_sql("INSERT INTO {$CFG->prefix}backup_ids
68 (backup_code, table_name, old_id, info)
69 SELECT '$backup_unique_code', 'question', q.id, ''
70 FROM {$CFG->prefix}question q, {$CFG->prefix}backup_ids bk
71 WHERE q.category = bk.old_id AND bk.table_name = 'question_categories' AND
72 bk.backup_code = '$backup_unique_code'", false);
74 // Then categories from parent contexts used by the quizzes we are backing up.
75 //TODO this will need generalising when we have modules other than quiz using shared questions above course level.
76 $parentcontexts = get_parent_contexts($coursecontext);
77 $from = "{$CFG->prefix}quiz quiz,";
78 $where = "AND quiz.course = '$course'
79 AND qqi.quiz = quiz.id";
80 if (!empty($instances) && is_array($instances) && count($instances)) {
81 $questionselectsqlfrom = '';
82 $questionselectsqlwhere = 'AND qqi.quiz IN ('.implode(',',array_keys($instances)).')';
84 $questionselectsqlfrom = "{$CFG->prefix}quiz quiz,";
85 $questionselectsqlwhere = "AND quiz.course = '$course'
86 AND qqi.quiz = quiz.id";
88 $categories = get_records_sql("
89 SELECT id, parent, 0 AS childrendone
90 FROM {$CFG->prefix}question_categories
91 WHERE contextid IN (".join($parentcontexts, ', ').")
93 SELECT DISTINCT question.category
94 FROM {$CFG->prefix}question question,
95 $questionselectsqlfrom
96 {$CFG->prefix}quiz_question_instances qqi
97 WHERE qqi.question = question.id
98 $questionselectsqlwhere
101 $categories = array();
103 //put the ids of the used questions from all these categories into the db.
104 $status = $status && execute_sql("INSERT INTO {$CFG->prefix}backup_ids
105 (backup_code, table_name, old_id, info)
106 SELECT '$backup_unique_code', 'question', q.id, ''
107 FROM {$CFG->prefix}question q,
109 {$CFG->prefix}question_categories qc,
110 {$CFG->prefix}quiz_question_instances qqi
111 WHERE (qqi.question = q.id
112 OR qqi.question = q.parent)
113 AND q.category = qc.id
114 AND qc.contextid IN (".join($parentcontexts, ', ').")
117 // Add the parent categories, of these categories up to the top of the category tree.
118 // not backing up the questions in these categories.
119 foreach ($categories as $category) {
120 while ($category->parent
!= 0) {
121 if (array_key_exists($category->parent
, $categories)) {
122 // Parent category already on the list.
125 $currentid = $category->id
;
126 $category = get_record('question_categories', 'id', $category->parent
, '', '', '', '', 'id, parent, 0 AS childrendone');
128 $categories[$category->id
] = $category;
130 // Parent not found: this indicates an error, but just fix it.
131 set_field('question_categories', 'parent', 0, 'id', $currentid);
137 // Now we look for categories from other courses containing random questions
138 // in our quizzes that select from the category and its subcategories. That implies
139 // those subcategories also need to be backed up. (The categories themselves
140 // and their parents will already have been included.)
141 $categorieswithrandom = get_records_sql("
142 SELECT question.category AS id, SUM(question.questiontext) as questiontext
143 FROM {$CFG->prefix}quiz_question_instances qqi,
145 {$CFG->prefix}question question
146 WHERE question.id = qqi.question
147 AND question.qtype = '" . RANDOM
. "'
149 GROUP BY question.category
151 $randomselectedquestions = array();
152 if ($categorieswithrandom) {
153 foreach ($categorieswithrandom as $category) {
154 if ($category->questiontext
){
155 $status = $status && quiz_backup_add_sub_categories($categories, $randomselectedquestions, $category->id
);
158 $returnval = get_records_sql("
160 FROM {$CFG->prefix}question question
161 WHERE question.category IN (".join(array_keys($categorieswithrandom), ', ').")");
163 $randomselectedquestions +
= $returnval;
167 // Finally, add all these extra categories to the backup_ids table.
168 foreach ($categories as $category) {
169 $status = $status && backup_putid($backup_unique_code, 'question_categories', $category->id
, 0);
171 // Finally, add all these extra categories to the backup_ids table.
172 foreach ($randomselectedquestions as $question) {
173 $status = $status && backup_putid($backup_unique_code, 'question', $question->id
, 0);
180 * Helper function adding the id of all the subcategories of a category to an array.
182 function quiz_backup_add_sub_categories(&$categories, &$questions, $categoryid) {
185 if ($categories[$categoryid]->childrendone
) {
188 if ($subcategories = get_records('question_categories', 'parent', $categoryid, '', 'id, 0 AS childrendone')) {
189 foreach ($subcategories as $subcategory) {
190 if (!array_key_exists($subcategory->id
, $categories)) {
191 $categories[$subcategory->id
] = $subcategory;
193 $status = $status && quiz_backup_add_sub_categories($categories, $questions, $subcategory->id
);
195 $subcatlist = join(array_keys($subcategories), ',');
196 $returnval = get_records_sql("
198 FROM {$CFG->prefix}question question
199 WHERE question.category IN ($subcatlist)
202 $questions +
= $returnval;
205 $categories[$categoryid]->childrendone
= 1;
210 //This function is used to detect orphaned questions (pointing to a
211 //non existing category) and to recreate such category. This function
212 //is used by the backup process, to ensure consistency and should be
213 //executed in the upgrade process and, perhaps in the health center.
214 function fix_orphaned_questions ($course) {
218 $categories = get_records_sql("SELECT DISTINCT t.category, t.category
219 FROM {$CFG->prefix}question t,
220 {$CFG->prefix}quiz_question_instances g,
222 WHERE q.course = '$course' AND
224 g.question = t.id",false);
226 foreach ($categories as $key => $category) {
227 $exist = get_record('question_categories','id', $key);
228 //If the category doesn't exist
230 //Build a new category
231 $db_cat = new stdClass
;
232 // always create missing categories in course context
233 $db_cat->contextid
= get_context_instance(CONTEXT_COURSE
, $course);
234 $db_cat->name
= get_string('recreatedcategory','',$key);
235 $db_cat->info
= get_string('recreatedcategory','',$key);
236 $db_cat->stamp
= make_unique_id_code();
237 //Insert the new category
238 $catid = insert_record('question_categories',$db_cat);
241 //Reasign orphaned questions to their new category
242 set_field ('question','category',$catid,'category',$key);
250 //STEP 2. Backup quizzes and associated structures
251 // (course dependent)
253 function quiz_backup_one_mod($bf,$preferences,$quiz) {
256 if (is_numeric($quiz)) {
257 $quiz = get_record('quiz','id',$quiz);
261 fwrite ($bf,start_tag("MOD",3,true));
263 fwrite ($bf,full_tag("ID",4,false,$quiz->id
));
264 fwrite ($bf,full_tag("MODTYPE",4,false,"quiz"));
265 fwrite ($bf,full_tag("NAME",4,false,$quiz->name
));
266 fwrite ($bf,full_tag("INTRO",4,false,$quiz->intro
));
267 fwrite ($bf,full_tag("TIMEOPEN",4,false,$quiz->timeopen
));
268 fwrite ($bf,full_tag("TIMECLOSE",4,false,$quiz->timeclose
));
269 fwrite ($bf,full_tag("OPTIONFLAGS",4,false,$quiz->optionflags
));
270 fwrite ($bf,full_tag("PENALTYSCHEME",4,false,$quiz->penaltyscheme
));
271 fwrite ($bf,full_tag("ATTEMPTS_NUMBER",4,false,$quiz->attempts
));
272 fwrite ($bf,full_tag("ATTEMPTONLAST",4,false,$quiz->attemptonlast
));
273 fwrite ($bf,full_tag("GRADEMETHOD",4,false,$quiz->grademethod
));
274 fwrite ($bf,full_tag("DECIMALPOINTS",4,false,$quiz->decimalpoints
));
275 fwrite ($bf,full_tag("REVIEW",4,false,$quiz->review
));
276 fwrite ($bf,full_tag("QUESTIONSPERPAGE",4,false,$quiz->questionsperpage
));
277 fwrite ($bf,full_tag("SHUFFLEQUESTIONS",4,false,$quiz->shufflequestions
));
278 fwrite ($bf,full_tag("SHUFFLEANSWERS",4,false,$quiz->shuffleanswers
));
279 fwrite ($bf,full_tag("QUESTIONS",4,false,$quiz->questions
));
280 fwrite ($bf,full_tag("SUMGRADES",4,false,$quiz->sumgrades
));
281 fwrite ($bf,full_tag("GRADE",4,false,$quiz->grade
));
282 fwrite ($bf,full_tag("TIMECREATED",4,false,$quiz->timecreated
));
283 fwrite ($bf,full_tag("TIMEMODIFIED",4,false,$quiz->timemodified
));
284 fwrite ($bf,full_tag("TIMELIMIT",4,false,$quiz->timelimit
));
285 fwrite ($bf,full_tag("PASSWORD",4,false,$quiz->password
));
286 fwrite ($bf,full_tag("SUBNET",4,false,$quiz->subnet
));
287 fwrite ($bf,full_tag("POPUP",4,false,$quiz->popup
));
288 fwrite ($bf,full_tag("DELAY1",4,false,$quiz->delay1
));
289 fwrite ($bf,full_tag("DELAY2",4,false,$quiz->delay2
));
290 //Now we print to xml question_instances (Course Level)
291 $status = backup_quiz_question_instances($bf,$preferences,$quiz->id
);
292 //Now we print to xml quiz_feedback (Course Level)
293 $status = backup_quiz_feedback($bf,$preferences,$quiz->id
);
294 //Now we print to xml question_versions (Course Level)
295 $status = backup_quiz_question_versions($bf,$preferences,$quiz->id
);
296 //if we've selected to backup users info, then execute:
297 // - backup_quiz_grades
298 // - backup_quiz_attempts
299 if (backup_userdata_selected($preferences,'quiz',$quiz->id
) && $status) {
300 $status = backup_quiz_grades($bf,$preferences,$quiz->id
);
302 $status = backup_quiz_attempts($bf,$preferences,$quiz->id
);
306 $status = fwrite ($bf,end_tag("MOD",3,true));
312 function quiz_backup_mods($bf,$preferences) {
318 //Iterate over quiz table
319 $quizzes = get_records ("quiz","course",$preferences->backup_course
,"id");
321 foreach ($quizzes as $quiz) {
322 if (backup_mod_selected($preferences,'quiz',$quiz->id
)) {
323 $status = quiz_backup_one_mod($bf,$preferences,$quiz);
330 //Backup quiz_question_instances contents (executed from quiz_backup_mods)
331 function backup_quiz_question_instances ($bf,$preferences,$quiz) {
334 $quiz_question_instances = get_records("quiz_question_instances","quiz",$quiz,"id");
335 //If there are question_instances
336 if ($quiz_question_instances) {
338 $status = fwrite ($bf,start_tag("QUESTION_INSTANCES",4,true));
339 //Iterate over each question_instance
340 foreach ($quiz_question_instances as $que_ins) {
341 //Start question instance
342 $status = fwrite ($bf,start_tag("QUESTION_INSTANCE",5,true));
343 //Print question_instance contents
344 fwrite ($bf,full_tag("ID",6,false,$que_ins->id
));
345 fwrite ($bf,full_tag("QUESTION",6,false,$que_ins->question
));
346 fwrite ($bf,full_tag("GRADE",6,false,$que_ins->grade
));
347 //End question instance
348 $status = fwrite ($bf,end_tag("QUESTION_INSTANCE",5,true));
351 $status = fwrite ($bf,end_tag("QUESTION_INSTANCES",4,true));
356 //Backup quiz_question_instances contents (executed from quiz_backup_mods)
357 function backup_quiz_feedback ($bf,$preferences,$quiz) {
360 $quiz_feedback = get_records('quiz_feedback', 'quizid', $quiz, 'id');
361 // If there are question_instances ...
362 if ($quiz_feedback) {
364 $status = $status & fwrite($bf,start_tag('FEEDBACKS', 4, true));
366 // Iterate over each question_instance.
367 foreach ($quiz_feedback as $feedback) {
369 //Start feedback instance
370 $status = $status & fwrite($bf, start_tag('FEEDBACK',5,true));
372 //Print question_instance contents.
373 $status = $status & fwrite($bf, full_tag('ID', 6, false, $feedback->id
));
374 $status = $status & fwrite($bf, full_tag('QUIZID', 6, false, $feedback->quizid
));
375 $status = $status & fwrite($bf, full_tag('FEEDBACKTEXT', 6, false, $feedback->feedbacktext
));
376 $status = $status & fwrite($bf, full_tag('MINGRADE', 6, false, $feedback->mingrade
));
377 $status = $status & fwrite($bf, full_tag('MAXGRADE', 6, false, $feedback->maxgrade
));
379 // End feedback instance.
380 $status = $status & fwrite($bf, end_tag('FEEDBACK', 5, true));
384 $status = $status & fwrite($bf, end_tag('FEEDBACKS', 4, true));
389 //Backup quiz_question_versions contents (executed from quiz_backup_mods)
390 function backup_quiz_question_versions ($bf,$preferences,$quiz) {
393 $quiz_question_versions = get_records("quiz_question_versions","quiz",$quiz,"id");
394 //If there are question_versions
395 if ($quiz_question_versions) {
397 $status = fwrite ($bf,start_tag("QUESTION_VERSIONS",4,true));
398 //Iterate over each question_version
399 foreach ($quiz_question_versions as $que_ver) {
400 //Start question version
401 $status = fwrite ($bf,start_tag("QUESTION_VERSION",5,true));
402 //Print question_version contents
403 fwrite ($bf,full_tag("ID",6,false,$que_ver->id
));
404 fwrite ($bf,full_tag("OLDQUESTION",6,false,$que_ver->oldquestion
));
405 fwrite ($bf,full_tag("NEWQUESTION",6,false,$que_ver->newquestion
));
406 fwrite ($bf,full_tag("ORIGINALQUESTION",6,false,$que_ver->originalquestion
));
407 fwrite ($bf,full_tag("USERID",6,false,$que_ver->userid
));
408 fwrite ($bf,full_tag("TIMESTAMP",6,false,$que_ver->timestamp
));
409 //End question version
410 $status = fwrite ($bf,end_tag("QUESTION_VERSION",5,true));
413 $status = fwrite ($bf,end_tag("QUESTION_VERSIONS",4,true));
419 //Backup quiz_grades contents (executed from quiz_backup_mods)
420 function backup_quiz_grades ($bf,$preferences,$quiz) {
423 $quiz_grades = get_records("quiz_grades","quiz",$quiz,"id");
424 //If there are grades
427 $status = fwrite ($bf,start_tag("GRADES",4,true));
428 //Iterate over each grade
429 foreach ($quiz_grades as $gra) {
431 $status = fwrite ($bf,start_tag("GRADE",5,true));
432 //Print grade contents
433 fwrite ($bf,full_tag("ID",6,false,$gra->id
));
434 fwrite ($bf,full_tag("USERID",6,false,$gra->userid
));
435 fwrite ($bf,full_tag("GRADEVAL",6,false,$gra->grade
));
436 fwrite ($bf,full_tag("TIMEMODIFIED",6,false,$gra->timemodified
));
438 $status = fwrite ($bf,end_tag("GRADE",5,true));
441 $status = fwrite ($bf,end_tag("GRADES",4,true));
446 //Backup quiz_attempts contents (executed from quiz_backup_mods)
447 function backup_quiz_attempts ($bf,$preferences,$quiz) {
450 $quiz_attempts = get_records("quiz_attempts","quiz",$quiz,"id");
451 //If there are attempts
452 if ($quiz_attempts) {
454 $status = fwrite ($bf,start_tag("ATTEMPTS",4,true));
455 //Iterate over each attempt
456 foreach ($quiz_attempts as $attempt) {
458 $status = fwrite ($bf,start_tag("ATTEMPT",5,true));
459 //Print attempt contents
460 fwrite ($bf,full_tag("ID",6,false,$attempt->id
));
461 fwrite ($bf,full_tag("UNIQUEID",6,false,$attempt->uniqueid
));
462 fwrite ($bf,full_tag("USERID",6,false,$attempt->userid
));
463 fwrite ($bf,full_tag("ATTEMPTNUM",6,false,$attempt->attempt
));
464 fwrite ($bf,full_tag("SUMGRADES",6,false,$attempt->sumgrades
));
465 fwrite ($bf,full_tag("TIMESTART",6,false,$attempt->timestart
));
466 fwrite ($bf,full_tag("TIMEFINISH",6,false,$attempt->timefinish
));
467 fwrite ($bf,full_tag("TIMEMODIFIED",6,false,$attempt->timemodified
));
468 fwrite ($bf,full_tag("LAYOUT",6,false,$attempt->layout
));
469 fwrite ($bf,full_tag("PREVIEW",6,false,$attempt->preview
));
470 //Now write to xml the states (in this attempt)
471 $status = backup_question_states ($bf,$preferences,$attempt->uniqueid
);
472 //Now write to xml the sessions (in this attempt)
473 $status = backup_question_sessions ($bf,$preferences,$attempt->uniqueid
);
475 $status = fwrite ($bf,end_tag("ATTEMPT",5,true));
478 $status = fwrite ($bf,end_tag("ATTEMPTS",4,true));
483 function quiz_check_backup_mods_instances($instance,$backup_unique_code) {
484 // the keys in this array need to be unique as they get merged...
485 $info[$instance->id
.'0'][0] = '<b>'.$instance->name
.'</b>';
486 $info[$instance->id
.'0'][1] = '';
489 $info[$instance->id
.'1'][0] = get_string("categories","quiz");
490 if ($ids = question_category_ids_by_backup ($backup_unique_code)) {
491 $info[$instance->id
.'1'][1] = count($ids);
493 $info[$instance->id
.'1'][1] = 0;
496 $info[$instance->id
.'2'][0] = get_string("questionsinclhidden","quiz");
497 if ($ids = question_ids_by_backup ($backup_unique_code)) {
498 $info[$instance->id
.'2'][1] = count($ids);
500 $info[$instance->id
.'2'][1] = 0;
503 //Now, if requested, the user_data
504 if (!empty($instance->userdata
)) {
506 $info[$instance->id
.'3'][0] = get_string("grades");
507 if ($ids = quiz_grade_ids_by_instance ($instance->id
)) {
508 $info[$instance->id
.'3'][1] = count($ids);
510 $info[$instance->id
.'3'][1] = 0;
516 ////Return an array of info (name,value)
517 /// $instances is an array with key = instanceid, value = object (name,id,userdata)
518 function quiz_check_backup_mods($course,$user_data= false,$backup_unique_code,$instances=null) {
519 //Deletes data from mdl_backup_ids (categories section)
520 delete_ids ($backup_unique_code, 'question_categories');
521 delete_ids ($backup_unique_code, 'question');
522 //this function selects all the questions / categories to be backed up.
523 insert_category_and_question_ids($course, $backup_unique_code, $instances);
524 if ($course != SITEID
){
525 question_insert_site_file_names($course, $backup_unique_code);
527 if (!empty($instances) && is_array($instances) && count($instances)) {
529 foreach ($instances as $id => $instance) {
530 $info +
= quiz_check_backup_mods_instances($instance,$backup_unique_code);
534 //First the course data
535 $info[0][0] = get_string("modulenameplural","quiz");
536 if ($ids = quiz_ids ($course)) {
537 $info[0][1] = count($ids);
542 $info[1][0] = get_string("categories","quiz");
543 if ($ids = question_category_ids_by_backup ($backup_unique_code)) {
544 $info[1][1] = count($ids);
549 $info[2][0] = get_string("questions","quiz");
550 if ($ids = question_ids_by_backup ($backup_unique_code)) {
551 $info[2][1] = count($ids);
556 //Now, if requested, the user_data
559 $info[3][0] = get_string("grades");
560 if ($ids = quiz_grade_ids_by_course ($course)) {
561 $info[3][1] = count($ids);
570 //Return a content encoded to support interactivities linking. Every module
571 //should have its own. They are called automatically from the backup procedure.
572 function quiz_encode_content_links ($content,$preferences) {
576 $base = preg_quote($CFG->wwwroot
,"/");
578 //Link to the list of quizs
579 $buscar="/(".$base."\/mod\/quiz\/index.php\?id\=)([0-9]+)/";
580 $result= preg_replace($buscar,'$@QUIZINDEX*$2@$',$content);
582 //Link to quiz view by moduleid
583 $buscar="/(".$base."\/mod\/quiz\/view.php\?id\=)([0-9]+)/";
584 $result= preg_replace($buscar,'$@QUIZVIEWBYID*$2@$',$result);
589 // INTERNAL FUNCTIONS. BASED IN THE MOD STRUCTURE
591 //Returns an array of quiz id
592 function quiz_ids ($course) {
596 return get_records_sql ("SELECT a.id, a.course
597 FROM {$CFG->prefix}quiz a
598 WHERE a.course = '$course'");
601 function quiz_grade_ids_by_course ($course) {
605 return get_records_sql ("SELECT g.id, g.quiz
606 FROM {$CFG->prefix}quiz a,
607 {$CFG->prefix}quiz_grades g
608 WHERE a.course = '$course' and
612 function quiz_grade_ids_by_instance($instanceid) {
616 return get_records_sql ("SELECT g.id, g.quiz
617 FROM {$CFG->prefix}quiz_grades g
618 WHERE g.quiz = $instanceid");