ÿØÿà 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-Ї Ç PKC~\|zBBmigrate_message_data_test.phpnu[. namespace core_message\task; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/message/tests/messagelib_test.php'); /** * Class for testing the migrate message data task. * * @package core_message * @category test * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class migrate_message_data_test extends \advanced_testcase { /** * Test set up. * * This is executed before running any test in this file. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * Test migrating legacy messages. */ public function test_migrating_messages(): void { global $DB; // Create users to test with. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); // Get the current time minus some, to make sure our data is migrated accurately and just not using the current timestamp. $now = time(); $timedeleted1 = $now - (2 * DAYSECS); $timedeleted2 = $now - (2 * DAYSECS) + 1; $timeread1 = $now - DAYSECS; $timeread2 = $now - DAYSECS + 1; $timeread3 = $now - DAYSECS + 2; // Send messages from user 1 to user 2. $m1 = $this->create_legacy_message_or_notification($user1->id, $user2->id, 1, false, $timeread1); $m2 = $this->create_legacy_message_or_notification($user1->id, $user2->id, 2); $m3 = $this->create_legacy_message_or_notification($user1->id, $user2->id, 3); // Send messages from user 3 to user 1. $m4 = $this->create_legacy_message_or_notification($user3->id, $user1->id, 4, false, $timeread2); $m5 = $this->create_legacy_message_or_notification($user3->id, $user1->id, 5); $m6 = $this->create_legacy_message_or_notification($user3->id, $user1->id, 6); // Send messages from user 3 to user 2. $m7 = $this->create_legacy_message_or_notification($user3->id, $user2->id, 7, false, $timeread3); $m8 = $this->create_legacy_message_or_notification($user3->id, $user2->id, 8); $m9 = $this->create_legacy_message_or_notification($user3->id, $user2->id, 9); // Let's delete some messages, not using API here as it does not use the legacy tables. $messageupdate = new \stdClass(); $messageupdate->id = $m1; $messageupdate->timeusertodeleted = $timedeleted1; $DB->update_record('message_read', $messageupdate); $messageupdate = new \stdClass(); $messageupdate->id = $m5; $messageupdate->timeuserfromdeleted = $timedeleted2; $DB->update_record('message', $messageupdate); // Now, let's execute the task for user 1. $task = new migrate_message_data(); $task->set_custom_data( [ 'userid' => $user1->id ] ); $task->execute(); // Ok, now we need to confirm all is good. // Remember - we are only converting the messages related to user 1. $this->assertEquals(2, $DB->count_records('message')); $this->assertEquals(1, $DB->count_records('message_read')); $this->assertEquals(6, $DB->count_records('messages')); $this->assertEquals(0, $DB->count_records('notifications')); $this->assertEquals(0, $DB->count_records('message_popup_notifications')); // Get the conversations. $conversation1 = \core_message\api::get_conversation_between_users([$user1->id, $user2->id]); $conversation2 = \core_message\api::get_conversation_between_users([$user1->id, $user3->id]); // Confirm what we have in the messages table is correct. $messages = $DB->get_records('messages', [], 'timecreated ASC'); $i = 1; foreach ($messages as $message) { $useridfrom = $user1->id; $conversationid = $conversation1; if ($i > 3) { $useridfrom = $user3->id; $conversationid = $conversation2; } if ($i == 1) { $messagereadid1 = $message->id; $messagedeletedid1 = $message->id; } else if ($i == 4) { $messagereadid2 = $message->id; } else if ($i == 5) { $messagedeletedid2 = $message->id; } $this->assertEquals($useridfrom, $message->useridfrom); $this->assertEquals($conversationid, $message->conversationid); $this->assertEquals('Subject ' . $i, $message->subject); $this->assertEquals('Full message ' . $i, $message->fullmessage); $this->assertEquals(FORMAT_PLAIN, $message->fullmessageformat); $this->assertEquals('Full message HTML '. $i, $message->fullmessagehtml); $this->assertEquals('Small message ' . $i, $message->smallmessage); $this->assertEquals($i, $message->timecreated); $i++; } // Confirm there are 4 actions. $this->assertEquals(4, $DB->count_records('message_user_actions')); // Confirm the messages that were marked as read have actions associated with them. $muas = $DB->get_records('message_user_actions', ['action' => \core_message\api::MESSAGE_ACTION_READ], 'timecreated DESC'); $this->assertCount(2, $muas); // Message user action for message read by user 1 (referring to $m4). $mua1 = array_shift($muas); // Message user action for message read by user 2 (referring to $m1). $mua2 = array_shift($muas); $this->assertEquals($user1->id, $mua1->userid); $this->assertEquals($messagereadid2, $mua1->messageid); $this->assertEquals($timeread2, $mua1->timecreated); $this->assertEquals($user2->id, $mua2->userid); $this->assertEquals($messagereadid1, $mua2->messageid); $this->assertEquals($timeread1, $mua2->timecreated); // Confirm the messages that were deleted have actions associated with them. $muas = $DB->get_records('message_user_actions', ['action' => \core_message\api::MESSAGE_ACTION_DELETED], 'timecreated DESC'); $this->assertCount(2, $muas); // Message user action for message deleted by user 3 (referring to $m5). $mua1 = array_shift($muas); // Message user action for message deleted by user 2 (referring to $m1). $mua2 = array_shift($muas); $this->assertEquals($user3->id, $mua1->userid); $this->assertEquals($messagedeletedid2, $mua1->messageid); $this->assertEquals($timedeleted2, $mua1->timecreated); $this->assertEquals($user2->id, $mua2->userid); $this->assertEquals($messagedeletedid1, $mua2->messageid); $this->assertEquals($timedeleted1, $mua2->timecreated); } /** * Test migrating legacy notifications. */ public function test_migrating_notifications(): void { global $DB; // Create users to test with. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); // Get the current time minus some, to make sure our data is migrated accurately and just not using the current timestamp. $timeread = time() - DAYSECS; // Send notifications from user 1 to user 2. $this->create_legacy_message_or_notification($user1->id, $user2->id, 1, true, $timeread); $this->create_legacy_message_or_notification($user1->id, $user2->id, 2, true); $this->create_legacy_message_or_notification($user1->id, $user2->id, 3, true); // Send notifications from user 3 to user 1. $this->create_legacy_message_or_notification($user3->id, $user1->id, 4, true, $timeread); $this->create_legacy_message_or_notification($user3->id, $user1->id, 5, true); $this->create_legacy_message_or_notification($user3->id, $user1->id, 6, true); // Send notifications from user 3 to user 2. $this->create_legacy_message_or_notification($user3->id, $user2->id, 7, true, $timeread); $this->create_legacy_message_or_notification($user3->id, $user2->id, 8, true); $this->create_legacy_message_or_notification($user3->id, $user2->id, 9, true); // Now, let's execute the task for user 1. $task = new migrate_message_data(); $task->set_custom_data( [ 'userid' => $user1->id ] ); $task->execute(); // Ok, now we need to confirm all is good. // Remember - we are only converting the notifications related to user 1. $this->assertEquals(2, $DB->count_records('message')); $this->assertEquals(1, $DB->count_records('message_read')); $this->assertEquals(3, $DB->count_records('message_popup')); $this->assertEquals(6, $DB->count_records('notifications')); $this->assertEquals(6, $DB->count_records('message_popup_notifications')); // Confirm what we have in the notifications table is correct. $notifications = $DB->get_records('notifications', [], 'timecreated ASC'); $popupnotifications = $DB->get_records('message_popup_notifications', [], 'notificationid ASC', 'notificationid'); $i = 1; foreach ($notifications as $notification) { // Assert the correct id is stored in the 'message_popup_notifications' table. $this->assertArrayHasKey($notification->id, $popupnotifications); $useridfrom = $user1->id; $useridto = $user2->id; if ($i > 3) { $useridfrom = $user3->id; $useridto = $user1->id; } $this->assertEquals($useridfrom, $notification->useridfrom); $this->assertEquals($useridto, $notification->useridto); $this->assertEquals('Subject ' . $i, $notification->subject); $this->assertEquals('Full message ' . $i, $notification->fullmessage); $this->assertEquals(FORMAT_PLAIN, $notification->fullmessageformat); $this->assertEquals('Full message HTML '. $i, $notification->fullmessagehtml); $this->assertEquals('Small message ' . $i, $notification->smallmessage); $this->assertEquals('mod_assign', $notification->component); $this->assertEquals('assign_notification', $notification->eventtype); $this->assertEquals('https://www.google.com', $notification->contexturl); $this->assertEquals('google', $notification->contexturlname); $this->assertEquals($i, $notification->timecreated); if (($i == 1) || ($i == 4)) { $this->assertEquals($timeread, $notification->timeread); } else { $this->assertNull($notification->timeread); } $i++; } } /** * Test migrating a legacy message that contains null as the format. */ public function test_migrating_message_null_format(): void { global $DB; // Create users to test with. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->create_legacy_message_or_notification($user1->id, $user2->id, null, false, null, null); // Now, let's execute the task for user 1. $task = new migrate_message_data(); $task->set_custom_data( [ 'userid' => $user1->id ] ); $task->execute(); $messages = $DB->get_records('messages'); $this->assertCount(1, $messages); $message = reset($messages); $this->assertEquals(FORMAT_MOODLE, $message->fullmessageformat); } /** * Test migrating a legacy notification that contains null as the format. */ public function test_migrating_notification_null_format(): void { global $DB; // Create users to test with. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->create_legacy_message_or_notification($user1->id, $user2->id, null, true, null, null); // Now, let's execute the task for user 1. $task = new migrate_message_data(); $task->set_custom_data( [ 'userid' => $user1->id ] ); $task->execute(); $notifications = $DB->get_records('notifications'); $this->assertCount(1, $notifications); $notification = reset($notifications); $this->assertEquals(FORMAT_MOODLE, $notification->fullmessageformat); } /** * Test migrating a legacy message that a user sent to themselves then deleted. */ public function test_migrating_message_deleted_message_sent_to_self(): void { global $DB; // Create user to test with. $user1 = $this->getDataGenerator()->create_user(); $m1 = $this->create_legacy_message_or_notification($user1->id, $user1->id, null, false, null, null); // Let's delete the message for the 'user to' and 'user from' which in this case is the same user. $messageupdate = new \stdClass(); $messageupdate->id = $m1; $messageupdate->timeuserfromdeleted = time(); $messageupdate->timeusertodeleted = time(); $DB->update_record('message', $messageupdate); // Now, let's execute the task for the user. $task = new migrate_message_data(); $task->set_custom_data( [ 'userid' => $user1->id ] ); $task->execute(); $this->assertEquals(0, $DB->count_records('message')); $this->assertEquals(1, $DB->count_records('message_user_actions')); } /** * Creates a legacy message or notification to be used for testing. * * @param int $useridfrom The user id from * @param int $useridto The user id to * @param int $timecreated * @param bool $notification * @param int|null $timeread The time the message/notification was read, null if it hasn't been. * @param string|int|null $format The format of the message. * @return int The id of the message (in either the message or message_read table) * @throws dml_exception */ private function create_legacy_message_or_notification($useridfrom, $useridto, $timecreated = null, $notification = false, $timeread = null, $format = FORMAT_PLAIN) { global $DB; $tabledata = new \stdClass(); if (is_null($timecreated)) { $timecreated = time(); } if (!is_null($timeread)) { $table = 'message_read'; $tabledata->timeread = $timeread; } else { $table = 'message'; } if ($notification) { $tabledata->eventtype = 'assign_notification'; $tabledata->component = 'mod_assign'; $tabledata->notification = 1; $tabledata->contexturl = 'https://www.google.com'; $tabledata->contexturlname = 'google'; } else { $tabledata->eventtype = 'instantmessage'; $tabledata->component = 'moodle'; $tabledata->notification = 0; } $tabledata->useridfrom = $useridfrom; $tabledata->useridto = $useridto; $tabledata->subject = 'Subject ' . $timecreated; $tabledata->fullmessage = 'Full message ' . $timecreated; $tabledata->fullmessageformat = $format; $tabledata->fullmessagehtml = 'Full message HTML ' . $timecreated; $tabledata->smallmessage = 'Small message ' . $timecreated; $tabledata->timecreated = $timecreated; $id = $DB->insert_record($table, $tabledata); // Insert into the legacy 'message_popup' table if it is a notification. if ($notification) { $mp = new \stdClass(); $mp->messageid = $id; $mp->isread = (!is_null($timeread)) ? 1 : 0; $DB->insert_record('message_popup', $mp); } return $id; } } PKp~\8 sync_task.phpnu[. /** * A scheduled task for LDAP user sync. * * @package auth_ldap * @copyright 2015 Vadim Dvorovenko * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_ldap\task; /** * A scheduled task class for LDAP user sync. * * @copyright 2015 Vadim Dvorovenko * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sync_task extends \core\task\scheduled_task { /** @var string Message prefix for mtrace */ protected const MTRACE_MSG = 'Synced ldap users'; /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('synctask', 'auth_ldap'); } /** * Run users sync. */ public function execute() { if (is_enabled_auth('ldap')) { /** @var auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); $count = 0; $auth->sync_users_update_callback(function ($users, $updatekeys) use (&$count) { $asynctask = new asynchronous_sync_task(); $asynctask->set_custom_data([ 'users' => $users, 'updatekeys' => $updatekeys, ]); \core\task\manager::queue_adhoc_task($asynctask); $count++; mtrace(sprintf(" %s (%d)", self::MTRACE_MSG, $count)); sleep(1); }); } } } PKp~\Esync_roles.phpnu[. /** * A scheduled task for LDAP roles sync. * * @package auth_ldap * @author David Balch * @copyright 2017 The Chancellor Masters and Scholars of the University of Oxford {@link http://www.tall.ox.ac.uk} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_ldap\task; defined('MOODLE_INTERNAL') || die(); /** * A scheduled task class for LDAP roles sync. * * @author David Balch * @copyright 2017 The Chancellor Masters and Scholars of the University of Oxford {@link http://www.tall.ox.ac.uk} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sync_roles extends \core\task\scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('syncroles', 'auth_ldap'); } /** * Synchronise role assignments from LDAP. */ public function execute() { global $DB; if (is_enabled_auth('ldap')) { $auth = get_auth_plugin('ldap'); $users = $DB->get_records('user', array('auth' => 'ldap')); foreach ($users as $user) { $auth->sync_roles($user); } } } } PKp~\*asynchronous_sync_task.phpnu[. /** * Adhoc task for LDAP user sync. * * @package auth_ldap * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_ldap\task; use core\task\adhoc_task; /** * Adhoc task class for LDAP user sync. * * @package auth_ldap * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class asynchronous_sync_task extends adhoc_task { /** @var string Message prefix for mtrace */ protected const MTRACE_MSG = 'Synced ldap users'; /** * Constructor */ public function __construct() { $this->set_component('auth_ldap'); } /** * Run users sync. */ public function execute() { $data = $this->get_custom_data(); /** @var auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); $auth->update_users($data->users, $data->updatekeys); mtrace(sprintf(" %s (%d)", self::MTRACE_MSG, count($data->users))); } } PKs\conversion_cleanup_task.phpnu[. /** * A scheduled task to clear up old conversion records. * * @package core_files * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_files\task; defined('MOODLE_INTERNAL') || die(); /** * A scheduled task to clear up old conversion records. * * @package core_files * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class conversion_cleanup_task extends \core\task\scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('fileconversioncleanuptask', 'admin'); } /** * Run task. */ public function execute() { \core_files\conversion::remove_old_conversion_records(); \core_files\conversion::remove_orphan_records(); } } PKs\])++&asynchronous_mimetype_upgrade_task.phpnu[. namespace core_files\task; use core\task\adhoc_task; /** * Ad-hoc task that performs asynchronous upgrades of a given file type. * * This ad-hoc task can be scheduled during core upgrades. * * @package core_files * @copyright 2025 Daniel Ziegenberg * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class asynchronous_mimetype_upgrade_task extends adhoc_task { /** * Run the adhoc task and update the mime type of files. */ public function execute(): void { global $DB; // Upgrade mime type for existing files. $customdata = $this->get_custom_data(); foreach ($customdata->extensions as $extension) { mtrace("Updating mime type for files with extension *.{$extension} to {$customdata->mimetype}"); $condition = $DB->sql_like('filename', ":extension", false); $select = "{$condition} AND mimetype <> :mimetype"; $params = [ 'extension' => '%' . $DB->sql_like_escape(".{$extension}"), 'mimetype' => $customdata->mimetype, ]; $count = $DB->count_records_select('files', $select, $params); $DB->set_field_select('files', 'mimetype', $customdata->mimetype, $select, $params); mtrace("Updated {$count} files with extension *.{$extension} to {$customdata->mimetype}"); } } } PK:\ 77 task_test.phpnu[. namespace core_competency\task; use core_competency\api; use core_competency\plan; use core_competency\template; /** * Task tests. * * @package core_competency * @copyright 2015 Issam Taboubi * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class task_test extends \advanced_testcase { public function test_sync_plans_from_cohorts_task(): void { global $DB; $this->resetAfterTest(true); $this->setAdminUser(); $dg = $this->getDataGenerator(); $lpg = $dg->get_plugin_generator('core_competency'); // Sql to simulate the execution in time. $cmsql = "UPDATE {cohort_members} SET timeadded = :currenttime WHERE cohortid = :cohortid AND userid = :userid"; $tplsql = "UPDATE {" . template::TABLE . "} SET timemodified = :currenttime WHERE id = :templateid"; $plansql = "UPDATE {" . plan::TABLE . "} SET timemodified = :currenttime WHERE id = :planid"; $currenttime = time(); $user1 = $dg->create_user(); $user2 = $dg->create_user(); $user3 = $dg->create_user(); $user4 = $dg->create_user(); $user5 = $dg->create_user(); $cohort = $dg->create_cohort(); $tpl = $lpg->create_template(); // Add 2 users to the cohort. cohort_add_member($cohort->id, $user1->id); cohort_add_member($cohort->id, $user2->id); // Creating plans from template cohort. $templatecohort = api::create_template_cohort($tpl->get('id'), $cohort->id); $created = api::create_plans_from_template_cohort($tpl->get('id'), $cohort->id); $this->assertEquals(2, $created); $task = \core\task\manager::get_scheduled_task('\\core\\task\\sync_plans_from_template_cohorts_task'); $this->assertInstanceOf('\core\task\sync_plans_from_template_cohorts_task', $task); // Add two more users to the cohort. cohort_add_member($cohort->id, $user3->id); cohort_add_member($cohort->id, $user4->id); $currenttime = $currenttime + 1; $task->execute(); $task->set_last_run_time($currenttime); $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get('id')))); // Test if remove user from cohort will affect plans. cohort_remove_member($cohort->id, $user3->id); cohort_remove_member($cohort->id, $user4->id); $currenttime = $currenttime + 1; $task->execute(); $task->set_last_run_time($currenttime); $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get('id')))); // The template is now hidden, and I've added a user with a missing plan. Nothing should happen. $currenttime = $currenttime + 1; $tpl->set('visible', false); $tpl->update(); $DB->execute($tplsql, array('currenttime' => $currenttime, 'templateid' => $tpl->get('id'))); $currenttime = $currenttime + 1; cohort_add_member($cohort->id, $user5->id); $DB->execute($cmsql, array('currenttime' => $currenttime, 'cohortid' => $cohort->id, 'userid' => $user5->id)); $this->assertFalse(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get('id')))); $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get('id')))); $currenttime = $currenttime + 1; $task->execute(); $task->set_last_run_time($currenttime); $this->assertFalse(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get('id')))); $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get('id')))); // Now I set the template as visible again, the plan is created. $currenttime = $currenttime + 1; $tpl->set('visible', true); $tpl->update(); $DB->execute($tplsql, array('currenttime' => $currenttime, 'templateid' => $tpl->get('id'))); $currenttime = $currenttime + 1; $task->execute(); $task->set_last_run_time($currenttime); $this->assertTrue(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get('id')))); $this->assertEquals(5, plan::count_records(array('templateid' => $tpl->get('id')))); // Let's unlink the plan and run the task again, it should not be recreated. $currenttime = $currenttime + 1; $plan = plan::get_record(array('userid' => $user5->id, 'templateid' => $tpl->get('id'))); api::unlink_plan_from_template($plan); $DB->execute($plansql, array('currenttime' => $currenttime, 'planid' => $plan->get('id'))); $this->assertTrue(plan::record_exists_select('userid = ?', array($user5->id))); $this->assertFalse(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get('id')))); $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get('id')))); $currenttime = $currenttime + 1; $task->execute(); $task->set_last_run_time($currenttime); $this->assertTrue(plan::record_exists_select('userid = ?', array($user5->id))); $this->assertFalse(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get('id')))); $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get('id')))); // Adding users to cohort that already exist in plans. $currenttime = $currenttime + 1; cohort_add_member($cohort->id, $user3->id); cohort_add_member($cohort->id, $user4->id); $DB->execute($cmsql, array('currenttime' => $currenttime, 'cohortid' => $cohort->id, 'userid' => $user3->id)); $DB->execute($cmsql, array('currenttime' => $currenttime, 'cohortid' => $cohort->id, 'userid' => $user3->id)); $currenttime = $currenttime + 1; $task->execute(); $task->set_last_run_time($currenttime); $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get('id')))); // Test a user plan deleted will not be recreated. $currenttime = $currenttime + 1; $plan = plan::get_record(array('userid' => $user4->id, 'templateid' => $tpl->get('id'))); api::delete_plan($plan->get('id')); $currenttime = $currenttime + 1; $task->execute(); $task->set_last_run_time($currenttime); $this->assertEquals(3, plan::count_records(array('templateid' => $tpl->get('id')))); } public function test_sync_plans_from_cohorts_with_templateduedate_task(): void { $this->resetAfterTest(true); $this->setAdminUser(); $dg = $this->getDataGenerator(); $lpg = $dg->get_plugin_generator('core_competency'); $user1 = $dg->create_user(); $user2 = $dg->create_user(); $user3 = $dg->create_user(); $user4 = $dg->create_user(); $user5 = $dg->create_user(); $cohort = $dg->create_cohort(); $tpl = $lpg->create_template(array('duedate' => time() + 400)); // Add 2 users to the cohort. cohort_add_member($cohort->id, $user1->id); cohort_add_member($cohort->id, $user2->id); // Creating plans from template cohort. $templatecohort = api::create_template_cohort($tpl->get('id'), $cohort->id); $created = api::create_plans_from_template_cohort($tpl->get('id'), $cohort->id); $this->assertEquals(2, $created); $task = \core\task\manager::get_scheduled_task('\\core\\task\\sync_plans_from_template_cohorts_task'); $this->assertInstanceOf('\core\task\sync_plans_from_template_cohorts_task', $task); // Add two more users to the cohort. cohort_add_member($cohort->id, $user3->id); cohort_add_member($cohort->id, $user4->id); $task->execute(); $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get('id')))); // Test if remove user from cohort will affect plans. cohort_remove_member($cohort->id, $user3->id); cohort_remove_member($cohort->id, $user4->id); $task->execute(); $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get('id')))); // The template is now hidden, and I've added a user with a missing plan. Nothing should happen. $tpl->set('visible', false); $tpl->update(); cohort_add_member($cohort->id, $user5->id); $this->assertFalse(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get('id')))); $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get('id')))); $task->execute(); $this->assertFalse(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get('id')))); $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get('id')))); // Now I set the template as visible again, the plan is created. $tpl->set('visible', true); $tpl->update(); $task->execute(); $this->assertTrue(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get('id')))); $this->assertEquals(5, plan::count_records(array('templateid' => $tpl->get('id')))); // Let's unlink the plan and run the task again, it should not be recreated. $plan = plan::get_record(array('userid' => $user5->id, 'templateid' => $tpl->get('id'))); api::unlink_plan_from_template($plan); $this->assertTrue(plan::record_exists_select('userid = ?', array($user5->id))); $this->assertFalse(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get('id')))); $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get('id')))); $task->execute(); $this->assertTrue(plan::record_exists_select('userid = ?', array($user5->id))); $this->assertFalse(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get('id')))); $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get('id')))); // Adding users to cohort that already exist in plans. cohort_add_member($cohort->id, $user3->id); cohort_add_member($cohort->id, $user4->id); $task->execute(); $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get('id')))); } public function test_sync_plans_from_cohorts_with_passed_duedate(): void { global $DB; $this->resetAfterTest(true); $this->setAdminUser(); $dg = $this->getDataGenerator(); $lpg = $dg->get_plugin_generator('core_competency'); $user1 = $dg->create_user(); $user2 = $dg->create_user(); $cohort = $dg->create_cohort(); $tpl = $lpg->create_template(array('duedate' => time() + 1000)); $templatecohort = api::create_template_cohort($tpl->get('id'), $cohort->id); $task = \core\task\manager::get_scheduled_task('\\core\\task\\sync_plans_from_template_cohorts_task'); // Add 1 user to the cohort. cohort_add_member($cohort->id, $user1->id); // Creating plans from template cohort. $task->execute(); $this->assertEquals(1, plan::count_records()); // Now add another user, but this time the template will be expired. cohort_add_member($cohort->id, $user2->id); $record = $tpl->to_record(); $record->duedate = time() - 10000; $DB->update_record(template::TABLE, $record); $tpl->read(); $task->execute(); $this->assertEquals(1, plan::count_records()); // Still only one plan. // Pretend it wasn't expired. $tpl->set('duedate', time() + 100); $tpl->update(); $task->execute(); $this->assertEquals(2, plan::count_records()); // Now there is two. } public function test_complete_plans_task(): void { global $DB; $this->resetAfterTest(true); $this->setAdminUser(); $dg = $this->getDataGenerator(); $lpg = $dg->get_plugin_generator('core_competency'); $user = $dg->create_user(); $up1 = $lpg->create_plan(array('userid' => $user->id, 'status' => plan::STATUS_DRAFT)); $up2 = $lpg->create_plan(array('userid' => $user->id, 'status' => plan::STATUS_ACTIVE)); // Set duedate in the past. $date = new \DateTime('yesterday'); $record1 = $up1->to_record(); $record2 = $up2->to_record(); $record1->duedate = $date->getTimestamp(); $record2->duedate = $date->getTimestamp(); $DB->update_record(plan::TABLE, $record1); $DB->update_record(plan::TABLE, $record2); $task = \core\task\manager::get_scheduled_task('\\core\\task\\complete_plans_task'); $this->assertInstanceOf('\\core\\task\\complete_plans_task', $task); // Test that draft plan can not be completed on running task. $task->execute(); $plandraft = api::read_plan($up1->get('id')); $this->assertEquals(plan::STATUS_DRAFT, $plandraft->get('status')); // Test that active plan can be completed on running task. $task->execute(); $planactive = api::read_plan($up2->get('id')); $this->assertEquals(plan::STATUS_COMPLETE, $planactive->get('status')); } } PK)\Э3content_notification_task.phpnu[. namespace core_course\task; use core\task\adhoc_task; /** * Class handling course content updates notifications. * * @package core_course * @copyright 2021 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class content_notification_task extends adhoc_task { // Use the logging trait for better logging. use \core\task\logging_trait; /** * Run the main task. */ public function execute() { global $CFG, $OUTPUT; require_once($CFG->libdir . '/enrollib.php'); $data = $this->get_custom_data(); $course = get_course($data->courseid); $modinfo = get_fast_modinfo($course); $cm = $modinfo->cms[$data->cmid]; $isupdate = !empty($data->update); if (!$course->visible || !$cm->visible) { // The course or module is hidden. We don't check if the user can see hidden courses, does not make sense here. // Permissions may have changed since it was queued. return; } // Get only active users. $coursecontext = \context_course::instance($course->id); $users = get_enrolled_users($coursecontext, '', 0, 'u.*', null, 0, 0, true); if (empty($users)) { return; } $userfrom = \core_user::get_user($data->userfrom); // Now send the messages $countusers = count($users); $sentcount = $errorcount = 0; $this->log_start("Sending course content update notifications to {$countusers} potential users from user with id {$userfrom->id}."); foreach ($users as $user) { \core\cron::setup_user($user, $course); // Ensure that the activity is available/visible to the user. $cm = get_fast_modinfo($course)->cms[$cm->id]; if (!\core_availability\info_module::is_user_visible($cm, $user->id, false)) { $this->log("Ignoring user {$user->id} (no permissions to see the module)", 1); continue; } // Get module names in the user's language. $modnames = get_module_types_names(); $a = [ 'coursename' => format_string(get_course_display_name_for_list($course), true, ['context' => $coursecontext]), 'courselink' => (new \moodle_url('/course/view.php', ['id' => $course->id]))->out(false), 'modulename' => $cm->get_formatted_name(), 'moduletypename' => $modnames[$cm->modname], 'link' => (new \moodle_url('/mod/' . $cm->modname . '/view.php', ['id' => $cm->id]))->out(false), 'notificationpreferenceslink' => (new \moodle_url('/message/notificationpreferences.php', ['userid' => $user->id]))->out(false), ]; if ($isupdate) { $messagesubject = get_string('coursecontentnotifupdate', 'course', $a); $messagebody = get_string('coursecontentnotifupdatebody', 'course', $a); } else { $messagesubject = get_string('coursecontentnotifnew', 'course', $a); $messagebody = get_string('coursecontentnotifnewbody', 'course', $a); } // Send notification. $eventdata = new \core\message\message(); $eventdata->courseid = $course->id; $eventdata->component = 'moodle'; $eventdata->name = 'coursecontentupdated'; $eventdata->userfrom = $userfrom; $eventdata->userto = $user; $eventdata->subject = $messagesubject; $eventdata->fullmessageformat = FORMAT_HTML; $eventdata->fullmessagehtml = $messagebody; $eventdata->smallmessage = strip_tags($eventdata->fullmessagehtml); $eventdata->contexturl = (new \moodle_url('/mod/' . $cm->modname . '/view.php', ['id' => $cm->id]))->out(false); $eventdata->contexturlname = $cm->get_formatted_name(); $eventdata->notification = 1; // Add notification custom data. $eventcustomdata = ['notificationiconurl' => $cm->get_icon_url()->out(false)]; if ($courseimage = \core_course\external\course_summary_exporter::get_course_image($course)) { $eventcustomdata['notificationpictureurl'] = $courseimage; } $eventdata->customdata = $eventcustomdata; $activitydates = \core\activity_dates::get_dates_for_module($cm, $user->id); if (!empty($activitydates)) { $data = (new \core_course\output\activity_dates($activitydates))->export_for_template($OUTPUT); foreach ($data->activitydates as $date) { $eventdata->fullmessagehtml .= \html_writer::div($date['label'] . ' ' . $date['datestring']); } } $eventdata->fullmessage = html_to_text($eventdata->fullmessagehtml); if (message_send($eventdata)) { $this->log("Notification sent to user with id {$user->id}", 1); $sentcount++; } else { $this->log("Failed to send notification to user with id {$user->id}", 1); $errorcount++; } } $this->log_finish("Sent {$sentcount} notifications with {$errorcount} failures"); } } PK)\v))course_delete_modules.phpnu[. /** * Adhoc task handling course module deletion. * * @package core_course * @copyright 2016 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_course\task; defined('MOODLE_INTERNAL') || die(); /** * Class handling course module deletion. * * This task supports an array of course module object as custom_data, and calls course_delete_module() in synchronous deletion * mode for each of them. * This will: * 1. call any 'mod_xxx_pre_course_module_deleted' functions (e.g. Recycle bin) * 2. delete the module * 3. fire the deletion event * * @package core_course * @copyright 2016 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_delete_modules extends \core\task\adhoc_task { /** * Run the deletion task. * * @throws \coding_exception if the module could not be removed. */ public function execute() { global $CFG; require_once($CFG->dirroot. '/course/lib.php'); // Set the proper user. if ($this->get_custom_data()->userid !== $this->get_custom_data()->realuserid) { $realuser = \core_user::get_user($this->get_custom_data()->realuserid, '*', MUST_EXIST); \core\cron::setup_user($realuser); \core\session\manager::loginas($this->get_custom_data()->userid, \context_system::instance(), false); } else { $user = \core_user::get_user($this->get_custom_data()->userid, '*', MUST_EXIST); \core\cron::setup_user($user); } $cms = $this->get_custom_data()->cms; $exceptions = []; $cmsfailed = []; foreach ($cms as $key => $cm) { try { course_delete_module($cm->id); } catch (\Exception $e) { // Keep the information instead of throw an exception and continue with next cms. $exceptions[] = ("The course module {$cm->id} could not be deleted. " . "{$e->getMessage()}: {$e->getFile()}({$e->getLine()}) {$e->getTraceAsString()}"); // Save the cms that has failed to set the data only with this values. $cmsfailed[$key] = $cm; continue; } } // Throw the existing exceptions if there is any. if (!empty($exceptions)) { // Save the failed CMS. $customdata = $this->get_custom_data(); $customdata->cms = $cmsfailed; $this->set_custom_data($customdata); throw new \coding_exception("The following course modules could not be deleted:\n " . implode('\n', $exceptions)); } } /** * Sets attemptsavailable to false. * * @return boolean */ public function retry_until_success(): bool { return false; } } PK)\Ҥ t t regrade_final_grades.phpnu[. namespace core_course\task; defined('MOODLE_INTERNAL') || die(); use core\task\adhoc_task; require_once($CFG->libdir . '/gradelib.php'); /** * Asynchronously regrade a course. * * @copyright 2024 onwards Catalyst IT Europe Ltd. * @author Mark Johnson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_course */ class regrade_final_grades extends adhoc_task { use \core\task\logging_trait; use \core\task\stored_progress_task_trait; /** * Create and return an instance of this task for a given course ID. * * @param int $courseid * @return self */ public static function create(int $courseid): self { $task = new regrade_final_grades(); $task->set_custom_data((object)['courseid' => $courseid]); $task->set_component('core'); return $task; } /** * Run regrade_final_grades for the provided course id. * * @return void * @throws \dml_exception */ public function execute(): void { $data = $this->get_custom_data(); $this->start_stored_progress(); $this->log_start("Recalculating grades for course ID {$data->courseid}"); // Ensure the course exists. try { $course = get_course($data->courseid); } catch (\dml_missing_record_exception $e) { $this->log("Course with id {$data->courseid} not found. It may have been deleted. Skipping regrade."); return; } $this->log("Found course {$course->shortname}. Starting regrade."); $results = grade_regrade_final_grades($course->id, progress: $this->get_progress()); if (is_array($results)) { $this->log('Errors reported during regrade:'); foreach ($results as $id => $result) { $this->log("Grade item {$id}: {$result}", 2); } } $this->log_finish('Regrade complete.'); } } PK\Fx,EE*queue_all_quiz_open_notification_tasks.phpnu[. namespace mod_quiz\task; use core\task\scheduled_task; use mod_quiz\notification_helper; /** * Scheduled task to queue tasks for notifying about quizzes with an approaching open date. * * @package mod_quiz * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class queue_all_quiz_open_notification_tasks extends scheduled_task { public function get_name(): string { return get_string('sendnotificationopendatesoon', 'mod_quiz'); } public function execute(): void { $quizzes = notification_helper::get_quizzes_within_date_range(); foreach ($quizzes as $quiz) { $task = new queue_quiz_open_notification_tasks_for_users(); $task->set_custom_data($quiz); \core\task\manager::queue_adhoc_task($task, true); } $quizzes->close(); } } PK\dd0quiz_notify_attempt_manual_grading_completed.phpnu[. namespace mod_quiz\task; defined('MOODLE_INTERNAL') || die(); use context_module; use core_user; use mod_quiz\quiz_attempt; use moodle_recordset; use question_display_options; use mod_quiz\question\display_options; require_once($CFG->dirroot . '/mod/quiz/locallib.php'); /** * Cron Quiz Notify Attempts Graded Task. * * @package mod_quiz * @copyright 2021 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * */ class quiz_notify_attempt_manual_grading_completed extends \core\task\scheduled_task { /** * @var int|null For using in unit testing only. Override the time we consider as now. */ protected $forcedtime = null; #[\Override] public function get_name(): string { return get_string('notifyattemptsgradedtask', 'mod_quiz'); } /** * To let this class be unit tested, we wrap all accesses to the current time in this method. * * @return int The current time. */ protected function get_time(): int { if (PHPUNIT_TEST && $this->forcedtime !== null) { return $this->forcedtime; } return time(); } /** * For testing only, pretend the current time is different. * * @param int $time The time to set as the current time. */ public function set_time_for_testing(int $time): void { if (!PHPUNIT_TEST) { throw new \coding_exception('set_time_for_testing should only be used in unit tests.'); } $this->forcedtime = $time; } #[\Override] public function execute(): void { global $DB; mtrace('Looking for quiz attempts which may need a graded notification sent...'); $attempts = $this->get_list_of_attempts(); $course = null; $quiz = null; $cm = null; foreach ($attempts as $attempt) { mtrace('Checking attempt ' . $attempt->id . ' at quiz ' . $attempt->quiz . '.'); if (!$quiz || $attempt->quiz != $quiz->id) { $quiz = $DB->get_record('quiz', ['id' => $attempt->quiz], '*', MUST_EXIST); $cm = get_coursemodule_from_instance('quiz', $attempt->quiz); $quizcontext = context_module::instance($cm->id); } if (!$course || $course->id != $quiz->course) { $course = get_course($quiz->course); } $quiz = quiz_update_effective_access($quiz, $attempt->userid); $attemptobj = new quiz_attempt($attempt, $quiz, $cm, $course, false); $options = display_options::make_from_quiz($quiz, quiz_attempt_state($quiz, $attempt)); if ($options->manualcomment == question_display_options::HIDDEN) { // User cannot currently see the feedback, so don't message them. // However, this may change in future, so leave them on the list. mtrace('Not sending an email because manualcomment review option is not set.'); continue; } if (!has_capability('mod/quiz:emailnotifyattemptgraded', $quizcontext, $attempt->userid, false)) { // User not eligible to get a notification. Mark them done while doing nothing. mtrace('Not sending an email because user does not have mod/quiz:emailnotifyattemptgraded capability.'); $DB->set_field('quiz_attempts', 'gradednotificationsenttime', $attempt->timefinish, ['id' => $attempt->id]); continue; } // OK, send notification. mtrace('Sending email to user ' . $attempt->userid . '...'); $ok = quiz_send_notify_manual_graded_message($attemptobj, core_user::get_user($attempt->userid)); if ($ok) { mtrace('Send email successfully!'); $attempt->gradednotificationsenttime = $this->get_time(); $DB->set_field('quiz_attempts', 'gradednotificationsenttime', $attempt->gradednotificationsenttime, ['id' => $attempt->id]); $attemptobj->fire_attempt_manual_grading_completed_event(); } } $attempts->close(); } /** * Get a number of records as an array of quiz_attempts using a SQL statement. * * @return moodle_recordset Of quiz_attempts that need to be processed. */ public function get_list_of_attempts(): moodle_recordset { global $DB; $delaytime = $this->get_time() - get_config('quiz', 'notifyattemptgradeddelay'); $sql = "SELECT qa.* FROM {quiz_attempts} qa JOIN {quiz} quiz ON quiz.id = qa.quiz WHERE qa.state = 'finished' AND qa.gradednotificationsenttime IS NULL AND qa.sumgrades IS NOT NULL AND qa.timemodified < :delaytime ORDER BY quiz.course, qa.quiz"; return $DB->get_recordset_sql($sql, ['delaytime' => $delaytime]); } } PK\v%%0queue_quiz_open_notification_tasks_for_users.phpnu[. namespace mod_quiz\task; use core\task\adhoc_task; use mod_quiz\notification_helper; /** * Ad-hoc task to queue another task for notifying a user about an approaching open date. * * @package mod_quiz * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class queue_quiz_open_notification_tasks_for_users extends adhoc_task { public function execute(): void { $quizid = $this->get_custom_data()->id; $users = notification_helper::get_users_within_quiz($quizid); foreach ($users as $user) { $user->quizid = $quizid; $task = new send_quiz_open_soon_notification_to_user(); $task->set_custom_data($user); $task->set_userid($user->id); \core\task\manager::queue_adhoc_task($task, true); } } } PK\update_overdue_attempts.phpnu[. /** * Update Overdue Attempts Task * * @package mod_quiz * @copyright 2017 Michael Hughes * @author Michael Hughes * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_quiz\task; use mod_quiz\quiz_attempt; use moodle_exception; use moodle_recordset; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/quiz/locallib.php'); /** * Update Overdue Attempts Task * * @package mod_quiz * @copyright 2017 Michael Hughes * @author Michael Hughes * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * */ class update_overdue_attempts extends \core\task\scheduled_task { public function get_name(): string { return get_string('updateoverdueattemptstask', 'mod_quiz'); } /** * Close off any overdue attempts. */ public function execute() { $timenow = time(); $processto = $timenow - get_config('quiz', 'graceperiodmin'); mtrace(' Looking for quiz overdue quiz attempts...'); list($count, $quizcount) = $this->update_all_overdue_attempts($timenow, $processto); mtrace(' Considered ' . $count . ' attempts in ' . $quizcount . ' quizzes.'); } /** * Do the processing required. * * @param int $timenow the time to consider as 'now' during the processing. * @param int $processto only process attempt with timecheckstate longer ago than this. * @return array with two elements, the number of attempt considered, and how many different quizzes that was. */ public function update_all_overdue_attempts(int $timenow, int $processto): array { global $DB; $attemptstoprocess = $this->get_list_of_overdue_attempts($processto); $course = null; $quiz = null; $cm = null; $count = 0; $quizcount = 0; foreach ($attemptstoprocess as $attemptinfo) { try { $attempt = $DB->get_record('quiz_attempts', ['id' => $attemptinfo->id], '*', IGNORE_MISSING); if (!$attempt) { continue; } $attempt->usertimeclose = $attemptinfo->usertimeclose; $attempt->usertimelimit = $attemptinfo->usertimelimit; // If we have moved on to a different quiz, fetch the new data. if (!$quiz || $attempt->quiz != $quiz->id) { $quiz = $DB->get_record('quiz', ['id' => $attempt->quiz], '*', MUST_EXIST); $cm = get_coursemodule_from_instance('quiz', $attempt->quiz); $quizcount += 1; } // If we have moved on to a different course, fetch the new data. if (!$course || $course->id != $quiz->course) { $course = get_course($quiz->course); } // Make a specialised version of the quiz settings, with the relevant overrides. $quizforuser = clone($quiz); $quizforuser->timeclose = $attempt->usertimeclose; $quizforuser->timelimit = $attempt->usertimelimit; // Trigger any transitions that are required. $attemptobj = new quiz_attempt($attempt, $quizforuser, $cm, $course); $attemptobj->handle_if_time_expired($timenow, false); $count += 1; } catch (moodle_exception $e) { // If an error occurs while processing one attempt, don't let that kill cron. mtrace("Error while processing attempt $attempt->id at $attempt->quiz quiz:"); mtrace($e->getMessage()); mtrace($e->getTraceAsString()); // Close down any currently open transactions, otherwise one error // will stop following DB changes from being committed. $DB->force_transaction_rollback(); } } $attemptstoprocess->close(); return [$count, $quizcount]; } /** * Get a recordset of all the attempts that need to be processed now. * * (Only public to allow unit testing. Do not use!) * * @param int $processto timestamp to process up to. * @return moodle_recordset of quiz_attempts that need to be processed because time has * passed, sorted by courseid then quizid. */ public function get_list_of_overdue_attempts(int $processto): moodle_recordset { global $DB; // SQL to compute timeclose and timelimit for each attempt. $quizausersql = quiz_get_attempt_usertime_sql( "iquiza.state IN ('inprogress', 'overdue') AND iquiza.timecheckstate <= :iprocessto"); // This query returns the attempt id and the relevant user time information. return $DB->get_recordset_sql(" SELECT quiza.id, quiza.quiz, quizauser.usertimeclose, quizauser.usertimelimit FROM {quiz_attempts} quiza JOIN {quiz} quiz ON quiz.id = quiza.quiz JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id WHERE quiza.state IN ('inprogress', 'overdue') AND quiza.timecheckstate <= :processto ORDER BY quiz.course, quiza.quiz", ['processto' => $processto, 'iprocessto' => $processto]); } } PK\{^,send_quiz_open_soon_notification_to_user.phpnu[. namespace mod_quiz\task; use core\task\adhoc_task; use mod_quiz\notification_helper; /** * Ad-hoc task to send a notification to a user about an approaching open date. * * @package mod_quiz * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class send_quiz_open_soon_notification_to_user extends adhoc_task { public function execute(): void { $user = $this->get_custom_data(); notification_helper::send_notification_to_user($user); } } PK\ӻAAprecreate_attempts.phpnu[. namespace mod_quiz\task; use core\task\scheduled_task; use mod_quiz\quiz_settings; use question_engine; /** * Pre-create attempts for quizzes that have passed their threshold. * * @package mod_quiz * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net} * @author Mark Johnson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class precreate_attempts extends scheduled_task { /** * Create new instance of the task. * * @param int $maxruntime The number of seconds to allow the task to start processing new quizzes. */ public function __construct( /** @var int $maxruntime The number of seconds to allow the task to start processing new quizzes. */ protected int $maxruntime = 600, ) { } #[\Override] public function get_name(): string { return get_string('precreatetask', 'mod_quiz'); } /** * Pre-create quiz attempts for configured quizzes. * * Find all quizzes with timeopen where the current time is later * than timeopen-precreateperiod, the quiz has questions, but no attempts. * * If the precreateperiod setting is unlocked, also filter by quizzes with precreateattempts enabled. * * Find all the users enrolled on the course who can attempt the quiz and create an attempt * in the NOT_STARTED state. * * This will run for $this->maxruntime seconds, then stop to avoid hogging the cron process. Remaining quizzes will be * processed on subsequent runs. * * @return void */ public function execute(): void { global $DB; $starttime = time(); $precreateperiod = (int)get_config('quiz', 'precreateperiod'); $precreatedefault = (int)get_config('quiz', 'precreateattempts'); if ($precreateperiod === 0) { mtrace('Pre-creation of quiz attempts is disabled. Nothing to do.'); return; } $sql = " SELECT DISTINCT q.id, q.name, q.course, q.timeopen FROM {quiz} q JOIN {quiz_slots} qs ON q.id = qs.quizid LEFT JOIN {quiz_attempts} qa ON q.id = qa.quiz AND qa.preview = 0 WHERE qa.id IS NULL AND q.timeopen > :now AND q.timeopen < :threshold AND ( q.precreateattempts = :precreateattempts OR (1 = :precreatedefault AND q.precreateattempts IS NULL) ) ORDER BY q.timeopen ASC"; $params = [ 'now' => $starttime, 'threshold' => $starttime + $precreateperiod, 'precreateattempts' => 1, 'precreatedefault' => $precreatedefault, ]; $quizzes = $DB->get_records_sql($sql, $params); mtrace('Found ' . count($quizzes) . ' quizzes to create attempts for.'); $quizcount = 0; foreach ($quizzes as $quiz) { $transaction = $DB->start_delegated_transaction(); try { $quizstart = microtime(true); mtrace('Creating attempts for ' . $quiz->name); $attemptcount = self::precreate_attempts_for_quiz($quiz->id, $quiz->course); $quizend = microtime(true); $quizduration = round($quizend - $quizstart, 2); mtrace('Created ' . $attemptcount . ' attempts for ' . $quiz->name . ' in ' . $quizduration . ' seconds'); $quizcount++; $transaction->allow_commit(); } catch (\Throwable $e) { mtrace('Failed to create attempts for ' . $quiz->name); $transaction->rollback($e); } if (microtime(true) - $starttime > $this->maxruntime) { // Stop to let other tasks run, then do some more next run. mtrace('Time limit reached.'); break; } } mtrace('Created attempts for ' . $quizcount . ' quizzes.'); } /** * Pre-create attempts for a quiz. * * @param int $quizid * @param int $courseid * @return int The number of attempts created. */ public static function precreate_attempts_for_quiz(int $quizid, int $courseid): int { global $DB; $coursecontext = \context_course::instance($courseid); $users = get_enrolled_users($coursecontext, 'mod/quiz:attempt'); $attemptcount = 0; $timenow = time(); foreach ($users as $user) { if ($DB->record_exists('quiz_attempts', ['userid' => $user->id, 'quiz' => $quizid])) { // Last-minute safety check in case the quiz opened and the user started an attempt since the task started. continue; } $quizobj = quiz_settings::create($quizid, $user->id); $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $user->id); quiz_start_new_attempt( $quizobj, $quba, $attempt, 1, $timenow, ); quiz_attempt_save_not_started($quba, $attempt); $attemptcount++; } return $attemptcount; } } PK\|-TTprecreate_attempts_test.phpnu[. namespace mod_quiz\task; use mod_quiz\quiz_attempt; use mod_quiz\quiz_settings; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php'); /** * Unit tests for precreate_attempts * * @package mod_quiz * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net} * @author Mark Johnson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \mod_quiz\task\precreate_attempts */ final class precreate_attempts_test extends \advanced_testcase { use \quiz_question_helper_test_trait; /** * Generate the various possible combinations precreation settings and the corresponding task output. * * @return array */ public static function precreate_settings_provider(): array { return [ [ 'period' => 0, 'output' => 'Pre-creation of quiz attempts is disabled. Nothing to do.', ], [ 'period' => 1, 'output' => 'Found 0 quizzes to create attempts for.', ], ]; } /** * The scheduled task only looks for quizzes to generate if pre-creation is configured. * * Only look for quizzes to generate attempts for if precreateperiod is not 0, and precreateattempts is 1 or is unlocked. * * @param int $period * @param string $output * @dataProvider precreate_settings_provider */ public function test_execute_disabled(int $period, string $output): void { $this->resetAfterTest(); set_config('precreateperiod', $period, 'quiz'); $task = new precreate_attempts(); ob_start(); $task->execute(); $log = ob_get_clean(); $this->assertMatchesRegularExpression("/{$output}/", $log); } /** * Test precreate attempts task. * * Generate quizzes with a variety of timeopen and precreateperiod settings, and ensure those that match the criteria * for attempt pre-generation are picked up by the scheduled task. */ public function test_execute(): void { $this->resetAfterTest(); // Generate a course. $course = $this->getDataGenerator()->create_course(); // Generate 3 users. $student1 = $this->getDataGenerator()->create_user(); $student2 = $this->getDataGenerator()->create_user(); $teacher = $this->getDataGenerator()->create_user(); // Enrol users on the course with the appropriate roles. $this->getDataGenerator()->enrol_user($student1->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($student2->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher'); set_config('precreateperiod', 12 * HOURSECS, 'quiz'); set_config('precreateattempts', 1, 'quiz'); $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); // Generate a quiz with timeopen 1 day in the future. $quizinfuture = $quizgenerator->create_instance([ 'course' => $course->id, 'timeopen' => time() + 86400, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2, ]); // Generate a quiz with timeopen 0. $quiznotimeopen = $quizgenerator->create_instance([ 'course' => $course->id, 'precreateperiod' => 43200, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2, ]); // Generate a quiz with timeopen in the past. $quizinpast = $quizgenerator->create_instance([ 'course' => $course->id, 'timeopen' => time() - 86400, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2, ]); // Generate a quiz with timeopen 11 hours in the future. $quizwithattempts = $quizgenerator->create_instance([ 'course' => $course->id, 'timeopen' => time() + 39600, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2, ]); // Generate second quiz with timeopen 11 hours in the future. $quizinprecreateperiod = $quizgenerator->create_instance([ 'course' => $course->id, 'timeopen' => time() + 39600, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2, ]); // Generate second quiz with timeopen 11 hours in the future, // but do not give it any questions. $quizwithoutquestions = $quizgenerator->create_instance([ 'course' => $course->id, 'timeopen' => time() + 39600, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2, ]); // Generate a quiz with teacher's preview and timeopen 11 hours in the future. $quizwithpreviewonly = $quizgenerator->create_instance([ 'course' => $course->id, 'timeopen' => time() + 39600, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2, ]); // Generate a quiz with student attempt, teacher's preview and timeopen 11 hours in the future. $quizwithpreviewandattempt = $quizgenerator->create_instance([ 'course' => $course->id, 'timeopen' => time() + 39600, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2, ]); // Add questions to the quizzes. $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); $this->add_two_regular_questions($questiongenerator, $quizinfuture); $this->add_two_regular_questions($questiongenerator, $quiznotimeopen); $this->add_two_regular_questions($questiongenerator, $quizinpast); $this->add_two_regular_questions($questiongenerator, $quizwithattempts); $this->add_two_regular_questions($questiongenerator, $quizinprecreateperiod); $this->add_two_regular_questions($questiongenerator, $quizwithpreviewonly); $this->add_two_regular_questions($questiongenerator, $quizwithpreviewandattempt); // Create attempts for one student on quiz 5. $this->add_quiz_attempt($quizwithattempts->id, $student1->id, false); // Create attempts for a teacher. $this->add_quiz_attempt($quizwithpreviewonly->id, $teacher->id, true); // Create attempts for a teacher in another quiz. $this->add_quiz_attempt($quizwithpreviewandattempt->id, $teacher->id, true); // Create attempts for student2. $this->add_quiz_attempt($quizwithpreviewandattempt->id, $student2->id, false); $this->assertEmpty( quiz_get_user_attempts( [ $quizinfuture->id, $quiznotimeopen->id, $quizinpast->id, $quizwithoutquestions->id, ], $student1->id, 'all' ) ); $this->assertEmpty( quiz_get_user_attempts( [ $quizinfuture->id, $quiznotimeopen->id, $quizinpast->id, $quizwithattempts->id, $quizwithoutquestions->id, ], $student2->id, 'all', ), ); $this->assertEmpty( quiz_get_user_attempts( [ $quizinfuture->id, $quiznotimeopen->id, $quizinpast->id, $quizwithattempts->id, $quizinprecreateperiod->id, $quizwithoutquestions->id, ], $teacher->id, 'all', ), ); $student1existingattempts = quiz_get_user_attempts($quizwithattempts->id, $student1->id, 'all'); $this->assertCount(1, $student1existingattempts); $this->assertEquals(reset($student1existingattempts)->state, quiz_attempt::IN_PROGRESS); $student1precreatedattempts = quiz_get_user_attempts($quizinprecreateperiod->id, $student1->id, 'all'); $this->assertEmpty($student1precreatedattempts); $student2precreatedattempts = quiz_get_user_attempts($quizinprecreateperiod->id, $student2->id, 'all'); $this->assertEmpty($student2precreatedattempts); $task = new precreate_attempts(); ob_start(); $task->execute(); $log = ob_get_clean(); $this->assertMatchesRegularExpression('/Found 2 quizzes to create attempts for/', $log); $this->assertDoesNotMatchRegularExpression("/Creating attempts for {$quizinfuture->name}/", $log); $this->assertDoesNotMatchRegularExpression("/Creating attempts for {$quiznotimeopen->name}/", $log); $this->assertDoesNotMatchRegularExpression("/Creating attempts for {$quizinpast->name}/", $log); $this->assertDoesNotMatchRegularExpression("/Creating attempts for {$quizwithattempts->name}/", $log); $this->assertMatchesRegularExpression("/Creating attempts for {$quizinprecreateperiod->name}/", $log); $this->assertMatchesRegularExpression("/Created 2 attempts for {$quizinprecreateperiod->name}/", $log); $this->assertDoesNotMatchRegularExpression("/Creating attempts for {$quizwithoutquestions->name}/", $log); $this->assertMatchesRegularExpression("/Created 2 attempts for {$quizwithpreviewonly->name}/", $log); $this->assertDoesNotMatchRegularExpression("/Creating attempts for {$quizwithpreviewandattempt->name}/", $log); $this->assertMatchesRegularExpression('/Created attempts for 2 quizzes./', $log); // Students should have no attempts on quizzes that didn't meet criteria for pre-creation. $this->assertEmpty( quiz_get_user_attempts( [ $quizinfuture->id, $quiznotimeopen->id, $quizinpast->id, $quizwithoutquestions->id, ], $student1->id, 'all' ) ); $this->assertEmpty( quiz_get_user_attempts( [ $quizinfuture->id, $quiznotimeopen->id, $quizinpast->id, $quizwithattempts->id, $quizwithoutquestions->id, ], $student2->id, 'all', ), ); // Teacher should not have any attempts on any quizzes. $this->assertEmpty( quiz_get_user_attempts( [ $quizinfuture->id, $quiznotimeopen->id, $quizinpast->id, $quizwithattempts->id, $quizinprecreateperiod->id, $quizwithoutquestions->id, ], $teacher->id, 'all', ), ); // Students existing attempts should remain. $student1existingattempts = quiz_get_user_attempts($quizwithattempts->id, $student1->id, 'all'); $this->assertCount(1, $student1existingattempts); $this->assertEquals(reset($student1existingattempts)->state, quiz_attempt::IN_PROGRESS); // They should have NOT_STARTED attempts on quizzes that meet the criteria for pre-creation. $student1precreatedattempts = quiz_get_user_attempts($quizinprecreateperiod->id, $student1->id, 'all'); $this->assertCount(1, $student1precreatedattempts); $this->assertEquals(reset($student1precreatedattempts)->state, quiz_attempt::NOT_STARTED); $student2precreatedattempts = quiz_get_user_attempts($quizinprecreateperiod->id, $student2->id, 'all'); $this->assertCount(1, $student2precreatedattempts); $this->assertEquals(reset($student1precreatedattempts)->state, quiz_attempt::NOT_STARTED); } /** * Processing should stop at the end of a quiz once maxruntime has been reached. * * @return void */ public function test_execute_maxruntime(): void { $this->resetAfterTest(); // Generate a course. $course = $this->getDataGenerator()->create_course(); // Generate 3 users. $student1 = $this->getDataGenerator()->create_user(); // Enrol users on the course with the appropriate roles. $this->getDataGenerator()->enrol_user($student1->id, $course->id, 'student'); set_config('precreateperiod', 12 * HOURSECS, 'quiz'); set_config('precreateattempts', 1, 'quiz'); $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); // Generate 3 quizzes within the pre-creation window. $timenow = time(); $quiz1 = $quizgenerator->create_instance([ 'course' => $course->id, 'timeopen' => $timenow + 39600, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2, ]); // This quiz opens first, so should be processed first. $quiz2 = $quizgenerator->create_instance([ 'course' => $course->id, 'timeopen' => $timenow + 39599, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2, ]); $quiz3 = $quizgenerator->create_instance([ 'course' => $course->id, 'timeopen' => $timenow + 39600, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2, ]); // Add questions to the quizzes. $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); $this->add_two_regular_questions($questiongenerator, $quiz1); $this->add_two_regular_questions($questiongenerator, $quiz2); $this->add_two_regular_questions($questiongenerator, $quiz3); // Run the task with a maxruntime of 0, so that it should stop after processing the first quiz. $task = new precreate_attempts(0); ob_start(); $task->execute(); $log = ob_get_clean(); // Verify that the task stopped after the quiz opening soonest. $this->assertMatchesRegularExpression('/Found 3 quizzes to create attempts for/', $log); $this->assertMatchesRegularExpression("/Creating attempts for {$quiz2->name}/", $log); $this->assertDoesNotMatchRegularExpression("/Creating attempts for {$quiz1->name}/", $log); $this->assertDoesNotMatchRegularExpression("/Creating attempts for {$quiz3->name}/", $log); $this->assertMatchesRegularExpression("/Created 1 attempts for {$quiz2->name}/", $log); $this->assertMatchesRegularExpression('/Time limit reached./', $log); $this->assertMatchesRegularExpression('/Created attempts for 1 quizzes./', $log); // Run the task again with the default maxruntime. ob_start(); $task = new precreate_attempts(); $task->execute(); $log = ob_get_clean(); // Verify that it picks up the remaining quiz for processing. $this->assertMatchesRegularExpression('/Found 2 quizzes to create attempts for/', $log); $this->assertMatchesRegularExpression("/Creating attempts for {$quiz1->name}/", $log); $this->assertMatchesRegularExpression("/Creating attempts for {$quiz3->name}/", $log); $this->assertDoesNotMatchRegularExpression("/Creating attempts for {$quiz2->name}/", $log); $this->assertMatchesRegularExpression("/Created 1 attempts for {$quiz1->name}/", $log); $this->assertMatchesRegularExpression("/Created 1 attempts for {$quiz3->name}/", $log); $this->assertDoesNotMatchRegularExpression('/Time limit reached./', $log); $this->assertMatchesRegularExpression('/Created attempts for 2 quizzes./', $log); } /** * Pre-creation is opt in based on quiz setting. * * @return void */ public function test_execute_optin(): void { $this->resetAfterTest(); // Generate a course. $course = $this->getDataGenerator()->create_course(); // Generate 3 users. $student1 = $this->getDataGenerator()->create_user(); $student2 = $this->getDataGenerator()->create_user(); $teacher = $this->getDataGenerator()->create_user(); // Enrol users on the course with the appropriate roles. $this->getDataGenerator()->enrol_user($student1->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($student2->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher'); // Precreation is disabled by default. set_config('precreateperiod', 12 * HOURSECS, 'quiz'); set_config('precreateattempts', 0, 'quiz'); $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); // Generate a quiz with timeopen 11 hours in the future, and precreateattempts set to 1. $quizprecreate = $quizgenerator->create_instance([ 'course' => $course->id, 'timeopen' => time() + 39600, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2, 'precreateattempts' => 1, ]); // Generate a quiz with timeopen 11 hours in the future, and precreateattempts set to 0. $quiznoprecreate = $quizgenerator->create_instance([ 'course' => $course->id, 'timeopen' => time() + 39600, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2, 'precreateattempts' => 0, ]); // Generate a quiz with timeopen 11 hours in the future, and precreateattempts set to null. $quizprecreatenull = $quizgenerator->create_instance([ 'course' => $course->id, 'timeopen' => time() + 39600, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2, ]); $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); $this->add_two_regular_questions($questiongenerator, $quizprecreate); $this->add_two_regular_questions($questiongenerator, $quiznoprecreate); $this->add_two_regular_questions($questiongenerator, $quizprecreatenull); // Run the task. ob_start(); $task = new precreate_attempts(); $task->execute(); $log = ob_get_clean(); // Attempts were now created for the opted-in quiz. $this->assertMatchesRegularExpression('/Found 1 quizzes to create attempts for/', $log); $this->assertMatchesRegularExpression("/Creating attempts for {$quizprecreate->name}/", $log); $this->assertDoesNotMatchRegularExpression("/Creating attempts for {$quiznoprecreate->name}/", $log); $this->assertDoesNotMatchRegularExpression("/Creating attempts for {$quizprecreatenull->name}/", $log); $this->assertMatchesRegularExpression('/Created attempts for 1 quizzes./', $log); // Now enabled by default. set_config('precreateattempts', 1, 'quiz'); // Run the task again. ob_start(); $task = new precreate_attempts(); $task->execute(); $log = ob_get_clean(); // The quiz with null now has attempts generated. $this->assertMatchesRegularExpression('/Found 1 quizzes to create attempts for/', $log); $this->assertDoesNotMatchRegularExpression("/Creating attempts for {$quizprecreate->name}/", $log); $this->assertDoesNotMatchRegularExpression("/Creating attempts for {$quiznoprecreate->name}/", $log); $this->assertMatchesRegularExpression("/Creating attempts for {$quizprecreatenull->name}/", $log); $this->assertMatchesRegularExpression('/Created attempts for 1 quizzes./', $log); } /** * Helper to add a quiz attempt for a user. * * @param int $quizid * @param int $userid * @param bool $ispreview * @return void */ protected function add_quiz_attempt($quizid, $userid, bool $ispreview) { $quiz5settings = quiz_settings::create($quizid); $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quiz5settings->get_context()); $quba->set_preferred_behaviour($quiz5settings->get_quiz()->preferredbehaviour); $attempt = quiz_create_attempt($quiz5settings, 1, false, time(), $ispreview, $userid); quiz_start_new_attempt($quiz5settings, $quba, $attempt, 1, time()); quiz_attempt_save_started($quiz5settings, $quba, $attempt); } } PK4\Knncleanup_task.phpnu[. /** * A scheduled task for gradebookservices. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_gradebookservices\task; use core\task\scheduled_task; use ltiservice_gradebookservices\local\service\gradebookservices; defined('MOODLE_INTERNAL') || die(); /** * Class containing the scheduled task for gradebookservices. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cleanup_task extends scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('taskcleanup', 'ltiservice_gradebookservices'); } /** * Run forum cron. */ public function execute() { gradebookservices::delete_orphans_ltiservice_gradebookservices_rows(); } } PKU\<clean_access_tokens_test.phpnu[. namespace mod_lti\task; /** * Tests cleaning up the access tokens task. * * @package mod_lti * @category test * @copyright 2019 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class clean_access_tokens_test extends \advanced_testcase { /** * Test set up. * * This is executed before running any test in this file. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * Test the cleanup task. */ public function test_cleanup_task(): void { global $DB; $time = time(); // Create an expired access token. $token = new \stdClass(); $token->typeid = 1; $token->scope = 'scope'; $token->token = 'token'; $token->validuntil = $time - DAYSECS; $token->timecreated = $time - DAYSECS; $t1id = $DB->insert_record('lti_access_tokens', $token); // New token, in the future. $token->validuntil = $time + DAYSECS; $token->token = 'token2'; $t2id = $DB->insert_record('lti_access_tokens', $token); // Run the task. $task = new clean_access_tokens(); $task->execute(); // Check there is only one token now. $tokens = $DB->get_records('lti_access_tokens'); $this->assertCount(1, $tokens); $token = reset($tokens); $this->assertEquals($t2id, $token->id); } } PKW\yycleanup_test.phpnu[. namespace ltiservice_gradebookservices\task; /** * Tests cleaning up the gradebook services task. * * @package ltiservice_gradebookservices * @category test * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class cleanup_test extends \advanced_testcase { /** * Test set up. * * This is executed before running any test in this file. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * Test the cleanup task. */ public function test_cleanup_task(): void { global $DB; // Create a course. $course = $this->getDataGenerator()->create_course(); // Create a few LTI items. $lti = $this->getDataGenerator()->create_module('lti', ['course' => $course->id]); $lti2 = $this->getDataGenerator()->create_module('lti', ['course' => $course->id]); $conditions = [ 'courseid' => $course->id, 'itemtype' => 'mod', 'itemmodule' => 'lti', 'iteminstance' => $lti->id ]; // Get the grade items. $gradeitem = $DB->get_record('grade_items', $conditions); $conditions['iteminstance'] = $lti2->id; $gradeitem2 = $DB->get_record('grade_items', $conditions); // Insert these into the 'ltiservice_gradebookservices' table. $data = new \stdClass(); $data->gradeitemid = $gradeitem->id; $data->courseid = $course->id; $DB->insert_record('ltiservice_gradebookservices', $data); $data->gradeitemid = $gradeitem2->id; $DB->insert_record('ltiservice_gradebookservices', $data); $task = new cleanup_task(); $task->execute(); // Check they both still exist. $this->assertEquals(2, $DB->count_records('ltiservice_gradebookservices')); // Delete the first LTI activity. course_delete_module($lti->cmid); // Run the task again. $task = new cleanup_task(); $task->execute(); // Check only the second grade item exists. $gradebookserviceitems = $DB->get_records('ltiservice_gradebookservices'); $this->assertCount(1, $gradebookserviceitems); $gradebookserviceitem = reset($gradebookserviceitems); $this->assertEquals($gradeitem2->id, $gradebookserviceitem->gradeitemid); } /** * Test the cleanup task with a manual grade item. */ public function test_cleanup_task_with_manual_item(): void { global $CFG, $DB; // This is required when running the unit test in isolation. require_once($CFG->libdir . '/gradelib.php'); // Create a manual grade item for a course. $course = $this->getDataGenerator()->create_course(); $params = [ 'courseid' => $course->id, 'itemtype' => 'manual' ]; $gradeitem = new \grade_item($params); $gradeitem->insert(); // Insert it into the 'ltiservice_gradebookservices' table. $data = new \stdClass(); $data->gradeitemid = $gradeitem->id; $data->courseid = $course->id; $DB->insert_record('ltiservice_gradebookservices', $data); // Run the task. $task = new cleanup_task(); $task->execute(); // Check it still exist. $this->assertEquals(1, $DB->count_records('ltiservice_gradebookservices')); // Delete the manual item. $gradeitem->delete(); // Run the task again. $task = new cleanup_task(); $task->execute(); // Check it has been removed. $this->assertEquals(0, $DB->count_records('ltiservice_gradebookservices')); } } PKC~\|zBBmigrate_message_data_test.phpnu[PKp~\8 Bsync_task.phpnu[PKp~\EKsync_roles.phpnu[PKp~\*Tasynchronous_sync_task.phpnu[PKs\Zconversion_cleanup_task.phpnu[PKs\])++&aasynchronous_mimetype_upgrade_task.phpnu[PK:\ 77 Mjtask_test.phpnu[PK)\Э3content_notification_task.phpnu[PK)\v))*course_delete_modules.phpnu[PK)\Ҥ t t regrade_final_grades.phpnu[PK\Fx,EE*Xqueue_all_quiz_open_notification_tasks.phpnu[PK\dd0quiz_notify_attempt_manual_grading_completed.phpnu[PK\v%%0queue_quiz_open_notification_tasks_for_users.phpnu[PK\@update_overdue_attempts.phpnu[PK\{^,Hsend_quiz_open_soon_notification_to_user.phpnu[PK\ӻAAprecreate_attempts.phpnu[PK\|-TT -precreate_attempts_test.phpnu[PK4\Knncleanup_task.phpnu[PKU\<ʼnclean_access_tokens_test.phpnu[PKW\yycleanup_test.phpnu[PK,T