From 14dca23e1f659849aa10dbf646c9aaaa73809d90 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 5 Apr 2022 22:24:20 +0800 Subject: [PATCH] MDL-74271 core: Replace upgrade steps for blocks to be more performant The previous upgrade step was both duplicating a lot of code, and also very non-performant as each record was fetched from the DB into PHP and updated there. Most of the operations can be performed straight into the database without requiring any fetch to PHP at all. This change includes unit tests for the new upgrade steps to ensure that only the relevant data is created, updated, or deleted. --- blocks/calendar_month/db/upgrade.php | 36 +- blocks/myoverview/db/upgrade.php | 110 +---- blocks/recentlyaccesseditems/db/upgrade.php | 34 +- blocks/timeline/db/upgrade.php | 36 +- lib/db/upgradelib.php | 217 ++++++++++ lib/tests/db/upgradelib_test.php | 442 ++++++++++++++++++++ 6 files changed, 694 insertions(+), 181 deletions(-) create mode 100644 lib/tests/db/upgradelib_test.php diff --git a/blocks/calendar_month/db/upgrade.php b/blocks/calendar_month/db/upgrade.php index fbb1189309a80..86b4ab3f4382e 100644 --- a/blocks/calendar_month/db/upgrade.php +++ b/blocks/calendar_month/db/upgrade.php @@ -37,6 +37,10 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +defined('MOODLE_INTERNAL') || die(); + +require_once("{$CFG->libdir}/db/upgradelib.php"); + /** * Upgrade the calendar_month block * @param int $oldversion @@ -58,36 +62,8 @@ function xmldb_block_calendar_month_upgrade($oldversion, $block) { // Put any upgrade step following this. if ($oldversion < 2022030200) { - $context = context_system::instance(); - - // Begin looking for any and all customised /my pages. - $pageselect = 'name = :name and private = :private'; - $pageparams['name'] = '__default'; - $pageparams['private'] = 1; - $pages = $DB->get_recordset_select('my_pages', $pageselect, $pageparams); - foreach ($pages as $subpage) { - $blockinstance = $DB->get_record('block_instances', ['blockname' => 'calendar_month', - 'pagetypepattern' => 'my-index', 'subpagepattern' => $subpage->id]); - - if (!$blockinstance) { - // Insert the calendar month into the default index page. - $blockinstance = new stdClass; - $blockinstance->blockname = 'calendar_month'; - $blockinstance->parentcontextid = $context->id; - $blockinstance->showinsubcontexts = false; - $blockinstance->pagetypepattern = 'my-index'; - $blockinstance->subpagepattern = $subpage->id; - $blockinstance->defaultregion = 'content'; - $blockinstance->defaultweight = 0; - $blockinstance->timecreated = time(); - $blockinstance->timemodified = time(); - $DB->insert_record('block_instances', $blockinstance); - } else if ($blockinstance->defaultregion !== 'content') { - $blockinstance->defaultregion = 'content'; - $DB->update_record('block_instances', $blockinstance); - } - } - $pages->close(); + // Update all calendar_month blocks in the my-index to be in the main content region. + upgrade_block_set_defaultregion('calendar_month', '__default', 'my-index', 'content'); upgrade_block_savepoint(true, 2022030200, 'calendar_month', false); } diff --git a/blocks/myoverview/db/upgrade.php b/blocks/myoverview/db/upgrade.php index f141f032a2e97..45d8da9b0284b 100644 --- a/blocks/myoverview/db/upgrade.php +++ b/blocks/myoverview/db/upgrade.php @@ -25,7 +25,8 @@ defined('MOODLE_INTERNAL') || die(); -require_once($CFG->dirroot . '/my/lib.php'); +require_once("{$CFG->dirroot}/my/lib.php"); +require_once("{$CFG->libdir}/db/upgradelib.php"); /** * Upgrade code for the MyOverview block. @@ -84,104 +85,31 @@ function xmldb_block_myoverview_upgrade($oldversion) { // Put any upgrade step following this. if ($oldversion < 2021052504) { - /** - * Small helper function for this version upgrade to delete instances of this block. - * - * @param stdClass $instance DB record of a block that we need to delete within Moodle. - */ - function delete_block_instance(stdClass $instance) { - global $DB; - if ($instance) { - list($sql, $params) = $DB->get_in_or_equal($instance->id, SQL_PARAMS_NAMED); - $params['contextlevel'] = CONTEXT_BLOCK; - $DB->delete_records_select('context', "contextlevel=:contextlevel AND instanceid " . $sql, $params); - $DB->delete_records('block_positions', ['blockinstanceid' => $instance->id]); - $DB->delete_records('block_instances', ['id' => $instance->id]); - $DB->delete_records_list('user_preferences', 'name', - ['block' . $instance->id . 'hidden', 'docked_block_instance_' . $instance->id]); - } - } + upgrade_block_delete_instances('myoverview', '__default', 'my-index'); - // Delete the default indexsys version of the block. - $mysubpagepattern = $DB->get_record( - 'my_pages', - ['userid' => null, 'name' => MY_PAGE_DEFAULT, 'private' => MY_PAGE_PRIVATE], - 'id', - IGNORE_MULTIPLE - )->id; - - $instanceselect = 'blockname = :blockname and pagetypepattern = :pagetypepattern and subpagepattern = :subpagepattern'; - $instanceparams['blockname'] = 'myoverview'; - $instanceparams['pagetypepattern'] = 'my-index'; - $instanceparams['subpagepattern'] = $mysubpagepattern; - - $total = $DB->count_records_select('block_instances', $instanceselect, $instanceparams); - // Check if where have blocks to delete. - if ($total > 0) { - $instances = $DB->get_recordset_select('block_instances', $instanceselect, $instanceparams); - // Show a progress bar. - $pbar = new progress_bar('deleteblockinstances', 500, true); - $i = 0; - $pbar->update($i, $total, "Deleting block instance - $i/$total."); - foreach ($instances as $instance) { - delete_block_instance($instance); - // Update progress. - $pbar->update($i, $total, "Deleting block instance - $i/$total."); - $i++; - } - $instances->close(); - // Update progress. - $pbar->update($total, $total, "Deleting block instance - $total/$total."); - } + // Add new instance to the /my/courses.php page. + $subpagepattern = $DB->get_record('my_pages', [ + 'userid' => null, + 'name' => MY_PAGE_COURSES, + 'private' => MY_PAGE_PUBLIC, + ], 'id', IGNORE_MULTIPLE)->id; - // Begin looking for any and all instances of course overview in customised /my pages. - $pageselect = 'name = :name and private = :private and userid IS NOT NULL'; - $pageparams['name'] = MY_PAGE_DEFAULT; - $pageparams['private'] = MY_PAGE_PRIVATE; - - $total = $DB->count_records_select('my_pages', $pageselect, $pageparams); - // Check if where have pages to check for blocks. - if ($total > 0) { - $pages = $DB->get_recordset_select('my_pages', $pageselect, $pageparams); - // Show a progress bar. - $pagepbar = new progress_bar('deletepageblockinstances', 500, true); - $i = 0; - $pagepbar->update($i, $total, "Deleting user page block instance - $i/$total."); - foreach ($pages as $page) { - $blocksql = 'blockname = :blockname and pagetypepattern = :pagetypepattern and subpagepattern = :subpagepattern'; - $blockparams['blockname'] = 'myoverview'; - $blockparams['pagetypepattern'] = 'my-index'; - $blockparams['subpagepattern'] = $page->id; - $instances = $DB->get_records_select('block_instances', $blocksql, $blockparams); - foreach ($instances as $instance) { - delete_block_instance($instance); - } - // Update progress. - $pagepbar->update($i, $total, "Deleting user page block instance - $i/$total."); - $i++; - } - $pages->close(); - // Update progress. - $pagepbar->update($total, $total, "Deleting user page block instance - $total/$total."); - } + $blockname = 'myoverview'; + $pagetypepattern = 'my-index'; - // Add new instance to the /my/courses.php page. - $subpagepattern = $DB->get_record( - 'my_pages', - ['userid' => null, 'name' => MY_PAGE_COURSES, 'private' => MY_PAGE_PUBLIC], - 'id', - IGNORE_MULTIPLE - )->id; + $blockparams = [ + 'blockname' => $blockname, + 'pagetypepattern' => $pagetypepattern, + 'subpagepattern' => $subpagepattern, + ]; // See if this block already somehow exists, it should not but who knows. - if (!$DB->record_exists('block_instances', ['blockname' => 'myoverview', - 'pagetypepattern' => 'my-index', 'subpagepattern' => $subpagepattern])) { + if (!$DB->record_exists('block_instances', $blockparams)) { $page = new moodle_page(); - $systemcontext = context_system::instance(); - $page->set_context($systemcontext); + $page->set_context(context_system::instance()); // Add the block to the default /my/courses. $page->blocks->add_region('content'); - $page->blocks->add_block('myoverview', 'content', 0, false, 'my-index', $subpagepattern); + $page->blocks->add_block($blockname, 'content', 0, false, $pagetypepattern, $subpagepattern); } upgrade_block_savepoint(true, 2021052504, 'myoverview', false); diff --git a/blocks/recentlyaccesseditems/db/upgrade.php b/blocks/recentlyaccesseditems/db/upgrade.php index 3c36d0852467d..060319e878f27 100644 --- a/blocks/recentlyaccesseditems/db/upgrade.php +++ b/blocks/recentlyaccesseditems/db/upgrade.php @@ -38,6 +38,8 @@ defined('MOODLE_INTERNAL') || die(); +require_once("{$CFG->libdir}/db/upgradelib.php"); + /** * Upgrade the recentlyaccesseditems db table. * @@ -76,36 +78,8 @@ function xmldb_block_recentlyaccesseditems_upgrade($oldversion, $block) { // Put any upgrade step following this. if ($oldversion < 2022030200) { - $context = context_system::instance(); - - // Begin looking for any and all customised /my pages. - $pageselect = 'name = :name and private = :private'; - $pageparams['name'] = '__default'; - $pageparams['private'] = 1; - $pages = $DB->get_recordset_select('my_pages', $pageselect, $pageparams); - foreach ($pages as $subpage) { - $blockinstance = $DB->get_record('block_instances', ['blockname' => 'recentlyaccesseditems', - 'pagetypepattern' => 'my-index', 'subpagepattern' => $subpage->id]); - - if (!$blockinstance) { - // Insert the recentlyaccesseditems into the default index page. - $blockinstance = new stdClass; - $blockinstance->blockname = 'recentlyaccesseditems'; - $blockinstance->parentcontextid = $context->id; - $blockinstance->showinsubcontexts = false; - $blockinstance->pagetypepattern = 'my-index'; - $blockinstance->subpagepattern = $subpage->id; - $blockinstance->defaultregion = 'side-post'; - $blockinstance->defaultweight = -10; - $blockinstance->timecreated = time(); - $blockinstance->timemodified = time(); - $DB->insert_record('block_instances', $blockinstance); - } else if ($blockinstance->defaultregion !== 'side-post') { - $blockinstance->defaultregion = 'side-post'; - $DB->update_record('block_instances', $blockinstance); - } - } - $pages->close(); + // Update all recentlyaccesseditems blocks in the my-index to be in the main side-post region. + upgrade_block_set_defaultregion('recentlyaccesseditems', '__default', 'my-index', 'side-post'); upgrade_block_savepoint(true, 2022030200, 'recentlyaccesseditems', false); } diff --git a/blocks/timeline/db/upgrade.php b/blocks/timeline/db/upgrade.php index ac1512ef7ef36..a019622a21c7f 100644 --- a/blocks/timeline/db/upgrade.php +++ b/blocks/timeline/db/upgrade.php @@ -36,6 +36,10 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +defined('MOODLE_INTERNAL') || die(); + +require_once("{$CFG->libdir}/db/upgradelib.php"); + /** * Upgrade the timeline block * @param int $oldversion @@ -57,36 +61,8 @@ function xmldb_block_timeline_upgrade($oldversion, $block) { // Put any upgrade step following this. if ($oldversion < 2022030200) { - $context = context_system::instance(); - - // Begin looking for any and all customised /my pages. - $pageselect = 'name = :name and private = :private'; - $pageparams['name'] = '__default'; - $pageparams['private'] = 1; - $pages = $DB->get_recordset_select('my_pages', $pageselect, $pageparams); - foreach ($pages as $subpage) { - $blockinstance = $DB->get_record('block_instances', ['blockname' => 'timeline', - 'pagetypepattern' => 'my-index', 'subpagepattern' => $subpage->id]); - - if (!$blockinstance) { - // Insert the timeline into the default index page. - $blockinstance = new stdClass; - $blockinstance->blockname = 'timeline'; - $blockinstance->parentcontextid = $context->id; - $blockinstance->showinsubcontexts = false; - $blockinstance->pagetypepattern = 'my-index'; - $blockinstance->subpagepattern = $subpage->id; - $blockinstance->defaultregion = 'content'; - $blockinstance->defaultweight = -10; - $blockinstance->timecreated = time(); - $blockinstance->timemodified = time(); - $DB->insert_record('block_instances', $blockinstance); - } else if ($blockinstance->defaultregion !== 'content') { - $blockinstance->defaultregion = 'content'; - $DB->update_record('block_instances', $blockinstance); - } - } - $pages->close(); + // Update all timeline blocks in the my-index to be in the main content region. + upgrade_block_set_defaultregion('timeline', '__default', 'my-index', 'content'); upgrade_block_savepoint(true, 2022030200, 'timeline', false); } diff --git a/lib/db/upgradelib.php b/lib/db/upgradelib.php index 2ac6b7553935f..4607164c8e19a 100644 --- a/lib/db/upgradelib.php +++ b/lib/db/upgradelib.php @@ -1294,3 +1294,220 @@ function upgrade_add_item_to_usermenu(string $menuitem): void { set_config('customusermenuitems', implode("\n", $lines)); } } + +/** + * Update all instances of a block shown on a pagetype to a new default region, adding missing block instances where + * none is found. + * + * Note: This is intended as a helper to add blocks to all instances of the standard my-page. It will only work where + * the subpagepattern is a string representation of an integer. If there are any string values this will not work. + * + * @param string $blockname The block name, without the block_ frankenstyle component + * @param string $pagename The type of my-page to match + * @param string $pagetypepattern The page type pattern to match for the block + * @param string $newdefaultregion The new region to set + */ +function upgrade_block_set_defaultregion( + string $blockname, + string $pagename, + string $pagetypepattern, + string $newdefaultregion +): void { + global $DB; + + // The subpagepattern is a string. + // In all core blocks it contains a string represnetation of an integer, but it is theoretically possible for a + // community block to do something different. + // This function is not suited to those cases. + $subpagepattern = $DB->sql_cast_char2int('bi.subpagepattern'); + $subpageempty = $DB->sql_isnotempty('block_instances', 'bi.subpagepattern', true, false); + + // If a subquery returns any NULL then the NOT IN returns no results at all. + // By adding a join in the inner select on my_pages we remove any possible nulls and prevent any need for + // additional casting to filter out the nulls. + $sql = <<execute($sql, [ + 'selectblockname' => $blockname, + 'selectparentcontext' => $context->id, + 'selectpagetypepattern' => $pagetypepattern, + 'selectdefaultregion' => $newdefaultregion, + 'selecttimecreated' => time(), + 'selecttimemodified' => time(), + 'pagetypepattern' => $pagetypepattern, + 'blockname' => $blockname, + 'pagename' => $pagename, + ]); + + // Update the existing instances. + $sql = << :existingnewdefaultregion + ) bid + ) + EOF; + + $DB->execute($sql, [ + 'newdefaultregion' => $newdefaultregion, + 'pagetypepattern' => $pagetypepattern, + 'blockname' => $blockname, + 'existingnewdefaultregion' => $newdefaultregion, + 'pagename' => $pagename, + ]); + + // Note: This can be time consuming! + \context_helper::create_instances(CONTEXT_BLOCK); +} + +/** + * Remove all instances of a block on pages of the specified pagetypepattern. + * + * Note: This is intended as a helper to add blocks to all instances of the standard my-page. It will only work where + * the subpagepattern is a string representation of an integer. If there are any string values this will not work. + * + * @param string $blockname The block name, without the block_ frankenstyle component + * @param string $pagename The type of my-page to match + * @param string $pagetypepattern This is typically used on the 'my-index' + */ +function upgrade_block_delete_instances( + string $blockname, + string $pagename, + string $pagetypepattern +): void { + global $DB; + + $deleteblockinstances = function (string $instanceselect, array $instanceparams) use ($DB) { + $deletesql = <<delete_records_subquery('context', 'id', 'cid', $deletesql, array_merge($instanceparams, [ + 'contextlevel' => CONTEXT_BLOCK, + ])); + + $deletesql = <<delete_records_subquery('block_positions', 'id', 'bpid', $deletesql, $instanceparams); + + $blockhidden = $DB->sql_concat("'block'", 'bi.id', "'hidden'"); + $blockdocked = $DB->sql_concat("'docked_block_instance_'", 'bi.id'); + $deletesql = <<delete_records_subquery('user_preferences', 'id', 'pid', $deletesql, $instanceparams); + + $deletesql = <<delete_records_subquery('block_instances', 'id', 'bid', $deletesql, $instanceparams); + }; + + // Delete the default indexsys version of the block. + $subpagepattern = $DB->get_record('my_pages', [ + 'userid' => null, + 'name' => $pagename, + 'private' => MY_PAGE_PRIVATE, + ], 'id', IGNORE_MULTIPLE)->id; + + $instanceselect = << $blockname, + 'pagetypepattern' => $pagetypepattern, + 'subpagepattern' => $subpagepattern, + ]; + $deleteblockinstances($instanceselect, $params); + + // The subpagepattern is a string. + // In all core blocks it contains a string represnetation of an integer, but it is theoretically possible for a + // community block to do something different. + // This function is not suited to those cases. + $subpagepattern = $DB->sql_cast_char2int('bi.subpagepattern'); + + // Look for any and all instances of the block in customised /my pages. + $subpageempty = $DB->sql_isnotempty('block_instances', 'bi.subpagepattern', true, false); + $instanceselect = << $blockname, + 'pagetypepattern' => $pagetypepattern, + 'pagename' => $pagename, + 'private' => MY_PAGE_PRIVATE, + ]; + + $deleteblockinstances($instanceselect, $params); +} diff --git a/lib/tests/db/upgradelib_test.php b/lib/tests/db/upgradelib_test.php new file mode 100644 index 0000000000000..742f3959c5086 --- /dev/null +++ b/lib/tests/db/upgradelib_test.php @@ -0,0 +1,442 @@ +. + +// Note: This namespace is not technically correct, but we have to make it different to the tests for lib/upgradelib.php +// and this is more correct than alternatives. +namespace core\db; + +/** + * Unit tests for the lib/db/upgradelib.php library. + * + * @package core + * @category phpunit + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class upgradelib_test extends \advanced_testcase { + + /** + * Shared setup for the testcase. + */ + public function setUp(): void { + global $CFG; + + require_once("{$CFG->libdir}/db/upgradelib.php"); + require_once("{$CFG->dirroot}/my/lib.php"); + } + + /** + * Ensure that the upgrade_block_set_defaultregion function performs as expected. + * + * Only targetted blocks and pages should be affected. + * + * @covers ::upgrade_block_set_defaultregion + */ + public function test_upgrade_block_set_defaultregion(): void { + global $DB; + + $this->resetAfterTest(); + + // Ensure that only the targetted blocks are affected. + + // Create a my-index entry for the Dashboard. + $dashboardid = $DB->insert_record('my_pages', (object) [ + 'name' => '__default', + 'private' => MY_PAGE_PRIVATE, + ]); + + // Create a page for the my-courses page. + $mycoursesid = $DB->insert_record('my_pages', (object) [ + 'name' => '__courses', + 'private' => MY_PAGE_PRIVATE, + ]); + + $unchanged = []; + $changed = []; + + // Create several blocks of different types. + // These are not linked to the my-index page above, so should not be modified. + $unchanged[] = $this->getDataGenerator()->create_block('online_users', [ + 'defaultregion' => 'left-side', + ]); + $unchanged[] = $this->getDataGenerator()->create_block('myoverview', [ + 'defaultregion' => 'left-side', + ]); + $unchanged[] = $this->getDataGenerator()->create_block('calendar_month', [ + 'defaultregion' => 'left-side', + ]); + + // These are on the my-index above, but are not the block being updated. + $unchanged[] = $this->getDataGenerator()->create_block('online_users', [ + 'pagetypepattern' => 'my-index', + 'subpagepattern' => $dashboardid, + 'defaultregion' => 'left-side', + ]); + $unchanged[] = $this->getDataGenerator()->create_block('myoverview', [ + 'pagetypepattern' => 'my-index', + 'subpagepattern' => $dashboardid, + 'defaultregion' => 'left-side', + ]); + + // This is on a my-index page, and is the affected block, but is on the mycourses page, not the dashboard. + $unchanged[] = $this->getDataGenerator()->create_block('calendar_month', [ + 'pagetypepattern' => 'my-index', + 'subpagepattern' => $mycoursesid, + 'defaultregion' => 'left-side', + ]); + + // This is on the default dashboard, and is the affected block, but not a my-index page. + $unchanged[] = $this->getDataGenerator()->create_block('calendar_month', [ + 'pagetypepattern' => 'not-my-index', + 'subpagepattern' => $dashboardid, + 'defaultregion' => 'left-side', + ]); + + // This is the match which should be changed. + $changed[] = $this->getDataGenerator()->create_block('calendar_month', [ + 'pagetypepattern' => 'my-index', + 'subpagepattern' => $dashboardid, + 'defaultregion' => 'left-side', + ]); + + // Perform the operation. + // Target all calendar_month blocks matching 'my-index' and update them to the 'content' region where they + // belong to the user dashboard ('pagename' == '__default'). + upgrade_block_set_defaultregion('calendar_month', '__default', 'my-index', 'content'); + + // Ensure that the relevant blocks remain unchanged. + foreach ($unchanged as $original) { + $block = $DB->get_record('block_instances', ['id' => $original->id]); + $this->assertEquals($original, $block); + } + + // Ensure that only the expected blocks were changed. + foreach ($changed as $original) { + $block = $DB->get_record('block_instances', ['id' => $original->id]); + $this->assertNotEquals($original, $block); + + // Only the defaultregion should be updated to content. No other changes are expected. + $expected = (object) $original; + $expected->defaultregion = 'content'; + $this->assertEquals($expected, $block); + } + } + + /** + * Ensure that the upgrade_block_set_defaultregion function performs as expected. + * + * Missing block entries will be created. + * + * @covers ::upgrade_block_set_defaultregion + */ + public function test_upgrade_block_set_defaultregion_create_missing(): void { + global $DB; + + $this->resetAfterTest(); + + // Ensure that only the targetted blocks are affected. + + $dashboards = []; + $mycourses = []; + // Create dashboard pages for a number of users. + while (count($dashboards) < 10) { + $user = $this->getDataGenerator()->create_user(); + $dashboards[] = $DB->insert_record('my_pages', (object) [ + 'userid' => $user->id, + 'name' => '__default', + 'private' => MY_PAGE_PRIVATE, + ]); + + $mycourses[] = $DB->insert_record('my_pages', (object) [ + 'userid' => $user->id, + 'name' => '__courses', + 'private' => MY_PAGE_PRIVATE, + ]); + } + + // Enusre that there are no blocks initially. + foreach ($dashboards as $dashboardid) { + $this->assertEquals(0, $DB->count_records('block_instances', [ + 'subpagepattern' => $dashboardid, + ])); + } + + // Perform the operation. + // Target all calendar_month blocks matching 'my-index' and update them to the 'content' region where they + // belong to the user dashboard ('pagename' == '__default'). + // Any dashboards which are missing the block will have it created by the operation. + upgrade_block_set_defaultregion('calendar_month', '__default', 'my-index', 'content'); + + // Each of the dashboards should not have a block instance of the calendar_month block in the 'content' region + // on 'my-index' only. + foreach ($dashboards as $dashboardid) { + // Only one block should have been created. + $blocks = $DB->get_records('block_instances', [ + 'subpagepattern' => $dashboardid, + ]); + $this->assertCount(1, $blocks); + + $theblock = reset($blocks); + $this->assertEquals('calendar_month', $theblock->blockname); + $this->assertEquals('content', $theblock->defaultregion); + $this->assertEquals('my-index', $theblock->pagetypepattern); + } + + // Enusre that there are no blocks on the mycourses page. + foreach ($mycourses as $pageid) { + $this->assertEquals(0, $DB->count_records('block_instances', [ + 'subpagepattern' => $pageid, + ])); + } + } + + /** + * Ensure that the upgrade_block_delete_instances function performs as expected. + * + * Missing block entries will be created. + * + * @covers ::upgrade_block_delete_instances + */ + public function test_upgrade_block_delete_instances(): void { + global $DB; + + $this->resetAfterTest(); + + $DB->delete_records('block_instances'); + + // Ensure that only the targetted blocks are affected. + + // Get the my-index entry for the Dashboard. + $dashboardid = $DB->get_record('my_pages', [ + 'userid' => null, + 'name' => '__default', + 'private' => MY_PAGE_PRIVATE, + ], 'id')->id; + + // Get the page for the my-courses page. + $mycoursesid = $DB->get_record('my_pages', [ + 'name' => MY_PAGE_COURSES, + ], 'id')->id; + + $dashboards = []; + $mycourses = []; + $unchanged = []; + $unchangedcontexts = []; + $unchangedpreferences = []; + $deleted = []; + $deletedcontexts = []; + $deletedpreferences = []; + + // Create several blocks of different types. + // These are not linked to the my page above, so should not be modified. + $unchanged[] = $this->getDataGenerator()->create_block('online_users', [ + 'defaultregion' => 'left-side', + ]); + $unchanged[] = $this->getDataGenerator()->create_block('myoverview', [ + 'defaultregion' => 'left-side', + ]); + $unchanged[] = $this->getDataGenerator()->create_block('calendar_month', [ + 'defaultregion' => 'left-side', + ]); + + // These are on the my-index above, but are not the block being updated. + $unchanged[] = $this->getDataGenerator()->create_block('online_users', [ + 'pagetypepattern' => 'my-index', + 'subpagepattern' => $dashboardid, + 'defaultregion' => 'left-side', + ]); + $unchanged[] = $this->getDataGenerator()->create_block('myoverview', [ + 'pagetypepattern' => 'my-index', + 'subpagepattern' => $dashboardid, + 'defaultregion' => 'left-side', + ]); + + // This is on a my-index page, and is the affected block, but is on the mycourses page, not the dashboard. + $unchanged[] = $this->getDataGenerator()->create_block('calendar_month', [ + 'pagetypepattern' => 'my-index', + 'subpagepattern' => $mycoursesid, + 'defaultregion' => 'left-side', + ]); + + // This is on the default dashboard, and is the affected block, but not a my-index page. + $unchanged[] = $this->getDataGenerator()->create_block('calendar_month', [ + 'pagetypepattern' => 'not-my-index', + 'subpagepattern' => $dashboardid, + 'defaultregion' => 'left-side', + ]); + + // This is the match which should be changed. + $deleted[] = $this->getDataGenerator()->create_block('calendar_month', [ + 'pagetypepattern' => 'my-index', + 'subpagepattern' => $dashboardid, + 'defaultregion' => 'left-side', + ]); + + // Create blocks for users with preferences now. + while (count($dashboards) < 10) { + $userunchangedblocks = []; + $userdeletedblocks = []; + + $user = $this->getDataGenerator()->create_user(); + $userdashboardid = $DB->insert_record('my_pages', (object) [ + 'userid' => $user->id, + 'name' => '__default', + 'private' => MY_PAGE_PRIVATE, + ]); + $dashboards[] = $userdashboardid; + + $usermycoursesid = $DB->insert_record('my_pages', (object) [ + 'userid' => $user->id, + 'name' => '__courses', + 'private' => MY_PAGE_PRIVATE, + ]); + $mycourses[] = $usermycoursesid; + + // These are on the my-index above, but are not the block being updated. + $userunchangedblocks[] = $this->getDataGenerator()->create_block('online_users', [ + 'pagetypepattern' => 'my-index', + 'subpagepattern' => $userdashboardid, + 'defaultregion' => 'left-side', + ]); + $userunchangedblocks[] = $this->getDataGenerator()->create_block('myoverview', [ + 'pagetypepattern' => 'my-index', + 'subpagepattern' => $userdashboardid, + 'defaultregion' => 'left-side', + ]); + + // This is on a my-index page, and is the affected block, but is on the mycourses page, not the dashboard. + $userunchangedblocks[] = $this->getDataGenerator()->create_block('calendar_month', [ + 'pagetypepattern' => 'my-index', + 'subpagepattern' => $usermycoursesid, + 'defaultregion' => 'left-side', + ]); + + // This is on the default dashboard, and is the affected block, but not a my-index page. + $userunchangedblocks[] = $this->getDataGenerator()->create_block('calendar_month', [ + 'pagetypepattern' => 'not-my-index', + 'subpagepattern' => $userdashboardid, + 'defaultregion' => 'left-side', + ]); + + // This is the match which should be changed. + $userdeletedblocks[] = $this->getDataGenerator()->create_block('calendar_month', [ + 'pagetypepattern' => 'my-index', + 'subpagepattern' => $userdashboardid, + 'defaultregion' => 'left-side', + ]); + + $unchanged += $userunchangedblocks; + $deleted += $userdeletedblocks; + + foreach ($userunchangedblocks as $block) { + // Create user preferences for these blocks. + set_user_preference("block{$block->id}hidden", 1, $user); + set_user_preference("docked_block_instance_{$block->id}", 1, $user); + $unchangedpreferences[] = $block->id; + } + + foreach ($userdeletedblocks as $block) { + // Create user preferences for these blocks. + set_user_preference("block{$block->id}hidden", 1, $user); + set_user_preference("docked_block_instance_{$block->id}", 1, $user); + $deletedpreferences[] = $block->id; + } + } + + // Create missing contexts. + \context_helper::create_instances(CONTEXT_BLOCK); + + // Ensure that other related test data is present. + $systemcontext = \context_system::instance(); + foreach ($unchanged as $block) { + // Get contexts. + $unchangedcontexts[] = \context_block::instance($block->id); + + // Create a block position. + $DB->insert_record('block_positions', [ + 'blockinstanceid' => $block->id, + 'contextid' => $systemcontext->id, + 'pagetype' => 'course-view-topics', + 'region' => 'site-post', + 'weight' => 1, + 'visible' => 1, + ]); + } + + foreach ($deleted as $block) { + // Get contexts. + $deletedcontexts[] = \context_block::instance($block->id); + + // Create a block position. + $DB->insert_record('block_positions', [ + 'blockinstanceid' => $block->id, + 'contextid' => $systemcontext->id, + 'pagetype' => 'course-view-topics', + 'region' => 'site-post', + 'weight' => 1, + 'visible' => 1, + ]); + } + + // Perform the operation. + // Target all calendar_month blocks matching 'my-index' and update them to the 'content' region where they + // belong to the user dashboard ('pagename' == '__default'). + upgrade_block_delete_instances('calendar_month', '__default', 'my-index'); + + // Ensure that the relevant blocks remain unchanged. + foreach ($unchanged as $original) { + $block = $DB->get_record('block_instances', ['id' => $original->id]); + $this->assertEquals($original, $block); + + // Ensure that the block positions remain. + $this->assertEquals(1, $DB->count_records('block_positions', ['blockinstanceid' => $original->id])); + } + + foreach ($unchangedcontexts as $context) { + // Ensure that the context still exists. + $this->assertEquals(1, $DB->count_records('context', ['id' => $context->id])); + } + + foreach ($unchangedpreferences as $blockid) { + // Ensure that the context still exists. + $this->assertEquals(1, $DB->count_records('user_preferences', ['name' => "block{$blockid}hidden"])); + $this->assertEquals(1, $DB->count_records('user_preferences', [ + 'name' => "docked_block_instance_{$blockid}", + ])); + } + + // Ensure that only the expected blocks were changed. + foreach ($deleted as $original) { + $this->assertCount(0, $DB->get_records('block_instances', ['id' => $original->id])); + + // Ensure that the block positions was removed. + $this->assertEquals(0, $DB->count_records('block_positions', ['blockinstanceid' => $original->id])); + } + + foreach ($deletedcontexts as $context) { + // Ensure that the context still exists. + $this->assertEquals(0, $DB->count_records('context', ['id' => $context->id])); + } + + foreach ($deletedpreferences as $blockid) { + // Ensure that the context still exists. + $this->assertEquals(0, $DB->count_records('user_preferences', ['name' => "block{$blockid}hidden"])); + $this->assertEquals(0, $DB->count_records('user_preferences', [ + 'name' => "docked_block_instance_{$blockid}", + ])); + } + } +}