MDL-83514 customfield_number: add missing event observers

This commit is contained in:
Marina Glancy 2024-10-22 10:42:23 +01:00
parent ec7711b9a6
commit e681b5219d
5 changed files with 525 additions and 1 deletions

View File

@ -0,0 +1,112 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// 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 <http://www.gnu.org/licenses/>.
namespace customfield_number;
use customfield_number\local\numberproviders\nofactivities;
use customfield_number\task\recalculate;
/**
* Event observers for customfield_number
*
* @package customfield_number
* @copyright Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class observer {
/**
* When a 'number' custom field was created, schedule recalculation for the field data
*
* @param \core_customfield\event\field_created $event
*/
public static function field_created(\core_customfield\event\field_created $event): void {
$field = $event->get_record_snapshot('customfield_field', $event->objectid);
if ($field->type === 'number') {
recalculate::schedule_for_field($event->objectid);
}
}
/**
* When a 'number' custom field was updated, schedule recalculation for the field data
*
* @param \core_customfield\event\field_updated $event
*/
public static function field_updated(\core_customfield\event\field_updated $event): void {
$field = $event->get_record_snapshot('customfield_field', $event->objectid);
if ($field->type === 'number') {
recalculate::schedule_for_field($event->objectid);
}
}
/**
* When a course module was created, schedule recalculation for all 'nofactivities' custom fields
*
* @param \core\event\course_module_created $event
*/
public static function course_module_created(\core\event\course_module_created $event): void {
if (self::has_nofactivities_fields()) {
recalculate::schedule_for_fieldtype(fieldtype: nofactivities::class,
component: 'core_course', area: 'course', instanceid: $event->courseid);
}
}
/**
* When a course module was deleted, schedule recalculation for all 'nofactivities' custom fields
*
* @param \core\event\course_module_deleted $event
*/
public static function course_module_deleted(\core\event\course_module_deleted $event): void {
if (self::has_nofactivities_fields()) {
recalculate::schedule_for_fieldtype(fieldtype: nofactivities::class,
component: 'core_course', area: 'course', instanceid: $event->courseid);
}
}
/**
* When a course module was updated, schedule recalculation for all 'nofactivities' custom fields
*
* Module visibility may change following an 'updated' event and it will affect the activities count
*
* @param \core\event\course_module_updated $event
*/
public static function course_module_updated(\core\event\course_module_updated $event): void {
if (self::has_nofactivities_fields()) {
recalculate::schedule_for_fieldtype(fieldtype: nofactivities::class,
component: 'core_course', area: 'course', instanceid: $event->courseid);
}
}
/**
* Checks if a 'number' field with 'nofactivities' provider exists in the course fields
*
* This method is very fast, it only performs one DB query and only once per request
*
* @return bool
*/
protected static function has_nofactivities_fields(): bool {
$handler = \core_course\customfield\course_handler::create();
foreach ($handler->get_categories_with_fields() as $category) {
foreach ($category->get_fields() as $field) {
if ($field->get('type') === 'number' &&
$field->get_configdata_property('fieldtype') === nofactivities::class) {
return true;
}
}
}
return false;
}
}

View File

@ -0,0 +1,118 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// 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 <http://www.gnu.org/licenses/>.
namespace customfield_number\task;
use coding_exception;
use core\task\adhoc_task;
use core_customfield\field_controller;
use customfield_number\provider_base;
/**
* Recalculates data for the given number field with a provider
*
* @since Moodle 4.5.1
* @package customfield_number
* @copyright Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class recalculate extends adhoc_task {
#[\Override]
public function execute() {
global $DB;
$customdata = $this->get_custom_data();
$fieldid = clean_param($customdata->fieldid ?? null, PARAM_INT);
$fieldtype = $customdata->fieldtype ?? null;
// Find all fields that we need to recalculate (either by 'fieldid' or 'fieldtype').
$fields = [];
if ($fieldid) {
try {
$fields[] = field_controller::create($fieldid);
} catch (\Exception $e) {
// Could be a race condition when the field was already deleted by the time ad-hoc task runs.
return;
}
} else if ($fieldtype) {
$records = $DB->get_records('customfield_field', ['type' => 'number']);
foreach ($records as $record) {
$configdata = @json_decode($record->configdata, true);
if (($configdata['fieldtype'] ?? '') === $fieldtype) {
$fields[] = field_controller::create(0, $record);
}
}
}
// Schedule recalculate for each field, checking component, area and the presense of provider.
$instanceid = clean_param($customdata->instanceid ?? null, PARAM_INT);
foreach ($fields as $field) {
if ($this->field_is_scheduled($field) && ($provider = provider_base::instance($field))) {
$provider->recalculate($instanceid ?: null);
}
}
}
/**
* Helper method validating that the field should be recalculated
*
* @param \core_customfield\field_controller $field
* @return bool
*/
protected function field_is_scheduled(field_controller $field): bool {
$customdata = $this->get_custom_data();
if (!empty($customdata->component) && $field->get_handler()->get_component() !== $customdata->component) {
return false;
}
if (!empty($customdata->area) && $field->get_handler()->get_area() !== $customdata->area) {
return false;
}
return true;
}
/**
* Schedule recalculation for the given number custom field (and optionally, instanceid)
*
* @param int $fieldid in of the custom field
* @param int|null $instanceid if specified, only recalculates for the given instance id
* @return void
*/
public static function schedule_for_field(int $fieldid, ?int $instanceid = null) {
$task = new static();
$task->set_custom_data(['fieldid' => $fieldid, 'instanceid' => $instanceid]);
\core\task\manager::queue_adhoc_task($task, true);
}
/**
* Schedule recalculation for all number custom fields that use the provider (optionally with instanceid)
*
* @param string $fieldtype name of the class extending provider_base
* @param string|null $component
* @param string|null $area
* @param int|null $instanceid
* @return void
*/
public static function schedule_for_fieldtype(string $fieldtype, ?string $component = null, ?string $area = null,
?int $instanceid = null) {
$task = new static();
$task->set_custom_data([
'fieldtype' => $fieldtype,
'component' => $component,
'area' => $area,
'instanceid' => $instanceid,
]);
\core\task\manager::queue_adhoc_task($task, true);
}
}

