This commit is contained in:
Jun Pataleta
2025-09-17 12:09:39 +08:00
43 changed files with 1431 additions and 59 deletions
@@ -0,0 +1,11 @@
issueNumber: MDL-86065
notes:
core_customfield:
- message: >-
Added parameters 'component', 'area' and 'itemid' to the
`api::get_instance_fields_data()` and `api::get_instances_fields_data()` methods.
Added a new field 'shared' to the customfield_category DB table.
Added 'component', 'area' and 'itemid' fields to the customfield_data DB table.
Modified the customfield_data DB table unique index to include the new fields.
type: changed
+45
View File
@@ -0,0 +1,45 @@
<?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/>.
/**
* Custom fields related settings.
*
* @package core_admin
* @copyright 2025 David Carrillo <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
defined('MOODLE_INTERNAL') || die;
/** @var admin_root $ADMIN */
$ADMIN->add(
'root',
new admin_category(
'customfield',
new lang_string('customfields', 'core_customfield')
)
);
$ADMIN->add(
'customfield',
new admin_externalpage(
'sharedcustomfields',
new lang_string('sharedcustomfields', 'core_customfield'),
new moodle_url("/customfield/customfield.php")
)
);
@@ -170,7 +170,7 @@ class content_handler extends \core_customfield\handler {
$courseid = $task->get_courseid();
$context = $this->get_instance_context($courseid);
$editablefields = $this->get_editable_fields($courseid);
$records = api::get_instance_fields_data($editablefields, $courseid);
$records = $this->get_instance_fields_data($editablefields, $courseid);
$target = $task->get_target();
$override = ($target != \backup::TARGET_CURRENT_ADDING && $target != \backup::TARGET_EXISTING_ADDING);
@@ -26,7 +26,6 @@ namespace core_course\customfield;
defined('MOODLE_INTERNAL') || die;
use core_customfield\api;
use core_customfield\field_controller;
/**
@@ -120,7 +119,7 @@ class course_handler extends \core_customfield\handler {
* @return bool true if the current can edit custom fields, false otherwise
*/
public function can_view(field_controller $field, int $instanceid): bool {
$visibility = $field->get_configdata_property('visibility');
$visibility = $field->get_configdata_property('visibility') ?? self::VISIBLETOALL;
if ($visibility == self::NOTVISIBLE) {
return false;
} else if ($visibility == self::VISIBLETOTEACHERS) {
@@ -222,7 +221,7 @@ class course_handler extends \core_customfield\handler {
$courseid = $task->get_courseid();
$context = $this->get_instance_context($courseid);
$editablefields = $this->get_editable_fields($courseid);
$records = api::get_instance_fields_data($editablefields, $courseid);
$records = $this->get_instance_fields_data($editablefields, $courseid);
$target = $task->get_target();
$override = ($target != \backup::TARGET_CURRENT_ADDING && $target != \backup::TARGET_EXISTING_ADDING);
@@ -0,0 +1,10 @@
define("core_customfield/repository/toggle_shared",["exports","core/ajax"],(function(_exports,_ajax){var obj;
/**
* Module to handle toggle shared category AJAX requests
*
* @module core_customfield/repository/toggle_shared
* @copyright 2025 David Carrillo <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.toggleCategory=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.toggleCategory=(categoryId,component,area,itemid,state)=>{const request={methodname:"core_customfield_toggle_shared",args:{categoryid:categoryId,component:component,area:area,itemid:itemid,state:state}};return _ajax.default.call([request])[0]}}));
//# sourceMappingURL=toggle_shared.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"toggle_shared.min.js","sources":["../../src/repository/toggle_shared.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 * Module to handle toggle shared category AJAX requests\n *\n * @module core_customfield/repository/toggle_shared\n * @copyright 2025 David Carrillo <[email protected]>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Toggle shared category enabled\n *\n * @method\n * @param {Number} categoryId\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n * @param {Boolean} state\n * @return {Promise}\n */\nexport const toggleCategory = (categoryId, component, area, itemid, state) => {\n const request = {\n methodname: 'core_customfield_toggle_shared',\n args: {categoryid: categoryId, component: component, area: area, itemid: itemid, state: state}\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["categoryId","component","area","itemid","state","request","methodname","args","categoryid","Ajax","call"],"mappings":";;;;;;;wKAoC8B,CAACA,WAAYC,UAAWC,KAAMC,OAAQC,eAC1DC,QAAU,CACZC,WAAY,iCACZC,KAAM,CAACC,WAAYR,WAAYC,UAAWA,UAAWC,KAAMA,KAAMC,OAAQA,OAAQC,MAAOA,eAGrFK,cAAKC,KAAK,CAACL,UAAU"}
+3
View File
@@ -0,0 +1,3 @@
define("core_customfield/toggle_shared",["exports","core/inplace_editable","core/notification","core/pending","core_customfield/repository/toggle_shared"],(function(_exports,_inplace_editable,_notification,_pending,_toggle_shared){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);let initialized=!1;_exports.init=()=>{initialized||(document.addEventListener("click",(event=>{const sharedToggle=event.target.closest('[data-action="shared-toggle"]');if(sharedToggle){const pendingPromise=new _pending.default("core_customfield/shared:toggle"),categoryId=sharedToggle.dataset.id,component=sharedToggle.dataset.component,area=sharedToggle.dataset.area,itemid=sharedToggle.dataset.itemid,sharedStateToggle=+!Number(sharedToggle.dataset.state);(0,_toggle_shared.toggleCategory)(categoryId,component,area,itemid,sharedStateToggle).then((()=>(sharedToggle.dataset.state=sharedStateToggle,pendingPromise.resolve()))).catch(_notification.default.exception)}})),initialized=!0)}}));
//# sourceMappingURL=toggle_shared.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"toggle_shared.min.js","sources":["../src/toggle_shared.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 * Custom fields shared category toggle\n *\n * @module core_customfield/toggle_shared\n * @copyright 2025 David Carrillo <[email protected]>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport 'core/inplace_editable';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {toggleCategory} from 'core_customfield/repository/toggle_shared';\n\nlet initialized = false;\n\n/**\n * Initialise toggle\n */\nexport const init = () => {\n if (initialized) {\n // We already added the event listeners (can be called multiple times by mustache template).\n return;\n }\n\n document.addEventListener('click', event => {\n // Toggle shared category.\n const sharedToggle = event.target.closest('[data-action=\"shared-toggle\"]');\n if (sharedToggle) {\n const pendingPromise = new Pending('core_customfield/shared:toggle');\n const categoryId = sharedToggle.dataset.id;\n const component = sharedToggle.dataset.component;\n const area = sharedToggle.dataset.area;\n const itemid = sharedToggle.dataset.itemid;\n const sharedStateToggle = +!Number(sharedToggle.dataset.state);\n\n toggleCategory(categoryId, component, area, itemid, sharedStateToggle)\n .then(() => {\n sharedToggle.dataset.state = sharedStateToggle;\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n }\n });\n\n initialized = true;\n};\n"],"names":["initialized","document","addEventListener","event","sharedToggle","target","closest","pendingPromise","Pending","categoryId","dataset","id","component","area","itemid","sharedStateToggle","Number","state","then","resolve","catch","Notification","exception"],"mappings":"weA8BIA,aAAc,gBAKE,KACZA,cAKJC,SAASC,iBAAiB,SAASC,cAEzBC,aAAeD,MAAME,OAAOC,QAAQ,oCACtCF,aAAc,OACRG,eAAiB,IAAIC,iBAAQ,kCAC7BC,WAAaL,aAAaM,QAAQC,GAClCC,UAAYR,aAAaM,QAAQE,UACjCC,KAAOT,aAAaM,QAAQG,KAC5BC,OAASV,aAAaM,QAAQI,OAC9BC,oBAAsBC,OAAOZ,aAAaM,QAAQO,yCAEzCR,WAAYG,UAAWC,KAAMC,OAAQC,mBAC/CG,MAAK,KACFd,aAAaM,QAAQO,MAAQF,kBACtBR,eAAeY,aAEzBC,MAAMC,sBAAaC,eAIhCtB,aAAc"}
@@ -0,0 +1,44 @@
// 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/>.
/**
* Module to handle toggle shared category AJAX requests
*
* @module core_customfield/repository/toggle_shared
* @copyright 2025 David Carrillo <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
/**
* Toggle shared category enabled
*
* @method
* @param {Number} categoryId
* @param {String} component
* @param {String} area
* @param {Number} itemid
* @param {Boolean} state
* @return {Promise}
*/
export const toggleCategory = (categoryId, component, area, itemid, state) => {
const request = {
methodname: 'core_customfield_toggle_shared',
args: {categoryid: categoryId, component: component, area: area, itemid: itemid, state: state}
};
return Ajax.call([request])[0];
};
@@ -0,0 +1,63 @@
// 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/>.
/**
* Custom fields shared category toggle
*
* @module core_customfield/toggle_shared
* @copyright 2025 David Carrillo <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
"use strict";
import 'core/inplace_editable';
import Notification from 'core/notification';
import Pending from 'core/pending';
import {toggleCategory} from 'core_customfield/repository/toggle_shared';
let initialized = false;
/**
* Initialise toggle
*/
export const init = () => {
if (initialized) {
// We already added the event listeners (can be called multiple times by mustache template).
return;
}
document.addEventListener('click', event => {
// Toggle shared category.
const sharedToggle = event.target.closest('[data-action="shared-toggle"]');
if (sharedToggle) {
const pendingPromise = new Pending('core_customfield/shared:toggle');
const categoryId = sharedToggle.dataset.id;
const component = sharedToggle.dataset.component;
const area = sharedToggle.dataset.area;
const itemid = sharedToggle.dataset.itemid;
const sharedStateToggle = +!Number(sharedToggle.dataset.state);
toggleCategory(categoryId, component, area, itemid, sharedStateToggle)
.then(() => {
sharedToggle.dataset.state = sharedStateToggle;
return pendingPromise.resolve();
})
.catch(Notification.exception);
}
});
initialized = true;
};
+52 -4
View File
@@ -49,13 +49,23 @@ class api {
* @param field_controller[] $fields list of fields indexed by field id
* @param int $instanceid
* @param bool $adddefaults
* @param string $component
* @param string $area
* @param int $itemid
* @return data_controller[] array of data_controller objects indexed by fieldid. All fields are present,
* some data_controller objects may have 'id', some not
* If ($adddefaults): All fieldids are present, some data_controller objects may have 'id', some not.
* If (!$adddefaults): Only fieldids with data are present, all data_controller objects have 'id'.
*/
public static function get_instance_fields_data(array $fields, int $instanceid, bool $adddefaults = true): array {
return self::get_instances_fields_data($fields, [$instanceid], $adddefaults)[$instanceid];
public static function get_instance_fields_data(
array $fields,
int $instanceid,
bool $adddefaults = true,
string $component = '',
string $area = '',
int $itemid = 0
): array {
return self::get_instances_fields_data($fields, [$instanceid], $adddefaults, $component, $area, $itemid)[$instanceid];
}
/**
@@ -64,12 +74,22 @@ class api {
* @param field_controller[] $fields list of fields indexed by field id
* @param int[] $instanceids
* @param bool $adddefaults
* @param string $component
* @param string $area
* @param int $itemid
* @return data_controller[][] 2-dimension array, first index is instanceid, second index is fieldid.
* If ($adddefaults): All instanceids and all fieldids are present, some data_controller objects may have 'id', some not.
* If (!$adddefaults): All instanceids are present but only fieldids with data are present, all
* data_controller objects have 'id'.
*/
public static function get_instances_fields_data(array $fields, array $instanceids, bool $adddefaults = true): array {
public static function get_instances_fields_data(
array $fields,
array $instanceids,
bool $adddefaults = true,
string $component = '',
string $area = '',
int $itemid = 0
): array {
global $DB;
// Create the results array where instances and fields order is the same as in the input arrays.
@@ -82,10 +102,18 @@ class api {
// Retrieve all existing data.
list($sqlfields, $params) = $DB->get_in_or_equal(array_keys($fields), SQL_PARAMS_NAMED, 'fld');
list($sqlinstances, $iparams) = $DB->get_in_or_equal($instanceids, SQL_PARAMS_NAMED, 'ins');
$sqlplugin = '';
if (!empty($component) && !empty($area)) {
// If component, area and itemid are provided, filter by them.
$params['component'] = $component;
$params['area'] = $area;
$params['itemid'] = $itemid;
$sqlplugin = "AND d.component = :component AND d.area = :area AND d.itemid = :itemid";
}
$sql = "SELECT d.*
FROM {customfield_field} f
JOIN {customfield_data} d ON (f.id = d.fieldid AND d.instanceid {$sqlinstances})
WHERE f.id {$sqlfields}";
WHERE f.id {$sqlfields} $sqlplugin";
$fieldsdata = $DB->get_recordset_sql($sql, $params + $iparams);
foreach ($fieldsdata as $data) {
$result[$data->instanceid][$data->fieldid] = data_controller::create(0, $data, $fields[$data->fieldid]);
@@ -439,4 +467,24 @@ class api {
}
return $ret;
}
/**
* Checks if a shared category is enabled for the given entity
*
* @param int $categoryid
* @param string $component
* @param string $area
* @param int $itemid
* @return bool
*/
public static function is_shared_category_enabled(int $categoryid, string $component, string $area, int $itemid): bool {
$sql = "categoryid = :categoryid AND component = :component AND area = :area AND itemid = :itemid";
$params = [
'categoryid' => $categoryid,
'component' => $component,
'area' => $area,
'itemid' => $itemid,
];
return shared::record_exists_select($sql, $params);
}
}
+5
View File
@@ -74,6 +74,11 @@ class category extends persistent {
'default' => -1,
'null' => NULL_ALLOWED,
],
'shared' => [
'type' => PARAM_BOOL,
'null' => NULL_NOT_ALLOWED,
'default' => false,
],
);
}
}
@@ -50,6 +50,15 @@ class category_controller {
/** @var handler */
protected $handler;
/** @var ?string */
protected $component = null;
/** @var ?string */
protected $area = null;
/** @var ?int */
protected $itemid = null;
/**
* category constructor.
*
@@ -107,6 +116,8 @@ class category_controller {
}
$record->itemid = $handler->get_itemid();
}
// Check if the category is shared.
$record->shared = $record->component === 'core_customfield' && $record->area === 'shared';
$category = new self(0, $record);
if (!$category->get('contextid')) {
// If contextid was not present in the record we can find it out from the handler.
@@ -115,6 +126,10 @@ class category_controller {
}
if ($handler) {
$category->set_handler($handler);
$category->set_original_component($category->get_handler()->get_component());
$category->set_original_area($category->get_handler()->get_area());
$category->set_original_itemid($category->get_handler()->get_itemid());
}
return $category;
}
@@ -183,7 +198,12 @@ class category_controller {
public function get_handler(): handler {
if ($this->handler === null) {
$this->handler = handler::get_handler($this->get('component'), $this->get('area'), $this->get('itemid'));
$this->set_original_component($this->handler->get_component());
$this->set_original_area($this->handler->get_area());
$this->set_original_itemid($this->handler->get_itemid());
}
return $this->handler;
}
@@ -208,6 +228,67 @@ class category_controller {
throw new \coding_exception('Context of the handler does not match the one from the record');
}
$this->handler = $handler;
$this->component = $handler->get_component();
$this->area = $handler->get_area();
$this->itemid = $handler->get_itemid();
}
/**
* Gets the original component.
*
* @return string|null
*/
public function get_original_component(): ?string {
return $this->component;
}
/**
* Gets the original area.
*
* @return string|null
*/
public function get_original_area(): ?string {
return $this->area;
}
/**
* Gets the original itemid.
*
* @return int|null
*/
public function get_original_itemid(): ?int {
return $this->itemid;
}
/**
* Sets the original component.
*
* @param string $component
* @return void
*/
public function set_original_component(string $component): void {
$this->component = $component;
}
/**
* Sets the original area.
*
* @param string $area
* @return void
*/
public function set_original_area(string $area): void {
$this->area = $area;
}
/**
* Sets the original itemid.
*
* @param int $itemid
* @return void
*/
public function set_original_itemid(int $itemid): void {
$this->itemid = $itemid;
}
/**
@@ -0,0 +1,122 @@
<?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/>.
declare(strict_types=1);
namespace core_customfield\customfield;
use core\context;
use core\context\system;
use core\exception\coding_exception;
use core\url;
use core_customfield\field_controller;
/**
* Shared handler for custom fields
*
* @package core_customfield
* @copyright 2025 David Carrillo <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class shared_handler extends \core_customfield\handler {
/**
* @var shared_handler|null
*/
protected static ?shared_handler $singleton = null;
/**
* Returns a singleton
*
* @param int $itemid
* @return self
*/
public static function create(int $itemid = 0): self {
if (static::$singleton === null) {
self::$singleton = new static($itemid);
}
return self::$singleton;
}
/**
* Run reset code after unit tests to reset the singleton usage.
*/
public static function reset_caches(): void {
if (!PHPUNIT_TEST) {
throw new coding_exception('This feature is only intended for use in unit tests');
}
static::$singleton = null;
}
/**
* The current user can configure custom fields on this component.
*
* @return bool true if the current user can configure custom fields, false otherwise
*/
public function can_configure(): bool {
return has_capability('moodle/customfield:configureshared', $this->get_configuration_context());
}
/**
* The current user can edit custom fields on the given report.
*
* @param field_controller $field
* @param int $instanceid id of the report to test edit permission
* @return bool true if the current user can edit custom fields, false otherwise
*/
public function can_edit(field_controller $field, int $instanceid = 0): bool {
return has_capability('moodle/customfield:configureshared', $this->get_configuration_context());
}
/**
* The current user can view custom fields on the given report.
*
* @param field_controller $field
* @param int $instanceid id of the report to test edit permission
* @return bool true if the current user can edit custom fields, false otherwise
*/
public function can_view(field_controller $field, int $instanceid): bool {
return true;
}
/**
* Context that should be used for new categories created by this handler
*
* @return context the context for configuration
*/
public function get_configuration_context(): context {
return system::instance();
}
/**
* URL for configuration of the fields on this handler.
*
* @return url The URL to configure custom fields for this component
*/
public function get_configuration_url(): url {
return new url('/customfield/customfield.php');
}
/**
* Returns the context for the data associated with the given instanceid.
*
* @param int $instanceid id of the record to get the context for
* @return context the context for the given record
*/
public function get_instance_context(int $instanceid = 0): context {
return system::instance();
}
}
+12 -1
View File
@@ -106,7 +106,18 @@ class data extends persistent {
'type' => PARAM_INT,
'optional' => false,
'null' => NULL_NOT_ALLOWED
]
],
'component' => [
'type' => PARAM_COMPONENT,
],
'area' => [
'type' => PARAM_COMPONENT,
],
'itemid' => [
'type' => PARAM_INT,
'optional' => true,
'default' => 0,
],
);
}
@@ -113,6 +113,18 @@ abstract class data_controller {
if (!class_exists($customfieldtype) || !is_subclass_of($customfieldtype, self::class)) {
throw new \moodle_exception('errorfieldtypenotfound', 'core_customfield', '', s($type));
}
$category = $field->get_category();
$record->component = $category->get_original_component();
$record->area = $category->get_original_area();
$record->itemid = $category->get_original_itemid();
if (!$record->component || !$record->area) {
$record->component = $field->get_handler()->get_component();
$record->area = $field->get_handler()->get_area();
$record->itemid = $field->get_handler()->get_itemid();
}
$datacontroller = new $customfieldtype(0, $record);
$datacontroller->field = $field;
return $datacontroller;
@@ -210,6 +222,14 @@ abstract class data_controller {
$this->data->set($datafield, $datafieldvalue);
$this->data->set('value', $value);
// Set component, area and itemid from the handler.
$category = $this->field->get_category();
$this->data->set_many([
'component' => $category->get_original_component(),
'area' => $category->get_original_area(),
'itemid' => $category->get_original_itemid(),
]);
$this->save();
}
@@ -0,0 +1,85 @@
<?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/>.
declare(strict_types=1);
namespace core_customfield\event;
use context;
use core_customfield\shared;
defined('MOODLE_INTERNAL') || die();
/**
* Custom field shared category usage disabled event class.
*
* @package core_customfield
* @copyright 2025 David Carrillo <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class shared_category_usage_disabled extends \core\event\base {
/**
* Initialise the event data.
*/
protected function init(): void {
$this->data['objecttable'] = 'customfield_shared';
$this->data['crud'] = 'd';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Creates an instance from a shared object
*
* @param shared $shared
* @param context $context
* @return category_updated
*/
public static function create_from_object(shared $shared, context $context): shared_category_usage_disabled {
$eventparams = [
'objectid' => $shared->get('id'),
'context' => $context,
'other' => [
'categoryid' => $shared->get('categoryid'),
'component' => $shared->get('component'),
'area' => $shared->get('area'),
'itemid' => $shared->get('itemid'),
],
];
$event = self::create($eventparams);
$event->add_record_snapshot($event->objecttable, $shared->to_record());
return $event;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name(): string {
return get_string('eventsharedcategoryusagedisabled', 'core_customfield');
}
/**
* Returns non-localised description of what happened.
*
* @return string
*/
public function get_description(): string {
return "The user with ID '$this->userid' disabled usage of shared category with ID '{$this->other['categoryid']}'" .
" in '{$this->other['component']}/{$this->other['area']}'";
}
}
@@ -0,0 +1,85 @@
<?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/>.
declare(strict_types=1);
namespace core_customfield\event;
use context;
use core_customfield\shared;
defined('MOODLE_INTERNAL') || die();
/**
* Custom field shared category usage enabled event class.
*
* @package core_customfield
* @copyright 2025 David Carrillo <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class shared_category_usage_enabled extends \core\event\base {
/**
* Initialise the event data.
*/
protected function init(): void {
$this->data['objecttable'] = 'customfield_shared';
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Creates an instance from a shared object
*
* @param shared $shared
* @param context $context
* @return category_updated
*/
public static function create_from_object(shared $shared, context $context): shared_category_usage_enabled {
$eventparams = [
'objectid' => $shared->get('id'),
'context' => $context,
'other' => [
'categoryid' => $shared->get('categoryid'),
'component' => $shared->get('component'),
'area' => $shared->get('area'),
'itemid' => $shared->get('itemid'),
],
];
$event = self::create($eventparams);
$event->add_record_snapshot($event->objecttable, $shared->to_record());
return $event;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name(): string {
return get_string('eventsharedcategoryusageenabled', 'core_customfield');
}
/**
* Returns non-localised description of what happened.
*
* @return string
*/
public function get_description(): string {
return "The user with ID '$this->userid' enabled usage of shared category with ID '{$this->other['categoryid']}'" .
" in '{$this->other['component']}/{$this->other['area']}'";
}
}
+116
View File
@@ -0,0 +1,116 @@
<?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/>.
declare(strict_types=1);
namespace core_customfield\external;
use core_customfield\event\shared_category_usage_disabled;
use core_customfield\event\shared_category_usage_enabled;
use core_customfield\handler;
use core_customfield\shared;
use core_external\external_api;
use core_external\external_value;
use core_external\external_function_parameters;
/**
* External method for toggling shared categories
*
* @package core_customfield
* @copyright 2025 David Carrillo <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class toggle_shared_category extends external_api {
/**
* External method parameters
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'categoryid' => new external_value(PARAM_INT, 'Category ID'),
'component' => new external_value(PARAM_COMPONENT, 'Component'),
'area' => new external_value(PARAM_AREA, 'Area'),
'itemid' => new external_value(PARAM_INT, 'Item ID'),
'state' => new external_value(PARAM_BOOL, 'New state'),
]);
}
/**
* External method execution
*
* @param int $categoryid
* @param string $component
* @param string $area
* @param int $itemid
* @param bool $state
* @return bool
*/
public static function execute(int $categoryid, string $component, string $area, int $itemid, bool $state): bool {
[
'categoryid' => $categoryid,
'component' => $component,
'area' => $area,
'itemid' => $itemid,
'state' => $state,
] = self::validate_parameters(self::execute_parameters(), [
'categoryid' => $categoryid,
'component' => $component,
'area' => $area,
'itemid' => $itemid,
'state' => $state,
]);
// Validate context.
$context = \core\context\system::instance();
self::validate_context($context);
$handler = handler::get_handler($component, $area, $itemid);
if (!$handler->can_configure()) {
throw new \moodle_exception('nopermissions', 'error', '', get_string('customfield:configureshared', 'core_role'));
}
global $DB;
$params = [
'categoryid' => $categoryid,
'component' => $component,
'area' => $area,
'itemid' => $itemid,
];
if ($state) {
$record = new shared(0, (object) $params);
$record->create();
shared_category_usage_enabled::create_from_object($record, $handler->get_configuration_context())->trigger();
} else {
$record = shared::get_record($params);
$DB->delete_records('customfield_shared', $params);
shared_category_usage_disabled::create_from_object($record, $handler->get_configuration_context())->trigger();
}
return true;
}
/**
* External method return value
*
* @return external_value
*/
public static function execute_returns(): external_value {
return new external_value(PARAM_BOOL);
}
}
+100 -17
View File
@@ -227,10 +227,41 @@ abstract class handler {
*/
protected function validate_category(category_controller $category): category_controller {
$categories = $this->get_categories_with_fields();
if (!array_key_exists($category->get('id'), $categories)) {
$category = $this->get_category_from_array(
$categories,
$category->get('id'),
$this->get_component(),
$this->get_area(),
$this->get_itemid()
);
if ($category === null) {
throw new \moodle_exception('categorynotfound', 'core_customfield');
}
return $categories[$category->get('id')];
return $category;
}
/**
* Retrieves a category_controller from an array of categories matching the given identifiers.
*
* @param array $categories Array of category_controller objects.
* @param int $categoryid The ID of the category to find.
* @param string $component The component name.
* @param string $area The area name.
* @param int $itemid The item ID.
* @return category_controller|null
*/
public function get_category_from_array(
array $categories,
int $categoryid,
string $component,
string $area,
int $itemid
): ?category_controller {
$category = array_filter($categories, fn($category) => $category->get('id') === $categoryid &&
$category->get_original_component() === $component &&
$category->get_original_area() === $area &&
$category->get_original_itemid() === $itemid);
return $category ? reset($category) : null;
}
/**
@@ -241,11 +272,16 @@ abstract class handler {
* @throws \moodle_exception
*/
protected function validate_field(field_controller $field): field_controller {
if (!array_key_exists($field->get('categoryid'), $this->get_categories_with_fields())) {
throw new \moodle_exception('fieldnotfound', 'core_customfield');
}
$category = $this->get_categories_with_fields()[$field->get('categoryid')];
if (!array_key_exists($field->get('id'), $category->get_fields())) {
$categories = $this->get_categories_with_fields();
$category = $this->get_category_from_array(
$categories,
$field->get('categoryid'),
$this->get_component(),
$this->get_area(),
$this->get_itemid()
);
if (!$category || !array_key_exists($field->get('id'), $category->get_fields())) {
throw new \moodle_exception('fieldnotfound', 'core_customfield');
}
return $category->get_fields()[$field->get('id')];
@@ -375,7 +411,26 @@ abstract class handler {
*/
public function get_instance_data(int $instanceid, bool $returnall = false): array {
$fields = $returnall ? $this->get_fields() : $this->get_visible_fields($instanceid);
return api::get_instance_fields_data($fields, $instanceid);
return $this->get_instance_fields_data($fields, $instanceid);
}
/**
* For the given instance and list of fields fields retrieves data associated with them using the current entity context
*
* @param field_controller[] $fields Array of field_controller objects.
* @param int $instanceid The instance ID.
* @param bool $adddefaults Whether to add default values for fields without data.
* @return data_controller[] Array of data_controller objects indexed by fieldid.
*/
public function get_instance_fields_data(array $fields, int $instanceid, bool $adddefaults = true): array {
return api::get_instance_fields_data(
$fields,
$instanceid,
$adddefaults,
$this->get_component(),
$this->get_area(),
$this->get_itemid()
);
}
/**
@@ -464,15 +519,43 @@ abstract class handler {
/**
* Returns array of categories, each of them contains a list of fields definitions.
*
* @param bool $ismanagementpage Whether we are on the management page to show all shared categories or not.
* @return category_controller[]
*/
public function get_categories_with_fields(): array {
public function get_categories_with_fields(bool $ismanagementpage = false): array {
if ($this->categories === null) {
$sharedcategories = [];
$this->categories = api::get_categories_with_fields($this->get_component(), $this->get_area(), $this->get_itemid());
// Avoid duplication when we are in the shared custom fields page.
if ($this->get_component() !== 'core_customfield' && $this->get_area() !== 'shared') {
$sharedcategories = api::get_categories_with_fields('core_customfield', 'shared', 0);
// Filter only by enabled shared categories.
if (!$ismanagementpage) {
$sharedcategories = array_filter($sharedcategories, function (category_controller $cc) {
return api::is_shared_category_enabled(
$cc->get('id'),
$this->get_component(),
$this->get_area(),
$this->get_itemid()
);
});
}
}
$this->categories = array_merge($this->categories, $sharedcategories);
}
$handler = $this;
array_walk($this->categories, function(category_controller $c) use ($handler) {
$c->set_handler($handler);
array_walk($this->categories, function (category_controller $cc) use ($handler) {
if ($cc->get('area') === 'shared') {
$sharedhandler = \core_customfield\customfield\shared_handler::create();
$cc->set_handler($sharedhandler);
} else {
// Set the handler for the category.
$cc->set_handler($handler);
}
$cc->set_original_component($handler->get_component());
$cc->set_original_area($handler->get_area());
$cc->set_original_itemid($handler->get_itemid());
});
return $this->categories;
}
@@ -566,7 +649,7 @@ abstract class handler {
*/
public function instance_form_definition_after_data(\MoodleQuickForm $mform, int $instanceid = 0) {
$editablefields = $this->get_editable_fields($instanceid);
$fields = api::get_instance_fields_data($editablefields, $instanceid);
$fields = $this->get_instance_fields_data($editablefields, $instanceid);
foreach ($fields as $formfield) {
$formfield->instance_form_definition_after_data($mform);
@@ -587,7 +670,7 @@ abstract class handler {
*/
public function instance_form_before_set_data(stdClass $instance) {
$instanceid = !empty($instance->id) ? $instance->id : 0;
$fields = api::get_instance_fields_data($this->get_editable_fields($instanceid), $instanceid);
$fields = $this->get_instance_fields_data($this->get_editable_fields($instanceid), $instanceid);
foreach ($fields as $formfield) {
$formfield->instance_form_before_set_data($instance);
@@ -616,7 +699,7 @@ abstract class handler {
return;
}
$editablefields = $this->get_editable_fields($isnewinstance ? 0 : $instance->id);
$fields = api::get_instance_fields_data($editablefields, $instance->id);
$fields = $this->get_instance_fields_data($editablefields, $instance->id);
foreach ($fields as $data) {
if (!$data->get('id')) {
$data->set('contextid', $this->get_instance_context($instance->id)->id);
@@ -643,7 +726,7 @@ abstract class handler {
public function instance_form_validation(array $data, array $files) {
$instanceid = empty($data['id']) ? 0 : $data['id'];
$editablefields = $this->get_editable_fields($instanceid);
$fields = api::get_instance_fields_data($editablefields, $instanceid);
$fields = $this->get_instance_fields_data($editablefields, $instanceid);
$errors = [];
foreach ($fields as $formfield) {
$errors += $formfield->instance_form_validation($data, $files);
@@ -670,7 +753,7 @@ abstract class handler {
?string $headerlangidentifier = null, ?string $headerlangcomponent = null) {
$editablefields = $this->get_editable_fields($instanceid);
$fieldswithdata = api::get_instance_fields_data($editablefields, $instanceid);
$fieldswithdata = $this->get_instance_fields_data($editablefields, $instanceid);
$lastcategoryid = null;
foreach ($fieldswithdata as $data) {
$categoryid = $data->get_field()->get_category()->get('id');
@@ -819,7 +902,7 @@ abstract class handler {
* @param int $instanceid
*/
public function delete_instance(int $instanceid) {
$fielddata = api::get_instance_fields_data($this->get_fields(), $instanceid, false);
$fielddata = $this->get_instance_fields_data($this->get_fields(), $instanceid, false);
foreach ($fielddata as $data) {
$data->delete();
}
@@ -26,6 +26,7 @@ namespace core_customfield\output;
use core_customfield\api;
use core_customfield\handler;
use core_customfield\shared;
use renderable;
use templatable;
@@ -65,6 +66,7 @@ class management implements renderable, templatable {
* @return array|object|\stdClass
*/
public function export_for_template(\renderer_base $output) {
global $DB;
$data = new \stdClass();
$fieldtypes = $this->handler->get_available_field_types();
@@ -73,17 +75,48 @@ class management implements renderable, templatable {
$data->area = $this->handler->get_area();
$data->itemid = $this->handler->get_itemid();
$data->usescategories = $this->handler->uses_categories();
$categories = $this->handler->get_categories_with_fields();
$categories = $this->handler->get_categories_with_fields(true);
// Get all enabled shared categories at once.
$sharedcategoriesenabled = shared::get_records([
'component' => $data->component,
'area' => $data->area,
'itemid' => $data->itemid,
]);
$categoriesarray = array();
foreach ($categories as $category) {
$canedit = $data->component === $category->get('component') && $data->area === $category->get('area');
$categoryarray = array();
$categoryarray['id'] = $category->get('id');
$categoryarray['nameeditable'] = $output->render(api::get_category_inplace_editable($category, true));
$categoryarray['nameeditable'] = $canedit ? $output->render(api::get_category_inplace_editable($category, true)) :
$category->get_formatted_name();
$categoryarray['movetitle'] = get_string('movecategory', 'core_customfield',
$category->get_formatted_name());
$categoryarray['canedit'] = $canedit;
$toggleenabled = (bool) array_filter(
$sharedcategoriesenabled,
fn($record) => $record->get('categoryid') === $category->get('id')
);
$attributes = [
['name' => 'data-id', 'value' => $category->get('id')],
['name' => 'data-action', 'value' => 'shared-toggle'],
['name' => 'data-state', 'value' => $toggleenabled],
['name' => 'data-component', 'value' => $data->component],
['name' => 'data-area', 'value' => $data->area],
['name' => 'data-itemid', 'value' => $data->itemid],
];
$categoryarray['toggle'] = $output->render_from_template('core/toggle', [
'id' => 'shared-toggle-' . $category->get('id'),
'checked' => $toggleenabled,
'extraattributes' => $attributes,
'label' => get_string('enableplugin', 'core_admin', $category->get_formatted_name()),
'labelclasses' => 'visually-hidden',
]);
$categoryarray['fields'] = array();
@@ -95,22 +128,31 @@ class management implements renderable, templatable {
$fieldarray['name'] = $fieldname;
$fieldarray['shortname'] = $field->get('shortname');
$fieldarray['movetitle'] = get_string('movefield', 'core_customfield', $fieldname);
$categoryarray['canedit'] = $canedit;
$categoryarray['fields'][] = $fieldarray;
}
$menu = new \action_menu();
$menu->set_menu_trigger(get_string('createnewcustomfield', 'core_customfield'));
if ($canedit) {
$menu = new \action_menu();
$menu->set_menu_trigger(get_string('createnewcustomfield', 'core_customfield'));
foreach ($fieldtypes as $type => $fieldname) {
$action = new \action_menu_link_secondary(new \moodle_url('#'), null, $fieldname,
['data-role' => 'addfield', 'data-categoryid' => $category->get('id'), 'data-type' => $type,
'data-typename' => $fieldname]);
$menu->add($action);
foreach ($fieldtypes as $type => $fieldname) {
$params = [
'data-role' => 'addfield',
'data-categoryid' => $category->get('id'),
'data-type' => $type,
'data-typename' => $fieldname,
];
$action = new \action_menu_link_secondary(new \core\url('#'), null, $fieldname, $params);
$menu->add($action);
}
$menu->attributes['class'] .= ' float-start me-1';
$categoryarray['addfieldmenu'] = $output->render($menu);
} else {
$categoryarray['addfieldmenu'] = '';
}
$menu->attributes['class'] .= ' float-start me-1';
$categoryarray['addfieldmenu'] = $output->render($menu);
$categoriesarray[] = $categoryarray;
}
+57
View File
@@ -0,0 +1,57 @@
<?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 core_customfield;
use core\persistent;
/**
* Customfield shared persistent class
*
* @package core_customfield
* @copyright 2025 David Carrillo <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class shared extends persistent {
/**
* Database table.
*/
const TABLE = 'customfield_shared';
/**
* Return the definition of the properties of this model.
*
* @return array
*/
protected static function define_properties(): array {
return [
'categoryid' => [
'type' => PARAM_INT,
],
'component' => [
'type' => PARAM_COMPONENT,
],
'area' => [
'type' => PARAM_COMPONENT,
],
'itemid' => [
'type' => PARAM_INT,
'optional' => true,
'default' => 0,
],
];
}
}
+38
View File
@@ -0,0 +1,38 @@
<?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/>.
/**
* Manage shared custom fields
*
* @package core_customfield
* @copyright 2025 David Carrillo <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once('../config.php');
require_once($CFG->libdir . '/adminlib.php');
admin_externalpage_setup('sharedcustomfields');
/** @var \core_customfield\output\renderer $output */
$output = $PAGE->get_renderer('core_customfield');
$handler = \core_customfield\customfield\shared_handler::create();
$outputpage = new \core_customfield\output\management($handler);
echo $output->header(),
$output->heading(new lang_string('sharedcustomfields', 'core_customfield')),
$output->render($outputpage),
$output->footer();
+2
View File
@@ -120,6 +120,7 @@ class core_customfield_external extends external_api {
'id' => new external_value(PARAM_INT, 'id'),
'nameeditable' => new external_value(PARAM_RAW, 'inplace editable name'),
'addfieldmenu' => new external_value(PARAM_RAW, 'addfieldmenu'),
'canedit' => new external_value(PARAM_BOOL, 'can edit'),
'fields' => new external_multiple_structure(
new external_single_structure(
array(
@@ -130,6 +131,7 @@ class core_customfield_external extends external_api {
)
)
, '', VALUE_OPTIONAL),
'toggle' => new external_value(PARAM_RAW, 'toggle'),
)
)
),
@@ -81,7 +81,7 @@ class recalculate extends external_api {
$provider->recalculate($instanceid);
$data = \core_customfield\api::get_instance_fields_data(
$data = $handler->get_instance_fields_data(
[$fieldid => $field], $instanceid)[$fieldid];
return ['value' => $data->export_value()];
@@ -129,7 +129,7 @@ class nofactivities extends provider_base {
}
} else if (empty($record->dataid) || (int)$record->decvalue != $value) {
// Stored value is out of date.
$data = \core_customfield\api::get_instance_fields_data(
$data = $this->field->get_handler()->get_instance_fields_data(
[$fieldid => $this->field], (int)$record->id)[$fieldid];
$data->set('contextid', context_course::instance($record->id)->id);
$data->set('decvalue', $value);
@@ -156,7 +156,13 @@ final class nofactivities_test extends advanced_testcase {
],
]);
$getdata = fn(\customfield_number\field_controller $field): \customfield_number\data_controller =>
\core_customfield\api::get_instance_fields_data([$field->get('id') => $field], (int)$course1->id)[$field->get('id')];
\core_customfield\api::get_instance_fields_data(
[$field->get('id') => $field],
(int)$course1->id,
true,
'core_course',
'course'
)[$field->get('id')];
// Recalculate the value of the field and assert it is set to 1 (one activity in the course).
(new \customfield_number\task\cron())->execute();
+19 -7
View File
@@ -42,6 +42,7 @@
{ "id": "0",
"nameeditable": "Other fields",
"addfieldmenu": "Add field",
"canedit": true,
"fields": [
{ "id": 0, "name": "Field name", "shortname": "shortname", "type": "Text" },
{ "id": 0, "name": "Another field", "shortname": "checkme", "type": "Checkbox" }
@@ -50,6 +51,7 @@
{ "id": "00",
"nameeditable": "Empty category",
"addfieldmenu": "Add field",
"canedit": true,
"fields": [] }
],
"singleselect" : "select"
@@ -71,19 +73,26 @@
{{{nocategories}}}
{{/categories}}
<div class="categorieslist">
{{#categories}}
{{#canedit}}<div class="categorieslist">{{/canedit}}
<div data-category-id="{{id}}" id="category-{{id}}" class="mt-2">
<div class="row justify-content-between align-items-end">
<div class="col-6 categoryinstance">
<div class="col-6 categoryinstance d-flex align-items-center gap-3">
{{#usescategories}}
<h3>
<span class="movecategory icon-size-3">{{> core/drag_handle}}</span>
{{#canedit}}<span class="movecategory icon-size-3">{{> core/drag_handle}}</span>{{/canedit}}
{{{nameeditable}}}
{{#canedit}}
<a class="icon-size-3" href="#" data-role="deletecategory" data-id="{{id}}">
{{#pix}} t/delete, core, {{#str}} delete, moodle {{/str}} {{/pix}}
</a>
{{/canedit}}
</h3>
{{^canedit}}
<div data-bs-toggle="tooltip" data-bs-placement="top" title="{{#str}} sharedcategoryhelptext, customfield {{/str}}">
{{{toggle}}}
</div>
{{/canedit}}
{{/usescategories}}
</div>
<div class="col-auto text-end">
@@ -91,7 +100,7 @@
</div>
</div>
<div>
<table class="table generaltable fieldslist">
<table class="table generaltable {{#canedit}}fieldslist{{/canedit}}">
<thead>
<tr>
<th scope="col" class="col-5">{{#str}} customfield, core_customfield {{/str}}</th>
@@ -103,14 +112,16 @@
<tbody>
{{#fields}}
<tr data-field-name="{{name}}" data-field-id="{{id}}" class="field">
<td class="col-5"><span class="movefield">{{> core/drag_handle}}</span>{{{name}}}</td>
<td class="col-5">{{#canedit}}<span class="movefield">{{> core/drag_handle}}</span>{{/canedit}}{{{name}}}</td>
<td class="col-3">{{{shortname}}}</td>
<td class="col-2">{{{type}}}</td>
<td class="col-2 text-end">
{{#canedit}}
<a href="#" data-role="editfield" data-name="{{name}}" data-id="{{id}}">{{#pix}}
t/edit, core, {{#str}} edit, moodle {{/str}} {{/pix}}</a>
<a href="#" data-id="{{id}}" data-role="deletefield">{{#pix}}
t/delete, core, {{#str}} delete, moodle {{/str}} {{/pix}}</a>
{{/canedit}}
</td>
</tr>
{{/fields}}
@@ -121,12 +132,13 @@
</table>
</div>
</div>
{{#canedit}}</div>{{/canedit}}
{{/categories}}
</div>
</div>
{{#js}}
require(['core_customfield/form'], function(s) {
require(['core_customfield/form', 'core_customfield/toggle_shared'], function(s, toggle) {
s.init();
toggle.init();
});
{{/js}}
+23 -3
View File
@@ -181,7 +181,14 @@ final class api_test extends \advanced_testcase {
$handler = \core_course\customfield\course_handler::create();
$c1id = $handler->create_category();
$c1 = $handler->get_categories_with_fields()[$c1id];
$categories = $handler->get_categories_with_fields();
$c1 = $handler->get_category_from_array(
$categories,
$c1id,
$handler->get_component(),
$handler->get_area(),
$handler->get_itemid()
);
$this->assertEquals('Other fields', $c1->get('name'));
$this->assertEquals($handler->get_component(), $c1->get('component'));
$this->assertEquals($handler->get_area(), $c1->get('area'));
@@ -191,8 +198,21 @@ final class api_test extends \advanced_testcase {
// Generate more categories and make sure they have different names.
$c2id = $handler->create_category();
$c3id = $handler->create_category();
$c2 = $handler->get_categories_with_fields()[$c2id];
$c3 = $handler->get_categories_with_fields()[$c3id];
$categories = $handler->get_categories_with_fields();
$c2 = $handler->get_category_from_array(
$categories,
$c2id,
$handler->get_component(),
$handler->get_area(),
$handler->get_itemid()
);
$c3 = $handler->get_category_from_array(
$categories,
$c3id,
$handler->get_component(),
$handler->get_area(),
$handler->get_itemid()
);
$this->assertEquals('Other fields 1', $c2->get('name'));
$this->assertEquals('Other fields 2', $c3->get('name'));
}
@@ -0,0 +1,83 @@
@core @core_course @core_customfield @javascript
Feature: Create shared categories and fields
In order to have shared custom fields
As a manager
I need to create, edit, remove and sort shared custom field's categories
Scenario: Create and delete a category for shared custom fields
Given I log in as "admin"
When I navigate to "Custom fields > Shared custom fields" in site administration
And I press "Add a new category"
And I wait until the page is ready
Then I should see "Other fields" in the "#customfield_catlist" "css_element"
And I click on "[data-role='deletecategory']" "css_element"
And I click on "Yes" "button" in the "Confirm" "dialogue"
And I wait until the page is ready
And I wait until "Other fields" "text" does not exist
Scenario: Shared customfields are displayed in other entities
Given the following "custom field categories" exist:
| name | component | area | itemid |
| My shared category | core_customfield | shared | 0 |
And the following "custom fields" exist:
| name | category | type | shortname |
| Shared field 1 | My shared category | text | f1 |
And I log in as "admin"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
Then I should see "My shared category" in the "#customfield_catlist" "css_element"
And I should see "Shared field 1" in the "#customfield_catlist" "css_element"
And I navigate to "Users > Accounts > Cohort custom fields" in site administration
And I should see "My shared category" in the "#customfield_catlist" "css_element"
And I should see "Shared field 1" in the "#customfield_catlist" "css_element"
Scenario: Shared custom fields cannot be reordered, edited or deleted from other entities
Given the following "custom field categories" exist:
| name | component | area | itemid |
| My shared category | core_customfield | shared | 0 |
| My course category | core_course | course | 0 |
And the following "custom fields" exist:
| name | category | type | shortname |
| Shared field 1 | My shared category | text | f1 |
| Course field 1 | My course category | text | f2 |
And I log in as "admin"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
# Check that the delete category link exists for course categories but not for shared categories.
Then "Delete" "link" should exist in the ".//div[contains(@class, 'categoryinstance')][.//span[@data-value='My course category']]" "xpath_element"
And "Delete" "link" should not exist in the ".//div[contains(@class,'categoryinstance')]/h3[normalize-space(text())='My shared category']" "xpath_element"
# Check that the inplaceeditable exists for course categories but not for shared categories.
And "//div[contains(@class,'categoryinstance') and contains(.,'My course category') and .//span[contains(@class,'inplaceeditable')]]" "xpath_element" should exist
And "//div[contains(@class,'categoryinstance') and contains(.,'My shared category') and .//span[contains(@class,'inplaceeditable')]]" "xpath_element" should not exist
# Check that the move category option exists for course categories but not for shared categories.
And "//span[contains(@class,'movecategory')][.//span[@title='Move \"My course category\"']]" "xpath_element" should exist
And "//span[contains(@class,'movecategory')][.//span[@title='Move \"My shared category\"']]" "xpath_element" should not exist
# Check that the move field option exists for course fields but not for shared fields.
And "//tr[@data-field-name='Course field 1']//span[@title='Move \"Course field 1\"']" "xpath_element" should exist
And "//tr[@data-field-name='Shared field 1']//span[@title='Move \"Shared field 1\"']" "xpath_element" should not exist
Scenario: Select which shared custom fields categories are used in the course entity
Given the following "custom field categories" exist:
| name | component | area | itemid |
| My shared category 1 | core_customfield | shared | 0 |
| My shared category 2 | core_customfield | shared | 0 |
| My course category | core_course | course | 0 |
And the following "custom fields" exist:
| name | category | type | shortname |
| Shared field 1 | My shared category 1 | text | f1 |
| Shared field 2 | My shared category 2 | text | f2 |
| Course field 1 | My course category | text | f3 |
And the following "courses" exist:
| shortname | fullname |
| C1 | Course 1 |
And I log in as "admin"
When I am on the "C1" "Course" page
And I navigate to "Settings" in current page administration
Then I should see "My course category"
And I should not see "My shared category 1"
And I should not see "My shared category 2"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
And I toggle the "Enable My shared category 1" admin switch "on"
And I am on the "C1" "Course" page
And I navigate to "Settings" in current page administration
And I should see "My course category"
And I should see "My shared category 1"
And I should not see "My shared category 2"
@@ -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/>.
declare(strict_types=1);
namespace core_customfield\external;
use core_external\external_api;
use core_customfield\shared;
use core_customfield_generator;
/**
* Unit tests for custom field toggle_shared_category external method
*
* @package core_customfield
* @covers \core_customfield\external\toggle_shared_category
* @copyright 2025 David Carrillo <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class toggle_shared_category_test extends \core_external\tests\externallib_testcase {
/**
* Test execute
*/
public function test_execute(): void {
$this->resetAfterTest();
$this->setAdminUser();
/** @var core_customfield_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_customfield');
$sharedcategory = $generator->create_category(['component' => 'core_customfield', 'area' => 'shared']);
$generator->create_field([
'categoryid' => $sharedcategory->get('id'),
'name' => 'My shared field',
'shortname' => 'mysharedfield',
'type' => 'text',
]);
$this->assertEmpty(shared::get_records());
$result = toggle_shared_category::execute(
$sharedcategory->get('id'),
'core_course',
'course',
0,
true
);
$result = external_api::clean_returnvalue(toggle_shared_category::execute_returns(), $result);
$this->assertTrue($result);
$records = shared::get_records();
$record = reset($records);
$this->assertEquals($sharedcategory->get('id'), $record->get('categoryid'));
$this->assertEquals('core_course', $record->get('component'));
$this->assertEquals('course', $record->get('area'));
$this->assertEquals(0, $record->get('itemid'));
$result = toggle_shared_category::execute(
$sharedcategory->get('id'),
'core_course',
'course',
0,
false
);
$result = external_api::clean_returnvalue(toggle_shared_category::execute_returns(), $result);
$this->assertTrue($result);
$this->assertEmpty(shared::get_records());
}
/**
* Test execute with no permission
*/
public function test_execute_no_permission(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
/** @var core_customfield_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_customfield');
$sharedcategory = $generator->create_category(['component' => 'core_customfield', 'area' => 'shared']);
$generator->create_field([
'categoryid' => $sharedcategory->get('id'),
'name' => 'My shared field',
'shortname' => 'mysharedfield',
'type' => 'text',
]);
$this->setUser($user);
$this->expectException(\moodle_exception::class);
$str = "Sorry, but you do not currently have permissions to do that (Configure shared custom fields).";
$this->expectExceptionMessage($str);
toggle_shared_category::execute(
$sharedcategory->get('id'),
'core_course',
'course',
0,
true
);
}
}
+17 -2
View File
@@ -71,7 +71,14 @@ class core_customfield_generator extends component_generator_base {
$handler = \core_customfield\handler::get_handler($record->component, $record->area, $record->itemid);
$categoryid = $handler->create_category($record->name);
return $handler->get_categories_with_fields()[$categoryid];
$categories = $handler->get_categories_with_fields();
return $handler->get_category_from_array(
$categories,
$categoryid,
$handler->get_component(),
$handler->get_area(),
$handler->get_itemid()
);
}
/**
@@ -139,7 +146,15 @@ class core_customfield_generator extends component_generator_base {
$field = field_controller::create(0, (object)['type' => $record->type], $category);
$handler->save_field_configuration($field, $record);
return $handler->get_categories_with_fields()[$field->get('categoryid')]->get_fields()[$field->get('id')];
$categories = $handler->get_categories_with_fields();
$category = $handler->get_category_from_array(
$categories,
$field->get('categoryid'),
$handler->get_component(),
$handler->get_area(),
$handler->get_itemid()
);
return $category->get_fields()[$field->get('id')];
}
/**
@@ -169,7 +169,7 @@ class group_handler extends handler {
$instanceid = $data['groupid'];
$context = $this->get_instance_context($instanceid);
$editablefields = $this->get_editable_fields($instanceid);
$records = api::get_instance_fields_data($editablefields, $instanceid);
$records = $this->get_instance_fields_data($editablefields, $instanceid);
foreach ($records as $d) {
$field = $d->get_field();
@@ -170,7 +170,7 @@ class grouping_handler extends handler {
$instanceid = $data['groupingid'];
$context = $this->get_instance_context($instanceid);
$editablefields = $this->get_editable_fields($instanceid);
$records = api::get_instance_fields_data($editablefields, $instanceid);
$records = $this->get_instance_fields_data($editablefields, $instanceid);
foreach ($records as $d) {
$field = $d->get_field();
+4
View File
@@ -50,6 +50,8 @@ $string['eventcategoryupdated'] = 'Custom field category updated';
$string['eventfieldcreated'] = 'Custom field created';
$string['eventfielddeleted'] = 'Custom field deleted';
$string['eventfieldupdated'] = 'Custom field updated';
$string['eventsharedcategoryusagedisabled'] = 'Shared category usage disabled';
$string['eventsharedcategoryusageenabled'] = 'Shared category usage enabled';
$string['fieldname'] = 'Name';
$string['fieldnotfound'] = 'Field not found';
$string['fieldshortname'] = 'Short name';
@@ -85,6 +87,8 @@ $string['privacy:metadata:customfield_data:valueformat'] = 'The format of the va
$string['privacy:metadata:customfield_data:valuetrust'] = 'The trust flag of the value, when it is text';
$string['privacy:metadata:customfieldpluginsummary'] = 'Fields for various components';
$string['privacy:metadata:filepurpose'] = 'File attached to the custom field data';
$string['sharedcategoryhelptext'] = 'When enabled, the custom fields in this category will be used';
$string['sharedcustomfields'] = 'Shared custom fields';
$string['shortname'] = 'Short name';
$string['shortname_help'] = 'The short name must be unique and can only contain alphanumeric lowercase characters and underscores (_). It is not displayed anywhere on the site, but may be used for synchronisation with external systems or in web services.';
$string['showdate'] = 'Show date';
+1
View File
@@ -302,6 +302,7 @@ $string['checkpermissionsin'] = 'Check permissions in {$a}';
$string['checksystempermissionsfor'] = 'Check system permissions for {$a->fullname}';
$string['checkuserspermissionshere'] = 'Check permissions for {$a->fullname} has in this {$a->contextlevel}';
$string['chooseroletoassign'] = 'Please choose a role to assign';
$string['customfield:configureshared'] = 'Configure shared custom fields';
$string['inactiveformorethan'] = 'inactive for more than {$a->timeperiod}';
$string['ingroup'] = 'in the group "{$a->group}"';
$string['inherit'] = 'Inherit';
+8
View File
@@ -2849,4 +2849,12 @@ $capabilities = array(
'manager' => CAP_ALLOW,
],
],
// Allow users to configure shared custom fields.
'moodle/customfield:configureshared' => [
'captype' => 'write',
'riskbitmap' => RISK_PERSONAL,
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => [],
],
);
+25 -1
View File
@@ -4292,6 +4292,7 @@
<FIELD NAME="area" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="itemid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="shared" TYPE="int" LENGTH="1" NOTNULL="false" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
@@ -4338,6 +4339,9 @@
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="area" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="itemid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
@@ -4345,12 +4349,32 @@
<KEY NAME="contextid" TYPE="foreign" FIELDS="contextid" REFTABLE="context" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="instanceid-fieldid" UNIQUE="true" FIELDS="instanceid, fieldid"/>
<INDEX NAME="instanceid-fieldid-component-area-itemid" UNIQUE="true" FIELDS="instanceid, fieldid, component, area, itemid"/>
<INDEX NAME="fieldid-intvalue" UNIQUE="false" FIELDS="fieldid, intvalue"/>
<INDEX NAME="fieldid-shortcharvalue" UNIQUE="false" FIELDS="fieldid, shortcharvalue"/>
<INDEX NAME="fieldid-decvalue" UNIQUE="false" FIELDS="fieldid, decvalue"/>
</INDEXES>
</TABLE>
<TABLE NAME="customfield_shared" COMMENT="core_customfield shared category table">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="categoryid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="area" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="itemid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="categoryid" TYPE="foreign" FIELDS="categoryid" REFTABLE="customfield_category" REFFIELDS="id"/>
<KEY NAME="usermodified" TYPE="foreign" FIELDS="usermodified" REFTABLE="user" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="categoryid-component-area-itemid" UNIQUE="true" FIELDS="categoryid, component, area, itemid"/>
</INDEXES>
</TABLE>
<TABLE NAME="h5p_libraries" COMMENT="Stores information about libraries used by H5P content.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" COMMENT="Primary Key: The id of the library"/>
+7
View File
@@ -2927,6 +2927,13 @@ $functions = array(
'type' => 'write',
'ajax' => true,
),
'core_customfield_toggle_shared' => [
'classname' => core_customfield\external\toggle_shared_category::class,
'methodname' => 'execute',
'description' => 'Toggle shared category state',
'type' => 'write',
'ajax' => true,
],
'core_h5p_get_trusted_h5p_file' => [
'classname' => 'core_h5p\external',
'methodname' => 'get_trusted_h5p_file',
+105
View File
@@ -2190,5 +2190,110 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2025090200.02);
}
if ($oldversion < 2025091600.01) {
// Define field shared to be added to customfield_category.
$table = new xmldb_table('customfield_category');
$field = new xmldb_field('shared', XMLDB_TYPE_INTEGER, '1', null, null, null, '0', 'contextid');
// Conditionally launch add field shared.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Define field component to be added to customfield_data.
$table = new xmldb_table('customfield_data');
$field = new xmldb_field('component', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, 'contextid');
// Conditionally launch add field component.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Define field area to be added to customfield_data.
$table = new xmldb_table('customfield_data');
$field = new xmldb_field('area', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, 'component');
// Conditionally launch add field area.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Define field itemid to be added to customfield_data.
$table = new xmldb_table('customfield_data');
$field = new xmldb_field('itemid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'area');
// Conditionally launch add field itemid.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Define index instanceid-fieldid (unique) to be dropped form customfield_data.
$table = new xmldb_table('customfield_data');
$index = new xmldb_index('instanceid-fieldid', XMLDB_INDEX_UNIQUE, ['instanceid', 'fieldid']);
// Conditionally launch drop index instanceid-fieldid.
if ($dbman->index_exists($table, $index)) {
$dbman->drop_index($table, $index);
}
// Define index instanceid-fieldid-component-area-itemid (unique) to be added to customfield_data.
$table = new xmldb_table('customfield_data');
$index = new xmldb_index(
'instanceid-fieldid-component-area-itemid',
XMLDB_INDEX_UNIQUE,
['instanceid', 'fieldid', 'component', 'area', 'itemid']
);
// Conditionally launch add index instanceid-fieldid-component-area-itemid.
if (!$dbman->index_exists($table, $index)) {
$dbman->add_index($table, $index);
}
// Populate component, area and itemid for each record in customfield_data table.
$sql = "SELECT d.id, c.component, c.area, c.itemid
FROM {customfield_data} d
JOIN {customfield_field} f ON d.fieldid = f.id
JOIN {customfield_category} c ON f.categoryid = c.id";
$records = $DB->get_records_sql($sql);
foreach ($records as $r) {
$DB->update_record('customfield_data', (object)[
'id' => $r->id,
'component' => $r->component,
'area' => $r->area,
'itemid' => $r->itemid,
]);
}
// Define table customfield_shared to be created.
$table = new xmldb_table('customfield_shared');
// Adding fields to table customfield_shared.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('categoryid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('component', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null);
$table->add_field('area', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null);
$table->add_field('itemid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
$table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
$table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
// Adding keys to table customfield_shared.
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$table->add_key('categoryid', XMLDB_KEY_FOREIGN, ['categoryid'], 'customfield_category', ['id']);
$table->add_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
// Adding indexes to table customfield_shared.
$table->add_index('categoryid-component-area-itemid', XMLDB_INDEX_UNIQUE, ['categoryid', 'component', 'area', 'itemid']);
// Conditionally launch create table for customfield_shared.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2025091600.01);
}
return true;
}
+3
View File
@@ -276,6 +276,9 @@ class phpunit_util extends testing_util {
if (class_exists('\core_reportbuilder\customfield\report_handler')) {
\core_reportbuilder\customfield\report_handler::reset_caches();
}
if (class_exists('\core_customfield\customfield\shared_handler')) {
\core_customfield\customfield\shared_handler::reset_caches();
}
// Clear static cache within restore.
if (class_exists('restore_section_structure_step')) {
@@ -208,7 +208,7 @@ class question_handler extends \core_customfield\handler {
*/
public function get_field_data(\core_customfield\field_controller $field, int $instanceid): \core_customfield\data_controller {
$fields = [$field->get('id') => $field];
$fieldsdata = api::get_instance_fields_data($fields, $instanceid);
$fieldsdata = $this->get_instance_fields_data($fields, $instanceid);
return $fieldsdata[$field->get('id')];
}
@@ -309,7 +309,7 @@ class question_handler extends \core_customfield\handler {
public function restore_instance_data_from_backup(\restore_task $task, array $data) {
$editablefields = $this->get_editable_fields($data['newquestion']);
$records = api::get_instance_fields_data($editablefields, $data['newquestion']);
$records = $this->get_instance_fields_data($editablefields, $data['newquestion']);
$target = $task->get_target();
$override = ($target != \backup::TARGET_CURRENT_ADDING && $target != \backup::TARGET_EXISTING_ADDING);
+1 -1
View File
@@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2025091600.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2025091600.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '5.1dev+ (Build: 20250916)'; // Human-friendly version name