From c6b1680655eb137dfe8083661bd2a600fb1e8ee6 Mon Sep 17 00:00:00 2001 From: Ilya Tregubov Date: Mon, 16 Sep 2024 08:42:43 +0800 Subject: [PATCH] MDL-82715 customfield_number: Add automatically populated providers. Added number of activity provider and also hooks for plugins. --- .upgradenotes/MDL-82715-2024091706584681.yml | 28 +++ .../field/number/amd/build/recalculate.min.js | 11 ++ .../number/amd/build/recalculate.min.js.map | 1 + .../field/number/amd/src/recalculate.js | 73 ++++++++ .../field/number/classes/data_controller.php | 27 ++- .../number/classes/external/recalculate.php | 97 ++++++++++ .../field/number/classes/field_controller.php | 61 +++++- .../classes/hook/add_custom_providers.php | 66 +++++++ .../local/numberproviders/nofactivities.php | 173 ++++++++++++++++++ .../field/number/classes/missing_provider.php | 44 +++++ .../field/number/classes/provider_base.php | 147 +++++++++++++++ .../field/number/classes/task/cron.php | 75 ++++++++ customfield/field/number/db/services.php | 38 ++++ customfield/field/number/db/tasks.php | 40 ++++ .../number/lang/en/customfield_number.php | 9 + .../number/templates/staticvalue.mustache | 39 ++++ .../field/number/tests/behat/field.feature | 105 +++++++++++ .../tests/external/recalculate_test.php | 121 ++++++++++++ .../nofactivities/nofactivities_test.php | 116 ++++++++++++ customfield/field/number/version.php | 2 +- 20 files changed, 1259 insertions(+), 14 deletions(-) create mode 100644 .upgradenotes/MDL-82715-2024091706584681.yml create mode 100644 customfield/field/number/amd/build/recalculate.min.js create mode 100644 customfield/field/number/amd/build/recalculate.min.js.map create mode 100644 customfield/field/number/amd/src/recalculate.js create mode 100644 customfield/field/number/classes/external/recalculate.php create mode 100644 customfield/field/number/classes/hook/add_custom_providers.php create mode 100644 customfield/field/number/classes/local/numberproviders/nofactivities.php create mode 100644 customfield/field/number/classes/missing_provider.php create mode 100644 customfield/field/number/classes/provider_base.php create mode 100644 customfield/field/number/classes/task/cron.php create mode 100644 customfield/field/number/db/services.php create mode 100644 customfield/field/number/db/tasks.php create mode 100644 customfield/field/number/templates/staticvalue.mustache create mode 100644 customfield/field/number/tests/external/recalculate_test.php create mode 100644 customfield/field/number/tests/local/numberproviders/nofactivities/nofactivities_test.php diff --git a/.upgradenotes/MDL-82715-2024091706584681.yml b/.upgradenotes/MDL-82715-2024091706584681.yml new file mode 100644 index 0000000000000..742b9a43841f7 --- /dev/null +++ b/.upgradenotes/MDL-82715-2024091706584681.yml @@ -0,0 +1,28 @@ +issueNumber: MDL-82715 +notes: + customfield_number: + - message: >+ + New 'customfield_number\hook\add_custom_providers' hook has been added. + + It allows automatic calculation of number course custom field. + + + Added new class + '\customfield_number\local\numberproviders\nofactivities' + + that allows to automatically calculate number of activities of a given + + type in a given course. + + + Added new webservice customfield_number_recalculate_value to recalculate + + a value of number course custom field. + + + Added 'customfield_number\task\cron' cron task that recalculates + + automatically calculated number course custom fields. + + + type: improved diff --git a/customfield/field/number/amd/build/recalculate.min.js b/customfield/field/number/amd/build/recalculate.min.js new file mode 100644 index 0000000000000..4d21dc56f0ed7 --- /dev/null +++ b/customfield/field/number/amd/build/recalculate.min.js @@ -0,0 +1,11 @@ +define("customfield_number/recalculate",["exports","core/ajax","core/notification","core/loadingicon","core/pending"],(function(_exports,_ajax,_notification,_loadingicon,_pending){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * Allows to recalculate a single value on demand + * + * @module customfield_number/recalculate + * @author 2024 Marina Glancy + * @copyright 2024 Moodle Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(){if(initialised)return;initialised=!0,document.addEventListener("click",(e=>{const target=e.target.closest(SELECTORS_wrapper+" "+SELECTORS_link);if(!target)return;const el=target.closest(SELECTORS_wrapper).querySelector(SELECTORS_value);if(!el)return;e.preventDefault();const fieldid=target.dataset.fieldid,instanceid=target.dataset.instanceid,pendingPromise=new _pending.default("recalculate_customfield_number");(0,_loadingicon.addIconToContainer)(el).then((()=>_ajax.default.call([{methodname:"customfield_number_recalculate_value",args:{fieldid:fieldid,instanceid:instanceid}}])[0])).then((data=>{el.innerHTML=data.value,pendingPromise.resolve()})).catch(_notification.default.exception)}))},_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);const SELECTORS_wrapper='[data-fieldtype="wrapper"]',SELECTORS_value='[data-fieldtype="value"]',SELECTORS_link='[data-fieldtype="link"]';let initialised=!1})); + +//# sourceMappingURL=recalculate.min.js.map \ No newline at end of file diff --git a/customfield/field/number/amd/build/recalculate.min.js.map b/customfield/field/number/amd/build/recalculate.min.js.map new file mode 100644 index 0000000000000..3f00b32152376 --- /dev/null +++ b/customfield/field/number/amd/build/recalculate.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"recalculate.min.js","sources":["../src/recalculate.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Allows to recalculate a single value on demand\n *\n * @module customfield_number/recalculate\n * @author 2024 Marina Glancy\n * @copyright 2024 Moodle Pty Ltd \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\nimport {addIconToContainer} from 'core/loadingicon';\nimport Pending from 'core/pending';\n\nconst SELECTORS = {\n wrapper: '[data-fieldtype=\"wrapper\"]',\n value: '[data-fieldtype=\"value\"]',\n link: '[data-fieldtype=\"link\"]',\n};\n\nlet initialised = false;\n\n/**\n * Init\n */\nexport function init() {\n if (initialised) {\n return;\n }\n\n initialised = true;\n\n document.addEventListener('click', (e) => {\n const target = e.target.closest(SELECTORS.wrapper + \" \" + SELECTORS.link);\n if (!target) {\n return;\n }\n const el = target.closest(SELECTORS.wrapper).querySelector(SELECTORS.value);\n if (!el) {\n return;\n }\n e.preventDefault();\n const fieldid = target.dataset.fieldid;\n const instanceid = target.dataset.instanceid;\n\n const pendingPromise = new Pending('recalculate_customfield_number');\n addIconToContainer(el).then(() => {\n return Ajax.call([{\n methodname: 'customfield_number_recalculate_value',\n args: {fieldid, instanceid}\n }])[0];\n }).then((data) => {\n el.innerHTML = data.value;\n pendingPromise.resolve();\n return;\n }).catch(Notification.exception);\n });\n}\n"],"names":["initialised","document","addEventListener","e","target","closest","SELECTORS","el","querySelector","preventDefault","fieldid","dataset","instanceid","pendingPromise","Pending","then","Ajax","call","methodname","args","data","innerHTML","value","resolve","catch","Notification","exception"],"mappings":";;;;;;;;yFAyCQA,mBAIJA,aAAc,EAEdC,SAASC,iBAAiB,SAAUC,UAC1BC,OAASD,EAAEC,OAAOC,QAAQC,kBAAoB,IAAMA,oBACrDF,oBAGCG,GAAKH,OAAOC,QAAQC,mBAAmBE,cAAcF,qBACtDC,UAGLJ,EAAEM,uBACIC,QAAUN,OAAOO,QAAQD,QACzBE,WAAaR,OAAOO,QAAQC,WAE5BC,eAAiB,IAAIC,iBAAQ,sEAChBP,IAAIQ,MAAK,IACjBC,cAAKC,KAAK,CAAC,CACdC,WAAY,uCACZC,KAAM,CAACT,QAAAA,QAASE,WAAAA,eAChB,KACLG,MAAMK,OACLb,GAAGc,UAAYD,KAAKE,MACpBT,eAAeU,aAEhBC,MAAMC,sBAAaC,uJAzCxBpB,kBACO,6BADPA,gBAEK,2BAFLA,eAGI,8BAGNN,aAAc"} \ No newline at end of file diff --git a/customfield/field/number/amd/src/recalculate.js b/customfield/field/number/amd/src/recalculate.js new file mode 100644 index 0000000000000..e8ee632b8d2a2 --- /dev/null +++ b/customfield/field/number/amd/src/recalculate.js @@ -0,0 +1,73 @@ +// 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 . + +/** + * Allows to recalculate a single value on demand + * + * @module customfield_number/recalculate + * @author 2024 Marina Glancy + * @copyright 2024 Moodle Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Ajax from 'core/ajax'; +import Notification from 'core/notification'; +import {addIconToContainer} from 'core/loadingicon'; +import Pending from 'core/pending'; + +const SELECTORS = { + wrapper: '[data-fieldtype="wrapper"]', + value: '[data-fieldtype="value"]', + link: '[data-fieldtype="link"]', +}; + +let initialised = false; + +/** + * Init + */ +export function init() { + if (initialised) { + return; + } + + initialised = true; + + document.addEventListener('click', (e) => { + const target = e.target.closest(SELECTORS.wrapper + " " + SELECTORS.link); + if (!target) { + return; + } + const el = target.closest(SELECTORS.wrapper).querySelector(SELECTORS.value); + if (!el) { + return; + } + e.preventDefault(); + const fieldid = target.dataset.fieldid; + const instanceid = target.dataset.instanceid; + + const pendingPromise = new Pending('recalculate_customfield_number'); + addIconToContainer(el).then(() => { + return Ajax.call([{ + methodname: 'customfield_number_recalculate_value', + args: {fieldid, instanceid} + }])[0]; + }).then((data) => { + el.innerHTML = data.value; + pendingPromise.resolve(); + return; + }).catch(Notification.exception); + }); +} diff --git a/customfield/field/number/classes/data_controller.php b/customfield/field/number/classes/data_controller.php index 16d2bb0bd9552..28a651e65871b 100644 --- a/customfield/field/number/classes/data_controller.php +++ b/customfield/field/number/classes/data_controller.php @@ -44,7 +44,19 @@ public function datafield(): string { * @param MoodleQuickForm $mform */ public function instance_form_definition(MoodleQuickForm $mform): void { + global $OUTPUT; + + $field = $this->get_field(); $elementname = $this->get_form_element_name(); + if (!$field->is_editable()) { + // Display the value as static text. + $instanceid = (int)$this->get('instanceid'); + $data = ['value' => $this->export_value(), 'fieldid' => $field->get('id'), 'instanceid' => $instanceid]; + $value = $OUTPUT->render_from_template('customfield_number/staticvalue', $data); + $mform->addElement('static', $elementname . '_static', $this->get_field()->get_formatted_name(), + $value); + return; + } $mform->addElement('float', $elementname, $this->get_field()->get_formatted_name()); if (!$this->get('id')) { @@ -63,8 +75,11 @@ public function instance_form_validation(array $data, array $files): array { $errors = parent::instance_form_validation($data, $files); $elementname = $this->get_form_element_name(); - $elementvalue = $data[$elementname]; - + $elementvalue = ''; + // Providers calculate values automatically, so nothing to validate. + if (!provider_base::instance($this->get_field())) { + $elementvalue = $data[$elementname]; + } $minimumvalue = $this->get_field()->get_configdata_property('minimumvalue') ?? ''; $maximumvalue = $this->get_field()->get_configdata_property('maximumvalue') ?? ''; @@ -107,6 +122,10 @@ protected function is_empty($value): bool { * @return float|null */ public function get_default_value(): ?float { + // If a provider is available, use its default value. + if ($provider = provider_base::instance($this->get_field())) { + return $provider->get_default_value(); + } $defaultvalue = $this->get_field()->get_configdata_property('defaultvalue'); if ($this->is_empty($defaultvalue)) { return null; @@ -117,9 +136,9 @@ public function get_default_value(): ?float { /** * Returns value in a human-readable format * - * @return string|null + * @return string|float|null */ - public function export_value(): ?string { + public function export_value(): string|float|null { /** @var field_controller $field */ $field = $this->get_field(); return $field->prepare_field_for_display($this->get_value(), $this->get_context()); diff --git a/customfield/field/number/classes/external/recalculate.php b/customfield/field/number/classes/external/recalculate.php new file mode 100644 index 0000000000000..2a01eef0f84d9 --- /dev/null +++ b/customfield/field/number/classes/external/recalculate.php @@ -0,0 +1,97 @@ +. + +declare(strict_types=1); + +namespace customfield_number\external; + +use core\exception\invalid_parameter_exception; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_api; +use core_external\external_value; +use customfield_number\provider_base; + +/** + * Implementation of web service customfield_number_recalculate_value + * + * @package customfield_number + * @author 2024 Marina Glancy + * @copyright 2024 Moodle Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recalculate extends external_api { + + /** + * Describes the parameters for customfield_number_recalculate_value + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'fieldid' => new external_value(PARAM_INT, 'Field id', VALUE_REQUIRED), + 'instanceid' => new external_value(PARAM_INT, 'Instance id', VALUE_REQUIRED), + ]); + } + + /** + * Implementation of web service customfield_number_recalculate_value + * + * @param int $fieldid + * @param int $instanceid + * @return array + */ + public static function execute(int $fieldid, int $instanceid): array { + // Parameter validation. + ['fieldid' => $fieldid, 'instanceid' => $instanceid] = self::validate_parameters( + self::execute_parameters(), + ['fieldid' => $fieldid, 'instanceid' => $instanceid] + ); + + // Access validation. + $context = \context_system::instance(); + self::validate_context($context); + + $field = \core_customfield\field_controller::create($fieldid); + $provider = provider_base::instance($field); + if (!$provider) { + throw new invalid_parameter_exception('Invalid parameter'); + } + + $handler = $field->get_handler(); + if (!$handler->can_edit($field, $instanceid)) { + throw new \moodle_exception('nopermissions', '', '', get_string('update')); + } + + $provider->recalculate($instanceid); + + $data = \core_customfield\api::get_instance_fields_data( + [$fieldid => $field], $instanceid)[$fieldid]; + + return ['value' => $data->export_value()]; + } + + /** + * Describe the return structure for customfield_number_recalculate_value + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'value' => new external_value(PARAM_RAW, 'Recalculated value (prepared for display)'), + ]); + } +} diff --git a/customfield/field/number/classes/field_controller.php b/customfield/field/number/classes/field_controller.php index 8b5318b810df7..115abf98233e5 100644 --- a/customfield/field/number/classes/field_controller.php +++ b/customfield/field/number/classes/field_controller.php @@ -31,7 +31,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class field_controller extends \core_customfield\field_controller { - /** * Add form elements for editing the custom field definition * @@ -41,6 +40,15 @@ public function config_form_definition(MoodleQuickForm $mform): void { $mform->addElement('header', 'specificsettings', get_string('specificsettings', 'customfield_number')); $mform->setExpanded('specificsettings'); + $providers = provider_base::get_all_providers($this); + if (count($providers) > 0) { + $this->add_field_type_select($mform, $providers); + // Add form config elements for each provider. + foreach ($providers as $provider) { + $provider->config_form_definition($mform); + } + } + // Default value. $mform->addElement('float', 'configdata[defaultvalue]', get_string('defaultvalue', 'core_customfield')); if ($this->get_configdata_property('defaultvalue') === null) { @@ -68,7 +76,7 @@ public function config_form_definition(MoodleQuickForm $mform): void { // Display format settings. // TODO: Change this after MDL-82996 fixed. - $randelname = 'str_' . random_string(); + $randelname = 'str_display_format'; $mform->addGroup([], $randelname, html_writer::tag('h4', get_string('headerdisplaysettings', 'customfield_number'))); // Display template. @@ -90,6 +98,22 @@ public function config_form_definition(MoodleQuickForm $mform): void { } } + /** + * Adds selector to provider for field population. + * + * @param MoodleQuickForm $mform + * @param array $providers + */ + protected function add_field_type_select(\MoodleQuickForm $mform, array $providers): void { + $autooptions = []; + foreach ($providers as $provider) { + $autooptions[get_class($provider)] = $provider->get_name(); + } + $options = [get_string('genericfield', 'customfield_number')]; + $options = array_merge($options, $autooptions); + $mform->addElement('select', 'configdata[fieldtype]', get_string('fieldtype', 'customfield_number'), $options); + } + /** * Validate the data on the field configuration form * @@ -110,6 +134,11 @@ public function config_form_validation(array $data, $files = []): array { $minimumvalue = $data['configdata']['minimumvalue'] ?? ''; $maximumvalue = $data['configdata']['maximumvalue'] ?? ''; + foreach (provider_base::get_all_providers($this) as $provider) { + if (array_key_exists('fieldtype', $data["configdata"]) && $data["configdata"]["fieldtype"] == get_class($provider)) { + $errors = array_merge($errors, $provider->config_form_validation($data, $files)); + } + } // Early exit if neither maximum/minimum are specified. if ($minimumvalue === '' && $maximumvalue === '') { return $errors; @@ -140,9 +169,9 @@ public function config_form_validation(array $data, $files = []): array { * * @param mixed $value * @param context|null $context - * @return string|null + * @return string|float|null */ - public function prepare_field_for_display(mixed $value, ?context $context = null): ?string { + public function prepare_field_for_display(mixed $value, ?context $context = null): string|null|float { if ($value === null) { return null; } @@ -154,12 +183,26 @@ public function prepare_field_for_display(mixed $value, ?context $context = null } } else { // Let's format the value. - $value = format_float((float) $value, $decimalplaces); - - // Apply the display format. - $format = $this->get_configdata_property('display') ?? '{value}'; - $value = str_replace('{value}', $value, $format); + $provider = provider_base::instance($this); + if ($provider) { + $value = $provider->prepare_export_value($value, $context); + } else { + $value = format_float((float)$value, $decimalplaces); + // Apply the display format. + $format = $this->get_configdata_property('display') ?? '{value}'; + $value = str_replace('{value}', $value, $format); + } } return format_string($value, true, ['context' => $context ?? system::instance()]); } + + /** + * Can the value of this field be manually editable in the edit forms + * + * @return bool + */ + public function is_editable(): bool { + return (string)$this->get_configdata_property('fieldtype') === '0'; + } + } diff --git a/customfield/field/number/classes/hook/add_custom_providers.php b/customfield/field/number/classes/hook/add_custom_providers.php new file mode 100644 index 0000000000000..d89f73968441e --- /dev/null +++ b/customfield/field/number/classes/hook/add_custom_providers.php @@ -0,0 +1,66 @@ +. + +declare(strict_types=1); + +namespace customfield_number\hook; + +use customfield_number\provider_base; +use customfield_number\field_controller; + +/** + * Hook for adding custom providers to the provider_base. + * + * @package customfield_number + * @copyright 2024 Ilya Tregubov + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +#[\core\attribute\label('This hook allows adding custom providers to calculate custom field automatically like price for course')] +class add_custom_providers { + /** + * @var array + */ + protected array $providers = []; + + /** + * Constructor. + * + * @param field_controller $field the custom field controller + */ + public function __construct( + /** @var field_controller the custom field controller */ + public readonly field_controller $field, + ) { + } + + /** + * Add a provider to the hook. + * + * @param provider_base $provider + */ + public function add_provider(provider_base $provider): void { + $this->providers[] = $provider; + } + + /** + * Get the list of providers added through the hook. + * + * @return provider_base[] + */ + public function get_providers(): array { + return $this->providers; + } +} diff --git a/customfield/field/number/classes/local/numberproviders/nofactivities.php b/customfield/field/number/classes/local/numberproviders/nofactivities.php new file mode 100644 index 0000000000000..8f5f50e0b3a82 --- /dev/null +++ b/customfield/field/number/classes/local/numberproviders/nofactivities.php @@ -0,0 +1,173 @@ +. + +declare(strict_types=1); + +namespace customfield_number\local\numberproviders; + +use context_course; +use core_plugin_manager; +use customfield_number\data_controller; +use customfield_number\provider_base; +use MoodleQuickForm; + +/** + * Class nofactivities to calculate number of activities in the course. + * + * @package customfield_number + * @author 2024 Marina Glancy + * @copyright 2024 Moodle Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class nofactivities extends provider_base { + + /** + * Provider name + */ + public function get_name(): string { + return get_string('nofactivities', 'customfield_number'); + } + + /** + * Check if the provider is available for the current field. + * + * @return bool + */ + public function is_available(): bool { + return $this->field->get_handler()->get_component() === 'core_course' && + $this->field->get_handler()->get_area() === 'course'; + } + + /** + * Add autocomplete field for selecting activity type. + * Also add checkbox to display the field when the number of activities is zero. + * + * @param \MoodleQuickForm $mform + */ + public function config_form_definition(MoodleQuickForm $mform): void { + $options = []; + $plugins = core_plugin_manager::instance()->get_plugins_of_type('mod'); + foreach ($plugins as $plugin) { + $options[$plugin->name] = $plugin->displayname; + } + + // Define the label for the autocomplete element. + $valuelabel = get_string('activitytypes', 'customfield_number'); + // Add autocomplete element. + $mform->addElement('autocomplete', 'configdata[activitytypes]', $valuelabel, $options, ['multiple' => true]) + ->setHiddenLabel(true); + $mform->hideIf('configdata[activitytypes]', 'configdata[fieldtype]', 'ne', get_class($this)); + $mform->hideIf('configdata[decimalplaces]', 'configdata[fieldtype]', 'eq', get_class($this)); + $mform->hideIf('configdata[display]', 'configdata[fieldtype]', 'eq', get_class($this)); + $mform->hideIf('str_display_format', 'configdata[fieldtype]', 'eq', get_class($this)); + $mform->hideIf('configdata[defaultvalue]', 'configdata[fieldtype]', 'eq', get_class($this)); + $mform->hideIf('configdata[minimumvalue]', 'configdata[fieldtype]', 'eq', get_class($this)); + $mform->hideIf('configdata[maximumvalue]', 'configdata[fieldtype]', 'eq', get_class($this)); + } + + /** + * Recalculate the number of activities in the course. + * + * @param int|null $instanceid + */ + public function recalculate(?int $instanceid = null): void { + global $DB; + $types = $this->field->get_configdata_property('activitytypes'); + $displaywhenzero = $this->field->get_configdata_property('displaywhenzero'); + if (!empty($types)) { + // Prepare the SQL for non-empty types. + [$sqlin, $params] = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED); + } else { + return; + } + + // Subquery to select all modules of selected types. + $cmsql = "SELECT m.id + FROM {modules} m + WHERE m.name $sqlin + AND m.visible = 1"; + + $where = ''; + if ($instanceid) { + $where = "AND c.id = :courseid "; + $params['courseid'] = $instanceid; + } + + // Number of activities is stored in database. So we count the number and check if it matches the stored value. + // We update value in database if it doesn't match counted value. + $sql = "SELECT c.id, COUNT(cm.id) AS cnt, d.id AS dataid, d.decvalue + FROM {course} c + LEFT JOIN {customfield_data} d + ON d.fieldid = :fieldid + AND d.instanceid = c.id + LEFT JOIN {course_modules} cm + ON cm.course = c.id + AND cm.visible = 1 + AND cm.deletioninprogress = 0 + AND cm.module IN ($cmsql) + WHERE c.id <> :siteid $where + GROUP BY c.id, d.id, d.decvalue + "; + $params['fieldid'] = $fieldid = $this->field->get('id'); + $records = $DB->get_records_sql($sql, $params + ['siteid' => SITEID]); + foreach ($records as $record) { + $value = (int)$record->cnt; + if (!isset($displaywhenzero) && !$value) { + // Do not display the field when the number of activities is zero. + if ($record->dataid) { + (new data_controller((int)$record->dataid, (object)['id' => $record->dataid]))->delete(); + } + } else if (empty($record->dataid) || (int)$record->decvalue != $value) { + // Stored value is out of date. + $data = \core_customfield\api::get_instance_fields_data( + [$fieldid => $this->field], (int)$record->id)[$fieldid]; + $data->set('contextid', context_course::instance($record->id)->id); + $data->set('decvalue', $value); + $data->save(); + } + } + } + + /** + * Validate the data on the field configuration form for number of activities provider. + * + * @param array $data + * @param array $files + * @return array associative array of error messages + */ + public function config_form_validation(array $data, array $files = []): array { + $errors = []; + if (empty($data['configdata']['activitytypes'])) { + $errors['configdata[activitytypes]'] = get_string('err_required', 'form'); + } + return $errors; + } + + /** + * Preparation for export for number of activities provider. + * + * @param mixed $value String or float + * @param \context|null $context |null $context Context + * @return ?string + */ + public function prepare_export_value(mixed $value, ?\context $context = null): ?string { + if (trim((string)$value) === '') { + return null; + } else { + return format_float((float)$value, 0); + } + } +} diff --git a/customfield/field/number/classes/missing_provider.php b/customfield/field/number/classes/missing_provider.php new file mode 100644 index 0000000000000..f29ba1f5cfb2d --- /dev/null +++ b/customfield/field/number/classes/missing_provider.php @@ -0,0 +1,44 @@ +. + +declare(strict_types=1); + +namespace customfield_number; + +/** + * Class missing_provider + * + * @package customfield_number + * @author 2024 Marina Glancy + * @copyright 2024 Moodle Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class missing_provider extends provider_base { + + /** + * {@inheritDoc} + */ + public function get_name(): string { + return get_string('invalidprovider', 'customfield_number'); + } + + /** + * {@inheritDoc} + */ + public function is_available(): bool { + return false; + } +} diff --git a/customfield/field/number/classes/provider_base.php b/customfield/field/number/classes/provider_base.php new file mode 100644 index 0000000000000..59660b6198742 --- /dev/null +++ b/customfield/field/number/classes/provider_base.php @@ -0,0 +1,147 @@ +. + +declare(strict_types=1); + +namespace customfield_number; + +use context; +use MoodleQuickForm; + +/** + * Class provider_base + * + * @package customfield_number + * @author 2024 Marina Glancy + * @copyright 2024 Moodle Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class provider_base { + + /** + * Constructor. + * + * @param field_controller $field A field controller. + */ + public function __construct( + /** @var field_controller the custom field controller */ + protected field_controller $field, + ) { + } + + /** + * Provider name + */ + abstract public function get_name(): string; + + /** + * If provide is available for the current field. + */ + abstract public function is_available(): bool; + + /** + * Add provider specific fields for form. + * + * @param \MoodleQuickForm $mform + */ + public function config_form_definition(MoodleQuickForm $mform): void { + } + + /** + * Recalculate field value. + * + * @param int|null $instanceid + */ + public function recalculate(?int $instanceid = null): void { + } + + /** + * Default value if there is no value in the database (or there is a null) + * + * Usually returns either null or 0 + * + * @return null|float + */ + public function get_default_value(): ?float { + return null; + } + + /** + * Provider specific value preparation for export. + * + * @param mixed $value String or float + * @param context|null $context Context + * @return ?string + */ + public function prepare_export_value(mixed $value, ?\context $context = null): ?string { + return $value; + } + + /** + * Returns a new provider instance. + * + * @param field_controller $field Field + */ + final public static function instance(\core_customfield\field_controller $field): ?self { + if ($field->get('type') !== 'number' || !($field instanceof field_controller)) { + return null; + } + $classname = $field->get_configdata_property('fieldtype'); + if (!$classname) { + return null; + } + if (!class_exists($classname) || !is_a($classname, self::class, true)) { + return new missing_provider($field); + } + return new $classname($field); + } + + /** + * List of applicable automatic providers for this field + * + * @param field_controller $field + * @return provider_base[] + */ + final public static function get_all_providers(field_controller $field): array { + /** @var provider_base[] $allproviders */ + $allproviders = [ + new \customfield_number\local\numberproviders\nofactivities($field), + ]; + + // Custom providers. + $hook = new \customfield_number\hook\add_custom_providers($field); + + // Dispatch the hook and collect custom providers. + \core\di::get(\core\hook\manager::class)->dispatch($hook); + + $allproviders = array_merge($allproviders, $hook->get_providers()); + + return array_filter($allproviders, fn($p) => $p->is_available()); + } + + /** + * Validate the data on the field configuration form + * + * Providers can override it + * + * @param array $data + * @param array $files + * @return array associative array of error messages + */ + public function config_form_validation(array $data, array $files = []): array { + return []; + } +} diff --git a/customfield/field/number/classes/task/cron.php b/customfield/field/number/classes/task/cron.php new file mode 100644 index 0000000000000..a816c99458ab7 --- /dev/null +++ b/customfield/field/number/classes/task/cron.php @@ -0,0 +1,75 @@ +. + +namespace customfield_number\task; + +use core\task\scheduled_task; +use core_customfield\category_controller; +use core_customfield\field_controller; +use customfield_number\provider_base; + +/** + * Scheduled task for customfield_number to recalculate automatically populated fields. + * + * @package customfield_number + * @author 2024 Marina Glancy + * @copyright 2024 Moodle Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cron extends scheduled_task { + + /** + * Get a descriptive name for the task (shown to admins) + * + * @return string + */ + public function get_name(): string { + return get_string('crontaskname', 'customfield_number'); + } + + /** + * Recalculate automatically populated number fields. + * + * Throw exceptions on errors (the job will be retried). + */ + public function execute(): void { + global $DB; + // Get all number custom fields. + $sql = "SELECT f.*, c.component, c.area, c.itemid, c.contextid + FROM {customfield_field} f + JOIN {customfield_category} c ON f.categoryid = c.id + WHERE f.type = ?"; + $res = $DB->get_records_sql($sql, ['number']); + foreach ($res as $row) { + $cat = (object)[ + 'id' => $row->categoryid, + 'component' => $row->component, + 'area' => $row->area, + 'itemid' => $row->itemid, + 'contextid' => $row->contextid, + ]; + unset($row->component, $row->area, $row->itemid, $row->contextid); + $category = category_controller::create(0, $cat); + // Create an instance of field controller for each field and recalculate the value if field provider is available. + $field = field_controller::create(0, $row, $category); + if ($provider = provider_base::instance($field)) { + if ($provider->is_available()) { + $provider->recalculate(); + } + } + } + } +} diff --git a/customfield/field/number/db/services.php b/customfield/field/number/db/services.php new file mode 100644 index 0000000000000..96c59527809f1 --- /dev/null +++ b/customfield/field/number/db/services.php @@ -0,0 +1,38 @@ +. + +/** + * External functions and service declaration for Number + * + * Documentation: {@link https://moodledev.io/docs/apis/subsystems/external/description} + * + * @package customfield_number + * @category webservice + * @author 2024 Marina Glancy + * @copyright 2024 Moodle Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$functions = [ + 'customfield_number_recalculate_value' => [ + 'classname' => customfield_number\external\recalculate::class, + 'description' => 'This web service is used to recalculate the value of automatically populated number custom field.', + 'type' => 'write', + 'ajax' => true, + ], +]; diff --git a/customfield/field/number/db/tasks.php b/customfield/field/number/db/tasks.php new file mode 100644 index 0000000000000..4e8b3728de24b --- /dev/null +++ b/customfield/field/number/db/tasks.php @@ -0,0 +1,40 @@ +. + +/** + * Scheduled task definitions for Number + * + * Documentation: {@link https://moodledev.io/docs/apis/subsystems/task/scheduled} + * + * @package customfield_number + * @category task + * @author 2024 Marina Glancy + * @copyright 2024 Moodle Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$tasks = [ + [ + 'classname' => customfield_number\task\cron::class, + 'minute' => 'R', + 'hour' => '2', + 'day' => '*', + 'month' => '*', + 'dayofweek' => '*', + ], +]; diff --git a/customfield/field/number/lang/en/customfield_number.php b/customfield/field/number/lang/en/customfield_number.php index dec2ae0cd0cc5..c3392bdca985a 100644 --- a/customfield/field/number/lang/en/customfield_number.php +++ b/customfield/field/number/lang/en/customfield_number.php @@ -24,6 +24,9 @@ defined('MOODLE_INTERNAL') || die; +$string['activitytypes'] = 'Activity types'; +$string['automaticallypopulated'] = 'Automatically populated'; +$string['crontaskname'] = 'Populating automatic number custom fields'; $string['decimalplaces'] = 'Decimal places'; $string['defaultvalueconfigerror'] = 'Default value must be between minimum and maximum'; $string['display'] = 'Display template'; @@ -37,12 +40,18 @@ $string['displaywhenzero_help'] = 'How to display the field value when the value is "0". For example, in case of a price you can display the word "Free" but in case of the duration you may want to leave it empty since it means that the duration was not estimated. Leave empty if you do not want to display anything at all when the value is set to "0".'; +$string['fieldtype'] = 'Field type'; +$string['genericfield'] = 'Generic field for any numeric data'; $string['headerdisplaysettings'] = 'Display format'; +$string['invalidprovider'] = 'Invalid provider'; +$string['manualinput'] = 'Manual input'; $string['maximumvalue'] = 'Maximum value'; $string['maximumvalueerror'] = 'Value must be less than or equal to {$a}'; $string['minimumvalue'] = 'Minimum value'; $string['minimumvalueconfigerror'] = 'Minimum value must be less than maximum'; $string['minimumvalueerror'] = 'Value must be greater than or equal to {$a}'; +$string['missingrequired'] = 'Missing instanceid or fieldid'; +$string['nofactivities'] = 'Number of activities in the course'; $string['pluginname'] = 'Number'; $string['privacy:metadata'] = 'The number custom field plugin does not store any personal data'; $string['specificsettings'] = 'Number field settings'; diff --git a/customfield/field/number/templates/staticvalue.mustache b/customfield/field/number/templates/staticvalue.mustache new file mode 100644 index 0000000000000..7504b84683c50 --- /dev/null +++ b/customfield/field/number/templates/staticvalue.mustache @@ -0,0 +1,39 @@ +{{! + 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 . +}} +{{! + @template customfield_number/staticvalue + + Automatically calculated value for a number field. + + Example context (json): + { + "value": 5, + "instanceid": 4, + "fieldid": 5 + } +}} +
+
{{{value}}}
+ {{#instanceid}} + + {{/instanceid}} +
+{{#js}} +require(['customfield_number/recalculate'], function(Recalculate) { + Recalculate.init(); +}); +{{/js}} diff --git a/customfield/field/number/tests/behat/field.feature b/customfield/field/number/tests/behat/field.feature index 37576b41d6578..d98bc3d29de9d 100644 --- a/customfield/field/number/tests/behat/field.feature +++ b/customfield/field/number/tests/behat/field.feature @@ -91,3 +91,108 @@ Feature: Managers can manage course custom fields number | template | whenzero | fieldvalue | expectedvalue | | $ {value} | 0 | 150 | $ 150.00 | | {value} | Free | 0 | Free | + + Scenario: Automatically populated field should hide some field form elements + Given I click on "Add a new custom field" "link" + And I click on "Number" "link" + And I should see "Default value" + And I should see "Minimum value" + And I should see "Maximum value" + And I should see "Decimal places" + And I should see "Display format" + And I should see "Display template" + And I should see "Display when zero" + And I should see "Display template" + And I should not see "No selection" + + When I set the following fields to these values: + | Name | Number field | + | Field type | Number of activities in the course | + Then I should not see "Default value" + And I should not see "Minimum value" + And I should not see "Maximum value" + And I should not see "Decimal places" + And I should not see "Display format" + And I should not see "Display template" + And I should see "Display when zero" + And I should not see "Display when empty" + And I should see "No selection" + And I open the autocomplete suggestions list + And "Assignment" "autocomplete_suggestions" should exist + + Scenario: Automatically populated field should be displayed in course settings + Given the following "courses" exist: + | fullname | shortname | category | + | Test course1 | C1 | 0 | + | Test course2 | C2 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher | Teacher | 1 | teacher@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher | C1 | editingteacher | + | teacher | C2 | editingteacher | + And the following "activities" exist: + | activity | name | intro | course | idnumber | section | visible | + | assign | Assignment1 | Test description | C1 | assign1 | 1 | 0 | + | assign | Assignment2 | Test description | C1 | assign2 | 1 | 1 | + | assign | Assignment3 | Test description | C1 | assign3 | 1 | 1 | + | quiz | Quiz1 | Test description | C1 | quiz1 | 1 | 0 | + | quiz | Quiz2 | Test description | C1 | quiz2 | 1 | 1 | + | forum | Forum1 | Test description | C1 | forum1 | 1 | 0 | + | forum | Forum2 | Test description | C1 | forum2 | 1 | 1 | + | forum | Forum3 | Test description | C1 | forum3 | 1 | 1 | + | forum | Forum4 | Test description | C1 | forum4 | 1 | 1 | + | quiz | QuizC2 | Test description | C2 | quizC2 | 1 | 1 | + Given I click on "Add a new custom field" "link" + And I click on "Number" "link" + When I set the following fields to these values: + | Name | Number field | + | Short name | numberfield | + | Field type | Number of activities in the course | + And I open the autocomplete suggestions list + And I click on "Save changes" "button" in the "Adding a new Number" "dialogue" + And I should see "You must supply a value here." in the "Adding a new Number" "dialogue" + And I set the following fields to these values: + | Activity types | Assignment, Forum | + And I click on "Save changes" "button" in the "Adding a new Number" "dialogue" + And I log in as "teacher" + And I am on "C1" course homepage + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I click on "Update" "link" + And I should see "5" in the ".customfield_number-recalculate-value" "css_element" + And I add a assign activity to course "C1" section "1" and I fill the form with: + | Assignment name | Assignment4 | + | ID number | assign4 | + | Description | Test description | + And I run the scheduled task "\customfield_number\task\cron" + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I should see "6" in the ".customfield_number-recalculate-value" "css_element" + And I add a quiz activity to course "C1" section "1" and I fill the form with: + | Name | Quiz3 | + | ID number | quiz3 | + | Description | Test description | + And I run the scheduled task "\customfield_number\task\cron" + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I should see "6" in the ".customfield_number-recalculate-value" "css_element" + + And I am on "C2" course homepage + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I should see "0" in the ".customfield_number-recalculate-value" "css_element" + + And I log in as "admin" + And I navigate to "Courses > Default settings > Course custom fields" in site administration + And I click on "Edit" "link" in the "Number field" "table_row" + And I set the following fields to these values: + | Display when zero | | + And I click on "Save changes" "button" in the "Updating Number field" "dialogue" + And I run the scheduled task "\customfield_number\task\cron" + And I log in as "teacher" + And I am on "C2" course homepage + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I should not see "0" in the ".customfield_number-recalculate-value" "css_element" diff --git a/customfield/field/number/tests/external/recalculate_test.php b/customfield/field/number/tests/external/recalculate_test.php new file mode 100644 index 0000000000000..30539a13b2631 --- /dev/null +++ b/customfield/field/number/tests/external/recalculate_test.php @@ -0,0 +1,121 @@ +. + +declare(strict_types=1); + +namespace customfield_number\external; + +use core_customfield_generator; +use core_external\external_api; +use moodle_exception; +use stdClass; + +defined('MOODLE_INTERNAL') || die; + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * Unit tests for the customfield_number\external\recalculate. + * + * @package customfield_number + * @category external + * @copyright 2024 Ilya Tregubov + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \customfield_number\external\recalculate + */ +final class recalculate_test extends \externallib_advanced_testcase { + + /** + * Tests when teacher can not edit locked field. + */ + public function test_execute_no_permission(): void { + global $DB; + + $this->resetAfterTest(); + [$course, $field] = $this->prepare_course(); + $configdata = [ + 'fieldtype' => 'customfield_number\\local\\numberproviders\\nofactivities', + 'activitytypes' => ['assign', 'forum'], + 'locked' => 1, + ]; + $field->set('configdata', json_encode($configdata)); + $field->save(); + + $context = \context_course::instance($course->id); + $roleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']); + $this->unassignUserCapability('moodle/course:changelockedcustomfields', $context->id, $roleid); + + $this->expectException(moodle_exception::class); + $this->expectExceptionMessage(get_string('update')); + recalculate::execute($field->get('id'), (int)$course->id); + } + + /** + * Tests when all data is valid. + */ + public function test_execute(): void { + $this->resetAfterTest(); + [$course, $field] = $this->prepare_course(); + $result = recalculate::execute($field->get('id'), (int)$course->id); + $result = external_api::clean_returnvalue(recalculate::execute_returns(), $result); + $this->assertEquals(3.0, $result['value']); + } + + /** + * Create a course with number custom field. + * @return array An array with the course object and field object. + */ + private function prepare_course(): array { + global $DB; + + $course = $this->getDataGenerator()->create_course(); + + // Add teacher to a course. + $roleids = $DB->get_records_menu('role', null, '', 'shortname, id'); + $teacher = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $roleids['editingteacher']); + + $this->getDataGenerator()->create_module('assign', ['course' => $course->id, 'name' => 'Assign1', 'visible' => 1]); + $this->getDataGenerator()->create_module('assign', ['course' => $course->id, 'name' => 'Assign2', 'visible' => 1]); + $this->getDataGenerator()->create_module('assign', ['course' => $course->id, 'name' => 'Assign3', 'visible' => 0]); + + $this->getDataGenerator()->create_module('quiz', ['course' => $course->id, 'name' => 'Quiz1', 'visible' => 1]); + $this->getDataGenerator()->create_module('quiz', ['course' => $course->id, 'name' => 'Quiz2', 'visible' => 0]); + + $this->getDataGenerator()->create_module('forum', ['course' => $course->id, 'name' => 'Forum1', 'visible' => 1]); + + /** @var core_customfield_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_customfield'); + + $category = $generator->create_category(); + $field = $generator->create_field( + [ + 'categoryid' => $category->get('id'), + 'shortname' => 'myfield', 'type' => 'number', + 'configdata' => [ + 'fieldtype' => 'customfield_number\\local\\numberproviders\\nofactivities', + 'activitytypes' => ['assign', 'forum'], + ], + ] + ); + $this->setUser($teacher); + + return [$course, $field]; + } + +} diff --git a/customfield/field/number/tests/local/numberproviders/nofactivities/nofactivities_test.php b/customfield/field/number/tests/local/numberproviders/nofactivities/nofactivities_test.php new file mode 100644 index 0000000000000..59500c531f900 --- /dev/null +++ b/customfield/field/number/tests/local/numberproviders/nofactivities/nofactivities_test.php @@ -0,0 +1,116 @@ +. + +declare(strict_types=1); + +namespace customfield_number\local\numberproviders\nofactivities; + +use advanced_testcase; +use customfield_number\local\numberproviders\nofactivities; +use customfield_number\provider_base; + +/** + * Tests for the number of activities + * + * @package customfield_number + * @covers \customfield_number\local\numberproviders\nofactivities + * @copyright 2024 Ilya Tregubov + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class nofactivities_test extends advanced_testcase { + + /** + * Test that we can automatically calculate number of activities in courses. + */ + public function test_recalculate(): void { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + + // Add some activities to the courses. + $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]); + $assigngenerator->create_instance(['course' => $course1->id, 'visible' => 0]); + + $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); + $quizgenerator->create_instance(['course' => $course1->id, 'visible' => 1]); + $quizgenerator->create_instance(['course' => $course1->id, 'visible' => 0]); + $quizgenerator->create_instance(['course' => $course1->id, 'visible' => 0]); + + $forumgenerator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); + $forumgenerator->create_instance(['course' => $course1->id, 'visible' => 1]); + $forumgenerator->create_instance(['course' => $course1->id, 'visible' => 0]); + $forumgenerator->create_instance(['course' => $course1->id, 'visible' => 1]); + + $assigngenerator->create_instance(['course' => $course2->id, 'visible' => 1]); + $assigngenerator->create_instance(['course' => $course2->id, 'visible' => 1]); + $assigngenerator->create_instance(['course' => $course2->id, 'visible' => 1]); + + /** @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' => [ + 'fieldtype' => 'customfield_number\local\numberproviders\nofactivities', + "activitytypes" => ["assign", "forum"], + ], + ]); + + // Test if the provider has been added correctly. + $providers = provider_base::get_all_providers($field); + $this->assertNotEmpty($providers); + $this->assertInstanceOf(nofactivities::class, $providers[0]); + + // Calculate only in course1. + $providers[0]->recalculate((int)$course1->id); + $course1customfield = $DB->get_field('customfield_data', 'decvalue', ['instanceid' => $course1->id]); + $course2customfield = $DB->get_field('customfield_data', 'decvalue', ['instanceid' => $course2->id]); + + $this->assertEquals(4.0000, $course1customfield); + $this->assertEquals(false, $course2customfield); + + // Calculate in all courses. + $providers[0]->recalculate(); + $course1customfield = $DB->get_field('customfield_data', 'decvalue', ['instanceid' => $course1->id]); + $course2customfield = $DB->get_field('customfield_data', 'decvalue', ['instanceid' => $course2->id]); + + $this->assertEquals(4.0000, $course1customfield); + $this->assertEquals(3.0000, $course2customfield); + + // Delete some assign module. + $cm = get_coursemodule_from_instance('assign', $assign1->id); + course_delete_module($cm->id); + $providers[0]->recalculate((int)$course1->id); + $course1customfield = $DB->get_field('customfield_data', 'decvalue', ['instanceid' => $course1->id]); + // Module is marked as deleted. + $this->assertEquals(3.0000, $course1customfield); + + // Now, run the course module deletion adhoc task. + \phpunit_util::run_all_adhoc_tasks(); + $providers[0]->recalculate((int)$course1->id); + $course1customfield = $DB->get_field('customfield_data', 'decvalue', ['instanceid' => $course1->id]); + $this->assertEquals(3.0000, $course1customfield); + } +} diff --git a/customfield/field/number/version.php b/customfield/field/number/version.php index 45b4af97c8078..23a0434c0e61d 100644 --- a/customfield/field/number/version.php +++ b/customfield/field/number/version.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die; $plugin->component = 'customfield_number'; -$plugin->version = 2024042200; +$plugin->version = 2024042202; $plugin->requires = 2024041600; $plugin->maturity = MATURITY_STABLE;