View File

@ -0,0 +1,49 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// 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 <http://www.gnu.org/licenses/>.
/**
* Event observers for Number
*
* @package customfield_number
* @category event
* @copyright Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$observers = [
[
'eventname' => core_customfield\event\field_created::class,
'callback' => 'customfield_number\observer::field_created',
],
[
'eventname' => core_customfield\event\field_updated::class,
'callback' => 'customfield_number\observer::field_updated',
],
[
'eventname' => core\event\course_module_created::class,
'callback' => 'customfield_number\observer::course_module_created',
],
[
'eventname' => core\event\course_module_deleted::class,
'callback' => 'customfield_number\observer::course_module_deleted',
],
[
'eventname' => core\event\course_module_updated::class,
'callback' => 'customfield_number\observer::course_module_updated',
],
];

View File

@ -0,0 +1,245 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// 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 <http://www.gnu.org/licenses/>.
namespace customfield_number;
use context_module;
use customfield_number\local\numberproviders\nofactivities;
use customfield_number\task\recalculate;
/**
* Testing event observers
*
* @covers \customfield_number\observer
* @covers \customfield_number\task\recalculate
* @package customfield_number
* @category test
* @copyright Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class observer_test extends \advanced_testcase {
/**
* Create a number custom field
*
* @param array $configdata
* @return \customfield_number\field_controller
*/
protected function create_number_custom_field(array $configdata): field_controller {
/** @var \core_customfield_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_customfield');
// Create a category and field.
$category = $generator->create_category();
$field = $generator->create_field([
'categoryid' => $category->get('id'),
'type' => 'number',
'configdata' => $configdata,
]);
return $field;
}
/**
* Helper function that checks if the recalculate ad-hoc task is scheduled
*
* @param bool $mustbescheduled - when false checks that the adhoc task is NOT scheduled
* @return void
*/
protected function ensure_number_adhoc_task_is_scheduled(bool $mustbescheduled): void {
$tasks = array_filter(
\core\task\manager::get_candidate_adhoc_tasks(time(), 1200, null),
fn($task) => $task->classname === '\\' . recalculate::class
);
if ($mustbescheduled && empty($tasks)) {
$this->fail('Recalculate ad-hoc task is not scheduled.');
} else if (!$mustbescheduled && !empty($tasks)) {
$this->fail('Recalculate ad-hoc task is scheduled when it is not expected.');
}
}
/**
* Test for observer for field_created event
*/
public function test_field_created(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$course1 = $this->getDataGenerator()->create_course();
$course2 = $this->getDataGenerator()->create_course();
$assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
$assign1 = $assigngenerator->create_instance(['course' => $course1->id, 'visible' => 1]);
$assigngenerator->create_instance(['course' => $course1->id, 'visible' => 1]);
// Create a number field with a provider.
$field = $this->create_number_custom_field(['fieldtype' => nofactivities::class, 'activitytypes' => ['assign', 'forum']]);
// Execute scheduled ad-hoc tasks and it will populate the data for the course.
$this->ensure_number_adhoc_task_is_scheduled(true);
$this->run_all_adhoc_tasks();
$alldata = $DB->get_records_menu('customfield_data',
['fieldid' => $field->get('id')], 'instanceid', 'instanceid, decvalue');
$this->assertEquals([$course1->id => 2], $alldata);
// Creating another field type does not schedule tasks.
$this->ensure_number_adhoc_task_is_scheduled(false);
$this->getDataGenerator()->get_plugin_generator('core_customfield')->create_field((object)[
'categoryid' => $field->get_category()->get('id'),
'type' => 'textarea',
]);
$this->ensure_number_adhoc_task_is_scheduled(false);
}
/**
* Test for observer for field_updated event
*/
public function test_field_updated(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$course1 = $this->getDataGenerator()->create_course();
$course2 = $this->getDataGenerator()->create_course();
/** @var \mod_assign_generator $assigngenerator */
$assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
$assign1 = $assigngenerator->create_instance(['course' => $course1->id, 'visible' => 1]);
$assigngenerator->create_instance(['course' => $course1->id, 'visible' => 1]);
// Create a simple number field.
$field = $this->create_number_custom_field([]);
// There is no data for this field yet.
$this->ensure_number_adhoc_task_is_scheduled(true);
$this->run_all_adhoc_tasks();
$this->assertEmpty($DB->get_records('customfield_data', ['fieldid' => $field->get('id')]));
// Update this field to use nofactivities as provider.
$params = ['fieldtype' => nofactivities::class, 'activitytypes' => ['assign', 'forum']];
\core_customfield\api::save_field_configuration($field, (object)['configdata' => $params]);
// Now an ad-hoc task is scheduled and the data is populated.
$this->ensure_number_adhoc_task_is_scheduled(true);
$this->run_all_adhoc_tasks();
$alldata = $DB->get_records_menu(
'customfield_data',
['fieldid' => $field->get('id')],
'instanceid',
'instanceid, decvalue'
);
$this->assertEquals([$course1->id => 2], $alldata);
}
/**
* Test for observer for course_module_created, course_module_updated and course_module_deleted events
*/
public function test_course_module_events(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$course1 = $this->getDataGenerator()->create_course();
$course2 = $this->getDataGenerator()->create_course();
// Create a number field with a provider.
$field = $this->create_number_custom_field(['fieldtype' => nofactivities::class, 'activitytypes' => ['assign', 'forum']]);
// There is no data for this field yet.
$this->ensure_number_adhoc_task_is_scheduled(true);
$this->run_all_adhoc_tasks();
$this->assertEmpty($DB->get_records('customfield_data', ['fieldid' => $field->get('id')]));
// Create modules.
$assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
$assign1 = $assigngenerator->create_instance(['course' => $course1->id, 'visible' => 1]);
$assign2 = $assigngenerator->create_instance(['course' => $course1->id, 'visible' => 0]);
$assigngenerator->create_instance(['course' => $course1->id, 'visible' => 1]);
// Execute scheduled ad-hoc tasks and it will populate the data for the course.
$this->ensure_number_adhoc_task_is_scheduled(true);
$this->run_all_adhoc_tasks();
$alldata = $DB->get_records_menu('customfield_data',
['fieldid' => $field->get('id')], 'instanceid', 'instanceid, decvalue');
$this->assertEquals([$course1->id => 2], $alldata);
// Update visibility of one module.
set_coursemodule_visible($assign2->cmid, 1);
[$course, $cm] = get_course_and_cm_from_cmid($assign2->cmid);
\core\event\course_module_updated::create_from_cm($cm, context_module::instance($assign2->cmid))->trigger();
$this->ensure_number_adhoc_task_is_scheduled(true);
$this->run_all_adhoc_tasks();
$alldata = $DB->get_records_menu('customfield_data',
['fieldid' => $field->get('id')], 'instanceid', 'instanceid, decvalue');
$this->assertEquals([$course1->id => 3], $alldata);
// Delete one module.
course_delete_module($assign1->cmid);
// Execute scheduled ad-hoc tasks and it will update the data for the course.
$this->ensure_number_adhoc_task_is_scheduled(true);
$this->run_all_adhoc_tasks();
$alldata = $DB->get_records_menu('customfield_data',
['fieldid' => $field->get('id')], 'instanceid', 'instanceid, decvalue');
$this->assertEquals([$course1->id => 2], $alldata);
}
/**
* Creating, updating and deleting modules when there are no 'nofactivities' custom fields does not schedule the ad-hoc task
*
* @return void
*/
public function test_course_module_events_without_custom_fields(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$course1 = $this->getDataGenerator()->create_course();
$course2 = $this->getDataGenerator()->create_course();
// Create a number field without a provider.
$field = $this->create_number_custom_field([]);
// Initial ad-hoc task was scheduled.
$this->ensure_number_adhoc_task_is_scheduled(true);
$this->run_all_adhoc_tasks();
// Create modules.
$assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
$assign1 = $assigngenerator->create_instance(['course' => $course1->id, 'visible' => 1]);
$assign2 = $assigngenerator->create_instance(['course' => $course1->id, 'visible' => 0]);
$assigngenerator->create_instance(['course' => $course1->id, 'visible' => 1]);
$this->ensure_number_adhoc_task_is_scheduled(false);
// Update visibility of one module.
set_coursemodule_visible($assign2->cmid, 1);
[$course, $cm] = get_course_and_cm_from_cmid($assign2->cmid);
\core\event\course_module_updated::create_from_cm($cm, context_module::instance($assign2->cmid))->trigger();
$this->ensure_number_adhoc_task_is_scheduled(false);
// Delete one module.
course_delete_module($assign1->cmid);
$this->ensure_number_adhoc_task_is_scheduled(false);
}
}

View File

@ -25,6 +25,6 @@
defined('MOODLE_INTERNAL') || die;
$plugin->component = 'customfield_number';
$plugin->version = 2024100700;
$plugin->version = 2024100703;
$plugin->requires = 2024100100;
$plugin->maturity = MATURITY_STABLE;