From 02ebbfdf9c701778da657f50145b848977f2c64e Mon Sep 17 00:00:00 2001 From: Alex Yeung Date: Fri, 15 Nov 2024 11:27:25 +0000 Subject: [PATCH] CTP-1685 add unfreeze function to lifecycle block --- block_lifecycle.php | 5 ++ classes/manager.php | 20 +++++ db/access.php | 8 ++ lang/en/block_lifecycle.php | 15 ++-- renderer.php | 19 +++++ templates/unfreeze_button.mustache | 47 +++++++++++ tests/behat/behat_lifecycle.php | 91 +++++++++++++++++++++ tests/behat/unfreeze_course.feature | 34 ++++++++ tests/freezecontext_test.php | 2 +- tests/manager_test.php | 121 +++++++++++++++++++--------- tests/privacy_provider_test.php | 2 +- unfreeze.php | 60 ++++++++++++++ version.php | 2 +- 13 files changed, 380 insertions(+), 46 deletions(-) create mode 100644 templates/unfreeze_button.mustache create mode 100644 tests/behat/behat_lifecycle.php create mode 100644 tests/behat/unfreeze_course.feature create mode 100644 unfreeze.php diff --git a/block_lifecycle.php b/block_lifecycle.php index 719f2ae..c99a038 100644 --- a/block_lifecycle.php +++ b/block_lifecycle.php @@ -73,6 +73,11 @@ public function get_content() { $html .= $renderer->fetch_clc_content($courseid); if (manager::is_course_frozen($courseid)) { $html .= $renderer->fetch_course_read_only_notification(); + + // Check if user has the capability to unfreeze the course. + if (has_capability("block/lifecycle:unfreezecourse", $context)) { + $html .= $renderer->fetch_unfreeze_button_html($context); + } } $html .= $renderer->fetch_course_dates($courseid); } diff --git a/classes/manager.php b/classes/manager.php index 066d911..9474b8e 100644 --- a/classes/manager.php +++ b/classes/manager.php @@ -346,6 +346,26 @@ public static function get_weeks_delay_in_seconds() { return $enddateextend; } + /** + * Unfreeze course context. + * + * @param int $courseid Course id. + * @return void + * @throws \coding_exception + * @throws \moodle_exception + */ + public static function unfreeze_course(int $courseid): void { + // Check user's permission. + if (!has_capability('block/lifecycle:unfreezecourse', context_course::instance($courseid))) { + throw new \moodle_exception('error:unfreeze_course', 'block_lifecycle'); + } + $context = context_course::instance($courseid); + if ($context->is_locked()) { + // Unlock the course context. + $context->set_locked(false); + } + } + /** * Get the furthest date among LSA end date and course end date, plus weeks delay. * diff --git a/db/access.php b/db/access.php index b320df8..486d107 100644 --- a/db/access.php +++ b/db/access.php @@ -57,4 +57,12 @@ 'manager' => CAP_ALLOW, ], ], + 'block/lifecycle:unfreezecourse' => [ + 'captype' => 'read', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => [ + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW, + ], + ], ]; diff --git a/lang/en/block_lifecycle.php b/lang/en/block_lifecycle.php index 1078b04..9b9f822 100644 --- a/lang/en/block_lifecycle.php +++ b/lang/en/block_lifecycle.php @@ -23,27 +23,30 @@ * @author Alex Yeung */ -$string['pluginname'] = 'Lifecycle'; $string['button:editsettings'] = 'Edit automatic Read-Only settings'; $string['button:toggleautoreadonly'] = 'Disable Automatic Read-Only'; -$string['error:dateformat'] = 'Date must be in format YYYY-MM-DD'; $string['error:cannotgetscheduledfreezedate'] = 'Could not get the automatically suggested date.'; -$string['error:updatepreferencessuccess'] = 'Auto read only settings updated successfully.'; +$string['error:dateformat'] = 'Date must be in format YYYY-MM-DD'; +$string['error:unfreeze_course'] = 'You do not have permission to enable editing.'; $string['error:updatepreferencesfailed'] = 'Failed to update read only settings.'; +$string['error:updatepreferencessuccess'] = 'Auto read only settings updated successfully.'; $string['generalsettings'] = 'General Settings'; -$string['help:togglefreezing'] = 'Disable Automatic Read-Only'; -$string['help:togglefreezing_help'] = 'Disable Automatic Read-Only.'; $string['help:delayfreezedate'] = 'override Read-Only date'; $string['help:delayfreezedate_help'] = 'The date for a Read-Only override must be post the automatically suggested date, earlier dates may not be used.'; +$string['help:togglefreezing'] = 'Disable Automatic Read-Only'; +$string['help:togglefreezing_help'] = 'Disable Automatic Read-Only.'; $string['label:readonlydate'] = 'This course will be made automatically Read Only on: '; $string['label:readonlydateinput'] = 'Overrides Read-Only date:'; +$string['label:unfreezebutton'] = 'Enable editing'; $string['lifecycle:addinstance'] = 'Add lifecycle block'; +$string['lifecycle:coursereadonly'] = 'This Course is Read Only'; $string['lifecycle:enddate'] = 'This course\'s end date: {$a}'; $string['lifecycle:myaddinstance'] = 'Add my lifecycle block'; $string['lifecycle:overridecontextfreeze'] = 'Override default course context freezing settings'; $string['lifecycle:startdate'] = 'This course\'s start date: {$a}'; -$string['lifecycle:coursereadonly'] = 'This Course is Read Only'; +$string['lifecycle:unfreezecourse'] = 'Unfreeze course'; $string['lifecycle:view'] = 'View lifecycle block'; +$string['pluginname'] = 'Lifecycle'; $string['privacy:metadata'] = 'The Lifecycle block does not store personal data'; $string['settings:academicyearstartdate'] = 'Academic year start date'; $string['settings:academicyearstartdate:desc'] = 'This field is used to calculate the current academic year period and in MM-DD format'; diff --git a/renderer.php b/renderer.php index 2be3701..ec86c6f 100644 --- a/renderer.php +++ b/renderer.php @@ -15,6 +15,7 @@ // along with Moodle. If not, see . use block_lifecycle\manager; +use core\context\course; /** * Class block_lifecycle_renderer @@ -151,4 +152,22 @@ public function fetch_course_read_only_notification(): string { return $content; } + + /** + * Return the html for the unfreeze button. + * + * @param context_course $context + * @return string + * @throws coding_exception|moodle_exception + */ + public function fetch_unfreeze_button_html(context_course $context): string { + $url = new moodle_url('/blocks/lifecycle/unfreeze.php', ['id' => $context->instanceid]); + return $this->output->render_from_template( + 'block_lifecycle/unfreeze_button_form', + [ + 'url' => $url->out(false), + 'coursename' => $context->get_context_name(), + ] + ); + } } diff --git a/templates/unfreeze_button.mustache b/templates/unfreeze_button.mustache new file mode 100644 index 0000000..5e26add --- /dev/null +++ b/templates/unfreeze_button.mustache @@ -0,0 +1,47 @@ +{{! + This file is part the Local Analytics plugin for Moodle + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_lifecycle/unfreeze_button + + Template for displaying the unfreeze button to unfreeze a frozen course. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * url - string The URL to the unfreeze page. + * coursefullname - string The full name of the course. + + Example context (json): + { + "url": "http://test.m44.local/blocks/lifecycle/unfreeze.php?id=5", + "coursefullname": 'Test Course 1' + } +}} + diff --git a/tests/behat/behat_lifecycle.php b/tests/behat/behat_lifecycle.php new file mode 100644 index 0000000..5686fef --- /dev/null +++ b/tests/behat/behat_lifecycle.php @@ -0,0 +1,91 @@ +. + +use Behat\Gherkin\Node\TableNode; +require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); + +/** + * Defines the behat steps for the block_lifecycle plugin. + * + * @package block_lifecycle + * @copyright 2024 onwards University College London {@link https://www.ucl.ac.uk/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Alex Yeung + */ +class behat_lifecycle extends behat_base { + /** + * Create custom field. + * + * @param TableNode $table + * @throws \dml_exception + * + * @Given /^the following custom field exists for lifecycle block:$/ + */ + public function the_following_custom_field_exists_for_lifecycle_block(TableNode $table): void { + global $DB; + + $data = $table->getRowsHash(); + + // Create a new custom field category if it doesn't exist. + $category = $DB->get_record( + 'customfield_category', + ['name' => $data['category'], + 'component' => 'core_course', + 'area' => 'course']); + + if (!$category) { + $category = (object)[ + 'name' => $data['category'], + 'component' => 'core_course', + 'area' => 'course', + 'sortorder' => 1, + 'timecreated' => time(), + 'timemodified' => time(), + ]; + $category->id = $DB->insert_record( + 'customfield_category', + $category + ); + } + + // Check if the field already exists. + $fieldexists = $DB->record_exists('customfield_field', ['shortname' => $data['shortname'], 'categoryid' => $category->id]); + + // Create the custom field if not exists. + if (!$fieldexists) { + $field = (object)[ + 'shortname' => $data['shortname'], + 'name' => $data['name'], + 'type' => $data['type'], + 'categoryid' => $category->id, + 'sortorder' => 0, + 'configdata' => json_encode([ + "required" => 0, + "uniquevalues" => 0, + "maxlength" => 4, + "defaultvalue" => "", + "ispassword" => 0, + "displaysize" => 4, + "locked" => 1, + "visibility" => 0, + ]), + 'timecreated' => time(), + 'timemodified' => time(), + ]; + $DB->insert_record('customfield_field', $field); + } + } +} diff --git a/tests/behat/unfreeze_course.feature b/tests/behat/unfreeze_course.feature new file mode 100644 index 0000000..ed86e5e --- /dev/null +++ b/tests/behat/unfreeze_course.feature @@ -0,0 +1,34 @@ +@block @block_lifecycle + +Feature: Unfreeze a frozen course + As a teacher with the appropriate permission + I can click on the "Enable editing" button in the lifecycle block to unfreeze a frozen course + + Background: + Given the following "users" exist: + | username | firstname | lastname | idnumber | email | + | teacher1 | Teacher1 | Test | tea1 | teacher1@example.com | + And the following custom field exists for lifecycle block: + | category | CLC | + | shortname | course_year | + | name | Course Year | + | type | text | + And the following "courses" exist: + | fullname | shortname | format | customfield_course_year | startdate | enddate | + | Course 1 | C1 | topics | ##now##%Y## | ## 2 days ago ## | ## yesterday ## | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "blocks" exist: + | blockname | contextlevel | reference | pagetypepattern | defaultregion | + | lifecycle | Course | C1 | course-view-* | side-pre | + And the "C1" "Course" is context frozen + + @javascript + Scenario: Unfreeze a frozen course + Given I am on the "C1" course page logged in as teacher1 + And edit mode should not be available on the current page + And I should see "Enable editing" in the "Lifecycle" "block" + And I click on "Enable editing" "text" + And I press "Confirm" + Then edit mode should be available on the current page diff --git a/tests/freezecontext_test.php b/tests/freezecontext_test.php index b015e80..29382a6 100644 --- a/tests/freezecontext_test.php +++ b/tests/freezecontext_test.php @@ -27,7 +27,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Alex Yeung */ -class freezecontext_test extends \advanced_testcase { +final class freezecontext_test extends \advanced_testcase { protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); diff --git a/tests/manager_test.php b/tests/manager_test.php index 03c6aee..b44bc0c 100644 --- a/tests/manager_test.php +++ b/tests/manager_test.php @@ -18,9 +18,8 @@ use coding_exception; use context_course; - -/** @var int TIME_NOW - set the current time to 2022-10-31 midnight*/ -const TIME_NOW = 1667174400; +use core_customfield\field_controller; +use moodle_exception; /** * Unit tests for block_lifecycle's manager class. @@ -30,10 +29,13 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Alex Yeung */ -class manager_test extends \advanced_testcase { +final class manager_test extends \advanced_testcase { + + /** @var field_controller field2 */ + private field_controller $field2; - /** @var \core_customfield\field_controller field2 */ - private $field2; + /** @var array years - Contains the year strings of past year, current year and future year */ + private array $years; protected function setUp(): void { global $DB; @@ -46,18 +48,31 @@ protected function setUp(): void { $this->field1 = $dg->create_custom_field(['categoryid' => $catid, 'type' => 'text', 'shortname' => 'course_year']); $this->field2 = $dg->create_custom_field(['categoryid' => $catid, 'type' => 'text', 'shortname' => 'new_field']); + // Put 4 years in associative array. + $this->years = [ + 'previous_year' => date('Y', strtotime('-2 years')), + 'last_year' => date('Y', strtotime('-1 years')), + 'current_year' => date('Y'), + 'next_year' => date('Y', strtotime('+1 years')), + ]; + // Create default courses. - $this->course1 = $dg->create_course(['customfield_course_year' => '2020']); - $this->course2 = $dg->create_course(['customfield_course_year' => '2021']); - $this->course3 = $dg->create_course(['customfield_course_year' => '2022']); - $this->course4 = $dg->create_course(['customfield_course_year' => '2023']); - $this->courseshouldbefrozen = $dg->create_course( - ['customfield_course_year' => '2021', 'startdate' => 1630450800, 'enddate' => 1656543600] - ); - $this->coursewithoutacademicyear = $dg->create_course(['startdate' => 1598914800, 'enddate' => 1625007600]); - $this->coursewithfutureenddate = $dg->create_course(['startdate' => 1598914800, 'enddate' => strtotime('+ 1 month')]); + $this->course1 = $dg->create_course(['customfield_course_year' => $this->years['previous_year']]); + $this->course2 = $dg->create_course(['customfield_course_year' => $this->years['last_year']]); + $this->course3 = $dg->create_course(['customfield_course_year' => $this->years['current_year']]); + $this->course4 = $dg->create_course(['customfield_course_year' => $this->years['next_year']]); + $this->courseshouldbefrozen = $dg->create_course([ + 'customfield_course_year' => $this->years['last_year'], + 'startdate' => strtotime('-10 months'), + 'enddate' => strtotime('-5 months'), + ]); + $this->coursewithoutacademicyear = $dg->create_course([ + 'startdate' => strtotime('-10 months'), + 'enddate' => strtotime('-5 months'), + ]); + $this->coursewithfutureenddate = $dg->create_course(['startdate' => time(), 'enddate' => strtotime('+ 1 month')]); $this->coursewithoutenddate = $dg->create_course( - ['customfield_course_year' => '2020', 'startdate' => 1598914800, 'enddate' => 0] + ['customfield_course_year' => $this->years['previous_year'], 'startdate' => strtotime('-2 years'), 'enddate' => 0] ); // Create roles. @@ -81,7 +96,7 @@ protected function setUp(): void { $this->preferences = new \stdClass(); $this->preferences->courseid = $this->courseshouldbefrozen->id; $this->preferences->freezeexcluded = 0; - $this->preferences->freezedate = strtotime('2022-10-31'); + $this->preferences->freezedate = strtotime(date('Y-m-d') . ' -1 day'); $this->preferences->timecreated = time(); $this->preferences->timemodified = time(); $this->preferencesrecordid = $DB->insert_record(manager::DEFAULT_TABLE, $this->preferences); @@ -98,9 +113,9 @@ public function test_get_potential_academic_years(): void { $years = manager::get_potential_academic_years(); $this->assertCount(4, $years); - // Set to use configured CLC field id. + // Test no potential academic years found. + // Set to use a dummy field, which is not the course academic year field. set_config('clcfield', $this->field2->get('id'), 'block_lifecycle'); - $years = manager::get_potential_academic_years(); $this->assertCount(0, $years); } @@ -274,17 +289,23 @@ public function test_get_course_clc_academic_year(): void { * @throws \ReflectionException */ public function test_get_course_lifecycle_info(): void { - // Test course academic year is 2020. + // Test course academic year is previous academic year. $result = manager::get_course_lifecycle_info($this->course1->id); - $this->assertEquals(['class' => '', 'text' => 'Moodle 2020/21'], $result); + $this->assertEquals(['class' => '', 'text' => $this->get_academic_year_string($this->years['previous_year'])], $result); // Test course academic year is current academic year. $result = manager::get_course_lifecycle_info($this->course3->id); - $this->assertEquals(['class' => 'current', 'text' => 'Moodle 2022/23'], $result); + $this->assertEquals( + ['class' => 'current', 'text' => $this->get_academic_year_string($this->years['current_year'])], + $result + ); // Test course academic year is future academic year. $result = manager::get_course_lifecycle_info($this->course4->id); - $this->assertEquals(['class' => 'future', 'text' => 'Moodle 2023/24'], $result); + $this->assertEquals( + ['class' => 'future', 'text' => $this->get_academic_year_string($this->years['next_year'])], + $result + ); } /** @@ -409,7 +430,7 @@ public function test_update_auto_freezing_preferences(): void { } /** - * Test Test is_course_frozen(). + * Test is_course_frozen(). * * @covers \block_lifecycle\manager::is_course_frozen() * @return void @@ -436,7 +457,7 @@ public function test_is_course_frozen(): void { public function test_get_auto_context_freezing_preferences(): void { $result = manager::get_auto_context_freezing_preferences($this->courseshouldbefrozen->id); $this->assertEquals('0', $result->freezeexcluded); - $this->assertEquals(strtotime('2022-10-31'), $result->freezedate); + $this->assertEquals(strtotime(date('Y-m-d') . ' -1 day'), $result->freezedate); } /** @@ -469,9 +490,9 @@ public function test_get_scheduled_freeze_date(): void { $this->preferences->freezedate = 0; $DB->update_record(manager::DEFAULT_TABLE, $this->preferences); set_config('weeks_delay', 1, 'block_lifecycle'); - set_config('late_summer_assessment_end_2021', date('Y-m-d', time()), 'block_lifecycle'); + set_config('late_summer_assessment_end_' . $this->years['last_year'], date('Y-m-d', time()), 'block_lifecycle'); $result = manager::get_scheduled_freeze_date($this->courseshouldbefrozen->id); - $datetime = new \DateTime(date('Y-m-d', TIME_NOW)); + $datetime = new \DateTime(date('Y-m-d')); $datetime->modify('+7 day'); $this->assertEquals($datetime->format('d/m/Y'), $result['scheduledfreezedate']); } @@ -554,15 +575,41 @@ public function test_get_furthest_date(): void { // Result equal to course end date plus weeks delay, 1 week in this case. $this->assertEquals(1646611200, $furthestdate); } -} -namespace block_lifecycle; -/** - * Overrides the PHP time() function to return a static time. - * - * @package block_lifecycle - * @return int - */ -function time(): int { - return TIME_NOW; + /** + * Test unfreeze_course(). + * + * @covers \block_lifecycle\manager::unfreeze_course() + * @return void + * @throws coding_exception + * @throws moodle_exception + */ + public function test_unfreeze_course(): void { + $this->setUser($this->user1); + try { + manager::unfreeze_course($this->courseshouldbefrozen->id); + } catch (moodle_exception $e) { + $this->assertEquals(get_string('error:unfreeze_course', 'block_lifecycle'), $e->getMessage()); + } + + // Freeze course. + $context = context_course::instance($this->courseshouldbefrozen->id); + $context->set_locked(true); + + // Test unlock course. + $this->getDataGenerator()->enrol_user($this->user1->id, $this->courseshouldbefrozen->id, $this->teacherroleid); + manager::unfreeze_course($this->courseshouldbefrozen->id); + $context = context_course::instance($this->courseshouldbefrozen->id); + $this->assertFalse($context->is_locked()); + } + + /** + * Get academic year string. + * + * @param int $year + * @return string + */ + private function get_academic_year_string(int $year): string { + return 'Moodle ' . $year . '/' . substr($year + 1, -2, 2); + } } diff --git a/tests/privacy_provider_test.php b/tests/privacy_provider_test.php index 136f00a..dd0a6f2 100644 --- a/tests/privacy_provider_test.php +++ b/tests/privacy_provider_test.php @@ -26,7 +26,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Alex Yeung */ -class privacy_provider_test extends \advanced_testcase { +final class privacy_provider_test extends \advanced_testcase { protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); diff --git a/unfreeze.php b/unfreeze.php new file mode 100644 index 0000000..c2bd907 --- /dev/null +++ b/unfreeze.php @@ -0,0 +1,60 @@ +. + +/** + * unfreeze page for block_lifecycle to unfreeze a frozen course. + * + * @package block_lifecycle + * @copyright 2024 onwards University College London {@link https://www.ucl.ac.uk/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Alex Yeung + */ + +namespace block_lifecycle; + +use context_course; +use core\output\notification; +use moodle_exception; +use moodle_url; + +require_once('../../config.php'); + +// Course ID. +$courseid = required_param('id', PARAM_INT); + +// Get course context. +$context = context_course::instance($courseid); + +// Get course instance. +if (!$course = get_course($courseid)) { + throw new moodle_exception('course not found.', 'block_lifecycle'); +} + +// Make sure user is authenticated. +require_login($course); + +// Check user's capability. +require_capability('block/lifecycle:unfreezecourse', $context); + +// Unfreeze the course. +try { + manager::unfreeze_course($courseid); + // Unfreeze done. Redirect back to the course page. + redirect(new moodle_url('/course/view.php', ['id' => $courseid])); +} catch (moodle_exception $e) { + // Unfreeze failed. Redirect back to the course page with error message. + redirect(new moodle_url('/course/view.php', ['id' => $courseid]), $e->getMessage(), null, notification::NOTIFY_ERROR); +} diff --git a/version.php b/version.php index c792932..04a9726 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2022120800; +$plugin->version = 2022120801; $plugin->release = '0.1'; $plugin->maturity = MATURITY_ALPHA; $plugin->requires = 2020061512;