MDL-80715 core_courseformat: new section update action

This commit is contained in:
Ferran Recio
2024-03-20 16:51:12 +01:00
committed by ferranrecio
parent 757be30c39
commit 9d3bbe6c02
4 changed files with 434 additions and 62 deletions
@@ -18,6 +18,7 @@ namespace core_courseformat\local;
use section_info;
use stdClass;
use core\event\course_module_updated;
use core\event\course_section_deleted;
/**
@@ -336,4 +337,95 @@ class sectionactions extends baseactions {
$sectioninfo = $this->get_section_info($sectioninfo->id);
return $this->delete_format_data($sectioninfo, $forcedeleteifnotempty, $event);
}
/**
* Update a course section.
*
* @param section_info $sectioninfo the section info or database record to update.
* @param array|stdClass $fields the fields to update.
* @return bool whether section was updated
*/
public function update(section_info $sectioninfo, array|stdClass $fields): bool {
global $DB;
$courseid = $this->course->id;
// Some fields can not be updated using this method.
$fields = array_diff_key((array) $fields, array_flip(['id', 'course', 'section', 'sequence']));
if (array_key_exists('name', $fields) && \core_text::strlen($fields['name']) > 255) {
throw new \moodle_exception('maximumchars', 'moodle', '', 255);
}
if (empty($fields)) {
return false;
}
$fields['id'] = $sectioninfo->id;
$fields['timemodified'] = time();
$DB->update_record('course_sections', $fields);
// We need to update the section cache before the format options are updated.
\course_modinfo::purge_course_section_cache_by_id($courseid, $sectioninfo->id);
rebuild_course_cache($courseid, false, true);
course_get_format($courseid)->update_section_format_options($fields);
$event = \core\event\course_section_updated::create(
[
'objectid' => $sectioninfo->id,
'courseid' => $courseid,
'context' => \context_course::instance($courseid),
'other' => ['sectionnum' => $sectioninfo->section],
]
);
$event->trigger();
if (isset($fields['visible'])) {
$this->transfer_visibility_to_cms($sectioninfo, (bool) $fields['visible']);
}
return true;
}
/**
* Transfer the visibility of the section to the course modules.
*
* @param section_info $sectioninfo the section info or database record to update.
* @param bool $visibility the new visibility of the section.
*/
protected function transfer_visibility_to_cms(section_info $sectioninfo, bool $visibility): void {
global $DB;
if (empty($sectioninfo->sequence) || $visibility == (bool) $sectioninfo->visible) {
return;
}
$modules = explode(',', $sectioninfo->sequence);
$cmids = [];
foreach ($modules as $moduleid) {
$cm = get_coursemodule_from_id(null, $moduleid, $this->course->id);
if (!$cm) {
continue;
}
$modupdated = false;
if ($visibility) {
// As we unhide the section, we use the previously saved visibility stored in visibleold.
$modupdated = set_coursemodule_visible($moduleid, $cm->visibleold, $cm->visibleoncoursepage, false);
} else {
// We hide the section, so we hide the module but we store the original state in visibleold.
$modupdated = set_coursemodule_visible($moduleid, 0, $cm->visibleoncoursepage, false);
if ($modupdated) {
$DB->set_field('course_modules', 'visibleold', $cm->visible, ['id' => $moduleid]);
}
}
if ($modupdated) {
$cmids[] = $cm->id;
course_module_updated::create_from_cm($cm)->trigger();
}
}
\course_modinfo::purge_course_modules_cache($this->course->id, $cmids);
rebuild_course_cache($this->course->id, false, true);
}
}
@@ -541,4 +541,325 @@ class sectionactions_test extends \advanced_testcase {
}
$this->assertEquals(3, $count);
}
/**
* Test section update method.
*
* @covers ::update
* @dataProvider update_provider
* @param string $fieldname the name of the field to update
* @param int|string $value the value to set
* @param int|string $expected the expected value after the update ('=' to specify the same value as original field)
* @param bool $expectexception if the method should throw an exception
*/
public function test_update(
string $fieldname,
int|string $value,
int|string $expected,
bool $expectexception
): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(
['format' => 'topics', 'numsections' => 1],
['createsections' => true]
);
$section = get_fast_modinfo($course)->get_section_info(1);
$sectionrecord = $DB->get_record('course_sections', ['id' => $section->id]);
$this->assertNotEquals($value, $sectionrecord->$fieldname);
$this->assertNotEquals($value, $section->$fieldname);
if ($expectexception) {
$this->expectException(\moodle_exception::class);
}
if ($expected === '=') {
$expected = $section->$fieldname;
}
$sectionactions = new sectionactions($course);
$sectionactions->update($section, [$fieldname => $value]);
$sectionrecord = $DB->get_record('course_sections', ['id' => $section->id]);
$this->assertEquals($expected, $sectionrecord->$fieldname);
$section = get_fast_modinfo($course)->get_section_info(1);
$this->assertEquals($expected, $section->$fieldname);
}
/**
* Data provider for test_update.
* @return array
*/
public static function update_provider(): array {
return [
'Id will not be updated' => [
'fieldname' => 'id',
'value' => -1,
'expected' => '=',
'expectexception' => false,
],
'Course will not be updated' => [
'fieldname' => 'course',
'value' => -1,
'expected' => '=',
'expectexception' => false,
],
'Section number will not be updated' => [
'fieldname' => 'section',
'value' => -1,
'expected' => '=',
'expectexception' => false,
],
'Sequence will be updated' => [
'fieldname' => 'name',
'value' => 'new name',
'expected' => 'new name',
'expectexception' => false,
],
'Summary can be updated' => [
'fieldname' => 'summary',
'value' => 'new summary',
'expected' => 'new summary',
'expectexception' => false,
],
'Visible can be updated' => [
'fieldname' => 'visible',
'value' => 0,
'expected' => 0,
'expectexception' => false,
],
'component can be updated' => [
'fieldname' => 'component',
'value' => 'mod_assign',
'expected' => 'mod_assign',
'expectexception' => false,
],
'itemid can be updated' => [
'fieldname' => 'itemid',
'value' => 1,
'expected' => 1,
'expectexception' => false,
],
'Long names throws and exception' => [
'fieldname' => 'name',
'value' => str_repeat('a', 256),
'expected' => '=',
'expectexception' => true,
],
];
}
/**
* Test section update method updating several values at once.
*
* @covers ::update
*/
public function test_update_multiple_fields(): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(
['format' => 'topics', 'numsections' => 1],
['createsections' => true]
);
$section = get_fast_modinfo($course)->get_section_info(1);
$sectionrecord = $DB->get_record('course_sections', ['id' => $section->id]);
$this->assertEquals(1, $sectionrecord->visible);
$this->assertNull($section->name);
$sectionactions = new sectionactions($course);
$sectionactions->update($section, ['name' => 'New name', 'visible' => 0]);
$sectionrecord = $DB->get_record('course_sections', ['id' => $section->id]);
$this->assertEquals('New name', $sectionrecord->name);
$this->assertEquals(0, $sectionrecord->visible);
$section = get_fast_modinfo($course)->get_section_info(1);
$this->assertEquals('New name', $section->name);
$this->assertEquals(0, $section->visible);
}
/**
* Test updating a section trigger a course section update log event.
*
* @covers ::update
*/
public function test_course_section_updated_event(): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(
['format' => 'topics', 'numsections' => 1],
['createsections' => true]
);
$section = get_fast_modinfo($course)->get_section_info(1);
$sink = $this->redirectEvents();
$sectionactions = new sectionactions($course);
$sectionactions->update($section, ['name' => 'New name', 'visible' => 0]);
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\course_section_updated', $event);
$data = $event->get_data();
$this->assertEquals(\context_course::instance($course->id), $event->get_context());
$this->assertEquals($section->id, $data['objectid']);
}
/**
* Test section update change the modified date.
*
* @covers ::update
*/
public function test_update_time_modified(): void {
global $DB;
$this->resetAfterTest();
// Create the course with sections.
$course = $this->getDataGenerator()->create_course(
['format' => 'topics', 'numsections' => 1],
['createsections' => true]
);
$section = get_fast_modinfo($course)->get_section_info(1);
$sectionrecord = $DB->get_record('course_sections', ['id' => $section->id]);
$oldtimemodified = $sectionrecord->timemodified;
$sectionactions = new sectionactions($course);
// Ensuring that the section update occurs at a different timestamp.
$this->waitForSecond();
// The timemodified should only be updated if the section is actually updated.
$result = $sectionactions->update($section, []);
$this->assertFalse($result);
$sectionrecord = $DB->get_record('course_sections', ['id' => $section->id]);
$this->assertEquals($oldtimemodified, $sectionrecord->timemodified);
// Now update something to prove timemodified changes.
$result = $sectionactions->update($section, ['name' => 'New name']);
$this->assertTrue($result);
$sectionrecord = $DB->get_record('course_sections', ['id' => $section->id]);
$this->assertGreaterThan($oldtimemodified, $sectionrecord->timemodified);
}
/**
* Test section updating visibility will hide or show section activities.
*
* @covers ::update
*/
public function test_update_hide_section_activities(): void {
global $DB;
$this->resetAfterTest();
// Create 4 activities (visible, visible, hidden, hidden).
$course = $this->getDataGenerator()->create_course(
['format' => 'topics', 'numsections' => 1],
['createsections' => true]
);
$activity1 = $this->getDataGenerator()->create_module(
'assign',
['course' => $course->id, 'section' => 1]
);
$activity2 = $this->getDataGenerator()->create_module(
'assign',
['course' => $course->id, 'section' => 1]
);
$activity3 = $this->getDataGenerator()->create_module(
'assign',
['course' => $course->id, 'section' => 1, 'visible' => 0]
);
$activity4 = $this->getDataGenerator()->create_module(
'assign',
['course' => $course->id, 'section' => 1, 'visible' => 0]
);
$modinfo = get_fast_modinfo($course);
$cm1 = $modinfo->get_cm($activity1->cmid);
$cm2 = $modinfo->get_cm($activity2->cmid);
$cm3 = $modinfo->get_cm($activity3->cmid);
$cm4 = $modinfo->get_cm($activity4->cmid);
$this->assertEquals(1, $cm1->visible);
$this->assertEquals(1, $cm2->visible);
$this->assertEquals(0, $cm3->visible);
$this->assertEquals(0, $cm4->visible);
$sectionactions = new sectionactions($course);
// Validate hidding section hides all activities.
$section = $modinfo->get_section_info(1);
$sectionactions->update($section, ['visible' => 0]);
$modinfo = get_fast_modinfo($course);
$cm1 = $modinfo->get_cm($activity1->cmid);
$cm2 = $modinfo->get_cm($activity2->cmid);
$cm3 = $modinfo->get_cm($activity3->cmid);
$cm4 = $modinfo->get_cm($activity4->cmid);
$this->assertEquals(0, $cm1->visible);
$this->assertEquals(0, $cm2->visible);
$this->assertEquals(0, $cm3->visible);
$this->assertEquals(0, $cm4->visible);
// Validate showing the section restores the previous visibility.
$section = $modinfo->get_section_info(1);
$sectionactions->update($section, ['visible' => 1]);
$modinfo = get_fast_modinfo($course);
$cm1 = $modinfo->get_cm($activity1->cmid);
$cm2 = $modinfo->get_cm($activity2->cmid);
$cm3 = $modinfo->get_cm($activity3->cmid);
$cm4 = $modinfo->get_cm($activity4->cmid);
$this->assertEquals(1, $cm1->visible);
$this->assertEquals(1, $cm2->visible);
$this->assertEquals(0, $cm3->visible);
$this->assertEquals(0, $cm4->visible);
// Swap two activities visibility to alter visible values.
set_coursemodule_visible($cm2->id, 0, 0, true);
set_coursemodule_visible($cm4->id, 1, 1, true);
$modinfo = get_fast_modinfo($course);
$cm1 = $modinfo->get_cm($activity1->cmid);
$cm2 = $modinfo->get_cm($activity2->cmid);
$cm3 = $modinfo->get_cm($activity3->cmid);
$cm4 = $modinfo->get_cm($activity4->cmid);
$this->assertEquals(1, $cm1->visible);
$this->assertEquals(0, $cm2->visible);
$this->assertEquals(0, $cm3->visible);
$this->assertEquals(1, $cm4->visible);
// Validate hidding the section again.
$section = $modinfo->get_section_info(1);
$sectionactions->update($section, ['visible' => 0]);
$modinfo = get_fast_modinfo($course);
$cm1 = $modinfo->get_cm($activity1->cmid);
$cm2 = $modinfo->get_cm($activity2->cmid);
$cm3 = $modinfo->get_cm($activity3->cmid);
$cm4 = $modinfo->get_cm($activity4->cmid);
$this->assertEquals(0, $cm1->visible);
$this->assertEquals(0, $cm2->visible);
$this->assertEquals(0, $cm3->visible);
$this->assertEquals(0, $cm4->visible);
// Validate showing the section once more to check previous state is restored.
$section = $modinfo->get_section_info(1);
$sectionactions->update($section, ['visible' => 1]);
$modinfo = get_fast_modinfo($course);
$cm1 = $modinfo->get_cm($activity1->cmid);
$cm2 = $modinfo->get_cm($activity2->cmid);
$cm3 = $modinfo->get_cm($activity3->cmid);
$cm4 = $modinfo->get_cm($activity4->cmid);
$this->assertEquals(1, $cm1->visible);
$this->assertEquals(0, $cm2->visible);
$this->assertEquals(0, $cm3->visible);
$this->assertEquals(1, $cm4->visible);
}
}
+6 -55
View File
@@ -1277,70 +1277,21 @@ function course_delete_section_async($section, $forcedeleteifnotempty = true) {
*
* This function does not check permissions or clean values - this has to be done prior to calling it.
*
* @param int|stdClass $course
* @param int|stdClass $courseorid
* @param stdClass $section record from course_sections table - it will be updated with the new values
* @param array|stdClass $data
*/
function course_update_section($course, $section, $data) {
global $DB;
function course_update_section($courseorid, $section, $data) {
$sectioninfo = get_fast_modinfo($courseorid)->get_section_info_by_id($section->id);
formatactions::section($courseorid)->update($sectioninfo, $data);
$courseid = (is_object($course)) ? $course->id : (int)$course;
// Some fields can not be updated using this method.
$data = array_diff_key((array)$data, array('id', 'course', 'section', 'sequence'));
$changevisibility = (array_key_exists('visible', $data) && (bool)$data['visible'] != (bool)$section->visible);
if (array_key_exists('name', $data) && \core_text::strlen($data['name']) > 255) {
throw new moodle_exception('maximumchars', 'moodle', '', 255);
}
// Update record in the DB and course format options.
$data['id'] = $section->id;
$data['timemodified'] = time();
$DB->update_record('course_sections', $data);
// Invalidate the section cache by given section id.
course_modinfo::purge_course_section_cache_by_id($courseid, $section->id);
rebuild_course_cache($courseid, false, true);
course_get_format($courseid)->update_section_format_options($data);
// Update fields of the $section object.
// Update $section object fields (for legacy compatibility).
$data = array_diff_key((array) $data, array_flip(['id', 'course', 'section', 'sequence']));
foreach ($data as $key => $value) {
if (property_exists($section, $key)) {
$section->$key = $value;
}
}
// Trigger an event for course section update.
$event = \core\event\course_section_updated::create(
array(
'objectid' => $section->id,
'courseid' => $courseid,
'context' => context_course::instance($courseid),
'other' => array('sectionnum' => $section->section)
)
);
$event->trigger();
// If section visibility was changed, hide the modules in this section too.
if ($changevisibility && !empty($section->sequence)) {
$modules = explode(',', $section->sequence);
$cmids = [];
foreach ($modules as $moduleid) {
if ($cm = get_coursemodule_from_id(null, $moduleid, $courseid)) {
$cmids[] = $cm->id;
if ($data['visible']) {
// As we unhide the section, we use the previously saved visibility stored in visibleold.
set_coursemodule_visible($moduleid, $cm->visibleold, $cm->visibleoncoursepage, false);
} else {
// We hide the section, so we hide the module but we store the original state in visibleold.
set_coursemodule_visible($moduleid, 0, $cm->visibleoncoursepage, false);
$DB->set_field('course_modules', 'visibleold', $cm->visible, ['id' => $moduleid]);
}
\core\event\course_module_updated::create_from_cm($cm)->trigger();
}
}
\course_modinfo::purge_course_modules_cache($courseid, $cmids);
rebuild_course_cache($courseid, false, true);
}
}
/**
+15 -7
View File
@@ -782,19 +782,27 @@ class courselib_test extends advanced_testcase {
$this->resetAfterTest();
// Create the course with sections.
$course = $this->getDataGenerator()->create_course(array('numsections' => 10), array('createsections' => true));
$sections = $DB->get_records('course_sections', array('course' => $course->id));
$course = $this->getDataGenerator()->create_course(
['numsections' => 10],
['createsections' => true]
);
$sections = $DB->get_records('course_sections', ['course' => $course->id]);
// Get the last section's time modified value.
$section = array_pop($sections);
$oldtimemodified = $section->timemodified;
// Update the section.
$this->waitForSecond(); // Ensuring that the section update occurs at a different timestamp.
course_update_section($course, $section, array());
// Ensuring that the section update occurs at a different timestamp.
$this->waitForSecond();
// Check that the time has changed.
$section = $DB->get_record('course_sections', array('id' => $section->id));
// The timemodified should only be updated if the section is actually updated.
course_update_section($course, $section, []);
$sectionrecord = $DB->get_record('course_sections', ['id' => $section->id]);
$this->assertEquals($oldtimemodified, $sectionrecord->timemodified);
// Now update something to prove timemodified changes.
course_update_section($course, $section, ['name' => 'New name']);
$section = $DB->get_record('course_sections', ['id' => $section->id]);
$newtimemodified = $section->timemodified;
$this->assertGreaterThan($oldtimemodified, $newtimemodified);
}