ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç smkb8269/public_html/lms.smk-almanshuriyah.sch.id/mod/quiz/tests/backup/repeated_restore_test.php000064400000137171152267131040030077 0ustar00home. namespace mod_quiz\backup; use advanced_testcase; use backup_controller; use restore_controller; use quiz_question_helper_test_trait; use backup; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); require_once($CFG->dirroot . '/question/engine/lib.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); require_once($CFG->dirroot . '/course/lib.php'); require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php'); /** * Test repeatedly restoring a quiz into another course. * * @package mod_quiz * @category test * @copyright Julien Rädler * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \restore_questions_parser_processor * @covers \restore_create_categories_and_questions */ final class repeated_restore_test extends advanced_testcase { use quiz_question_helper_test_trait; /** * Create 2 courses, and a quiz with questions on the first course. * * @param bool $sharedquestions If true, create the questions in a qbank module rather than the quiz itself. * @return array */ protected function create_courses_and_quiz(bool $sharedquestions = false): array { global $USER; // Create two courses and a user with editing teacher capabilities. $generator = $this->getDataGenerator(); $course1 = $generator->create_course(); $course2 = $generator->create_course(); $teacher = $USER; $generator->enrol_user($teacher->id, $course1->id, 'editingteacher'); $generator->enrol_user($teacher->id, $course2->id, 'editingteacher'); // Create a quiz with questions in the first course. $quiz = $this->create_test_quiz($course1); if ($sharedquestions) { $qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]); $context = \context_module::instance($qbank->cmid); } else { $context = \context_module::instance($quiz->cmid); } $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); // Create a question category. $cat = $questiongenerator->create_question_category(['contextid' => $context->id]); // Create a short answer question. $saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]); // Update the question to simulate editing. $questiongenerator->update_question($saq); // Add question to quiz. quiz_add_quiz_question($saq->id, $quiz); // Create a numerical question. $numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]); // Update the question to simulate multiple versions. $questiongenerator->update_question($numq); $questiongenerator->update_question($numq); // Add question to quiz. quiz_add_quiz_question($numq->id, $quiz); // Create a true false question. $tfq = $questiongenerator->create_question('truefalse', null, ['category' => $cat->id]); // Update the question to simulate multiple versions. $questiongenerator->update_question($tfq); $questiongenerator->update_question($tfq); // Add question to quiz. quiz_add_quiz_question($tfq->id, $quiz); // Capture original question IDs for verification after import. $modules1 = get_fast_modinfo($course1->id)->get_instances_of('quiz'); $module1 = reset($modules1); $questionscourse1 = \mod_quiz\question\bank\qbank_helper::get_question_structure( $module1->instance, $module1->context, ); $originalquestionids = []; foreach ($questionscourse1 as $slot) { array_push($originalquestionids, intval($slot->questionid)); } return [ $course1, $course2, $quiz, $teacher, $originalquestionids, ]; } /** * Restore a quiz using private questions twice into the same target course, * and verify the quiz uses a newly-restored copy of the questions each time. */ public function test_restore_quiz_with_own_questions_into_other_course_twice(): void { $this->resetAfterTest(); $this->setAdminUser(); [, $course2, $quiz, $teacher, $originalquestionids] = $this->create_courses_and_quiz(); // Backup the quiz course. $bc = new backup_controller( backup::TYPE_1ACTIVITY, $quiz->cmid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, ); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Import the backup into the second course. $rc = new restore_controller( $backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, backup::TARGET_CURRENT_ADDING, ); $rc->execute_precheck(); $rc->execute_plan(); $rc->destroy(); // Verify the question ids from the quiz in the original course are different // from the question ids in the duplicated quiz in the second course. $modules2 = get_fast_modinfo($course2->id)->get_instances_of('quiz'); $module2 = reset($modules2); $questionscourse2firstimport = \mod_quiz\question\bank\qbank_helper::get_question_structure( $module2->instance, $module2->context, ); foreach ($questionscourse2firstimport as $slot) { $this->assertNotContains( intval($slot->questionid), $originalquestionids, "Question ID $slot->questionid should not be in the original course's question IDs.", ); } // Repeat the backup and import process to simulate a second import. $bc = new backup_controller( backup::TYPE_1ACTIVITY, $quiz->cmid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, ); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); $rc = new restore_controller( $backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, backup::TARGET_CURRENT_ADDING, ); $rc->execute_precheck(); $rc->execute_plan(); $rc->destroy(); // Verify that the quiz in the second restore uses a third copy of the questions. $modules3 = get_fast_modinfo($course2->id)->get_instances_of('quiz'); $module3 = end($modules3); $questionscourse2secondimport = \mod_quiz\question\bank\qbank_helper::get_question_structure( $module3->instance, $module3->context, ); foreach ($questionscourse2secondimport as $slot) { $this->assertNotEquals($questionscourse2firstimport[$slot->slot]->questionid, $slot->questionid); $this->assertNotContains( intval($slot->questionid), $originalquestionids, "Question ID $slot->questionid should not be in the original course's question IDs.", ); } } /** * Restore a quiz using shared questions twice into the same target course, * and verify the quiz uses the original questions each time. */ public function test_restore_quiz_with_shared_questions_into_other_course_twice(): void { $this->resetAfterTest(); $this->setAdminUser(); [, $course2, $quiz, $teacher, $originalquestionids] = $this->create_courses_and_quiz(true); // Backup the quiz. $bc = new backup_controller( backup::TYPE_1ACTIVITY, $quiz->cmid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, ); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Import the backup into the second course. $rc = new restore_controller( $backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, backup::TARGET_CURRENT_ADDING, ); $rc->execute_precheck(); $rc->execute_plan(); $rc->destroy(); // Verify the question ids from the quiz in the original course are the same as // the question ids in the duplicated quiz in the second course. $modules2 = get_fast_modinfo($course2->id)->get_instances_of('quiz'); $module2 = reset($modules2); $questionscourse2firstimport = \mod_quiz\question\bank\qbank_helper::get_question_structure( $module2->instance, $module2->context, ); foreach ($questionscourse2firstimport as $slot) { $this->assertContains( intval($slot->questionid), $originalquestionids, "Question ID $slot->questionid should be in the original course's question IDs.", ); } // Repeat the backup and import process to simulate a second import. $bc = new backup_controller( backup::TYPE_1ACTIVITY, $quiz->cmid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, ); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); $rc = new restore_controller( $backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, backup::TARGET_CURRENT_ADDING, ); $rc->execute_precheck(); $rc->execute_plan(); $rc->destroy(); // Verify that the quiz in the second restore uses the same questions from the original quiz and the first restore. $modules3 = get_fast_modinfo($course2->id)->get_instances_of('quiz'); $module3 = end($modules3); $questionscourse2secondimport = \mod_quiz\question\bank\qbank_helper::get_question_structure( $module3->instance, $module3->context, ); foreach ($questionscourse2secondimport as $slot) { $this->assertEquals($questionscourse2firstimport[$slot->slot]->questionid, $slot->questionid); $this->assertContains( intval($slot->questionid), $originalquestionids, "Question ID $slot->questionid should be in the original course's question IDs.", ); } } /** * Restore a quiz using shared questions twice into the same target course with the qbank, * and verify the quiz uses a newly-restored copy of the questions each time. */ public function test_restore_quiz_with_qbank_into_other_course_twice(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); [$course1, $course2, , $teacher, $originalquestionids] = $this->create_courses_and_quiz(true); // Backup the first course. $bc = new backup_controller( backup::TYPE_1COURSE, $course1->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, ); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Import the backup into the second course. $rc = new restore_controller( $backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, backup::TARGET_CURRENT_ADDING, ); $rc->execute_precheck(); $rc->execute_plan(); $rc->destroy(); // Verify the question ids from the quiz in the original course are different // from the question ids in the duplicated quiz in the second course. $modules2 = get_fast_modinfo($course2->id)->get_instances_of('quiz'); $module2 = reset($modules2); $questionscourse2firstimport = \mod_quiz\question\bank\qbank_helper::get_question_structure( $module2->instance, $module2->context, ); foreach ($questionscourse2firstimport as $slot) { $this->assertNotContains( intval($slot->questionid), $originalquestionids, "Question ID $slot->questionid should not be in the original course's question IDs.", ); } // Repeat the backup and import process to simulate a second import. $bc = new backup_controller( backup::TYPE_1COURSE, $course1->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, ); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); $rc = new restore_controller( $backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, backup::TARGET_CURRENT_ADDING, ); $rc->execute_precheck(); $rc->execute_plan(); $rc->destroy(); // Verify that the quiz in the second restore uses a third copy of the questions. $modules3 = get_fast_modinfo($course2->id)->get_instances_of('quiz'); $module3 = end($modules3); $questionscourse2secondimport = \mod_quiz\question\bank\qbank_helper::get_question_structure( $module3->instance, $module3->context, ); foreach ($questionscourse2secondimport as $slot) { $this->assertNotEquals($questionscourse2firstimport[$slot->slot]->questionid, $slot->questionid); $this->assertNotContains( intval($slot->questionid), $originalquestionids, "Question ID $slot->questionid should not be in the original course's question IDs.", ); } } /** * Import the quiz and qbank from course 1 to course 2, then import just the quiz a second time. * * As the user can use questions from the original qbank, the second quiz will use the original questions. */ public function test_restore_quiz_with_shared_questions_then_just_quiz(): void { $this->resetAfterTest(); $this->setAdminUser(); [$course1, $course2, $quiz, $teacher, $originalquestionids] = $this->create_courses_and_quiz(true); // Backup the first course. $bc = new backup_controller( backup::TYPE_1COURSE, $course1->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, ); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Import the backup into the second course. $rc = new restore_controller( $backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, backup::TARGET_CURRENT_ADDING, ); $rc->execute_precheck(); $rc->execute_plan(); $rc->destroy(); // Verify the question ids from the quiz in the original course are different // from the question ids in the duplicated quiz in the second course. $modules2 = get_fast_modinfo($course2->id)->get_instances_of('quiz'); $module2 = reset($modules2); $questionscourse2firstimport = \mod_quiz\question\bank\qbank_helper::get_question_structure( $module2->instance, $module2->context, ); foreach ($questionscourse2firstimport as $slot) { $this->assertNotContains( intval($slot->questionid), $originalquestionids, "Question ID $slot->questionid should not be in the original course's question IDs.", ); } // Repeat the backup and import process with just the quiz. $bc = new backup_controller( backup::TYPE_1ACTIVITY, $quiz->cmid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, ); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); $rc = new restore_controller( $backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, backup::TARGET_CURRENT_ADDING, ); $rc->execute_precheck(); $rc->execute_plan(); $rc->destroy(); // Verify that the quiz in the second restore uses the questions from the original course. $modules3 = get_fast_modinfo($course2->id)->get_instances_of('quiz'); $module3 = end($modules3); $questionscourse2secondimport = \mod_quiz\question\bank\qbank_helper::get_question_structure( $module3->instance, $module3->context, ); foreach ($questionscourse2secondimport as $slot) { $this->assertNotEquals($questionscourse2firstimport[$slot->slot]->questionid, $slot->questionid); $this->assertContains( intval($slot->questionid), $originalquestionids, "Question ID $slot->questionid should be in the original course's question IDs.", ); } } /** * Restore a copy of a quiz to the same course, using questions that include line breaks in the text. */ public function test_restore_question_with_linebreaks(): void { global $USER; $this->resetAfterTest(); $this->setAdminUser(); // Step 1: Create two courses and a user with editing teacher capabilities. $generator = $this->getDataGenerator(); $course1 = $generator->create_course(); $course2 = $generator->create_course(); $teacher = $USER; $generator->enrol_user($teacher->id, $course1->id, 'editingteacher'); $generator->enrol_user($teacher->id, $course2->id, 'editingteacher'); // Create a quiz with questions in the first course. $quiz = $this->create_test_quiz($course1); $qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]); $context = \context_module::instance($qbank->cmid); $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); // Create a question category. $cat = $questiongenerator->create_question_category(['contextid' => $context->id]); // Create questions and add to the quiz. $q1 = $questiongenerator->create_question('truefalse', null, [ 'category' => $cat->id, 'questiontext' => ['text' => "

Question

\r\n

One

", 'format' => FORMAT_MOODLE] ]); $q2 = $questiongenerator->create_question('truefalse', null, [ 'category' => $cat->id, 'questiontext' => ['text' => "

Question

\n

Two

", 'format' => FORMAT_MOODLE] ]); // Add question to quiz. quiz_add_quiz_question($q1->id, $quiz); quiz_add_quiz_question($q2->id, $quiz); // Capture original question IDs for verification after import. $modules1 = get_fast_modinfo($course1->id)->get_instances_of('quiz'); $module1 = reset($modules1); $originalslots = \mod_quiz\question\bank\qbank_helper::get_question_structure( $module1->instance, $module1->context); $originalquestionids = []; foreach ($originalslots as $slot) { array_push($originalquestionids, intval($slot->questionid)); } $this->assertCount(2, get_questions_category($cat, false)); // Step 2: Backup the quiz $bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz->cmid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Step 3: Import the backup into the same course. $rc = new restore_controller($backupid, $course1->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, backup::TARGET_CURRENT_ADDING); $rc->execute_precheck(); $rc->execute_plan(); $rc->destroy(); // Verify the question ids from the new quiz match the first. $modules2 = get_fast_modinfo($course1->id)->get_instances_of('quiz'); $this->assertCount(2, $modules2); $module2 = end($modules2); $copyslots = \mod_quiz\question\bank\qbank_helper::get_question_structure( $module2->instance, $module2->context); foreach ($copyslots as $slot) { $this->assertContains(intval($slot->questionid), $originalquestionids); } // The category should still only contain 2 question, neither question should be duplicated. $this->assertCount(2, get_questions_category($cat, false)); } /** * Return a list of qtypes with valid generators in their helper class. * * This will check all installed qtypes for a test helper class, then find a defined test question which has a corresponding * form_data method and return it. If the helper doesn't have a form_data method for any test question, it will return a * null test question name for that qtype. * * @return array */ public static function get_qtype_generators(): array { global $CFG; $generators = []; foreach (\core\plugin_manager::instance()->get_plugins_of_type('qtype') as $qtype) { if ($qtype->name == 'random') { continue; } $helperpath = "{$CFG->dirroot}/question/type/{$qtype->name}/tests/helper.php"; if (!file_exists($helperpath)) { continue; } require_once($helperpath); $helperclass = "qtype_{$qtype->name}_test_helper"; if (!class_exists($helperclass)) { continue; } $helper = new $helperclass(); $testquestion = null; foreach ($helper->get_test_questions() as $question) { if (method_exists($helper, "get_{$qtype->name}_question_form_data_{$question}")) { $testquestion = $question; break; } } $generators[$qtype->name] = [ 'qtype' => $qtype->name, 'testquestion' => $testquestion, ]; } return $generators; } /** * Restore a quiz with questions of same stamp into the same course, but different answers. * * @dataProvider get_qtype_generators * @param string $qtype The name of the qtype plugin to test * @param ?string $testquestion The test question to generate for the plugin. If null, the plugin will be skipped * with a message. */ public function test_restore_quiz_with_same_stamp_questions(string $qtype, ?string $testquestion): void { global $DB, $USER; if (is_null($testquestion)) { $this->markTestSkipped( "Cannot test qtype_{$qtype} as there is no test question with a form_data method in the " . "test helper class." ); } $this->resetAfterTest(); $this->setAdminUser(); // Create a course and a user with editing teacher capabilities. $generator = $this->getDataGenerator(); $course1 = $generator->create_course(); $teacher = $USER; $generator->enrol_user($teacher->id, $course1->id, 'editingteacher'); $qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]); $context = \context_module::instance($qbank->cmid); $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); // Create a question category. $cat = $questiongenerator->create_question_category(['contextid' => $context->id]); // Create 2 quizzes with 2 questions multichoice. $quiz1 = $this->create_test_quiz($course1); $question1 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]); quiz_add_quiz_question($question1->id, $quiz1, 0); $question2 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]); quiz_add_quiz_question($question2->id, $quiz1, 0); // Update question2 to have the same stamp as question1. $DB->set_field('question', 'stamp', $question1->stamp, ['id' => $question2->id]); // Change the answers of the question2 to be different to question1. $question2data = \question_bank::load_question_data($question2->id); if (!isset($question2data->options->answers) || empty($question2data->options->answers)) { $this->markTestSkipped( "Cannot test edited answers for qtype_{$qtype} as it does not use answers.", ); } foreach ($question2data->options->answers as $answer) { $DB->set_field('question_answers', 'answer', 'edited', ['id' => $answer->id]); } // Backup quiz1. $bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz1->cmid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Restore the backup into the same course. $rc = new restore_controller($backupid, $course1->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, backup::TARGET_CURRENT_ADDING); $rc->execute_precheck(); $rc->execute_plan(); $rc->destroy(); // Verify that the newly-restored quiz uses the same question as quiz2. $modules = get_fast_modinfo($course1->id)->get_instances_of('quiz'); $this->assertCount(2, $modules); $quiz2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure( $quiz1->id, \context_module::instance($quiz1->cmid), ); $quiz2 = end($modules); $quiz2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz2->instance, $quiz2->context); $this->assertEquals($quiz2structure[1]->questionid, $quiz2structure[1]->questionid); $this->assertEquals($quiz2structure[2]->questionid, $quiz2structure[2]->questionid); } /** * Restore a quiz with duplicate questions (same stamp and questions) into the same course. * * This is a contrived case, but this test serves as a control for the other tests in this class, proving that the hashing * process will match an identical question. * * @dataProvider get_qtype_generators * @param string $qtype The name of the qtype plugin to test * @param ?string $testquestion The test question to generate for the plugin. If null, the plugin will be skipped * with a message. */ public function test_restore_quiz_with_duplicate_questions(string $qtype, ?string $testquestion): void { global $DB, $USER; if (is_null($testquestion)) { $this->markTestSkipped( "Cannot test qtype_{$qtype} as there is no test question with a form_data method in the " . "test helper class." ); } $this->resetAfterTest(); $this->setAdminUser(); // Create a course and a user with editing teacher capabilities. $generator = $this->getDataGenerator(); $course1 = $generator->create_course(); $teacher = $USER; $generator->enrol_user($teacher->id, $course1->id, 'editingteacher'); $qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]); $context = \context_module::instance($qbank->cmid); $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); // Create a question category. $cat = $questiongenerator->create_question_category(['contextid' => $context->id]); // Create a quiz with 2 identical but separate questions. $quiz1 = $this->create_test_quiz($course1); $question1 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]); quiz_add_quiz_question($question1->id, $quiz1, 0); $question2 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]); quiz_add_quiz_question($question2->id, $quiz1, 0); // Update question2 to have the same times and stamp as question1. $DB->update_record('question', [ 'id' => $question2->id, 'stamp' => $question1->stamp, 'timecreated' => $question1->timecreated, 'timemodified' => $question1->timemodified, ]); // Backup quiz. $bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz1->cmid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Restore the backup into the same course. $rc = new restore_controller($backupid, $course1->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, backup::TARGET_CURRENT_ADDING); $rc->execute_precheck(); $rc->execute_plan(); $rc->destroy(); // Expect that the restored quiz will have the second question in both its slots // by virtue of identical stamp, version, and hash of question answer texts. $modules = get_fast_modinfo($course1->id)->get_instances_of('quiz'); $this->assertCount(2, $modules); $quiz2 = end($modules); $quiz2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz2->instance, $quiz2->context); $this->assertEquals($quiz2structure[1]->questionid, $quiz2structure[2]->questionid); } /** * Restore a quiz with questions that have the same stamp but different text. * * @dataProvider get_qtype_generators * @param string $qtype The name of the qtype plugin to test * @param ?string $testquestion The test question to generate for the plugin. If null, the plugin will be skipped * with a message. */ public function test_restore_quiz_with_edited_questions(string $qtype, ?string $testquestion): void { global $DB, $USER; if (is_null($testquestion)) { $this->markTestSkipped( "Cannot test qtype_{$qtype} as there is no test question with a form_data method in the " . "test helper class." ); } $this->resetAfterTest(); $this->setAdminUser(); // Create a course and a user with editing teacher capabilities. $generator = $this->getDataGenerator(); $course1 = $generator->create_course(); $teacher = $USER; $generator->enrol_user($teacher->id, $course1->id, 'editingteacher'); $qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]); $context = \context_module::instance($qbank->cmid); $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); // Create a question category. $cat = $questiongenerator->create_question_category(['contextid' => $context->id]); // Create a quiz with 2 identical but separate questions. $quiz1 = $this->create_test_quiz($course1); $question1 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]); quiz_add_quiz_question($question1->id, $quiz1); $question2 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]); // Edit question 2 to have the same stamp and times as question1, but different text. $DB->update_record('question', [ 'id' => $question2->id, 'questiontext' => 'edited', 'stamp' => $question1->stamp, 'timecreated' => $question1->timecreated, 'timemodified' => $question1->timemodified, ]); quiz_add_quiz_question($question2->id, $quiz1); // Backup quiz. $bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz1->cmid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Restore the backup into the same course. $rc = new restore_controller($backupid, $course1->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, backup::TARGET_CURRENT_ADDING); $rc->execute_precheck(); $rc->execute_plan(); $rc->destroy(); // The quiz should contain both questions, as they have different text. $modules = get_fast_modinfo($course1->id)->get_instances_of('quiz'); $this->assertCount(2, $modules); $quiz2 = end($modules); $quiz2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz2->instance, $quiz2->context); $this->assertEquals($quiz2structure[1]->questionid, $question1->id); $this->assertEquals($quiz2structure[2]->questionid, $question2->id); } /** * Restore a course to another course having questions with the same stamp in a shared question bank context category. * * @dataProvider get_qtype_generators * @param string $qtype The name of the qtype plugin to test * @param ?string $testquestion The test question to generate for the plugin. If null, the plugin will be skipped * with a message. */ public function test_restore_course_with_same_stamp_questions(string $qtype, ?string $testquestion): void { global $DB, $USER; if (is_null($testquestion)) { $this->markTestSkipped( "Cannot test qtype_{$qtype} as there is no test question with a form_data method in the " . "test helper class." ); } $this->resetAfterTest(); $this->setAdminUser(); // Create two courses and a user with editing teacher capabilities. $generator = $this->getDataGenerator(); $course1 = $generator->create_course(); $course2 = $generator->create_course(); $qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course2->id]); $teacher = $USER; $generator->enrol_user($teacher->id, $course1->id, 'editingteacher'); $generator->enrol_user($teacher->id, $course2->id, 'editingteacher'); $context = \context_module::instance($qbank->cmid); $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); // Create a question category. $cat = $questiongenerator->create_question_category(['contextid' => $context->id]); // Create quiz with question. $quiz1 = $this->create_test_quiz($course1); $question1 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]); quiz_add_quiz_question($question1->id, $quiz1, 0); $quiz2 = $this->create_test_quiz($course1); $question2 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]); quiz_add_quiz_question($question2->id, $quiz2, 0); // Update question2 to have the same stamp as question1. $DB->set_field('question', 'stamp', $question1->stamp, ['id' => $question2->id]); // Change the answers of the question2 to be different to question1. $question2data = \question_bank::load_question_data($question2->id); if (!isset($question2data->options->answers) || empty($question2data->options->answers)) { $this->markTestSkipped( "Cannot test edited answers for qtype_{$qtype} as it does not use answers.", ); } if ($DB->count_records('question_answers') === 0) { $this->markTestSkipped( "Cannot test edited answers for qtype_{$qtype} as it does not use the question_answers table.", ); } foreach ($question2data->options->answers as $answer) { $answer->answer = 'New answer ' . $answer->id; $DB->update_record('question_answers', $answer); } $course1q1structure = \mod_quiz\question\bank\qbank_helper::get_question_structure( $quiz1->id, \context_module::instance($quiz1->cmid)); $this->assertEquals($question1->id, $course1q1structure[1]->questionid); $course1q2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure( $quiz2->id, \context_module::instance($quiz2->cmid)); $this->assertEquals($question2->id, $course1q2structure[1]->questionid); // Backup course1. $bc = new backup_controller(backup::TYPE_1COURSE, $course1->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Restore the backup, adding to course2. $rc = new restore_controller($backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, backup::TARGET_CURRENT_ADDING); $rc->execute_precheck(); $rc->execute_plan(); $rc->destroy(); // Verify that the newly-restored course's quizzes use the same questions as their counterparts of course1. $modules = get_fast_modinfo($course2->id)->get_instances_of('quiz'); $course1q1structure = \mod_quiz\question\bank\qbank_helper::get_question_structure( $quiz1->id, \context_module::instance($quiz1->cmid)); $course2quiz1 = array_shift($modules); $course2q1structure = \mod_quiz\question\bank\qbank_helper::get_question_structure( $course2quiz1->instance, $course2quiz1->context); $this->assertEquals($question1->id, $course1q1structure[1]->questionid); $this->assertEquals($question1->id, $course2q1structure[1]->questionid); $course1q2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure( $quiz2->id, \context_module::instance($quiz2->cmid)); $course2quiz2 = array_shift($modules); $course2q2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure( $course2quiz2->instance, $course2quiz2->context); $this->assertEquals($question2->id, $course1q2structure[1]->questionid); $this->assertEquals($question2->id, $course2q2structure[1]->questionid); } /** * Restore a quiz with questions of same stamp into the same course, but different hints. * * @dataProvider get_qtype_generators * @param string $qtype The name of the qtype plugin to test * @param ?string $testquestion The test question to generate for the plugin. If null, the plugin will be skipped * with a message. */ public function test_restore_quiz_with_same_stamp_questions_edited_hints(string $qtype, ?string $testquestion): void { global $DB, $USER; if (is_null($testquestion)) { $this->markTestSkipped( "Cannot test qtype_{$qtype} as there is no test question with a form_data method in the " . "test helper class." ); } $this->resetAfterTest(); $this->setAdminUser(); // Create a course and a user with editing teacher capabilities. $generator = $this->getDataGenerator(); $course1 = $generator->create_course(); $teacher = $USER; $generator->enrol_user($teacher->id, $course1->id, 'editingteacher'); $qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]); $context = \context_module::instance($qbank->cmid); $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); // Create a question category. $cat = $questiongenerator->create_question_category(['contextid' => $context->id]); // Create 2 questions multichoice. $quiz1 = $this->create_test_quiz($course1); $question1 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]); quiz_add_quiz_question($question1->id, $quiz1, 0); $question2 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]); quiz_add_quiz_question($question2->id, $quiz1, 0); // Update question2 to have the same stamp as question1. $DB->set_field('question', 'stamp', $question1->stamp, ['id' => $question2->id]); // Change the hints of the question2 to be different to question1. $hints = $DB->get_records('question_hints', ['questionid' => $question2->id]); if (empty($hints)) { $this->markTestSkipped( "Cannot test edited hints for qtype_{$qtype} as test question {$testquestion} does not use hints.", ); } foreach ($hints as $hint) { $DB->set_field('question_hints', 'hint', "{$hint->hint} edited", ['id' => $hint->id]); } // Backup quiz1. $bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz1->cmid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Restore the backup into the same course. $rc = new restore_controller($backupid, $course1->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, backup::TARGET_CURRENT_ADDING); $rc->execute_precheck(); $rc->execute_plan(); $rc->destroy(); // Verify that the newly-restored quiz uses the same question as quiz2. $modules = get_fast_modinfo($course1->id)->get_instances_of('quiz'); $this->assertCount(2, $modules); $quiz1structure = \mod_quiz\question\bank\qbank_helper::get_question_structure( $quiz1->id, \context_module::instance($quiz1->cmid), ); $quiz2 = end($modules); $quiz2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz2->instance, $quiz2->context); $this->assertEquals($quiz1structure[1]->questionid, $quiz2structure[1]->questionid); $this->assertEquals($quiz1structure[2]->questionid, $quiz2structure[2]->questionid); } /** * Return a set of options fields and new values. * * @return array */ public static function get_edited_option_fields(): array { return [ 'single' => [ 'single', '0', ], 'shuffleanswers' => [ 'shuffleanswers', '0', ], 'answernumbering' => [ 'answernumbering', 'ABCD', ], 'shownumcorrect' => [ 'shownumcorrect', '0', ], 'showstandardinstruction' => [ 'showstandardinstruction', '1', ], 'correctfeedback' => [ 'correctfeedback', 'edited', ], 'partiallycorrectfeedback' => [ 'partiallycorrectfeedback', 'edited', ], 'incorrectfeedback' => [ 'incorrectfeedback', 'edited', ], ]; } /** * Restore a quiz with questions of same stamp into the same course, but different qtype-specific options. * * @dataProvider get_edited_option_fields * @param string $field The answer field to edit * @param string $value The value to set */ public function test_restore_quiz_with_same_stamp_questions_edited_options(string $field, string $value): void { global $DB, $USER; $this->resetAfterTest(); $this->setAdminUser(); // Create a course and a user with editing teacher capabilities. $generator = $this->getDataGenerator(); $course1 = $generator->create_course(); $teacher = $USER; $generator->enrol_user($teacher->id, $course1->id, 'editingteacher'); $qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]); $context = \context_module::instance($qbank->cmid); $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); // Create a question category. $cat = $questiongenerator->create_question_category(['contextid' => $context->id]); // A quiz with 2 multichoice questions. $quiz1 = $this->create_test_quiz($course1); $question1 = $questiongenerator->create_question('multichoice', 'one_of_four', ['category' => $cat->id]); quiz_add_quiz_question($question1->id, $quiz1, 0); $question2 = $questiongenerator->create_question('multichoice', 'one_of_four', ['category' => $cat->id]); quiz_add_quiz_question($question2->id, $quiz1, 0); // Update question2 to have the same stamp as question1. $DB->set_field('question', 'stamp', $question1->stamp, ['id' => $question2->id]); // Change the options of question2 to be different to question1. $DB->set_field('qtype_multichoice_options', $field, $value, ['questionid' => $question2->id]); // Backup quiz. $bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz1->cmid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Restore the backup into the same course. $rc = new restore_controller($backupid, $course1->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id, backup::TARGET_CURRENT_ADDING); $rc->execute_precheck(); $rc->execute_plan(); $rc->destroy(); // Verify that the newly-restored quiz questions match their quiz1 counterparts. $modules = get_fast_modinfo($course1->id)->get_instances_of('quiz'); $this->assertCount(2, $modules); $quiz1structure = \mod_quiz\question\bank\qbank_helper::get_question_structure( $quiz1->id, \context_module::instance($quiz1->cmid), ); $quiz2 = end($modules); $quiz2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz2->instance, $quiz2->context); $this->assertEquals($quiz1structure[1]->questionid, $quiz2structure[1]->questionid); $this->assertEquals($quiz1structure[2]->questionid, $quiz2structure[2]->questionid); } }