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
36 * This backs up ids for quiz module. It backs up :
37 * all categories and questions in course
38 * all categories and questions in contexts of quiz module instances which have been selected for backup
39 * all categories and questions in contexts above course level that are used by quizzes that have been selected for backup
41 function quiz_insert_category_and_question_ids($course, $backup_unique_code, $instances = null) {
45 // Create missing categories and reasign orphaned questions.
46 quiz_fix_orphaned_questions($course);
48 $coursecontext = get_context_instance(CONTEXT_COURSE
, $course);
49 $status = $status && question_insert_c_and_q_ids_for_course($coursecontext, $backup_unique_code);
51 // then, all categories and questions from this course's modules' contexts.
52 $status = $status && question_insert_c_and_q_ids_for_module($backup_unique_code, $course, 'quiz', $instances);
54 // Then categories from parent contexts used by the quizzes we are backing up.
55 //TODO this will need generalising when we have modules other than quiz using shared questions above course level.
56 $parentcontexts = get_parent_contexts($coursecontext);
57 $from = "{$CFG->prefix}quiz quiz,";
58 $where = "AND quiz.course = '$course'
59 AND qqi.quiz = quiz.id";
60 if (!empty($instances) && is_array($instances) && count($instances)) {
61 $questionselectsqlfrom = '';
62 $questionselectsqlwhere = 'AND qqi.quiz IN ('.implode(',',array_keys($instances)).')';
64 $questionselectsqlfrom = "{$CFG->prefix}quiz quiz,";
65 $questionselectsqlwhere = "AND quiz.course = '$course'
66 AND qqi.quiz = quiz.id";
68 $categories = get_records_sql("
69 SELECT id, parent, 0 AS childrendone
70 FROM {$CFG->prefix}question_categories
71 WHERE contextid IN (".join($parentcontexts, ', ').")
73 SELECT DISTINCT question.category
74 FROM {$CFG->prefix}question question,
75 $questionselectsqlfrom
76 {$CFG->prefix}quiz_question_instances qqi
77 WHERE qqi.question = question.id
78 $questionselectsqlwhere
81 $categories = array();
83 //put the ids of the used questions from all these categories into the db.
84 $status = $status && execute_sql("INSERT INTO {$CFG->prefix}backup_ids
85 (backup_code, table_name, old_id, info)
86 SELECT DISTINCT $backup_unique_code, 'question', q.id, ''
87 FROM {$CFG->prefix}question q,
89 {$CFG->prefix}question_categories qc,
90 {$CFG->prefix}quiz_question_instances qqi
91 WHERE (qqi.question = q.id
92 OR qqi.question = q.parent)
93 AND q.category = qc.id
94 AND qc.contextid IN (".join($parentcontexts, ', ').")
97 // Add the parent categories, of these categories up to the top of the category tree.
98 // not backing up the questions in these categories.
99 foreach ($categories as $category) {
100 while ($category->parent
!= 0) {
101 if (array_key_exists($category->parent
, $categories)) {
102 // Parent category already on the list.
105 $currentid = $category->id
;
106 $category = get_record('question_categories', 'id', $category->parent
, '', '', '', '', 'id, parent, 0 AS childrendone');
108 $categories[$category->id
] = $category;
110 // Parent not found: this indicates an error, but just fix it.
111 set_field('question_categories', 'parent', 0, 'id', $currentid);
117 // Now we look for categories from other courses containing random questions
118 // in our quizzes that select from the category and its subcategories. That implies
119 // those subcategories also need to be backed up. (The categories themselves
120 // and their parents will already have been included.)
121 $categorieswithrandom = get_records_sql("
122 SELECT question.category AS id, SUM(" .
123 sql_cast_char2int('questiontext', true) . ") AS numqsusingsubcategories
124 FROM {$CFG->prefix}quiz_question_instances qqi,
126 {$CFG->prefix}question question
127 WHERE question.id = qqi.question
128 AND question.qtype = '" . RANDOM
. "'
130 GROUP BY question.category
132 $randomselectedquestions = array();
133 if ($categorieswithrandom) {
134 foreach ($categorieswithrandom as $category) {
135 if ($category->numqsusingsubcategories
> 0) {
136 $status = $status && quiz_backup_add_sub_categories($categories, $randomselectedquestions, $category->id
);
139 $returnval = get_records_sql("
141 FROM {$CFG->prefix}question question
142 WHERE question.category IN (".join(array_keys($categorieswithrandom), ', ').")");
144 $randomselectedquestions +
= $returnval;
148 // Finally, add all these extra categories to the backup_ids table.
149 foreach ($categories as $category) {
150 $status = $status && backup_putid($backup_unique_code, 'question_categories', $category->id
, 0);
152 // Finally, add all these extra categories to the backup_ids table.
153 foreach ($randomselectedquestions as $question) {
154 $status = $status && backup_putid($backup_unique_code, 'question', $question->id
, 0);
161 * Helper function adding the id of all the subcategories of a category to an array.
163 function quiz_backup_add_sub_categories(&$categories, &$questions, $categoryid) {
166 if ($categories[$categoryid]->childrendone
) {
169 if ($subcategories = get_records('question_categories', 'parent', $categoryid, '', 'id, 0 AS childrendone')) {
170 foreach ($subcategories as $subcategory) {
171 if (!array_key_exists($subcategory->id
, $categories)) {
172 $categories[$subcategory->id
] = $subcategory;
174 $status = $status && quiz_backup_add_sub_categories($categories, $questions, $subcategory->id
);
176 $subcatlist = join(array_keys($subcategories), ',');
177 $returnval = get_records_sql("
179 FROM {$CFG->prefix}question question
180 WHERE question.category IN ($subcatlist)
183 $questions +
= $returnval;
186 $categories[$categoryid]->childrendone
= 1;
191 //This function is used to detect orphaned questions (pointing to a
192 //non existing category) and to recreate such category. This function
193 //is used by the backup process, to ensure consistency and should be
194 //executed in the upgrade process and, perhaps in the health center.
195 function quiz_fix_orphaned_questions ($course) {
199 $categories = get_records_sql("SELECT DISTINCT t.category, t.category
200 FROM {$CFG->prefix}question t,
201 {$CFG->prefix}quiz_question_instances g,
203 WHERE q.course = '$course' AND
205 g.question = t.id",false);
207 foreach ($categories as $key => $category) {
208 $exist = get_record('question_categories','id', $key);
209 //If the category doesn't exist
211 //Build a new category
212 $db_cat = new stdClass
;
213 // always create missing categories in course context
214 $db_cat->contextid
= get_context_instance(CONTEXT_COURSE
, $course);
215 $db_cat->name
= get_string('recreatedcategory','',$key);
216 $db_cat->info
= get_string('recreatedcategory','',$key);
217 $db_cat->stamp
= make_unique_id_code();
218 //Insert the new category
219 $catid = insert_record('question_categories',$db_cat);
222 //Reasign orphaned questions to their new category
223 set_field ('question','category',$catid,'category',$key);
231 //STEP 2. Backup quizzes and associated structures
232 // (course dependent)
234 function quiz_backup_one_mod($bf,$preferences,$quiz) {
237 if (is_numeric($quiz)) {
238 $quiz = get_record('quiz','id',$quiz);
242 fwrite ($bf,start_tag("MOD",3,true));
244 fwrite ($bf,full_tag("ID",4,false,$quiz->id
));
245 fwrite ($bf,full_tag("MODTYPE",4,false,"quiz"));
246 fwrite ($bf,full_tag("NAME",4,false,$quiz->name
));
247 fwrite ($bf,full_tag("INTRO",4,false,$quiz->intro
));
248 fwrite ($bf,full_tag("TIMEOPEN",4,false,$quiz->timeopen
));
249 fwrite ($bf,full_tag("TIMECLOSE",4,false,$quiz->timeclose
));
250 fwrite ($bf,full_tag("OPTIONFLAGS",4,false,$quiz->optionflags
));
251 fwrite ($bf,full_tag("PENALTYSCHEME",4,false,$quiz->penaltyscheme
));
252 fwrite ($bf,full_tag("ATTEMPTS_NUMBER",4,false,$quiz->attempts
));
253 fwrite ($bf,full_tag("ATTEMPTONLAST",4,false,$quiz->attemptonlast
));
254 fwrite ($bf,full_tag("GRADEMETHOD",4,false,$quiz->grademethod
));
255 fwrite ($bf,full_tag("DECIMALPOINTS",4,false,$quiz->decimalpoints
));
256 fwrite ($bf,full_tag("REVIEW",4,false,$quiz->review
));
257 fwrite ($bf,full_tag("QUESTIONSPERPAGE",4,false,$quiz->questionsperpage
));
258 fwrite ($bf,full_tag("SHUFFLEQUESTIONS",4,false,$quiz->shufflequestions
));
259 fwrite ($bf,full_tag("SHUFFLEANSWERS",4,false,$quiz->shuffleanswers
));
260 fwrite ($bf,full_tag("QUESTIONS",4,false,$quiz->questions
));
261 fwrite ($bf,full_tag("SUMGRADES",4,false,$quiz->sumgrades
));
262 fwrite ($bf,full_tag("GRADE",4,false,$quiz->grade
));
263 fwrite ($bf,full_tag("TIMECREATED",4,false,$quiz->timecreated
));
264 fwrite ($bf,full_tag("TIMEMODIFIED",4,false,$quiz->timemodified
));
265 fwrite ($bf,full_tag("TIMELIMIT",4,false,$quiz->timelimit
));
266 fwrite ($bf,full_tag("PASSWORD",4,false,$quiz->password
));
267 fwrite ($bf,full_tag("SUBNET",4,false,$quiz->subnet
));
268 fwrite ($bf,full_tag("POPUP",4,false,$quiz->popup
));
269 fwrite ($bf,full_tag("DELAY1",4,false,$quiz->delay1
));
270 fwrite ($bf,full_tag("DELAY2",4,false,$quiz->delay2
));
271 //Now we print to xml question_instances (Course Level)
272 $status = backup_quiz_question_instances($bf,$preferences,$quiz->id
);
273 //Now we print to xml quiz_feedback (Course Level)
274 $status = backup_quiz_feedback($bf,$preferences,$quiz->id
);
275 //Now we print to xml question_versions (Course Level)
276 $status = backup_quiz_question_versions($bf,$preferences,$quiz->id
);
277 //if we've selected to backup users info, then execute:
278 // - backup_quiz_grades
279 // - backup_quiz_attempts
280 if (backup_userdata_selected($preferences,'quiz',$quiz->id
) && $status) {
281 $status = backup_quiz_grades($bf,$preferences,$quiz->id
);
283 $status = backup_quiz_attempts($bf,$preferences,$quiz->id
);
287 $status = fwrite ($bf,end_tag("MOD",3,true));
293 function quiz_backup_mods($bf,$preferences) {
299 //Iterate over quiz table
300 $quizzes = get_records ("quiz","course",$preferences->backup_course
,"id");
302 foreach ($quizzes as $quiz) {
303 if (backup_mod_selected($preferences,'quiz',$quiz->id
)) {
304 $status = quiz_backup_one_mod($bf,$preferences,$quiz);
311 //Backup quiz_question_instances contents (executed from quiz_backup_mods)
312 function backup_quiz_question_instances ($bf,$preferences,$quiz) {
315 $quiz_question_instances = get_records("quiz_question_instances","quiz",$quiz,"id");
316 //If there are question_instances
317 if ($quiz_question_instances) {
319 $status = fwrite ($bf,start_tag("QUESTION_INSTANCES",4,true));
320 //Iterate over each question_instance
321 foreach ($quiz_question_instances as $que_ins) {
322 //Start question instance
323 $status = fwrite ($bf,start_tag("QUESTION_INSTANCE",5,true));
324 //Print question_instance contents
325 fwrite ($bf,full_tag("ID",6,false,$que_ins->id
));
326 fwrite ($bf,full_tag("QUESTION",6,false,$que_ins->question
));
327 fwrite ($bf,full_tag("GRADE",6,false,$que_ins->grade
));
328 //End question instance
329 $status = fwrite ($bf,end_tag("QUESTION_INSTANCE",5,true));
332 $status = fwrite ($bf,end_tag("QUESTION_INSTANCES",4,true));
337 //Backup quiz_question_instances contents (executed from quiz_backup_mods)
338 function backup_quiz_feedback ($bf,$preferences,$quiz) {
341 $quiz_feedback = get_records('quiz_feedback', 'quizid', $quiz, 'id');
342 // If there are question_instances ...
343 if ($quiz_feedback) {
345 $status = $status & fwrite($bf,start_tag('FEEDBACKS', 4, true));
347 // Iterate over each question_instance.
348 foreach ($quiz_feedback as $feedback) {
350 //Start feedback instance
351 $status = $status & fwrite($bf, start_tag('FEEDBACK',5,true));
353 //Print question_instance contents.
354 $status = $status & fwrite($bf, full_tag('ID', 6, false, $feedback->id
));
355 $status = $status & fwrite($bf, full_tag('QUIZID', 6, false, $feedback->quizid
));
356 $status = $status & fwrite($bf, full_tag('FEEDBACKTEXT', 6, false, $feedback->feedbacktext
));
357 $status = $status & fwrite($bf, full_tag('MINGRADE', 6, false, $feedback->mingrade
));
358 $status = $status & fwrite($bf, full_tag('MAXGRADE', 6, false, $feedback->maxgrade
));
360 // End feedback instance.
361 $status = $status & fwrite($bf, end_tag('FEEDBACK', 5, true));
365 $status = $status & fwrite($bf, end_tag('FEEDBACKS', 4, true));
370 //Backup quiz_question_versions contents (executed from quiz_backup_mods)
371 function backup_quiz_question_versions ($bf,$preferences,$quiz) {
374 $quiz_question_versions = get_records("quiz_question_versions","quiz",$quiz,"id");
375 //If there are question_versions
376 if ($quiz_question_versions) {
378 $status = fwrite ($bf,start_tag("QUESTION_VERSIONS",4,true));
379 //Iterate over each question_version
380 foreach ($quiz_question_versions as $que_ver) {
381 //Start question version
382 $status = fwrite ($bf,start_tag("QUESTION_VERSION",5,true));
383 //Print question_version contents
384 fwrite ($bf,full_tag("ID",6,false,$que_ver->id
));
385 fwrite ($bf,full_tag("OLDQUESTION",6,false,$que_ver->oldquestion
));
386 fwrite ($bf,full_tag("NEWQUESTION",6,false,$que_ver->newquestion
));
387 fwrite ($bf,full_tag("ORIGINALQUESTION",6,false,$que_ver->originalquestion
));
388 fwrite ($bf,full_tag("USERID",6,false,$que_ver->userid
));
389 fwrite ($bf,full_tag("TIMESTAMP",6,false,$que_ver->timestamp
));
390 //End question version
391 $status = fwrite ($bf,end_tag("QUESTION_VERSION",5,true));
394 $status = fwrite ($bf,end_tag("QUESTION_VERSIONS",4,true));
400 //Backup quiz_grades contents (executed from quiz_backup_mods)
401 function backup_quiz_grades ($bf,$preferences,$quiz) {
404 $quiz_grades = get_records("quiz_grades","quiz",$quiz,"id");
405 //If there are grades
408 $status = fwrite ($bf,start_tag("GRADES",4,true));
409 //Iterate over each grade
410 foreach ($quiz_grades as $gra) {
412 $status = fwrite ($bf,start_tag("GRADE",5,true));
413 //Print grade contents
414 fwrite ($bf,full_tag("ID",6,false,$gra->id
));
415 fwrite ($bf,full_tag("USERID",6,false,$gra->userid
));
416 fwrite ($bf,full_tag("GRADEVAL",6,false,$gra->grade
));
417 fwrite ($bf,full_tag("TIMEMODIFIED",6,false,$gra->timemodified
));
419 $status = fwrite ($bf,end_tag("GRADE",5,true));
422 $status = fwrite ($bf,end_tag("GRADES",4,true));
427 //Backup quiz_attempts contents (executed from quiz_backup_mods)
428 function backup_quiz_attempts ($bf,$preferences,$quiz) {
431 $quiz_attempts = get_records("quiz_attempts","quiz",$quiz,"id");
432 //If there are attempts
433 if ($quiz_attempts) {
435 $status = fwrite ($bf,start_tag("ATTEMPTS",4,true));
436 //Iterate over each attempt
437 foreach ($quiz_attempts as $attempt) {
439 $status = fwrite ($bf,start_tag("ATTEMPT",5,true));
440 //Print attempt contents
441 fwrite ($bf,full_tag("ID",6,false,$attempt->id
));
442 fwrite ($bf,full_tag("UNIQUEID",6,false,$attempt->uniqueid
));
443 fwrite ($bf,full_tag("USERID",6,false,$attempt->userid
));
444 fwrite ($bf,full_tag("ATTEMPTNUM",6,false,$attempt->attempt
));
445 fwrite ($bf,full_tag("SUMGRADES",6,false,$attempt->sumgrades
));
446 fwrite ($bf,full_tag("TIMESTART",6,false,$attempt->timestart
));
447 fwrite ($bf,full_tag("TIMEFINISH",6,false,$attempt->timefinish
));
448 fwrite ($bf,full_tag("TIMEMODIFIED",6,false,$attempt->timemodified
));
449 fwrite ($bf,full_tag("LAYOUT",6,false,$attempt->layout
));
450 fwrite ($bf,full_tag("PREVIEW",6,false,$attempt->preview
));
451 //Now write to xml the states (in this attempt)
452 $status = backup_question_states ($bf,$preferences,$attempt->uniqueid
);
453 //Now write to xml the sessions (in this attempt)
454 $status = backup_question_sessions ($bf,$preferences,$attempt->uniqueid
);
456 $status = fwrite ($bf,end_tag("ATTEMPT",5,true));
459 $status = fwrite ($bf,end_tag("ATTEMPTS",4,true));
464 function quiz_check_backup_mods_instances($instance,$backup_unique_code) {
465 // the keys in this array need to be unique as they get merged...
466 $info[$instance->id
.'0'][0] = '<b>'.$instance->name
.'</b>';
467 $info[$instance->id
.'0'][1] = '';
470 $info[$instance->id
.'1'][0] = get_string("categories","quiz");
471 if ($ids = question_category_ids_by_backup ($backup_unique_code)) {
472 $info[$instance->id
.'1'][1] = count($ids);
474 $info[$instance->id
.'1'][1] = 0;
477 $info[$instance->id
.'2'][0] = get_string("questionsinclhidden","quiz");
478 if ($ids = question_ids_by_backup ($backup_unique_code)) {
479 $info[$instance->id
.'2'][1] = count($ids);
481 $info[$instance->id
.'2'][1] = 0;
484 //Now, if requested, the user_data
485 if (!empty($instance->userdata
)) {
487 $info[$instance->id
.'3'][0] = get_string("grades");
488 if ($ids = quiz_grade_ids_by_instance ($instance->id
)) {
489 $info[$instance->id
.'3'][1] = count($ids);
491 $info[$instance->id
.'3'][1] = 0;
497 ////Return an array of info (name,value)
498 /// $instances is an array with key = instanceid, value = object (name,id,userdata)
499 function quiz_check_backup_mods($course,$user_data= false,$backup_unique_code,$instances=null) {
500 //this function selects all the questions / categories to be backed up.
501 quiz_insert_category_and_question_ids($course, $backup_unique_code, $instances);
502 if ($course != SITEID
){
503 question_insert_site_file_names($course, $backup_unique_code);
505 if (!empty($instances) && is_array($instances) && count($instances)) {
507 foreach ($instances as $id => $instance) {
508 $info +
= quiz_check_backup_mods_instances($instance,$backup_unique_code);
512 //First the course data
513 $info[0][0] = get_string("modulenameplural","quiz");
514 if ($ids = quiz_ids ($course)) {
515 $info[0][1] = count($ids);
520 $info[1][0] = get_string("categories","quiz");
521 if ($ids = question_category_ids_by_backup ($backup_unique_code)) {
522 $info[1][1] = count($ids);
527 $info[2][0] = get_string("questions","quiz");
528 if ($ids = question_ids_by_backup ($backup_unique_code)) {
529 $info[2][1] = count($ids);
534 //Now, if requested, the user_data
537 $info[3][0] = get_string("grades");
538 if ($ids = quiz_grade_ids_by_course ($course)) {
539 $info[3][1] = count($ids);
548 //Return a content encoded to support interactivities linking. Every module
549 //should have its own. They are called automatically from the backup procedure.
550 function quiz_encode_content_links ($content,$preferences) {
554 $base = preg_quote($CFG->wwwroot
,"/");
556 //Link to the list of quizs
557 $buscar="/(".$base."\/mod\/quiz\/index.php\?id\=)([0-9]+)/";
558 $result= preg_replace($buscar,'$@QUIZINDEX*$2@$',$content);
560 //Link to quiz view by moduleid
561 $buscar="/(".$base."\/mod\/quiz\/view.php\?id\=)([0-9]+)/";
562 $result= preg_replace($buscar,'$@QUIZVIEWBYID*$2@$',$result);
564 //Link to quiz view by quizid
565 $buscar="/(".$base."\/mod\/quiz\/view.php\?q\=)([0-9]+)/";
566 $result= preg_replace($buscar,'$@QUIZVIEWBYQ*$2@$',$result);
571 // INTERNAL FUNCTIONS. BASED IN THE MOD STRUCTURE
573 //Returns an array of quiz id
574 function quiz_ids ($course) {
578 return get_records_sql ("SELECT a.id, a.course
579 FROM {$CFG->prefix}quiz a
580 WHERE a.course = '$course'");
583 function quiz_grade_ids_by_course ($course) {
587 return get_records_sql ("SELECT g.id, g.quiz
588 FROM {$CFG->prefix}quiz a,
589 {$CFG->prefix}quiz_grades g
590 WHERE a.course = '$course' and
594 function quiz_grade_ids_by_instance($instanceid) {
598 return get_records_sql ("SELECT g.id, g.quiz
599 FROM {$CFG->prefix}quiz_grades g
600 WHERE g.quiz = $instanceid");