MDL-86065 customfield: Allow to select shared categories on each entity

This commit is contained in:
David Carrillo
2025-09-16 18:07:39 +02:00
parent 78dc974150
commit a0001c8776
20 changed files with 707 additions and 4 deletions
@@ -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;
};
+20
View File
@@ -467,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);
}
}
@@ -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);
}
}
+13 -1
View File
@@ -519,15 +519,27 @@ 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);
}
@@ -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,7 +75,14 @@ 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();
@@ -89,6 +98,26 @@ class management implements renderable, templatable {
$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();
foreach ($category->get_fields() as $field) {
+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,
],
];
}
}
+1
View File
@@ -131,6 +131,7 @@ class core_customfield_external extends external_api {
)
)
, '', VALUE_OPTIONAL),
'toggle' => new external_value(PARAM_RAW, 'toggle'),
)
)
),
+8 -2
View File
@@ -77,7 +77,7 @@
{{#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>
{{#canedit}}<span class="movecategory icon-size-3">{{> core/drag_handle}}</span>{{/canedit}}
@@ -88,6 +88,11 @@
</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">
@@ -132,7 +137,8 @@
</div>
{{#js}}
require(['core_customfield/form'], function(s) {
require(['core_customfield/form', 'core_customfield/toggle_shared'], function(s, toggle) {
s.init();
toggle.init();
});
{{/js}}
@@ -53,3 +53,31 @@ Feature: Create shared categories and fields
# 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
);
}
}
+3
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,7 @@ $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.';
+20
View File
@@ -4355,6 +4355,26 @@
<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',