MDL-87714 customfield: Add component/area to field number provider

Fixes ambiguous instances when using shared custom fields.
The component and area parameters are now used to get the proper handler.
This commit is contained in:
Sebastian Gundersen
2026-01-28 16:24:35 +01:00
parent 96dd901bea
commit e4c3310295
10 changed files with 108 additions and 23 deletions
@@ -6,6 +6,6 @@ define("customfield_number/recalculate",["exports","core/ajax","core/notificatio
* @author 2024 Marina Glancy
* @copyright 2024 Moodle Pty Ltd <support@moodle.com>
* @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;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:fieldid,instanceid:instanceid}=target.dataset,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)})),initialised=!0},_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}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(){if(initialised)return;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:fieldid,instanceid:instanceid,component:component,area:area,itemid:itemid}=target.dataset,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,component:component,area:area,itemid:itemid}}])[0])).then((data=>(el.innerHTML=data.value,pendingPromise.resolve()))).catch(_notification.default.exception)})),initialised=!0},_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
@@ -1 +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 <http://www.gnu.org/licenses/>.\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 <[email protected]>\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 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\n const {fieldid, instanceid} = target.dataset;\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 return pendingPromise.resolve();\n }).catch(Notification.exception);\n });\n\n initialised = true;\n}\n"],"names":["initialised","document","addEventListener","e","target","closest","SELECTORS","el","querySelector","preventDefault","fieldid","instanceid","dataset","pendingPromise","Pending","then","Ajax","call","methodname","args","data","innerHTML","value","resolve","catch","Notification","exception"],"mappings":";;;;;;;;yFAyCQA,mBAIJC,SAASC,iBAAiB,SAAUC,UAC1BC,OAASD,EAAEC,OAAOC,QAAQC,kBAAoB,IAAMA,oBACrDF,oBAGCG,GAAKH,OAAOC,QAAQC,mBAAmBE,cAAcF,qBACtDC,UAGLJ,EAAEM,uBAEIC,QAACA,QAADC,WAAUA,YAAcP,OAAOQ,QAE/BC,eAAiB,IAAIC,iBAAQ,sEAChBP,IAAIQ,MAAK,IACjBC,cAAKC,KAAK,CAAC,CACdC,WAAY,uCACZC,KAAM,CAACT,QAAAA,QAASC,WAAAA,eAChB,KACLI,MAAMK,OACLb,GAAGc,UAAYD,KAAKE,MACbT,eAAeU,aACvBC,MAAMC,sBAAaC,cAG1B1B,aAAc,2IAzCZM,kBACO,6BADPA,gBAEK,2BAFLA,eAGI,8BAGNN,aAAc"}
{"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 <http://www.gnu.org/licenses/>.\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 <[email protected]>\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 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\n const {fieldid, instanceid, component, area, itemid} = target.dataset;\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, component, area, itemid}\n }])[0];\n }).then((data) => {\n el.innerHTML = data.value;\n return pendingPromise.resolve();\n }).catch(Notification.exception);\n });\n\n initialised = true;\n}\n"],"names":["initialised","document","addEventListener","e","target","closest","SELECTORS","el","querySelector","preventDefault","fieldid","instanceid","component","area","itemid","dataset","pendingPromise","Pending","then","Ajax","call","methodname","args","data","innerHTML","value","resolve","catch","Notification","exception"],"mappings":";;;;;;;;yFAyCQA,mBAIJC,SAASC,iBAAiB,SAAUC,UAC1BC,OAASD,EAAEC,OAAOC,QAAQC,kBAAoB,IAAMA,oBACrDF,oBAGCG,GAAKH,OAAOC,QAAQC,mBAAmBE,cAAcF,qBACtDC,UAGLJ,EAAEM,uBAEIC,QAACA,QAADC,WAAUA,WAAVC,UAAsBA,UAAtBC,KAAiCA,KAAjCC,OAAuCA,QAAUV,OAAOW,QAExDC,eAAiB,IAAIC,iBAAQ,sEAChBV,IAAIW,MAAK,IACjBC,cAAKC,KAAK,CAAC,CACdC,WAAY,uCACZC,KAAM,CAACZ,QAAAA,QAASC,WAAAA,WAAYC,UAAAA,UAAWC,KAAAA,KAAMC,OAAAA,WAC7C,KACLI,MAAMK,OACLhB,GAAGiB,UAAYD,KAAKE,MACbT,eAAeU,aACvBC,MAAMC,sBAAaC,cAG1B7B,aAAc,2IAzCZM,kBACO,6BADPA,gBAEK,2BAFLA,eAGI,8BAGNN,aAAc"}
@@ -54,13 +54,13 @@ export function init() {
}
e.preventDefault();
const {fieldid, instanceid} = target.dataset;
const {fieldid, instanceid, component, area, itemid} = target.dataset;
const pendingPromise = new Pending('recalculate_customfield_number');
addIconToContainer(el).then(() => {
return Ajax.call([{
methodname: 'customfield_number_recalculate_value',
args: {fieldid, instanceid}
args: {fieldid, instanceid, component, area, itemid}
}])[0];
}).then((data) => {
el.innerHTML = data.value;
@@ -52,7 +52,14 @@ class data_controller extends \core_customfield\data_controller {
$field = $this->get_field();
if (!$field->is_editable()) {
$instanceid = (int)$this->get('instanceid');
$data = ['value' => $this->export_value(), 'fieldid' => $field->get('id'), 'instanceid' => $instanceid];
$data = [
'value' => $this->export_value(),
'fieldid' => $field->get('id'),
'instanceid' => $instanceid,
'component' => $this->data->get('component'),
'area' => $this->data->get('area'),
'itemid' => $this->data->get('itemid'),
];
$value = $OUTPUT->render_from_template('customfield_number/staticvalue', $data);
$mform->addElement('static', $elementname . '_static', $this->get_field()->get_formatted_name(),
$value);
@@ -19,6 +19,7 @@ declare(strict_types=1);
namespace customfield_number\external;
use core\exception\invalid_parameter_exception;
use core_customfield\handler;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_api;
@@ -44,6 +45,9 @@ class recalculate extends external_api {
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),
'component' => new external_value(PARAM_COMPONENT, 'Component related to instance id', VALUE_DEFAULT, 'core_course'),
'area' => new external_value(PARAM_AREA, 'Area related to instance id', VALUE_DEFAULT, 'course'),
'itemid' => new external_value(PARAM_INT, 'Item id', VALUE_DEFAULT, 0),
]);
}
@@ -52,16 +56,31 @@ class recalculate extends external_api {
*
* @param int $fieldid
* @param int $instanceid
* @param string $component
* @param string $area
* @param int $itemid
* @return array
*/
public static function execute(int $fieldid, int $instanceid): array {
public static function execute(
int $fieldid,
int $instanceid,
string $component = 'core_course',
string $area = 'course',
int $itemid = 0,
): array {
// Parameter validation.
[
'fieldid' => $fieldid,
'instanceid' => $instanceid,
'component' => $component,
'area' => $area,
'itemid' => $itemid,
] = self::validate_parameters(self::execute_parameters(), [
'fieldid' => $fieldid,
'instanceid' => $instanceid,
'component' => $component,
'area' => $area,
'itemid' => $itemid,
]);
// Access validation.
@@ -70,19 +89,25 @@ class recalculate extends external_api {
$field = \core_customfield\field_controller::create($fieldid);
$provider = provider_base::instance($field);
if (!$provider) {
if (!$provider || !$provider->is_available()) {
throw new invalid_parameter_exception('Invalid parameter');
}
$handler = $field->get_handler();
$handler = handler::get_handler($component, $area, $itemid);
if (!$handler->can_edit($field, $instanceid)) {
throw new \moodle_exception('nopermissions', '', '', get_string('update'));
}
$provider->recalculate($instanceid);
$provider->recalculate($instanceid, $component, $area, $itemid);
$data = $handler->get_instance_fields_data(
[$fieldid => $field], $instanceid)[$fieldid];
$datas = \core_customfield\api::get_instance_fields_data(
fields: [$fieldid => $field],
instanceid: $instanceid,
component: $component,
area: $area,
itemid: $itemid,
);
$data = $datas[$fieldid];
return ['value' => $data->export_value()];
}
@@ -19,6 +19,7 @@ declare(strict_types=1);
namespace customfield_number\local\numberproviders;
use context_course;
use core\exception\coding_exception;
use core_plugin_manager;
use customfield_number\data_controller;
use customfield_number\provider_base;
@@ -80,9 +81,21 @@ class nofactivities extends provider_base {
* Recalculate the number of activities in the course.
*
* @param int|null $instanceid
* @param string $component
* @param string $area
* @param int $itemid
*/
public function recalculate(?int $instanceid = null): void {
public function recalculate(
?int $instanceid = null,
string $component = 'core_course',
string $area = 'course',
int $itemid = 0,
): void {
global $DB;
if ($component !== 'core_course' || $area !== 'course') {
throw new coding_exception('Can only calculate number of activities for courses');
}
$types = $this->field->get_configdata_property('activitytypes');
$displaywhenzero = $this->field->get_configdata_property('displaywhenzero');
@@ -64,8 +64,16 @@ abstract class provider_base {
* Recalculate field value.
*
* @param int|null $instanceid
* @param string $component
* @param string $area
* @param int $itemid
*/
public function recalculate(?int $instanceid = null): void {
public function recalculate(
?int $instanceid = null,
string $component = 'core_course',
string $area = 'course',
int $itemid = 0,
): void {
}
/**
@@ -67,7 +67,7 @@ class cron extends scheduled_task {
$field = field_controller::create(0, $row, $category);
if ($provider = provider_base::instance($field)) {
if ($provider->is_available()) {
$provider->recalculate();
$provider->recalculate(null, $cat->component, $cat->area, $cat->itemid);
}
}
}
@@ -16,7 +16,6 @@
namespace customfield_number\task;
use coding_exception;
use core\task\adhoc_task;
use core_customfield\field_controller;
use customfield_number\provider_base;
@@ -58,9 +57,15 @@ class recalculate extends adhoc_task {
// Schedule recalculate for each field, checking component, area and the presense of provider.
$instanceid = clean_param($customdata->instanceid ?? null, PARAM_INT);
$component = clean_param($customdata->component ?? 'core_course', PARAM_COMPONENT);
$area = clean_param($customdata->area ?? 'course', PARAM_AREA);
$itemid = clean_param($customdata->itemid ?? 0, PARAM_INT);
foreach ($fields as $field) {
if ($this->field_is_scheduled($field) && ($provider = provider_base::instance($field))) {
$provider->recalculate($instanceid ?: null);
if ($this->field_is_scheduled($field)) {
$provider = provider_base::instance($field);
if ($provider?->is_available()) {
$provider->recalculate($instanceid ?: null, $component, $area, $itemid);
}
}
}
}
@@ -87,11 +92,26 @@ class recalculate extends adhoc_task {
*
* @param int $fieldid in of the custom field
* @param int|null $instanceid if specified, only recalculates for the given instance id
* @param string|null $component the component related to the instance
* @param string|null $area the area related to the instance
* @param int $itemid
* @return void
*/
public static function schedule_for_field(int $fieldid, ?int $instanceid = null) {
public static function schedule_for_field(
int $fieldid,
?int $instanceid = null,
?string $component = null,
?string $area = null,
int $itemid = 0,
) {
$task = new static();
$task->set_custom_data(['fieldid' => $fieldid, 'instanceid' => $instanceid]);
$task->set_custom_data([
'fieldid' => $fieldid,
'instanceid' => $instanceid,
'component' => $component,
'area' => $area,
'itemid' => $itemid,
]);
\core\task\manager::queue_adhoc_task($task, true);
}
@@ -102,16 +122,23 @@ class recalculate extends adhoc_task {
* @param string|null $component
* @param string|null $area
* @param int|null $instanceid
* @param int $itemid
* @return void
*/
public static function schedule_for_fieldtype(string $fieldtype, ?string $component = null, ?string $area = null,
?int $instanceid = null) {
public static function schedule_for_fieldtype(
string $fieldtype,
?string $component = null,
?string $area = null,
?int $instanceid = null,
int $itemid = 0,
) {
$task = new static();
$task->set_custom_data([
'fieldtype' => $fieldtype,
'component' => $component,
'area' => $area,
'instanceid' => $instanceid,
'itemid' => $itemid,
]);
\core\task\manager::queue_adhoc_task($task, true);
}
@@ -20,7 +20,10 @@
{
"value": 5,
"instanceid": 4,
"fieldid": 5
"fieldid": 5,
"component": "core_course",
"area": "course",
"itemid": 0
}
}}
<div class="customfield_number-recalculate-wrapper d-flex mt-2" data-fieldtype="wrapper">
@@ -28,7 +31,9 @@
{{#instanceid}}
<div class="customfield_number-recalculate-link ms-2">
<a href="#" data-customfield_number-action="recalculate" data-fieldtype="link"
data-fieldid="{{fieldid}}" data-instanceid="{{instanceid}}">{{#pix}}t/reload, core{{/pix}}{{#str}} update, core {{/str}}</a>
data-fieldid="{{fieldid}}" data-instanceid="{{instanceid}}"
data-component="{{component}}" data-area="{{area}}" data-itemid="{{itemid}}"
>{{#pix}}t/reload, core{{/pix}}{{#str}} update, core {{/str}}</a>
</div>
{{/instanceid}}
</div>