first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+335
View File
@@ -0,0 +1,335 @@
// 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 Field interaction management for Moodle.
*
* @module core_customfield/form
* @copyright 2018 Toni Barbera
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import 'core/inplace_editable';
import {call as fetchMany} from 'core/ajax';
import {
getString,
getStrings,
} from 'core/str';
import ModalForm from 'core_form/modalform';
import Notification from 'core/notification';
import Pending from 'core/pending';
import SortableList from 'core/sortable_list';
import Templates from 'core/templates';
import jQuery from 'jquery';
/**
* Display confirmation dialogue
*
* @param {Number} id
* @param {String} type
* @param {String} component
* @param {String} area
* @param {Number} itemid
*/
const confirmDelete = (id, type, component, area, itemid) => {
const pendingPromise = new Pending('core_customfield/form:confirmDelete');
getStrings([
{'key': 'confirm'},
{'key': 'confirmdelete' + type, component: 'core_customfield'},
{'key': 'yes'},
{'key': 'no'},
])
.then(strings => {
return Notification.confirm(strings[0], strings[1], strings[2], strings[3], function() {
const pendingDeletePromise = new Pending('core_customfield/form:confirmDelete');
fetchMany([
{
methodname: (type === 'field') ? 'core_customfield_delete_field' : 'core_customfield_delete_category',
args: {id},
},
{methodname: 'core_customfield_reload_template', args: {component, area, itemid}}
])[1]
.then(response => Templates.render('core_customfield/list', response))
.then((html, js) => Templates.replaceNode(jQuery('[data-region="list-page"]'), html, js))
.then(pendingDeletePromise.resolve)
.catch(Notification.exception);
});
})
.then(pendingPromise.resolve)
.catch(Notification.exception);
};
/**
* Creates a new custom fields category with default name and updates the list
*
* @param {String} component
* @param {String} area
* @param {Number} itemid
*/
const createNewCategory = (component, area, itemid) => {
const pendingPromise = new Pending('core_customfield/form:createNewCategory');
const promises = fetchMany([
{methodname: 'core_customfield_create_category', args: {component, area, itemid}},
{methodname: 'core_customfield_reload_template', args: {component, area, itemid}}
]);
promises[1].then(response => Templates.render('core_customfield/list', response))
.then((html, js) => Templates.replaceNode(jQuery('[data-region="list-page"]'), html, js))
.then(() => pendingPromise.resolve())
.catch(Notification.exception);
};
/**
* Create new custom field
*
* @param {HTMLElement} element
* @param {String} component
* @param {String} area
* @param {Number} itemid
*/
const createNewField = (element, component, area, itemid) => {
const pendingPromise = new Pending('core_customfield/form:createNewField');
const returnFocus = element.closest(".action-menu").querySelector(".dropdown-toggle");
const form = new ModalForm({
formClass: "core_customfield\\field_config_form",
args: {
categoryid: element.getAttribute('data-categoryid'),
type: element.getAttribute('data-type'),
},
modalConfig: {
title: getString('addingnewcustomfield', 'core_customfield', element.getAttribute('data-typename')),
},
returnFocus,
});
form.addEventListener(form.events.FORM_SUBMITTED, () => {
const pendingCreatedPromise = new Pending('core_customfield/form:createdNewField');
const promises = fetchMany([
{methodname: 'core_customfield_reload_template', args: {component: component, area: area, itemid: itemid}}
]);
promises[0].then(response => Templates.render('core_customfield/list', response))
.then((html, js) => Templates.replaceNode(jQuery('[data-region="list-page"]'), html, js))
.then(() => pendingCreatedPromise.resolve())
.catch(() => window.location.reload());
});
form.show();
pendingPromise.resolve();
};
/**
* Edit custom field
*
* @param {HTMLElement} element
* @param {String} component
* @param {String} area
* @param {Number} itemid
*/
const editField = (element, component, area, itemid) => {
const pendingPromise = new Pending('core_customfield/form:editField');
const form = new ModalForm({
formClass: "core_customfield\\field_config_form",
args: {
id: element.getAttribute('data-id'),
},
modalConfig: {
title: getString('editingfield', 'core_customfield', element.getAttribute('data-name')),
},
returnFocus: element,
});
form.addEventListener(form.events.FORM_SUBMITTED, () => {
const pendingCreatedPromise = new Pending('core_customfield/form:createdNewField');
const promises = fetchMany([
{methodname: 'core_customfield_reload_template', args: {component: component, area: area, itemid: itemid}}
]);
promises[0].then(response => Templates.render('core_customfield/list', response))
.then((html, js) => Templates.replaceNode(jQuery('[data-region="list-page"]'), html, js))
.then(() => pendingCreatedPromise.resolve())
.catch(() => window.location.reload());
});
form.show();
pendingPromise.resolve();
};
/**
* Fetch the category name from an inplace editable, given a child node of that field.
*
* @param {NodeElement} nodeElement
* @returns {String}
*/
const getCategoryNameFor = nodeElement => nodeElement
.closest('[data-category-id]')
.find('[data-inplaceeditable][data-itemtype=category][data-component=core_customfield]')
.attr('data-value');
const setupSortableLists = rootNode => {
// Sort category.
const sortCat = new SortableList(
'#customfield_catlist .categorieslist',
{
moveHandlerSelector: '.movecategory [data-drag-type=move]',
}
);
sortCat.getElementName = nodeElement => Promise.resolve(getCategoryNameFor(nodeElement));
// Note: The sortable list currently uses jQuery events.
jQuery('[data-category-id]').on(SortableList.EVENTS.DROP, (evt, info) => {
if (info.positionChanged) {
const pendingPromise = new Pending('core_customfield/form:categoryid:on:sortablelist-drop');
fetchMany([{
methodname: 'core_customfield_move_category',
args: {
id: info.element.data('category-id'),
beforeid: info.targetNextElement.data('category-id')
}
}])[0]
.then(pendingPromise.resolve)
.catch(Notification.exception);
}
evt.stopPropagation(); // Important for nested lists to prevent multiple targets.
});
// Sort fields.
var sort = new SortableList(
'#customfield_catlist .fieldslist tbody',
{
moveHandlerSelector: '.movefield [data-drag-type=move]',
}
);
sort.getDestinationName = (parentElement, afterElement) => {
if (!afterElement.length) {
return getString('totopofcategory', 'customfield', getCategoryNameFor(parentElement));
} else if (afterElement.attr('data-field-name')) {
return getString('afterfield', 'customfield', afterElement.attr('data-field-name'));
} else {
return Promise.resolve('');
}
};
jQuery('[data-field-name]').on(SortableList.EVENTS.DROP, (evt, info) => {
if (info.positionChanged) {
const pendingPromise = new Pending('core_customfield/form:fieldname:on:sortablelist-drop');
fetchMany([{
methodname: 'core_customfield_move_field',
args: {
id: info.element.data('field-id'),
beforeid: info.targetNextElement.data('field-id'),
categoryid: Number(info.targetList.closest('[data-category-id]').attr('data-category-id'))
},
}])[0]
.then(pendingPromise.resolve)
.catch(Notification.exception);
}
evt.stopPropagation(); // Important for nested lists to prevent multiple targets.
});
jQuery('[data-field-name]').on(SortableList.EVENTS.DRAG, evt => {
var pendingPromise = new Pending('core_customfield/form:fieldname:on:sortablelist-drag');
evt.stopPropagation(); // Important for nested lists to prevent multiple targets.
// Refreshing fields tables.
Templates.render('core_customfield/nofields', {})
.then(html => {
rootNode.querySelectorAll('.categorieslist > *')
.forEach(category => {
const fields = category.querySelectorAll('.field:not(.sortable-list-is-dragged)');
const noFields = category.querySelector('.nofields');
if (!fields.length && !noFields) {
category.querySelector('tbody').innerHTML = html;
} else if (fields.length && noFields) {
noFields.remove();
}
});
return;
})
.then(pendingPromise.resolve)
.catch(Notification.exception);
});
jQuery('[data-category-id], [data-field-name]').on(SortableList.EVENTS.DRAGSTART, (evt, info) => {
setTimeout(() => {
jQuery('.sortable-list-is-dragged').width(info.element.width());
}, 501);
});
};
/**
* Initialise the custom fields manager.
*/
export const init = () => {
const rootNode = document.querySelector('#customfield_catlist');
const component = rootNode.dataset.component;
const area = rootNode.dataset.area;
const itemid = rootNode.dataset.itemid;
rootNode.addEventListener('click', e => {
const roleHolder = e.target.closest('[data-role]');
if (!roleHolder) {
return;
}
if (roleHolder.dataset.role === 'deletefield') {
e.preventDefault();
confirmDelete(roleHolder.dataset.id, 'field', component, area, itemid);
return;
}
if (roleHolder.dataset.role === 'deletecategory') {
e.preventDefault();
confirmDelete(roleHolder.dataset.id, 'category', component, area, itemid);
return;
}
if (roleHolder.dataset.role === 'addnewcategory') {
e.preventDefault();
createNewCategory(component, area, itemid);
return;
}
if (roleHolder.dataset.role === 'addfield') {
e.preventDefault();
createNewField(roleHolder, component, area, itemid);
return;
}
if (roleHolder.dataset.role === 'editfield') {
e.preventDefault();
editField(roleHolder, component, area, itemid);
return;
}
});
setupSortableLists(rootNode, component, area, itemid);
};
+442
View File
@@ -0,0 +1,442 @@
<?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/>.
/**
* Api customfield package
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield;
use core\output\inplace_editable;
use core_customfield\event\category_created;
use core_customfield\event\category_deleted;
use core_customfield\event\category_updated;
use core_customfield\event\field_created;
use core_customfield\event\field_deleted;
use core_customfield\event\field_updated;
defined('MOODLE_INTERNAL') || die;
/**
* Class api
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class api {
/**
* For the given instance and list of fields fields retrieves data associated with them
*
* @param field_controller[] $fields list of fields indexed by field id
* @param int $instanceid
* @param bool $adddefaults
* @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];
}
/**
* For given list of instances and fields retrieves data associated with them
*
* @param field_controller[] $fields list of fields indexed by field id
* @param int[] $instanceids
* @param bool $adddefaults
* @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 {
global $DB;
// Create the results array where instances and fields order is the same as in the input arrays.
$result = array_fill_keys($instanceids, array_fill_keys(array_keys($fields), null));
if (empty($instanceids) || empty($fields)) {
return $result;
}
// 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');
$sql = "SELECT d.*
FROM {customfield_field} f
JOIN {customfield_data} d ON (f.id = d.fieldid AND d.instanceid {$sqlinstances})
WHERE f.id {$sqlfields}";
$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]);
}
$fieldsdata->close();
if ($adddefaults) {
// Add default data where it was not retrieved.
foreach ($instanceids as $instanceid) {
foreach ($fields as $fieldid => $field) {
if ($result[$instanceid][$fieldid] === null) {
$result[$instanceid][$fieldid] =
data_controller::create(0, (object)['instanceid' => $instanceid], $field);
}
}
}
} else {
// Remove null-placeholders for data that was not retrieved.
foreach ($instanceids as $instanceid) {
$result[$instanceid] = array_filter($result[$instanceid]);
}
}
return $result;
}
/**
* Retrieve a list of all available custom field types
*
* @return array a list of the fieldtypes suitable to use in a select statement
*/
public static function get_available_field_types() {
$fieldtypes = array();
$plugins = \core\plugininfo\customfield::get_enabled_plugins();
foreach ($plugins as $type => $unused) {
$fieldtypes[$type] = get_string('pluginname', 'customfield_' . $type);
}
asort($fieldtypes);
return $fieldtypes;
}
/**
* Updates or creates a field with data that came from a form
*
* @param field_controller $field
* @param \stdClass $formdata
*/
public static function save_field_configuration(field_controller $field, \stdClass $formdata) {
foreach ($formdata as $key => $value) {
if ($key === 'configdata' && is_array($formdata->configdata)) {
$field->set($key, json_encode($value));
} else if ($key === 'id' || ($key === 'type' && $field->get('id'))) {
continue;
} else if (field::has_property($key)) {
$field->set($key, $value);
}
}
$isnewfield = empty($field->get('id'));
// Process files in description.
if (isset($formdata->description_editor)) {
if (!$field->get('id')) {
// We need 'id' field to store files used in description.
$field->save();
}
$data = (object) ['description_editor' => $formdata->description_editor];
$textoptions = $field->get_handler()->get_description_text_options();
$data = file_postupdate_standard_editor($data, 'description', $textoptions, $textoptions['context'],
'core_customfield', 'description', $field->get('id'));
$field->set('description', $data->description);
$field->set('descriptionformat', $data->descriptionformat);
}
// Save the field.
$field->save();
if ($isnewfield) {
// Move to the end of the category.
self::move_field($field, $field->get('categoryid'));
}
if ($isnewfield) {
field_created::create_from_object($field)->trigger();
} else {
field_updated::create_from_object($field)->trigger();
}
}
/**
* Change fields sort order, move field to another category
*
* @param field_controller $field field that needs to be moved
* @param int $categoryid category that needs to be moved
* @param int $beforeid id of the category this category needs to be moved before, 0 to move to the end
*/
public static function move_field(field_controller $field, int $categoryid, int $beforeid = 0) {
global $DB;
if ($field->get('categoryid') != $categoryid) {
// Move field to another category. Validate that this category exists and belongs to the same component/area/itemid.
$category = $field->get_category();
$DB->get_record(category::TABLE, [
'component' => $category->get('component'),
'area' => $category->get('area'),
'itemid' => $category->get('itemid'),
'id' => $categoryid], 'id', MUST_EXIST);
$field->set('categoryid', $categoryid);
$field->save();
field_updated::create_from_object($field)->trigger();
}
// Reorder fields in the target category.
$records = $DB->get_records(field::TABLE, ['categoryid' => $categoryid], 'sortorder, id', '*');
$id = $field->get('id');
$fieldsids = array_values(array_diff(array_keys($records), [$id]));
$idx = $beforeid ? array_search($beforeid, $fieldsids) : false;
if ($idx === false) {
// Set as the last field.
$fieldsids = array_merge($fieldsids, [$id]);
} else {
// Set before field with id $beforeid.
$fieldsids = array_merge(array_slice($fieldsids, 0, $idx), [$id], array_slice($fieldsids, $idx));
}
foreach (array_values($fieldsids) as $idx => $fieldid) {
// Use persistent class to update the sortorder for each field that needs updating.
if ($records[$fieldid]->sortorder != $idx) {
$f = ($fieldid == $id) ? $field : new field(0, $records[$fieldid]);
$f->set('sortorder', $idx);
$f->save();
}
}
}
/**
* Delete a field
*
* @param field_controller $field
*/
public static function delete_field_configuration(field_controller $field): bool {
$event = field_deleted::create_from_object($field);
get_file_storage()->delete_area_files($field->get_handler()->get_configuration_context()->id, 'core_customfield',
'description', $field->get('id'));
$result = $field->delete();
$event->trigger();
return $result;
}
/**
* Returns an object for inplace editable
*
* @param category_controller $category category that needs to be moved
* @param bool $editable
* @return inplace_editable
*/
public static function get_category_inplace_editable(category_controller $category, bool $editable = true): inplace_editable {
return new inplace_editable('core_customfield',
'category',
$category->get('id'),
$editable,
$category->get_formatted_name(),
$category->get('name'),
get_string('editcategoryname', 'core_customfield'),
get_string('newvaluefor', 'core_form', $category->get_formatted_name())
);
}
/**
* Reorder categories, move given category before another category
*
* @param category_controller $category category that needs to be moved
* @param int $beforeid id of the category this category needs to be moved before, 0 to move to the end
*/
public static function move_category(category_controller $category, int $beforeid = 0) {
global $DB;
$records = $DB->get_records(category::TABLE, [
'component' => $category->get('component'),
'area' => $category->get('area'),
'itemid' => $category->get('itemid')
], 'sortorder, id', '*');
$id = $category->get('id');
$categoriesids = array_values(array_diff(array_keys($records), [$id]));
$idx = $beforeid ? array_search($beforeid, $categoriesids) : false;
if ($idx === false) {
// Set as the last category.
$categoriesids = array_merge($categoriesids, [$id]);
} else {
// Set before category with id $beforeid.
$categoriesids = array_merge(array_slice($categoriesids, 0, $idx), [$id], array_slice($categoriesids, $idx));
}
foreach (array_values($categoriesids) as $idx => $categoryid) {
// Use persistent class to update the sortorder for each category that needs updating.
if ($records[$categoryid]->sortorder != $idx) {
$c = ($categoryid == $id) ? $category : category_controller::create(0, $records[$categoryid]);
$c->set('sortorder', $idx);
$c->save();
}
}
}
/**
* Insert or update custom field category
*
* @param category_controller $category
*/
public static function save_category(category_controller $category) {
$isnewcategory = empty($category->get('id'));
$category->save();
if ($isnewcategory) {
// Move to the end.
self::move_category($category);
category_created::create_from_object($category)->trigger();
} else {
category_updated::create_from_object($category)->trigger();
}
}
/**
* Delete a custom field category
*
* @param category_controller $category
* @return bool
*/
public static function delete_category(category_controller $category): bool {
$event = category_deleted::create_from_object($category);
// Delete all fields.
foreach ($category->get_fields() as $field) {
self::delete_field_configuration($field);
}
$result = $category->delete();
$event->trigger();
return $result;
}
/**
* Returns a list of categories with their related fields.
*
* @param string $component
* @param string $area
* @param int $itemid
* @return category_controller[]
*/
public static function get_categories_with_fields(string $component, string $area, int $itemid): array {
global $DB;
$categories = [];
$options = [
'component' => $component,
'area' => $area,
'itemid' => $itemid
];
$plugins = \core\plugininfo\customfield::get_enabled_plugins();
list($sqlfields, $params) = $DB->get_in_or_equal(array_keys($plugins), SQL_PARAMS_NAMED, 'param', true, null);
$fields = 'f.*, ' . join(', ', array_map(function($field) {
return "c.$field AS category_$field";
}, array_diff(array_keys(category::properties_definition()), ['usermodified', 'timemodified'])));
$sql = "SELECT $fields
FROM {customfield_category} c
LEFT JOIN {customfield_field} f ON c.id = f.categoryid AND f.type $sqlfields
WHERE c.component = :component AND c.area = :area AND c.itemid = :itemid
ORDER BY c.sortorder, f.sortorder";
$fieldsdata = $DB->get_recordset_sql($sql, $options + $params);
foreach ($fieldsdata as $data) {
if (!array_key_exists($data->category_id, $categories)) {
$categoryobj = new \stdClass();
foreach ($data as $key => $value) {
if (preg_match('/^category_(.*)$/', $key, $matches)) {
$categoryobj->{$matches[1]} = $value;
}
}
$category = category_controller::create(0, $categoryobj);
$categories[$categoryobj->id] = $category;
} else {
$category = $categories[$data->categoryid];
}
if ($data->id) {
$fieldobj = new \stdClass();
foreach ($data as $key => $value) {
if (!preg_match('/^category_/', $key)) {
$fieldobj->{$key} = $value;
}
}
$field = field_controller::create(0, $fieldobj, $category);
}
}
$fieldsdata->close();
return $categories;
}
/**
* Prepares the object to pass to field configuration form set_data() method
*
* @param field_controller $field
* @return \stdClass
*/
public static function prepare_field_for_config_form(field_controller $field): \stdClass {
if ($field->get('id')) {
$formdata = $field->to_record();
$formdata->configdata = $field->get('configdata');
// Preprocess the description.
$textoptions = $field->get_handler()->get_description_text_options();
file_prepare_standard_editor($formdata, 'description', $textoptions, $textoptions['context'], 'core_customfield',
'description', $formdata->id);
} else {
$formdata = (object)['categoryid' => $field->get('categoryid'), 'type' => $field->get('type'), 'configdata' => []];
}
// Allow field to do more preprocessing (usually for editor or filemanager elements).
$field->prepare_for_config_form($formdata);
return $formdata;
}
/**
* Get a list of the course custom fields that support course grouping in
* block_myoverview
* @return array $shortname => $name
*/
public static function get_fields_supporting_course_grouping() {
global $DB;
$sql = "
SELECT f.*
FROM {customfield_field} f
JOIN {customfield_category} cat ON cat.id = f.categoryid
WHERE cat.component = 'core_course' AND cat.area = 'course'
ORDER BY f.name
";
$ret = [];
$fields = $DB->get_records_sql($sql);
foreach ($fields as $field) {
$inst = field_controller::create(0, $field);
$isvisible = $inst->get_configdata_property('visibility') == \core_course\customfield\course_handler::VISIBLETOALL;
// Only visible fields to everybody supporting course grouping will be displayed.
if ($inst->supports_course_grouping() && $isvisible) {
$ret[$inst->get('shortname')] = $inst->get('name');
}
}
return $ret;
}
}
+79
View File
@@ -0,0 +1,79 @@
<?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 category persistent class
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class category extends persistent {
/**
* Database table.
*/
const TABLE = 'customfield_category';
/**
* Return the definition of the properties of this model.
*
* @return array
*/
protected static function define_properties(): array {
return array(
'name' => [
'type' => PARAM_TEXT,
],
'description' => [
'type' => PARAM_RAW,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'descriptionformat' => [
'type' => PARAM_INT,
'default' => FORMAT_MOODLE,
'optional' => true,
'null' => NULL_ALLOWED,
],
'component' => [
'type' => PARAM_COMPONENT
],
'area' => [
'type' => PARAM_COMPONENT
],
'itemid' => [
'type' => PARAM_INT,
'optional' => true,
'default' => 0
],
'contextid' => [
'type' => PARAM_INT,
'optional' => false
],
'sortorder' => [
'type' => PARAM_INT,
'optional' => true,
'default' => -1,
'null' => NULL_ALLOWED,
],
);
}
}
+231
View File
@@ -0,0 +1,231 @@
<?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/>.
/**
* Customfield catecory controller class
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield;
defined('MOODLE_INTERNAL') || die;
/**
* Class category
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class category_controller {
/**
* Category persistent
*
* @var category
*/
protected $category;
/**
* @var field_controller[]
*/
protected $fields = [];
/** @var handler */
protected $handler;
/**
* category constructor.
*
* This class is not abstract, however the constructor was made protected to be consistent with
* field_controller and data_controller
*
* @param int $id
* @param \stdClass|null $record
*/
protected function __construct(int $id = 0, \stdClass $record = null) {
$this->category = new category($id, $record);
}
/**
* Creates an instance of category_controller
*
* Either $id or $record or $handler need to be specified
* If handler is known pass it to constructor to avoid retrieving it later
* Component, area and itemid must not conflict with the ones in handler
*
* @param int $id
* @param \stdClass|null $record
* @param handler|null $handler
* @return category_controller
* @throws \moodle_exception
* @throws \coding_exception
*/
public static function create(int $id, \stdClass $record = null, handler $handler = null): category_controller {
global $DB;
if ($id && $record) {
// This warning really should be in persistent as well.
debugging('Too many parameters, either id need to be specified or a record, but not both.',
DEBUG_DEVELOPER);
}
if ($id) {
if (!$record = $DB->get_record(category::TABLE, array('id' => $id), '*', IGNORE_MISSING)) {
throw new \moodle_exception('categorynotfound', 'core_customfield');
}
}
if (empty($record->component)) {
if (!$handler) {
throw new \coding_exception('Not enough parameters to initialise category_controller - unknown component');
}
$record->component = $handler->get_component();
}
if (empty($record->area)) {
if (!$handler) {
throw new \coding_exception('Not enough parameters to initialise category_controller - unknown area');
}
$record->area = $handler->get_area();
}
if (!isset($record->itemid)) {
if (!$handler) {
throw new \coding_exception('Not enough parameters to initialise category_controller - unknown itemid');
}
$record->itemid = $handler->get_itemid();
}
$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.
$handlernew = $handler ?? $category->get_handler();
$category->set('contextid', $handlernew->get_configuration_context()->id);
}
if ($handler) {
$category->set_handler($handler);
}
return $category;
}
/**
* Persistent getter parser.
*
* @param string $property
* @return mixed
*/
final public function get($property) {
return $this->category->get($property);
}
/**
* Persistent setter parser.
*
* @param string $property
* @param mixed $value
*/
final public function set($property, $value) {
return $this->category->set($property, $value);
}
/**
* Persistent delete parser.
*
* @return bool
*/
final public function delete() {
return $this->category->delete();
}
/**
* Persistent save parser.
*
* @return void
*/
final public function save() {
$this->category->save();
}
/**
* Return an array of field objects associated with this category.
*
* @return field_controller[]
*/
public function get_fields() {
return $this->fields;
}
/**
* Adds a child field
*
* @param field_controller $field
*/
public function add_field(field_controller $field) {
$this->fields[$field->get('id')] = $field;
}
/**
* Gets a handler, if not known retrieve it
*
* @return handler
*/
public function get_handler(): handler {
if ($this->handler === null) {
$this->handler = handler::get_handler($this->get('component'), $this->get('area'), $this->get('itemid'));
}
return $this->handler;
}
/**
* Allows to set handler so we don't need to retrieve it later
*
* @param handler $handler
* @throws \coding_exception
*/
public function set_handler(handler $handler) {
// Make sure there are no conflicts.
if ($this->get('component') !== $handler->get_component()) {
throw new \coding_exception('Component of the handler does not match the one from the record');
}
if ($this->get('area') !== $handler->get_area()) {
throw new \coding_exception('Area of the handler does not match the one from the record');
}
if ($this->get('itemid') != $handler->get_itemid()) {
throw new \coding_exception('Itemid of the handler does not match the one from the record');
}
if ($this->get('contextid') != $handler->get_configuration_context()->id) {
throw new \coding_exception('Context of the handler does not match the one from the record');
}
$this->handler = $handler;
}
/**
* Persistent to_record parser.
*
* @return \stdClass
*/
final public function to_record() {
return $this->category->to_record();
}
/**
* Returns the category name formatted according to configuration context.
*
* @return string
*/
public function get_formatted_name(): string {
$context = $this->get_handler()->get_configuration_context();
return format_string($this->get('name'), true, ['context' => $context]);
}
}
+113
View File
@@ -0,0 +1,113 @@
<?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/>.
/**
* Data persistent class
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield;
use core\persistent;
defined('MOODLE_INTERNAL') || die;
/**
* Class data
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class data extends persistent {
/**
* Database data.
*/
const TABLE = 'customfield_data';
/**
* Return the definition of the properties of this model.
*
* @return array
*/
protected static function define_properties(): array {
return array(
'fieldid' => [
'type' => PARAM_INT,
'optional' => false,
'null' => NULL_NOT_ALLOWED
],
'instanceid' => [
'type' => PARAM_INT,
'optional' => false,
'null' => NULL_NOT_ALLOWED
],
'intvalue' => [
'type' => PARAM_INT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'decvalue' => [
'type' => PARAM_FLOAT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'charvalue' => [
'type' => PARAM_TEXT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'shortcharvalue' => [
'type' => PARAM_TEXT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
// Mandatory field.
'value' => [
'type' => PARAM_RAW,
'null' => NULL_NOT_ALLOWED,
'default' => ''
],
// Mandatory field.
'valueformat' => [
'type' => PARAM_INT,
'null' => NULL_NOT_ALLOWED,
'default' => FORMAT_MOODLE,
'optional' => true
],
'valuetrust' => [
'type' => PARAM_BOOL,
'null' => NULL_NOT_ALLOWED,
'default' => false,
'optional' => true,
],
'contextid' => [
'type' => PARAM_INT,
'optional' => false,
'null' => NULL_NOT_ALLOWED
]
);
}
}
+395
View File
@@ -0,0 +1,395 @@
<?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/>.
/**
* Customfield component data controller abstract class
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield;
use backup_nested_element;
use core_customfield\output\field_data;
defined('MOODLE_INTERNAL') || die;
/**
* Base class for custom fields data controllers
*
* This class is a wrapper around the persistent data class that allows to define
* how the element behaves in the instance edit forms.
*
* Custom field plugins must define a class
* \{pluginname}\data_controller extends \core_customfield\data_controller
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class data_controller {
/**
* Data persistent
*
* @var data
*/
protected $data;
/**
* Field that this data belongs to.
*
* @var field_controller
*/
protected $field;
/**
* data_controller constructor.
*
* @param int $id
* @param \stdClass|null $record
*/
public function __construct(int $id, \stdClass $record) {
$this->data = new data($id, $record);
}
/**
* Creates an instance of data_controller
*
* Parameters $id, $record and $field can complement each other but not conflict.
* If $id is not specified, fieldid must be present either in $record or in $field.
* If $id is not specified, instanceid must be present in $record
*
* No DB queries are performed if both $record and $field are specified.
* @param int $id
* @param \stdClass|null $record
* @param field_controller|null $field
* @return data_controller
* @throws \coding_exception
* @throws \moodle_exception
*/
public static function create(int $id, \stdClass $record = null, field_controller $field = null): data_controller {
global $DB;
if ($id && $record) {
// This warning really should be in persistent as well.
debugging('Too many parameters, either id need to be specified or a record, but not both.',
DEBUG_DEVELOPER);
}
if ($id) {
$record = $DB->get_record(data::TABLE, array('id' => $id), '*', MUST_EXIST);
} else if (!$record) {
$record = new \stdClass();
}
if (!$field && empty($record->fieldid)) {
throw new \coding_exception('Not enough parameters to initialise data_controller - unknown field');
}
if (!$field) {
$field = field_controller::create($record->fieldid);
}
if (empty($record->fieldid)) {
$record->fieldid = $field->get('id');
}
if ($field->get('id') != $record->fieldid) {
throw new \coding_exception('Field id from the record does not match field from the parameter');
}
$type = $field->get('type');
$customfieldtype = "\\customfield_{$type}\\data_controller";
if (!class_exists($customfieldtype) || !is_subclass_of($customfieldtype, self::class)) {
throw new \moodle_exception('errorfieldtypenotfound', 'core_customfield', '', s($type));
}
$datacontroller = new $customfieldtype(0, $record);
$datacontroller->field = $field;
return $datacontroller;
}
/**
* Returns the name of the field to be used on HTML forms.
*
* @return string
*/
public function get_form_element_name(): string {
return 'customfield_' . $this->get_field()->get('shortname');
}
/**
* Persistent getter parser.
*
* @param string $property
* @return mixed
*/
final public function get($property) {
return $this->data->get($property);
}
/**
* Persistent setter parser.
*
* @param string $property
* @param mixed $value
* @return data
*/
final public function set($property, $value) {
return $this->data->set($property, $value);
}
/**
* Return the name of the field in the db table {customfield_data} where the data is stored
*
* Must be one of the following:
* intvalue - can store integer values, this field is indexed
* decvalue - can store decimal values
* shortcharvalue - can store character values up to 255 characters long, this field is indexed
* charvalue - can store character values up to 1333 characters long, this field is not indexed but
* full text search is faster than on field 'value'
* value - can store character values of unlimited length ("text" field in the db)
*
* @return string
*/
abstract public function datafield(): string;
/**
* Delete data. Element can override it if related information needs to be deleted as well (such as files)
*
* @return bool
*/
public function delete() {
return $this->data->delete();
}
/**
* Persistent save parser.
*
* @return void
*/
public function save() {
$this->data->save();
}
/**
* Field associated with this data
*
* @return field_controller
*/
public function get_field(): field_controller {
return $this->field;
}
/**
* Saves the data coming from form
*
* @param \stdClass $datanew data coming from the form
*/
public function instance_form_save(\stdClass $datanew) {
$elementname = $this->get_form_element_name();
if (!property_exists($datanew, $elementname)) {
return;
}
$value = $datanew->$elementname;
$this->data->set($this->datafield(), $value);
$this->data->set('value', $value);
$this->save();
}
/**
* Prepares the custom field data related to the object to pass to mform->set_data() and adds them to it
*
* This function must be called before calling $form->set_data($object);
*
* @param \stdClass $instance the instance that has custom fields, if 'id' attribute is present the custom
* fields for this instance will be added, otherwise the default values will be added.
*/
public function instance_form_before_set_data(\stdClass $instance) {
$instance->{$this->get_form_element_name()} = $this->get_value();
}
/**
* Checks if the value is empty
*
* @param mixed $value
* @return bool
*/
protected function is_empty($value): bool {
if ($this->datafield() === 'value' || $this->datafield() === 'charvalue' || $this->datafield() === 'shortcharvalue') {
return '' . $value === '';
}
return empty($value);
}
/**
* Checks if the value is unique
*
* @param mixed $value
* @return bool
*/
protected function is_unique($value): bool {
global $DB;
// Ensure the "value" datafield can be safely compared across all databases.
$datafield = $this->datafield();
if ($datafield === 'value') {
$datafield = $DB->sql_cast_to_char($datafield);
}
$where = "fieldid = ? AND {$datafield} = ?";
$params = [$this->get_field()->get('id'), $value];
if ($this->get('id')) {
$where .= ' AND id <> ?';
$params[] = $this->get('id');
}
return !$DB->record_exists_select('customfield_data', $where, $params);
}
/**
* Called from instance edit form in validation()
*
* @param array $data
* @param array $files
* @return array array of errors
*/
public function instance_form_validation(array $data, array $files): array {
$errors = [];
$elementname = $this->get_form_element_name();
if ($this->get_field()->get_configdata_property('uniquevalues') == 1) {
$value = $data[$elementname];
if (!$this->is_empty($value) && !$this->is_unique($value)) {
$errors[$elementname] = get_string('erroruniquevalues', 'core_customfield');
}
}
return $errors;
}
/**
* Called from instance edit form in definition_after_data()
*
* @param \MoodleQuickForm $mform
*/
public function instance_form_definition_after_data(\MoodleQuickForm $mform) {
}
/**
* Used by handlers to display data on various places.
*
* @return string
*/
public function display(): string {
global $PAGE;
$output = $PAGE->get_renderer('core_customfield');
return $output->render(new field_data($this));
}
/**
* Returns the default value as it would be stored in the database (not in human-readable format).
*
* @return mixed
*/
abstract public function get_default_value();
/**
* Returns the value as it is stored in the database or default value if data record is not present
*
* @return mixed
*/
public function get_value() {
if (!$this->get('id')) {
return $this->get_default_value();
}
return $this->get($this->datafield());
}
/**
* Return the context of the field
*
* @return \context
*/
public function get_context(): \context {
if ($this->get('contextid')) {
return \context::instance_by_id($this->get('contextid'));
} else if ($this->get('instanceid')) {
return $this->get_field()->get_handler()->get_instance_context($this->get('instanceid'));
} else {
// Context is not yet known (for example, entity is not yet created).
return \context_system::instance();
}
}
/**
* Add a field to the instance edit form.
*
* @param \MoodleQuickForm $mform
*/
abstract public function instance_form_definition(\MoodleQuickForm $mform);
/**
* Returns value in a human-readable format or default value if data record is not present
*
* This is the default implementation that most likely needs to be overridden
*
* @return mixed|null value or null if empty
*/
public function export_value() {
$value = $this->get_value();
if ($this->is_empty($value)) {
return null;
}
if ($this->datafield() === 'intvalue') {
return (int)$value;
} else if ($this->datafield() === 'decvalue') {
return (float)$value;
} else if ($this->datafield() === 'value') {
return format_text($value, $this->get('valueformat'), [
'context' => $this->get_context(),
'trusted' => $this->get('valuetrust'),
]);
} else {
return format_string($value, true, ['context' => $this->get_context()]);
}
}
/**
* Callback for backup, allowing custom fields to add additional data to the backup.
* It is not an abstract method for backward compatibility reasons.
*
* @param \backup_nested_element $customfieldelement The custom field element to be backed up.
*/
public function backup_define_structure(backup_nested_element $customfieldelement): void {
}
/**
* Callback for restore, allowing custom fields to restore additional data from the backup.
* It is not an abstract method for backward compatibility reasons.
*
* @param \restore_structure_step $step The restore step instance.
* @param int $newid The new ID for the custom field data after restore.
* @param int $oldid The original ID of the custom field data before backup.
*/
public function restore_define_structure(\restore_structure_step $step, int $newid, int $oldid): void {
}
/**
* Persistent to_record parser.
*
* @return \stdClass
*/
final public function to_record() {
return $this->data->to_record();
}
}
@@ -0,0 +1,84 @@
<?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 field category created event.
*
* @package core_customfield
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\event;
use core_customfield\category_controller;
defined('MOODLE_INTERNAL') || die();
/**
* Custom field category created event class.
*
* @package core_customfield
* @since Moodle 3.6
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class category_created extends \core\event\base {
/**
* Initialise the event data.
*/
protected function init() {
$this->data['objecttable'] = 'customfield_category';
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Creates an instance from a category controller object
*
* @param category_controller $category
* @return category_created
*/
public static function create_from_object(category_controller $category): category_created {
$eventparams = [
'objectid' => $category->get('id'),
'context' => $category->get_handler()->get_configuration_context(),
'other' => ['name' => $category->get('name')]
];
$event = self::create($eventparams);
$event->add_record_snapshot($event->objecttable, $category->to_record());
return $event;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventcategorycreated', 'core_customfield');
}
/**
* Returns non-localised description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' created the category with id '$this->objectid'.";
}
}
@@ -0,0 +1,84 @@
<?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 field category created event.
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\event;
use core_customfield\category_controller;
defined('MOODLE_INTERNAL') || die();
/**
* Custom field category created event class.
*
* @package core_customfield
* @since Moodle 3.6
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class category_deleted extends \core\event\base {
/**
* Initialise the event data.
*/
protected function init() {
$this->data['objecttable'] = 'customfield_category';
$this->data['crud'] = 'd';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Creates an instance from a category controller object
*
* @param category_controller $category
* @return category_deleted
*/
public static function create_from_object(category_controller $category): category_deleted {
$eventparams = [
'objectid' => $category->get('id'),
'context' => $category->get_handler()->get_configuration_context(),
'other' => ['name' => $category->get('name')]
];
$event = self::create($eventparams);
$event->add_record_snapshot($event->objecttable, $category->to_record());
return $event;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventcategorydeleted', 'core_customfield');
}
/**
* Returns non-localised description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' deleted the category with id '$this->objectid'.";
}
}
@@ -0,0 +1,84 @@
<?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 field category updated event.
*
* @package core_customfield
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\event;
use core_customfield\category_controller;
defined('MOODLE_INTERNAL') || die();
/**
* Custom field category updated event class.
*
* @package core_customfield
* @since Moodle 3.6
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class category_updated extends \core\event\base {
/**
* Initialise the event data.
*/
protected function init() {
$this->data['objecttable'] = 'customfield_category';
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Creates an instance from a category controller object
*
* @param category_controller $category
* @return category_updated
*/
public static function create_from_object(category_controller $category): category_updated {
$eventparams = [
'objectid' => $category->get('id'),
'context' => $category->get_handler()->get_configuration_context(),
'other' => ['name' => $category->get('name')]
];
$event = self::create($eventparams);
$event->add_record_snapshot($event->objecttable, $category->to_record());
return $event;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventcategoryupdated', 'core_customfield');
}
/**
* Returns non-localised description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' updated the category with id '$this->objectid'.";
}
}
@@ -0,0 +1,87 @@
<?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 field created event.
*
* @package core_customfield
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\event;
use core_customfield\field_controller;
defined('MOODLE_INTERNAL') || die();
/**
* Custom field created event class.
*
* @package core_customfield
* @since Moodle 3.6
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class field_created extends \core\event\base {
/**
* Initialise the event data.
*/
protected function init() {
$this->data['objecttable'] = 'customfield_field';
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Creates an instance from a field controller object
*
* @param field_controller $field
* @return field_created
*/
public static function create_from_object(field_controller $field): field_created {
$eventparams = [
'objectid' => $field->get('id'),
'context' => $field->get_handler()->get_configuration_context(),
'other' => [
'shortname' => $field->get('shortname'),
'name' => $field->get('name')
]
];
$event = self::create($eventparams);
$event->add_record_snapshot($event->objecttable, $field->to_record());
return $event;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventfieldcreated', 'core_customfield');
}
/**
* Returns non-localised description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' created the field with id '$this->objectid'.";
}
}
@@ -0,0 +1,87 @@
<?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 field updated event.
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\event;
use core_customfield\field_controller;
defined('MOODLE_INTERNAL') || die();
/**
* Custom field updated event class.
*
* @package core_customfield
* @since Moodle 3.6
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class field_deleted extends \core\event\base {
/**
* Initialise the event data.
*/
protected function init() {
$this->data['objecttable'] = 'customfield_field';
$this->data['crud'] = 'd';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Creates an instance from a field controller object
*
* @param field_controller $field
* @return field_deleted
*/
public static function create_from_object(field_controller $field): field_deleted {
$eventparams = [
'objectid' => $field->get('id'),
'context' => $field->get_handler()->get_configuration_context(),
'other' => [
'shortname' => $field->get('shortname'),
'name' => $field->get('name')
]
];
$event = self::create($eventparams);
$event->add_record_snapshot($event->objecttable, $field->to_record());
return $event;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventfielddeleted', 'core_customfield');
}
/**
* Returns non-localised description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' deleted the field with id '$this->objectid'.";
}
}
@@ -0,0 +1,87 @@
<?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 field updated event.
*
* @package core_customfield
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\event;
use core_customfield\field_controller;
defined('MOODLE_INTERNAL') || die();
/**
* Custom field updated event class.
*
* @package core_customfield
* @since Moodle 3.6
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class field_updated extends \core\event\base {
/**
* Initialise the event data.
*/
protected function init() {
$this->data['objecttable'] = 'customfield_field';
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Creates an instance from a field controller object
*
* @param field_controller $field
* @return field_updated
*/
public static function create_from_object(field_controller $field): field_updated {
$eventparams = [
'objectid' => $field->get('id'),
'context' => $field->get_handler()->get_configuration_context(),
'other' => [
'shortname' => $field->get('shortname'),
'name' => $field->get('name')
]
];
$event = self::create($eventparams);
$event->add_record_snapshot($event->objecttable, $field->to_record());
return $event;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventfieldupdated', 'core_customfield');
}
/**
* Returns non-localised description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' updated the field with id '$this->objectid'.";
}
}
+89
View File
@@ -0,0 +1,89 @@
<?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 field persistent class
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class field extends persistent {
/**
* Database table.
*/
const TABLE = 'customfield_field';
/**
* Return the definition of the properties of this model.
*
* @return array
*/
protected static function define_properties(): array {
return array(
'name' => [
'type' => PARAM_TEXT,
],
'shortname' => [
'type' => PARAM_TEXT,
],
'type' => [
'type' => PARAM_PLUGIN,
],
'description' => [
'type' => PARAM_RAW,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'descriptionformat' => [
'type' => PARAM_INT,
'default' => FORMAT_MOODLE,
'optional' => true,
'null' => NULL_ALLOWED,
],
'sortorder' => [
'type' => PARAM_INT,
'optional' => true,
'default' => -1,
'null' => NULL_ALLOWED,
],
'categoryid' => [
'type' => PARAM_INT
],
'configdata' => [
'type' => PARAM_RAW,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
);
}
/**
* Get decoded configdata.
*
* @return array
*/
protected function get_configdata(): array {
return json_decode($this->raw_get('configdata') ?? '', true) ?? array();
}
}
+214
View File
@@ -0,0 +1,214 @@
<?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/>.
/**
* Customfield package
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield;
defined('MOODLE_INTERNAL') || die;
/**
* Class field_config_form
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class field_config_form extends \core_form\dynamic_form {
/** @var field_controller */
protected $field;
/**
* Class definition
*
* @throws \coding_exception
*/
public function definition() {
$mform = $this->_form;
$field = $this->get_field();
$handler = $field->get_handler();
$mform->addElement('header', '_commonsettings', get_string('commonsettings', 'core_customfield'));
$mform->addElement('text', 'name', get_string('fieldname', 'core_customfield'), 'size="50"');
$mform->addRule('name', null, 'required', null, 'client');
$mform->setType('name', PARAM_TEXT);
// Accepted values for 'shortname' would follow [a-z0-9_] pattern,
// but we are accepting any PARAM_TEXT value here,
// and checking [a-zA-Z0-9_] pattern in validation() function to throw an error when needed.
$mform->addElement('text', 'shortname', get_string('fieldshortname', 'core_customfield'), 'size=20');
$mform->addHelpButton('shortname', 'shortname', 'core_customfield');
$mform->addRule('shortname', null, 'required', null, 'client');
$mform->setType('shortname', PARAM_TEXT);
$desceditoroptions = $handler->get_description_text_options();
$mform->addElement('editor', 'description_editor', get_string('description', 'core_customfield'), null, $desceditoroptions);
$mform->addHelpButton('description_editor', 'description', 'core_customfield');
// If field is required.
$mform->addElement('selectyesno', 'configdata[required]', get_string('isfieldrequired', 'core_customfield'));
$mform->addHelpButton('configdata[required]', 'isfieldrequired', 'core_customfield');
$mform->setType('configdata[required]', PARAM_BOOL);
// If field data is unique.
$mform->addElement('selectyesno', 'configdata[uniquevalues]', get_string('isdataunique', 'core_customfield'));
$mform->addHelpButton('configdata[uniquevalues]', 'isdataunique', 'core_customfield');
$mform->setType('configdata[uniquevalues]', PARAM_BOOL);
// Field specific settings from field type.
$field->config_form_definition($mform);
// Handler/component settings.
$handler->config_form_definition($mform);
// We add hidden fields.
$mform->addElement('hidden', 'categoryid');
$mform->setType('categoryid', PARAM_INT);
$mform->addElement('hidden', 'type');
$mform->setType('type', PARAM_COMPONENT);
$mform->addElement('hidden', 'id');
$mform->setType('id', PARAM_INT);
// This form is only used inside modal dialogues and never needs action buttons.
}
/**
* Field data validation
*
* @param array $data
* @param array $files
* @return array
*/
public function validation($data, $files = array()) {
global $DB;
$errors = array();
$field = $this->get_field();
$handler = $field->get_handler();
// Check the shortname is specified and is unique for this component-area-itemid combination.
if (!preg_match('/^[a-z0-9_]+$/', $data['shortname'])) {
// Check allowed pattern (numbers, letters and underscore).
$errors['shortname'] = get_string('invalidshortnameerror', 'core_customfield');
} else if ($DB->record_exists_sql('SELECT 1 FROM {customfield_field} f ' .
'JOIN {customfield_category} c ON c.id = f.categoryid ' .
'WHERE f.shortname = ? AND f.id <> ? AND c.component = ? AND c.area = ? AND c.itemid = ?',
[$data['shortname'], $data['id'],
$handler->get_component(), $handler->get_area(), $handler->get_itemid()])) {
$errors['shortname'] = get_string('formfieldcheckshortname', 'core_customfield');
}
$errors = array_merge($errors, $field->config_form_validation($data, $files));
return $errors;
}
/**
* Get field
*
* @return field_controller
* @throws \moodle_exception
*/
protected function get_field(): field_controller {
if ($this->field === null) {
if (!empty($this->_ajaxformdata['id'])) {
$this->field = \core_customfield\field_controller::create((int)$this->_ajaxformdata['id']);
} else if (!empty($this->_ajaxformdata['categoryid']) && !empty($this->_ajaxformdata['type'])) {
$category = \core_customfield\category_controller::create((int)$this->_ajaxformdata['categoryid']);
$type = clean_param($this->_ajaxformdata['type'], PARAM_PLUGIN);
$this->field = \core_customfield\field_controller::create(0, (object)['type' => $type], $category);
} else {
throw new \moodle_exception('fieldnotfound', 'core_customfield');
}
}
return $this->field;
}
/**
* Check if current user has access to this form, otherwise throw exception
*
* Sometimes permission check may depend on the action and/or id of the entity.
* If necessary, form data is available in $this->_ajaxformdata
*/
protected function check_access_for_dynamic_submission(): void {
$field = $this->get_field();
$handler = $field->get_handler();
if (!$handler->can_configure()) {
throw new \moodle_exception('nopermissionconfigure', 'core_customfield');
}
}
/**
* Load in existing data as form defaults
*
* Can be overridden to retrieve existing values from db by entity id and also
* to preprocess editor and filemanager elements
*
* Example:
* $this->set_data(get_entity($this->_ajaxformdata['id']));
*/
public function set_data_for_dynamic_submission(): void {
$this->set_data(api::prepare_field_for_config_form($this->get_field()));
}
/**
* Process the form submission
*
* This method can return scalar values or arrays that can be json-encoded, they will be passed to the caller JS.
*
* @return mixed
*/
public function process_dynamic_submission() {
$data = $this->get_data();
$field = $this->get_field();
$handler = $field->get_handler();
$handler->save_field_configuration($field, $data);
return null;
}
/**
* Form context
* @return \context
*/
protected function get_context_for_dynamic_submission(): \context {
return $this->get_field()->get_handler()->get_configuration_context();
}
/**
* Page url
* @return \moodle_url
*/
protected function get_page_url_for_dynamic_submission(): \moodle_url {
$field = $this->get_field();
if ($field->get('id')) {
$params = ['action' => 'editfield', 'id' => $field->get('id')];
} else {
$params = ['action' => 'addfield', 'categoryid' => $field->get('categoryid'), 'type' => $field->get('type')];
}
return new \moodle_url($field->get_handler()->get_configuration_url(), $params);
}
}
+287
View File
@@ -0,0 +1,287 @@
<?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/>.
/**
* Field controller abstract class
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield;
defined('MOODLE_INTERNAL') || die;
/**
* Base class for custom fields controllers
*
* This class is a wrapper around the persistent field class that allows to define the field
* configuration
*
* Custom field plugins must define a class
* \{pluginname}\field_controller extends \core_customfield\field_controller
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class field_controller {
/**
* Field persistent class
*
* @var field
*/
protected $field;
/**
* Category of the field.
*
* @var category_controller
*/
protected $category;
/**
* Constructor.
*
* @param int $id
* @param \stdClass|null $record
*/
public function __construct(int $id = 0, \stdClass $record = null) {
$this->field = new field($id, $record);
}
/**
* Creates an instance of field_controller
*
* Parameters $id, $record and $category can complement each other but not conflict.
* If $id is not specified, categoryid must be present either in $record or in $category.
* If $id is not specified, type must be present in $record
*
* No DB queries are performed if both $record and $category are specified.
*
* @param int $id
* @param \stdClass|null $record
* @param category_controller|null $category
* @return field_controller will return the instance of the class from the customfield element plugin
* @throws \coding_exception
* @throws \moodle_exception
*/
public static function create(int $id, \stdClass $record = null, category_controller $category = null): field_controller {
global $DB;
if ($id && $record) {
// This warning really should be in persistent as well.
debugging('Too many parameters, either id need to be specified or a record, but not both.',
DEBUG_DEVELOPER);
}
if ($id) {
if (!$record = $DB->get_record(field::TABLE, array('id' => $id), '*', IGNORE_MISSING)) {
throw new \moodle_exception('fieldnotfound', 'core_customfield');
}
}
if (empty($record->categoryid)) {
if (!$category) {
throw new \coding_exception('Not enough parameters to initialise field_controller - unknown category');
} else {
$record->categoryid = $category->get('id');
}
}
if (empty($record->type)) {
throw new \coding_exception('Not enough parameters to initialise field_controller - unknown field type');
}
$type = $record->type;
if (!$category) {
$category = category_controller::create($record->categoryid);
}
if ($category->get('id') != $record->categoryid) {
throw new \coding_exception('Category of the field does not match category from the parameter');
}
$customfieldtype = "\\customfield_{$type}\\field_controller";
if (!class_exists($customfieldtype) || !is_subclass_of($customfieldtype, self::class)) {
throw new \moodle_exception('errorfieldtypenotfound', 'core_customfield', '', s($type));
}
$fieldcontroller = new $customfieldtype(0, $record);
$fieldcontroller->category = $category;
$category->add_field($fieldcontroller);
return $fieldcontroller;
}
/**
* Perform pre-processing of field values, for example those that originate from an external source (e.g. upload course tool)
*
* Override in plugin classes as necessary
*
* @param string $value
* @return mixed
*/
public function parse_value(string $value) {
return $value;
}
/**
* Validate the data on the field configuration form
*
* Plugins can override it
*
* @param array $data from the add/edit profile field form
* @param array $files
* @return array associative array of error messages
*/
public function config_form_validation(array $data, $files = array()): array {
return array();
}
/**
* Persistent getter parser.
*
* @param string $property
* @return mixed
*/
final public function get(string $property) {
return $this->field->get($property);
}
/**
* Persistent setter parser.
*
* @param string $property
* @param mixed $value
* @return field
*/
final public function set($property, $value) {
return $this->field->set($property, $value);
}
/**
* Delete a field and all associated data
*
* Plugins may override it if it is necessary to delete related data (such as files)
*
* Not that the delete() method from data_controller is not called here.
*
* @return bool
*/
public function delete(): bool {
global $DB;
$DB->delete_records('customfield_data', ['fieldid' => $this->get('id')]);
return $this->field->delete();
}
/**
* Save or update the persistent class to database.
*
* @return void
*/
public function save() {
$this->field->save();
}
/**
* Persistent to_record parser.
*
* @return \stdClass
*/
final public function to_record() {
return $this->field->to_record();
}
/**
* Get the category associated with this field
*
* @return category_controller
*/
final public function get_category(): category_controller {
return $this->category;
}
/**
* Get configdata property.
*
* @param string $property name of the property
* @return mixed
*/
public function get_configdata_property(string $property) {
$configdata = $this->field->get('configdata');
if (!isset($configdata[$property])) {
return null;
}
return $configdata[$property];
}
/**
* Returns a handler for this field
*
* @return handler
*/
final public function get_handler(): handler {
return $this->get_category()->get_handler();
}
/**
* Prepare the field data to set in the configuration form
*
* Plugin can override if some preprocessing required for editor or filemanager fields
*
* @param \stdClass $formdata
*/
public function prepare_for_config_form(\stdClass $formdata) {
}
/**
* Add specific settings to the field configuration form, for example "default value"
*
* @param \MoodleQuickForm $mform
*/
abstract public function config_form_definition(\MoodleQuickForm $mform);
/**
* Returns the field name formatted according to configuration context.
*
* @param bool $escape
* @return string
*/
public function get_formatted_name(bool $escape = true): string {
$context = $this->get_handler()->get_configuration_context();
return format_string($this->get('name'), true, [
'context' => $context,
'escape' => $escape,
]);
}
/**
* Does this custom field type support being used as part of the block_myoverview
* custom field grouping?
* @return bool
*/
public function supports_course_grouping(): bool {
return false;
}
/**
* If this field supports course filtering, then this function needs overriding to
* return the formatted values for this.
* @param array $values the used values that need grouping
* @return array
*/
public function course_grouping_format_values($values): array {
return [];
}
}
+824
View File
@@ -0,0 +1,824 @@
<?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/>.
/**
* The abstract custom fields handler
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield;
use backup_nested_element;
use core_customfield\output\field_data;
use stdClass;
defined('MOODLE_INTERNAL') || die;
/**
* Base class for custom fields handlers
*
* This handler provides callbacks for field configuration form and also allows to add the fields to the instance editing form
*
* Every plugin that wants to use custom fields must define a handler class:
* <COMPONENT_OR_PLUGIN>\customfield\<AREA>_handler extends \core_customfield\handler
*
* To initiate a class use an appropriate static method:
* - <handlerclass>::create - to create an instance of a known handler
* - \core_customfield\handler::get_handler - to create an instance of a handler for given component/area/itemid
*
* Also handler is automatically created when the following methods are called:
* - \core_customfield\api::get_field($fieldid)
* - \core_customfield\api::get_category($categoryid)
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class handler {
/**
* The component this handler handles
*
* @var string $component
*/
private $component;
/**
* The area within the component
*
* @var string $area
*/
private $area;
/**
* The id of the item within the area and component
* @var int $itemid
*/
private $itemid;
/**
* @var category_controller[]
*/
protected $categories = null;
/**
* Handler constructor.
*
* @param int $itemid
*/
final protected function __construct(int $itemid = 0) {
if (!preg_match('|^(\w+_[\w_]+)\\\\customfield\\\\([\w_]+)_handler$|', static::class, $matches)) {
throw new \coding_exception('Handler class name must have format: <PLUGIN>\\customfield\\<AREA>_handler');
}
$this->component = $matches[1];
$this->area = $matches[2];
$this->itemid = $itemid;
}
/**
* Returns an instance of the handler
*
* Some areas may choose to use singleton/caching here
*
* @param int $itemid
* @return handler
*/
public static function create(int $itemid = 0): handler {
return new static($itemid);
}
/**
* Returns an instance of handler by component/area/itemid
*
* @param string $component component name of full frankenstyle plugin name
* @param string $area name of the area (each component/plugin may define handlers for multiple areas)
* @param int $itemid item id if the area uses them (usually not used)
* @return handler
*/
public static function get_handler(string $component, string $area, int $itemid = 0): handler {
$classname = $component . '\\customfield\\' . $area . '_handler';
if (class_exists($classname) && is_subclass_of($classname, self::class)) {
return $classname::create($itemid);
}
$a = ['component' => s($component), 'area' => s($area)];
throw new \moodle_exception('unknownhandler', 'core_customfield', '', $a);
}
/**
* Get component
*
* @return string
*/
public function get_component(): string {
return $this->component;
}
/**
* Get area
*
* @return string
*/
public function get_area(): string {
return $this->area;
}
/**
* Context that should be used for new categories created by this handler
*
* @return \context
*/
abstract public function get_configuration_context(): \context;
/**
* URL for configuration of the fields on this handler.
*
* @return \moodle_url
*/
abstract public function get_configuration_url(): \moodle_url;
/**
* Context that should be used for data stored for the given record
*
* @param int $instanceid id of the instance or 0 if the instance is being created
* @return \context
*/
abstract public function get_instance_context(int $instanceid = 0): \context;
/**
* Get itemid
*
* @return int|null
*/
public function get_itemid(): int {
return $this->itemid;
}
/**
* Uses categories
*
* @return bool
*/
public function uses_categories(): bool {
return true;
}
/**
* Generates a name for the new category
*
* @param int $suffix
* @return string
*/
protected function generate_category_name($suffix = 0): string {
if ($suffix) {
return get_string('otherfieldsn', 'core_customfield', $suffix);
} else {
return get_string('otherfields', 'core_customfield');
}
}
/**
* Creates a new category and inserts it to the database
*
* @param string $name name of the category, null to generate automatically
* @return int id of the new category
*/
public function create_category(string $name = null): int {
global $DB;
$params = ['component' => $this->get_component(), 'area' => $this->get_area(), 'itemid' => $this->get_itemid()];
if (empty($name)) {
for ($suffix = 0; $suffix < 100; $suffix++) {
$name = $this->generate_category_name($suffix);
if (!$DB->record_exists(category::TABLE, $params + ['name' => $name])) {
break;
}
}
}
$category = category_controller::create(0, (object)['name' => $name], $this);
api::save_category($category);
$this->clear_configuration_cache();
return $category->get('id');
}
/**
* Validate that the given category belongs to this handler
*
* @param category_controller $category
* @return category_controller
* @throws \moodle_exception
*/
protected function validate_category(category_controller $category): category_controller {
$categories = $this->get_categories_with_fields();
if (!array_key_exists($category->get('id'), $categories)) {
throw new \moodle_exception('categorynotfound', 'core_customfield');
}
return $categories[$category->get('id')];
}
/**
* Validate that the given field belongs to this handler
*
* @param field_controller $field
* @return field_controller
* @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())) {
throw new \moodle_exception('fieldnotfound', 'core_customfield');
}
return $category->get_fields()[$field->get('id')];
}
/**
* Change name for a field category
*
* @param category_controller $category
* @param string $name
*/
public function rename_category(category_controller $category, string $name) {
$this->validate_category($category);
$category->set('name', $name);
api::save_category($category);
$this->clear_configuration_cache();
}
/**
* Change sort order of the categories
*
* @param category_controller $category category that needs to be moved
* @param int $beforeid id of the category this category needs to be moved before, 0 to move to the end
*/
public function move_category(category_controller $category, int $beforeid = 0) {
$category = $this->validate_category($category);
api::move_category($category, $beforeid);
$this->clear_configuration_cache();
}
/**
* Permanently delete category, all fields in it and all associated data
*
* @param category_controller $category
* @return bool
*/
public function delete_category(category_controller $category): bool {
$category = $this->validate_category($category);
$result = api::delete_category($category);
$this->clear_configuration_cache();
return $result;
}
/**
* Deletes all data and all fields and categories defined in this handler
*/
public function delete_all() {
$categories = $this->get_categories_with_fields();
foreach ($categories as $category) {
api::delete_category($category);
}
$this->clear_configuration_cache();
}
/**
* Permanently delete a custom field configuration and all associated data
*
* @param field_controller $field
* @return bool
*/
public function delete_field_configuration(field_controller $field): bool {
$field = $this->validate_field($field);
$result = api::delete_field_configuration($field);
$this->clear_configuration_cache();
return $result;
}
/**
* Change fields sort order, move field to another category
*
* @param field_controller $field field that needs to be moved
* @param int $categoryid category that needs to be moved
* @param int $beforeid id of the category this category needs to be moved before, 0 to move to the end
*/
public function move_field(field_controller $field, int $categoryid, int $beforeid = 0) {
$field = $this->validate_field($field);
api::move_field($field, $categoryid, $beforeid);
$this->clear_configuration_cache();
}
/**
* The current user can configure custom fields on this component.
*
* @return bool
*/
abstract public function can_configure(): bool;
/**
* The current user can edit given custom fields on the given instance
*
* Called to filter list of fields displayed on the instance edit form
*
* Capability to edit/create instance is checked separately
*
* @param field_controller $field
* @param int $instanceid id of the instance or 0 if the instance is being created
* @return bool
*/
abstract public function can_edit(field_controller $field, int $instanceid = 0): bool;
/**
* The current user can view the value of the custom field for a given custom field and instance
*
* Called to filter list of fields returned by methods get_instance_data(), get_instances_data(),
* export_instance_data(), export_instance_data_object()
*
* Access to the instance itself is checked by handler before calling these methods
*
* @param field_controller $field
* @param int $instanceid
* @return bool
*/
abstract public function can_view(field_controller $field, int $instanceid): bool;
/**
* Returns the custom field values for an individual instance
*
* The caller must check access to the instance itself before invoking this method
*
* The result is an array of data_controller objects
*
* @param int $instanceid
* @param bool $returnall return data for all fields (by default only visible fields)
* @return data_controller[] array of data_controller objects indexed by fieldid. All fields are present,
* some data_controller objects may have 'id', some not
* In the last case data_controller::get_value() and export_value() functions will return default values.
*/
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);
}
/**
* Returns the custom fields values for multiple instances
*
* The caller must check access to the instance itself before invoking this method
*
* The result is an array of data_controller objects
*
* @param int[] $instanceids
* @param bool $returnall return data for all fields (by default only visible fields)
* @return data_controller[][] 2-dimension array, first index is instanceid, second index is fieldid.
* All instanceids and all fieldids are present, some data_controller objects may have 'id', some not.
* In the last case data_controller::get_value() and export_value() functions will return default values.
*/
public function get_instances_data(array $instanceids, bool $returnall = false): array {
$result = api::get_instances_fields_data($this->get_fields(), $instanceids);
if (!$returnall) {
// Filter only by visible fields (list of visible fields may be different for each instance).
$handler = $this;
foreach ($instanceids as $instanceid) {
$result[$instanceid] = array_filter($result[$instanceid], function(data_controller $d) use ($handler) {
return $handler->can_view($d->get_field(), $d->get('instanceid'));
});
}
}
return $result;
}
/**
* Returns the custom field values for an individual instance ready to be displayed
*
* The caller must check access to the instance itself before invoking this method
*
* The result is an array of \core_customfield\output\field_data objects
*
* @param int $instanceid
* @param bool $returnall
* @return \core_customfield\output\field_data[]
*/
public function export_instance_data(int $instanceid, bool $returnall = false): array {
return array_map(function($d) {
return new field_data($d);
}, $this->get_instance_data($instanceid, $returnall));
}
/**
* Returns the custom field values for an individual instance ready to be displayed
*
* The caller must check access to the instance itself before invoking this method
*
* The result is a class where properties are fields short names and the values their export values for this instance
*
* @param int $instanceid
* @param bool $returnall
* @return stdClass
*/
public function export_instance_data_object(int $instanceid, bool $returnall = false): stdClass {
$rv = new stdClass();
foreach ($this->export_instance_data($instanceid, $returnall) as $d) {
$rv->{$d->get_shortname()} = $d->get_value();
}
return $rv;
}
/**
* Display visible custom fields.
* This is a sample implementation that can be overridden in each handler.
*
* @param data_controller[] $fieldsdata
* @return string
*/
public function display_custom_fields_data(array $fieldsdata): string {
global $PAGE;
$output = $PAGE->get_renderer('core_customfield');
$content = '';
foreach ($fieldsdata as $data) {
$fd = new field_data($data);
$content .= $output->render($fd);
}
return $content;
}
/**
* Returns array of categories, each of them contains a list of fields definitions.
*
* @return category_controller[]
*/
public function get_categories_with_fields(): array {
if ($this->categories === null) {
$this->categories = api::get_categories_with_fields($this->get_component(), $this->get_area(), $this->get_itemid());
}
$handler = $this;
array_walk($this->categories, function(category_controller $c) use ($handler) {
$c->set_handler($handler);
});
return $this->categories;
}
/**
* Clears a list of categories with corresponding fields definitions.
*/
protected function clear_configuration_cache() {
$this->categories = null;
}
/**
* Checks if current user can backup a given field
*
* Capability to backup the instance does not need to be checked here
*
* @param field_controller $field
* @param int $instanceid
* @return bool
*/
protected function can_backup(field_controller $field, int $instanceid): bool {
return $this->can_view($field, $instanceid) || $this->can_edit($field, $instanceid);
}
/**
* Run the custom field backup callback for each controller.
*
* @param int $instanceid The instance ID.
* @param \backup_nested_element $customfieldselement The custom field element to be backed up.
*/
public function backup_define_structure(int $instanceid, backup_nested_element $customfieldselement): void {
$datacontrollers = $this->get_instance_data($instanceid);
foreach ($datacontrollers as $controller) {
if ($this->can_backup($controller->get_field(), $instanceid)) {
$controller->backup_define_structure($customfieldselement);
}
}
}
/**
* Run the custom field restore callback for each controller.
*
* @param \restore_structure_step $step The restore step instance.
* @param int $newid The new ID for the custom field data after restore.
* @param int $oldid The original ID of the custom field data before backup.
*/
public function restore_define_structure(\restore_structure_step $step, int $newid, int $oldid): void {
$datacontrollers = $this->get_instance_data($newid);
foreach ($datacontrollers as $controller) {
$controller->restore_define_structure($step, $newid, $oldid);
}
}
/**
* Get raw data associated with all fields current user can view or edit
*
* @param int $instanceid
* @return array
*/
public function get_instance_data_for_backup(int $instanceid): array {
$finalfields = [];
$data = $this->get_instance_data($instanceid, true);
foreach ($data as $d) {
if ($d->get('id') && $this->can_backup($d->get_field(), $instanceid)) {
$finalfields[] = [
'id' => $d->get('id'),
'shortname' => $d->get_field()->get('shortname'),
'type' => $d->get_field()->get('type'),
'value' => $d->get_value(),
'valueformat' => $d->get('valueformat'),
'valuetrust' => $d->get('valuetrust'),
];
}
}
return $finalfields;
}
/**
* Form data definition callback.
*
* This method is called from moodleform::definition_after_data and allows to tweak
* mform with some data coming directly from the field plugin data controller.
*
* @param \MoodleQuickForm $mform
* @param int $instanceid
*/
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);
foreach ($fields as $formfield) {
$formfield->instance_form_definition_after_data($mform);
}
}
/**
* Prepares the custom fields data related to the instance to pass to mform->set_data()
*
* Example:
* $instance = $DB->get_record(...);
* // .... prepare editor, filemanager, add tags, etc.
* $handler->instance_form_before_set_data($instance);
* $form->set_data($instance);
*
* @param stdClass $instance the instance that has custom fields, if 'id' attribute is present the custom
* fields for this instance will be added, otherwise the default values will be added.
*/
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);
foreach ($fields as $formfield) {
$formfield->instance_form_before_set_data($instance);
}
}
/**
* Saves the given data for custom fields, must be called after the instance is saved and id is present
*
* Example:
* if ($data = $form->get_data()) {
* // ... save main instance, set $data->id if instance was created.
* $handler->instance_form_save($data);
* redirect(...);
* }
*
* @param stdClass $instance data received from a form
* @param bool $isnewinstance if this is call is made during instance creation
*/
public function instance_form_save(stdClass $instance, bool $isnewinstance = false) {
if (empty($instance->id)) {
throw new \coding_exception('Caller must ensure that id is already set in data before calling this method');
}
if (!preg_grep('/^customfield_/', array_keys((array)$instance))) {
// For performance.
return;
}
$editablefields = $this->get_editable_fields($isnewinstance ? 0 : $instance->id);
$fields = api::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);
}
$data->instance_form_save($instance);
}
}
/**
* Validates the given data for custom fields, used in moodleform validation() function
*
* Example:
* public function validation($data, $files) {
* $errors = [];
* // .... check other fields.
* $errors = array_merge($errors, $handler->instance_form_validation($data, $files));
* return $errors;
* }
*
* @param array $data
* @param array $files
* @return array validation errors
*/
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);
$errors = [];
foreach ($fields as $formfield) {
$errors += $formfield->instance_form_validation($data, $files);
}
return $errors;
}
/**
* Adds custom fields to instance editing form
*
* Example:
* public function definition() {
* // ... normal instance definition, including hidden 'id' field.
* $handler->instance_form_definition($this->_form, $instanceid);
* $this->add_action_buttons();
* }
*
* @param \MoodleQuickForm $mform
* @param int $instanceid id of the instance, can be null when instance is being created
* @param string $headerlangidentifier If specified, a lang string will be used for field category headings
* @param string $headerlangcomponent
*/
public function instance_form_definition(\MoodleQuickForm $mform, int $instanceid = 0,
?string $headerlangidentifier = null, ?string $headerlangcomponent = null) {
$editablefields = $this->get_editable_fields($instanceid);
$fieldswithdata = api::get_instance_fields_data($editablefields, $instanceid);
$lastcategoryid = null;
foreach ($fieldswithdata as $data) {
$categoryid = $data->get_field()->get_category()->get('id');
if ($categoryid != $lastcategoryid) {
$categoryname = $data->get_field()->get_category()->get_formatted_name();
// Load category header lang string if specified.
if (!empty($headerlangidentifier)) {
$categoryname = get_string($headerlangidentifier, $headerlangcomponent, $categoryname);
}
$mform->addElement('header', 'category_' . $categoryid, $categoryname);
$lastcategoryid = $categoryid;
}
$data->instance_form_definition($mform);
$field = $data->get_field()->to_record();
if (strlen((string)$field->description)) {
// Add field description.
$context = $this->get_configuration_context();
$value = file_rewrite_pluginfile_urls($field->description, 'pluginfile.php',
$context->id, 'core_customfield', 'description', $field->id);
$value = format_text($value, $field->descriptionformat, ['context' => $context]);
$mform->addElement('static', 'customfield_' . $field->shortname . '_static', '', $value);
}
}
}
/**
* Get field types array
*
* @return array
*/
public function get_available_field_types(): array {
return api::get_available_field_types();
}
/**
* Options for processing embedded files in the field description.
*
* Handlers may want to extend it to disable files support and/or specify 'noclean'=>true
* Context is not necessary here
*
* @return array
*/
public function get_description_text_options(): array {
global $CFG;
require_once($CFG->libdir.'/formslib.php');
return [
'maxfiles' => EDITOR_UNLIMITED_FILES,
'maxbytes' => $CFG->maxbytes,
'context' => $this->get_configuration_context()
];
}
/**
* Save the field configuration with the data from the form
*
* @param field_controller $field
* @param stdClass $data data from the form
*/
public function save_field_configuration(field_controller $field, stdClass $data) {
if ($field->get('id')) {
$field = $this->validate_field($field);
} else {
$this->validate_category($field->get_category());
}
api::save_field_configuration($field, $data);
$this->clear_configuration_cache();
}
/**
* Creates or updates custom field data for a instanceid from backup data.
* The handlers have to override it if they support backup.
*
* @param \restore_task $task
* @param array $data
*
* @return int|void Implementations should conditionally return the ID of the created or updated record.
*/
public function restore_instance_data_from_backup(\restore_task $task, array $data) {
throw new \coding_exception('Must be implemented in the handler');
}
/**
* Returns list of fields defined for this instance as an array (not groupped by categories)
*
* Fields are sorted in the same order they would appear on the instance edit form
*
* Note that this function returns all fields in all categories regardless of whether the current user
* can view or edit data associated with them
*
* @return field_controller[]
*/
public function get_fields(): array {
$categories = $this->get_categories_with_fields();
$fields = [];
foreach ($categories as $category) {
foreach ($category->get_fields() as $field) {
$fields[$field->get('id')] = $field;
}
}
return $fields;
}
/**
* Get visible fields
*
* @param int $instanceid
* @return field_controller[]
*/
protected function get_visible_fields(int $instanceid): array {
$handler = $this;
return array_filter($this->get_fields(),
function($field) use($handler, $instanceid) {
return $handler->can_view($field, $instanceid);
}
);
}
/**
* Get editable fields
*
* @param int $instanceid
* @return field_controller[]
*/
public function get_editable_fields(int $instanceid): array {
$handler = $this;
return array_filter($this->get_fields(),
function($field) use($handler, $instanceid) {
return $handler->can_edit($field, $instanceid);
}
);
}
/**
* Allows to add custom controls to the field configuration form that will be saved in configdata
*
* @param \MoodleQuickForm $mform
*/
public function config_form_definition(\MoodleQuickForm $mform) {
}
/**
* Deletes all data related to all fields of an instance.
*
* @param int $instanceid
*/
public function delete_instance(int $instanceid) {
$fielddata = api::get_instance_fields_data($this->get_fields(), $instanceid, false);
foreach ($fielddata as $data) {
$data->delete();
}
}
}
+114
View File
@@ -0,0 +1,114 @@
<?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/>.
/**
* core_customfield field value renderable.
*
* @package core_customfield
* @copyright 2018 Daniel Neis Araujo <danielneis@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\output;
use core_customfield\data_controller;
defined('MOODLE_INTERNAL') || die;
/**
* core_customfield field value renderable class.
*
* @package core_customfield
* @copyright 2018 Daniel Neis Araujo <danielneis@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class field_data implements \renderable, \templatable {
/** @var \core_customfield\data_controller */
protected $data;
/**
* Renderable constructor.
*
* @param \core_customfield\data_controller $data
*/
public function __construct(\core_customfield\data_controller $data) {
$this->data = $data;
}
/**
* Returns the data value formatted for the output
*
* @return mixed|null
*/
public function get_value() {
return $this->data->export_value();
}
/**
* Returns the field type (checkbox, date, text, ...)
*
* @return string
*/
public function get_type(): string {
return $this->data->get_field()->get('type');
}
/**
* Returns the field short name
*
* @return string
*/
public function get_shortname(): string {
return $this->data->get_field()->get('shortname');
}
/**
* Returns the field name formatted for the output
*
* @return string
*/
public function get_name(): string {
return $this->data->get_field()->get_formatted_name();
}
/**
* Returns the data controller used to create this object if additional attributes are needed
*
* @return data_controller
*/
public function get_data_controller(): data_controller {
return $this->data;
}
/**
* Export data for using as template context.
*
* @param \renderer_base $output
* @return \stdClass
*/
public function export_for_template(\renderer_base $output) {
$value = $this->get_value();
return (object)[
'value' => $value,
'type' => $this->get_type(),
'shortname' => $this->get_shortname(),
'name' => $this->get_name(),
'hasvalue' => ($value !== null),
'instanceid' => $this->data->get('instanceid')
];
}
}
+126
View File
@@ -0,0 +1,126 @@
<?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/>.
/**
* Customfield component output.
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\output;
use core_customfield\api;
use core_customfield\handler;
use renderable;
use templatable;
defined('MOODLE_INTERNAL') || die;
/**
* Class management
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class management implements renderable, templatable {
/**
* @var handler
*/
protected $handler;
/**
* @var
*/
protected $categoryid;
/**
* management constructor.
*
* @param \core_customfield\handler $handler
*/
public function __construct(\core_customfield\handler $handler) {
$this->handler = $handler;
}
/**
* Export for template
*
* @param \renderer_base $output
* @return array|object|\stdClass
*/
public function export_for_template(\renderer_base $output) {
$data = new \stdClass();
$fieldtypes = $this->handler->get_available_field_types();
$data->component = $this->handler->get_component();
$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();
$categoriesarray = array();
foreach ($categories as $category) {
$categoryarray = array();
$categoryarray['id'] = $category->get('id');
$categoryarray['nameeditable'] = $output->render(api::get_category_inplace_editable($category, true));
$categoryarray['movetitle'] = get_string('movecategory', 'core_customfield',
$category->get_formatted_name());
$categoryarray['fields'] = array();
foreach ($category->get_fields() as $field) {
$fieldname = $field->get_formatted_name();
$fieldarray['type'] = $fieldtypes[$field->get('type')];
$fieldarray['id'] = $field->get('id');
$fieldarray['name'] = $fieldname;
$fieldarray['shortname'] = $field->get('shortname');
$fieldarray['movetitle'] = get_string('movefield', 'core_customfield', $fieldname);
$categoryarray['fields'][] = $fieldarray;
}
$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);
}
$menu->attributes['class'] .= ' float-left mr-1';
$categoryarray['addfieldmenu'] = $output->render($menu);
$categoriesarray[] = $categoryarray;
}
$data->categories = $categoriesarray;
if (empty($data->categories)) {
$data->nocategories = get_string('nocategories', 'core_customfield');
}
return $data;
}
}
+62
View File
@@ -0,0 +1,62 @@
<?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/>.
/**
* Renderer.
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\output;
defined('MOODLE_INTERNAL') || die();
use plugin_renderer_base;
/**
* Renderer class.
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class renderer extends plugin_renderer_base {
/**
* Render custom field management interface.
*
* @param \core_customfield\output\management $list
* @return string HTML
*/
protected function render_management(\core_customfield\output\management $list) {
$context = $list->export_for_template($this);
return $this->render_from_template('core_customfield/list', $context);
}
/**
* Render single custom field value
*
* @param \core_customfield\output\field_data $field
* @return string HTML
*/
protected function render_field_data(\core_customfield\output\field_data $field) {
$context = $field->export_for_template($this);
return $this->render_from_template('core_customfield/field_data', $context);
}
}
@@ -0,0 +1,84 @@
<?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/>.
/**
* Contains interface customfield_provider
*
* @package core_customfield
* @copyright 2018 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\privacy;
use core_customfield\data_controller;
defined('MOODLE_INTERNAL') || die();
/**
* Interface customfield_provider, all customfield plugins need to implement it
*
* @package core_customfield
* @copyright 2018 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface customfield_provider extends
\core_privacy\local\request\plugin\subplugin_provider,
// The customfield plugins do not need to do anything themselves for the shared_userlist.
// This is all handled by the component core_customfield.
\core_privacy\local\request\shared_userlist_provider
{
/**
* Preprocesses data object that is going to be exported
*
* Minimum implementation:
* writer::with_context($data->get_context())->export_data($subcontext, $exportdata);
*
* @param data_controller $data
* @param \stdClass $exportdata generated object to be exported
* @param array $subcontext subcontext to use when exporting
* @return mixed
*/
public static function export_customfield_data(data_controller $data, \stdClass $exportdata, array $subcontext);
/**
* Allows plugins to delete everything they store related to the data (usually files)
*
* If plugin does not store any related files or other information, implement as an empty function
*
* @param string $dataidstest select query for data id (note that it may also return data for other field types)
* @param array $params named parameters for the select query
* @param array $contextids list of affected data contexts
* @return mixed
*/
public static function before_delete_data(string $dataidstest, array $params, array $contextids);
/**
* Allows plugins to delete everything they store related to the field configuration (usually files)
*
* The implementation should not delete data or anything related to the data, since "before_delete_data" is
* invoked separately.
*
* If plugin does not store any related files or other information, implement as an empty function
*
* @param string $fieldidstest select query for field id (note that it may also return fields of other types)
* @param array $params named parameters for the select query
* @param int[] $contextids list of affected configuration contexts
*/
public static function before_delete_fields(string $fieldidstest, array $params, array $contextids);
}
+495
View File
@@ -0,0 +1,495 @@
<?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/>.
/**
* Customfield component provider class
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\privacy;
defined('MOODLE_INTERNAL') || die();
use core_customfield\data_controller;
use core_customfield\handler;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\writer;
use core_privacy\manager;
/**
* Class provider
*
* Customfields API does not directly store userid and does not perform any export or delete functionality by itself
*
* However this class defines several functions that can be utilized by components that use customfields API to
* export/delete user data.
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
// Customfield store data.
\core_privacy\local\metadata\provider,
// The customfield subsystem stores data on behalf of other components.
\core_privacy\local\request\subsystem\plugin_provider,
\core_privacy\local\request\shared_userlist_provider {
/**
* Return the fields which contain personal data.
*
* @param collection $collection a reference to the collection to use to store the metadata.
* @return collection the updated collection of metadata items.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_database_table(
'customfield_data',
[
'fieldid' => 'privacy:metadata:customfield_data:fieldid',
'instanceid' => 'privacy:metadata:customfield_data:instanceid',
'intvalue' => 'privacy:metadata:customfield_data:intvalue',
'decvalue' => 'privacy:metadata:customfield_data:decvalue',
'shortcharvalue' => 'privacy:metadata:customfield_data:shortcharvalue',
'charvalue' => 'privacy:metadata:customfield_data:charvalue',
'value' => 'privacy:metadata:customfield_data:value',
'valueformat' => 'privacy:metadata:customfield_data:valueformat',
'valuetrust' => 'privacy:metadata:customfield_data:valuetrust',
'timecreated' => 'privacy:metadata:customfield_data:timecreated',
'timemodified' => 'privacy:metadata:customfield_data:timemodified',
'contextid' => 'privacy:metadata:customfield_data:contextid',
],
'privacy:metadata:customfield_data'
);
// Link to subplugins.
$collection->add_plugintype_link('customfield', [], 'privacy:metadata:customfieldpluginsummary');
$collection->link_subsystem('core_files', 'privacy:metadata:filepurpose');
return $collection;
}
/**
* Returns contexts that have customfields data
*
* To be used in implementations of core_user_data_provider::get_contexts_for_userid
* Caller needs to transfer the $userid to the select subqueries for
* customfield_category->itemid and/or customfield_data->instanceid
*
* @param string $component
* @param string $area
* @param string $itemidstest subquery for selecting customfield_category->itemid
* @param string $instanceidstest subquery for selecting customfield_data->instanceid
* @param array $params array of named parameters
* @return contextlist
*/
public static function get_customfields_data_contexts(string $component, string $area,
string $itemidstest = 'IS NOT NULL', string $instanceidstest = 'IS NOT NULL', array $params = []): contextlist {
$sql = "SELECT d.contextid FROM {customfield_category} c
JOIN {customfield_field} f ON f.categoryid = c.id
JOIN {customfield_data} d ON d.fieldid = f.id AND d.instanceid $instanceidstest
WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest";
$contextlist = new contextlist();
$contextlist->add_from_sql($sql, self::get_params($component, $area, $params));
return $contextlist;
}
/**
* Returns contexts that have customfields configuration (categories and fields)
*
* To be used in implementations of core_user_data_provider::get_contexts_for_userid in cases when user is
* an owner of the fields configuration
* Caller needs to transfer the $userid to the select subquery for customfield_category->itemid
*
* @param string $component
* @param string $area
* @param string $itemidstest subquery for selecting customfield_category->itemid
* @param array $params array of named parameters for itemidstest subquery
* @return contextlist
*/
public static function get_customfields_configuration_contexts(string $component, string $area,
string $itemidstest = 'IS NOT NULL', array $params = []): contextlist {
$sql = "SELECT c.contextid FROM {customfield_category} c
WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest";
$params['component'] = $component;
$params['area'] = $area;
$contextlist = new contextlist();
$contextlist->add_from_sql($sql, self::get_params($component, $area, $params));
return $contextlist;
}
/**
* Exports customfields data
*
* To be used in implementations of core_user_data_provider::export_user_data
* Caller needs to transfer the $userid to the select subqueries for
* customfield_category->itemid and/or customfield_data->instanceid
*
* @param approved_contextlist $contextlist
* @param string $component
* @param string $area
* @param string $itemidstest subquery for selecting customfield_category->itemid
* @param string $instanceidstest subquery for selecting customfield_data->instanceid
* @param array $params array of named parameters for itemidstest and instanceidstest subqueries
* @param array $subcontext subcontext to use in context_writer::export_data, if null (default) the
* "Custom fields data" will be used;
* the data id will be appended to the subcontext array.
*/
public static function export_customfields_data(approved_contextlist $contextlist, string $component, string $area,
string $itemidstest = 'IS NOT NULL', string $instanceidstest = 'IS NOT NULL', array $params = [],
array $subcontext = null) {
global $DB;
// This query is very similar to api::get_instances_fields_data() but also works for multiple itemids
// and has a context filter.
list($contextidstest, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED, 'cfctx');
$sql = "SELECT d.*, f.type AS fieldtype, f.name as fieldname, f.shortname as fieldshortname, c.itemid
FROM {customfield_category} c
JOIN {customfield_field} f ON f.categoryid = c.id
JOIN {customfield_data} d ON d.fieldid = f.id AND d.instanceid $instanceidstest AND d.contextid $contextidstest
WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest
ORDER BY c.itemid, c.sortorder, f.sortorder";
$params = self::get_params($component, $area, $params) + $contextparams;
$records = $DB->get_recordset_sql($sql, $params);
if ($subcontext === null) {
$subcontext = [get_string('customfielddata', 'core_customfield')];
}
/** @var handler $handler */
$handler = null;
$fields = null;
foreach ($records as $record) {
if (!$handler || $handler->get_itemid() != $record->itemid) {
$handler = handler::get_handler($component, $area, $record->itemid);
$fields = $handler->get_fields();
}
$field = (object)['type' => $record->fieldtype, 'shortname' => $record->fieldshortname, 'name' => $record->fieldname];
unset($record->itemid, $record->fieldtype, $record->fieldshortname, $record->fieldname);
try {
$field = array_key_exists($record->fieldid, $fields) ? $fields[$record->fieldid] : null;
$data = data_controller::create(0, $record, $field);
self::export_customfield_data($data, array_merge($subcontext, [$record->id]));
} catch (\Exception $e) {
// We store some data that we can not initialise controller for. We still need to export it.
self::export_customfield_data_unknown($record, $field, array_merge($subcontext, [$record->id]));
}
}
$records->close();
}
/**
* Deletes customfields data
*
* To be used in implementations of core_user_data_provider::delete_data_for_user
* Caller needs to transfer the $userid to the select subqueries for
* customfield_category->itemid and/or customfield_data->instanceid
*
* @param approved_contextlist $contextlist
* @param string $component
* @param string $area
* @param string $itemidstest subquery for selecting customfield_category->itemid
* @param string $instanceidstest subquery for selecting customfield_data->instanceid
* @param array $params array of named parameters for itemidstest and instanceidstest subqueries
*/
public static function delete_customfields_data(approved_contextlist $contextlist, string $component, string $area,
string $itemidstest = 'IS NOT NULL', string $instanceidstest = 'IS NOT NULL', array $params = []) {
global $DB;
list($contextidstest, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED, 'cfctx');
$sql = "SELECT d.id
FROM {customfield_category} c
JOIN {customfield_field} f ON f.categoryid = c.id
JOIN {customfield_data} d ON d.fieldid = f.id AND d.instanceid $instanceidstest AND d.contextid $contextidstest
WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest";
$params = self::get_params($component, $area, $params) + $contextparams;
self::before_delete_data('IN (' . $sql . ') ', $params);
$DB->execute("DELETE FROM {customfield_data}
WHERE instanceid $instanceidstest
AND contextid $contextidstest
AND fieldid IN (SELECT f.id
FROM {customfield_category} c
JOIN {customfield_field} f ON f.categoryid = c.id
WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest)", $params);
}
/**
* Deletes customfields configuration (categories and fields) and all relevant data
*
* To be used in implementations of core_user_data_provider::delete_data_for_user in cases when user is
* an owner of the fields configuration and it is considered user information (quite unlikely situtation but we never
* know what customfields API can be used for)
*
* Caller needs to transfer the $userid to the select subquery for customfield_category->itemid
*
* @param approved_contextlist $contextlist
* @param string $component
* @param string $area
* @param string $itemidstest subquery for selecting customfield_category->itemid
* @param array $params array of named parameters for itemidstest subquery
*/
public static function delete_customfields_configuration(approved_contextlist $contextlist, string $component, string $area,
string $itemidstest = 'IS NOT NULL', array $params = []) {
global $DB;
list($contextidstest, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED, 'cfctx');
$params = self::get_params($component, $area, $params) + $contextparams;
$categoriesids = $DB->get_fieldset_sql("SELECT c.id
FROM {customfield_category} c
WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest AND c.contextid $contextidstest",
$params);
self::delete_categories($contextlist->get_contextids(), $categoriesids);
}
/**
* Deletes all customfields configuration (categories and fields) and all relevant data for the given category context
*
* To be used in implementations of core_user_data_provider::delete_data_for_all_users_in_context
*
* @param string $component
* @param string $area
* @param \context $context
*/
public static function delete_customfields_configuration_for_context(string $component, string $area, \context $context) {
global $DB;
$categoriesids = $DB->get_fieldset_sql("SELECT c.id
FROM {customfield_category} c
JOIN {context} ctx ON ctx.id = c.contextid AND ctx.path LIKE :ctxpath
WHERE c.component = :cfcomponent AND c.area = :cfarea",
self::get_params($component, $area, ['ctxpath' => $context->path]));
self::delete_categories([$context->id], $categoriesids);
}
/**
* Deletes all customfields data for the given context
*
* To be used in implementations of core_user_data_provider::delete_data_for_all_users_in_context
*
* @param string $component
* @param string $area
* @param \context $context
*/
public static function delete_customfields_data_for_context(string $component, string $area, \context $context) {
global $DB;
$sql = "SELECT d.id
FROM {customfield_category} c
JOIN {customfield_field} f ON f.categoryid = c.id
JOIN {customfield_data} d ON d.fieldid = f.id
JOIN {context} ctx ON ctx.id = d.contextid AND ctx.path LIKE :ctxpath
WHERE c.component = :cfcomponent AND c.area = :cfarea";
$params = self::get_params($component, $area, ['ctxpath' => $context->path . '%']);
self::before_delete_data('IN (' . $sql . ') ', $params);
$DB->execute("DELETE FROM {customfield_data}
WHERE fieldid IN (SELECT f.id
FROM {customfield_category} c
JOIN {customfield_field} f ON f.categoryid = c.id
WHERE c.component = :cfcomponent AND c.area = :cfarea)
AND contextid IN (SELECT id FROM {context} WHERE path LIKE :ctxpath)",
$params);
}
/**
* Checks that $params is an associative array and adds parameters for component and area
*
* @param string $component
* @param string $area
* @param array $params
* @return array
* @throws \coding_exception
*/
protected static function get_params(string $component, string $area, array $params): array {
if (!empty($params) && (array_keys($params) === range(0, count($params) - 1))) {
// Argument $params is not an associative array.
throw new \coding_exception('Argument $params must be an associative array!');
}
return $params + ['cfcomponent' => $component, 'cfarea' => $area];
}
/**
* Delete custom fields categories configurations, all their fields and data
*
* @param array $contextids
* @param array $categoriesids
*/
protected static function delete_categories(array $contextids, array $categoriesids) {
global $DB;
if (!$categoriesids) {
return;
}
list($categoryidstest, $catparams) = $DB->get_in_or_equal($categoriesids, SQL_PARAMS_NAMED, 'cfcat');
$datasql = "SELECT d.id FROM {customfield_data} d JOIN {customfield_field} f ON f.id = d.fieldid " .
"WHERE f.categoryid $categoryidstest";
$fieldsql = "SELECT f.id AS fieldid FROM {customfield_field} f WHERE f.categoryid $categoryidstest";
self::before_delete_data("IN ($datasql)", $catparams);
self::before_delete_fields($categoryidstest, $catparams);
$DB->execute('DELETE FROM {customfield_data} WHERE fieldid IN (' . $fieldsql . ')', $catparams);
$DB->execute("DELETE FROM {customfield_field} WHERE categoryid $categoryidstest", $catparams);
$DB->execute("DELETE FROM {customfield_category} WHERE id $categoryidstest", $catparams);
}
/**
* Executes callbacks from the customfield plugins to delete anything related to the data records (usually files)
*
* @param string $dataidstest
* @param array $params
*/
protected static function before_delete_data(string $dataidstest, array $params) {
global $DB;
// Find all field types and all contexts for each field type.
$records = $DB->get_recordset_sql("SELECT ff.type, dd.contextid
FROM {customfield_data} dd
JOIN {customfield_field} ff ON ff.id = dd.fieldid
WHERE dd.id $dataidstest
GROUP BY ff.type, dd.contextid",
$params);
$fieldtypes = [];
foreach ($records as $record) {
$fieldtypes += [$record->type => []];
$fieldtypes[$record->type][] = $record->contextid;
}
$records->close();
// Call plugin callbacks to delete data customfield_provider::before_delete_data().
foreach ($fieldtypes as $fieldtype => $contextids) {
$classname = manager::get_provider_classname_for_component('customfield_' . $fieldtype);
if (class_exists($classname) && is_subclass_of($classname, customfield_provider::class)) {
component_class_callback($classname, 'before_delete_data', [$dataidstest, $params, $contextids]);
}
}
}
/**
* Executes callbacks from the plugins to delete anything related to the fields (usually files)
*
* Also deletes description files
*
* @param string $categoryidstest
* @param array $params
*/
protected static function before_delete_fields(string $categoryidstest, array $params) {
global $DB;
// Find all field types and contexts.
$fieldsql = "SELECT f.id AS fieldid FROM {customfield_field} f WHERE f.categoryid $categoryidstest";
$records = $DB->get_recordset_sql("SELECT f.type, c.contextid
FROM {customfield_field} f
JOIN {customfield_category} c ON c.id = f.categoryid
WHERE c.id $categoryidstest",
$params);
$contexts = [];
$fieldtypes = [];
foreach ($records as $record) {
$contexts[$record->contextid] = $record->contextid;
$fieldtypes += [$record->type => []];
$fieldtypes[$record->type][] = $record->contextid;
}
$records->close();
// Delete description files.
foreach ($contexts as $contextid) {
get_file_storage()->delete_area_files_select($contextid, 'core_customfield', 'description',
" IN ($fieldsql) ", $params);
}
// Call plugin callbacks to delete fields customfield_provider::before_delete_fields().
foreach ($fieldtypes as $type => $contextids) {
$classname = manager::get_provider_classname_for_component('customfield_' . $type);
if (class_exists($classname) && is_subclass_of($classname, customfield_provider::class)) {
component_class_callback($classname, 'before_delete_fields',
[" IN ($fieldsql) ", $params, $contextids]);
}
}
$records->close();
}
/**
* Exports one instance of custom field data
*
* @param data_controller $data
* @param array $subcontext subcontext to pass to content_writer::export_data
*/
public static function export_customfield_data(data_controller $data, array $subcontext) {
$context = $data->get_context();
$exportdata = $data->to_record();
$exportdata->fieldtype = $data->get_field()->get('type');
$exportdata->fieldshortname = $data->get_field()->get('shortname');
$exportdata->fieldname = $data->get_field()->get_formatted_name();
$exportdata->timecreated = \core_privacy\local\request\transform::datetime($exportdata->timecreated);
$exportdata->timemodified = \core_privacy\local\request\transform::datetime($exportdata->timemodified);
unset($exportdata->contextid);
// Use the "export_value" by default for the 'value' attribute, however the plugins may override it in their callback.
$exportdata->value = $data->export_value();
$classname = manager::get_provider_classname_for_component('customfield_' . $data->get_field()->get('type'));
if (class_exists($classname) && is_subclass_of($classname, customfield_provider::class)) {
component_class_callback($classname, 'export_customfield_data', [$data, $exportdata, $subcontext]);
} else {
// Custom field plugin does not implement customfield_provider, just export default value.
writer::with_context($context)->export_data($subcontext, $exportdata);
}
}
/**
* Export data record of unknown type when we were not able to create instance of data_controller
*
* @param \stdClass $record record from db table {customfield_data}
* @param \stdClass $field field record with at least fields type, shortname, name
* @param array $subcontext
*/
protected static function export_customfield_data_unknown(\stdClass $record, \stdClass $field, array $subcontext) {
$context = \context::instance_by_id($record->contextid);
$record->fieldtype = $field->type;
$record->fieldshortname = $field->shortname;
$record->fieldname = format_string($field->name);
$record->timecreated = \core_privacy\local\request\transform::datetime($record->timecreated);
$record->timemodified = \core_privacy\local\request\transform::datetime($record->timemodified);
unset($record->contextid);
$record->value = format_text($record->value, $record->valueformat, [
'context' => $context,
'trusted' => $record->valuetrust,
]);
writer::with_context($context)->export_data($subcontext, $record);
}
}
+290
View File
@@ -0,0 +1,290 @@
<?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/>.
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
/**
* External interface library for customfields component
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_customfield_external extends external_api {
/**
* Parameters for delete_field
*
* @return external_function_parameters
*/
public static function delete_field_parameters() {
return new external_function_parameters(
array('id' => new external_value(PARAM_INT, 'Custom field ID to delete', VALUE_REQUIRED))
);
}
/**
* Delete custom field function
*
* @param int $id
*/
public static function delete_field($id) {
$params = self::validate_parameters(self::delete_field_parameters(), ['id' => $id]);
$record = \core_customfield\field_controller::create($params['id']);
$handler = $record->get_handler();
if (!$handler->can_configure()) {
throw new moodle_exception('nopermissionconfigure', 'core_customfield');
}
$handler->delete_field_configuration($record);
}
/**
* Return for delete_field
*/
public static function delete_field_returns() {
}
/**
* Parameters for reload template function
*
* @return external_function_parameters
*/
public static function reload_template_parameters() {
return new external_function_parameters(
array(
'component' => new external_value(PARAM_COMPONENT, 'component', VALUE_REQUIRED),
'area' => new external_value(PARAM_ALPHANUMEXT, 'area', VALUE_REQUIRED),
'itemid' => new external_value(PARAM_INT, 'itemid', VALUE_REQUIRED)
)
);
}
/**
* Reload template function
*
* @param string $component
* @param string $area
* @param int $itemid
* @return array|object|stdClass
*/
public static function reload_template($component, $area, $itemid) {
global $PAGE;
$params = self::validate_parameters(self::reload_template_parameters(),
['component' => $component, 'area' => $area, 'itemid' => $itemid]);
$PAGE->set_context(context_system::instance());
$handler = \core_customfield\handler::get_handler($params['component'], $params['area'], $params['itemid']);
self::validate_context($handler->get_configuration_context());
if (!$handler->can_configure()) {
throw new moodle_exception('nopermissionconfigure', 'core_customfield');
}
$output = $PAGE->get_renderer('core_customfield');
$outputpage = new \core_customfield\output\management($handler);
return $outputpage->export_for_template($output);
}
/**
* Ajax returns on reload template.
*
* @return external_single_structure
*/
public static function reload_template_returns() {
return new external_single_structure(
array(
'component' => new external_value(PARAM_COMPONENT, 'component'),
'area' => new external_value(PARAM_ALPHANUMEXT, 'area'),
'itemid' => new external_value(PARAM_INT, 'itemid'),
'usescategories' => new external_value(PARAM_BOOL, 'view has categories'),
'categories' => new external_multiple_structure(
new external_single_structure(
array(
'id' => new external_value(PARAM_INT, 'id'),
'nameeditable' => new external_value(PARAM_RAW, 'inplace editable name'),
'addfieldmenu' => new external_value(PARAM_RAW, 'addfieldmenu'),
'fields' => new external_multiple_structure(
new external_single_structure(
array(
'name' => new external_value(PARAM_NOTAGS, 'name'),
'shortname' => new external_value(PARAM_NOTAGS, 'shortname'),
'type' => new external_value(PARAM_NOTAGS, 'type'),
'id' => new external_value(PARAM_INT, 'id'),
)
)
, '', VALUE_OPTIONAL),
)
)
),
)
);
}
/**
* Parameters for delete category
*
* @return external_function_parameters
*/
public static function delete_category_parameters() {
return new external_function_parameters(
array('id' => new external_value(PARAM_INT, 'category ID to delete', VALUE_REQUIRED))
);
}
/**
* Delete category function
*
* @param int $id
*/
public static function delete_category($id) {
$category = core_customfield\category_controller::create($id);
$handler = $category->get_handler();
self::validate_context($handler->get_configuration_context());
if (!$handler->can_configure()) {
throw new moodle_exception('nopermissionconfigure', 'core_customfield');
}
$handler->delete_category($category);
}
/**
* Return for delete category
*/
public static function delete_category_returns() {
}
/**
* Parameters for create category
*
* @return external_function_parameters
*/
public static function create_category_parameters() {
return new external_function_parameters(
array(
'component' => new external_value(PARAM_COMPONENT, 'component', VALUE_REQUIRED),
'area' => new external_value(PARAM_ALPHANUMEXT, 'area', VALUE_REQUIRED),
'itemid' => new external_value(PARAM_INT, 'itemid', VALUE_REQUIRED)
)
);
}
/**
* Create category function
*
* @param string $component
* @param string $area
* @param int $itemid
* @return mixed
*/
public static function create_category($component, $area, $itemid) {
$params = self::validate_parameters(self::create_category_parameters(),
['component' => $component, 'area' => $area, 'itemid' => $itemid]);
$handler = \core_customfield\handler::get_handler($params['component'], $params['area'], $params['itemid']);
self::validate_context($handler->get_configuration_context());
if (!$handler->can_configure()) {
throw new moodle_exception('nopermissionconfigure', 'core_customfield');
}
return $handler->create_category();
}
/**
* Return for create category
*/
public static function create_category_returns() {
return new external_value(PARAM_INT, 'Id of the category');
}
/**
* Parameters for move field.
*
* @return external_function_parameters
*/
public static function move_field_parameters() {
return new external_function_parameters(
['id' => new external_value(PARAM_INT, 'Id of the field to move', VALUE_REQUIRED),
'categoryid' => new external_value(PARAM_INT, 'New parent category id', VALUE_REQUIRED),
'beforeid' => new external_value(PARAM_INT, 'Id of the field before which it needs to be moved',
VALUE_DEFAULT, 0)]
);
}
/**
* Move/reorder field. Move a field to another category and/or change sortorder of fields
*
* @param int $id field id
* @param int $categoryid
* @param int $beforeid
*/
public static function move_field($id, $categoryid, $beforeid) {
$params = self::validate_parameters(self::move_field_parameters(),
['id' => $id, 'categoryid' => $categoryid, 'beforeid' => $beforeid]);
$field = \core_customfield\field_controller::create($params['id']);
$handler = $field->get_handler();
self::validate_context($handler->get_configuration_context());
if (!$handler->can_configure()) {
throw new moodle_exception('nopermissionconfigure', 'core_customfield');
}
$handler->move_field($field, $params['categoryid'], $params['beforeid']);
}
/**
* Return for move field
*/
public static function move_field_returns() {
}
/**
* Return for move category
*
* @return external_function_parameters
*/
public static function move_category_parameters() {
return new external_function_parameters(
['id' => new external_value(PARAM_INT, 'Category ID to move', VALUE_REQUIRED),
'beforeid' => new external_value(PARAM_INT, 'Id of the category before which it needs to be moved',
VALUE_DEFAULT, 0)]
);
}
/**
* Reorder categories. Move category to the new position
*
* @param int $id category id
* @param int $beforeid
*/
public static function move_category(int $id, int $beforeid) {
$params = self::validate_parameters(self::move_category_parameters(),
['id' => $id, 'beforeid' => $beforeid]);
$category = core_customfield\category_controller::create($id);
$handler = $category->get_handler();
self::validate_context($handler->get_configuration_context());
if (!$handler->can_configure()) {
throw new moodle_exception('nopermissionconfigure', 'core_customfield');
}
$handler->move_category($category, $params['beforeid']);
}
/**
* Return for move category
*/
public static function move_category_returns() {
}
}
@@ -0,0 +1,87 @@
<?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/>.
/**
* Customfield Checkbox plugin
*
* @package customfield_checkbox
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace customfield_checkbox;
use core_customfield\api;
use core_customfield\output\field_data;
defined('MOODLE_INTERNAL') || die;
/**
* Class data
*
* @package customfield_checkbox
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class data_controller extends \core_customfield\data_controller {
/**
* Return the name of the field where the information is stored
* @return string
*/
public function datafield(): string {
return 'intvalue';
}
/**
* Add fields for editing a checkbox field.
*
* @param \MoodleQuickForm $mform
*/
public function instance_form_definition(\MoodleQuickForm $mform) {
$field = $this->get_field();
$config = $field->get('configdata');
$elementname = $this->get_form_element_name();
// If checkbox is required (i.e. "agree to terms") then use 'checkbox' form element.
// The advcheckbox element cannot be used for required fields because advcheckbox elements always provide a value.
$isrequired = $field->get_configdata_property('required');
$mform->addElement($isrequired ? 'checkbox' : 'advcheckbox', $elementname, $this->get_field()->get_formatted_name());
$mform->setDefault($elementname, $config['checkbydefault']);
$mform->setType($elementname, PARAM_BOOL);
if ($isrequired) {
$mform->addRule($elementname, null, 'required', null, 'client');
}
}
/**
* Returns the default value as it would be stored in the database (not in human-readable format).
*
* @return mixed
*/
public function get_default_value() {
return $this->get_field()->get_configdata_property('checkbydefault') ? 1 : 0;
}
/**
* Returns value in a human-readable format
*
* @return mixed|null value or null if empty
*/
public function export_value() {
$value = $this->get_value();
return $value ? get_string('yes') : get_string('no');
}
}
@@ -0,0 +1,94 @@
<?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/>.
/**
* Customfields checkbox plugin
*
* @package customfield_checkbox
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace customfield_checkbox;
defined('MOODLE_INTERNAL') || die;
/**
* Class field
*
* @package customfield_checkbox
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class field_controller extends \core_customfield\field_controller {
/**
* Plugin type
*/
const TYPE = 'checkbox';
/**
* Add fields for editing a checkbox field.
*
* @param \MoodleQuickForm $mform
*/
public function config_form_definition(\MoodleQuickForm $mform) {
$mform->addElement('header', 'header_specificsettings', get_string('specificsettings', 'customfield_checkbox'));
$mform->setExpanded('header_specificsettings', true);
$mform->addElement('selectyesno', 'configdata[checkbydefault]', get_string('checkedbydefault', 'customfield_checkbox'));
$mform->setType('configdata[checkbydefault]', PARAM_BOOL);
}
/**
* Validate the data on the field configuration form
*
* @param array $data from the add/edit profile field form
* @param array $files
* @return array associative array of error messages
*/
public function config_form_validation(array $data, $files = array()): array {
$errors = parent::config_form_validation($data, $files);
if ($data['configdata']['uniquevalues']) {
$errors['configdata[uniquevalues]'] = get_string('errorconfigunique', 'customfield_checkbox');
}
return $errors;
}
/**
* Does this custom field type support being used as part of the block_myoverview
* custom field grouping?
* @return bool
*/
public function supports_course_grouping(): bool {
return true;
}
/**
* If this field supports course grouping, then this function needs overriding to
* return the formatted values for this.
* @param array $values the used values that need formatting
* @return array
*/
public function course_grouping_format_values($values): array {
$name = $this->get_formatted_name();
return [
1 => $name.': '.get_string('yes'),
BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY => $name.': '.get_string('no'),
];
}
}
@@ -0,0 +1,81 @@
<?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/>.
/**
* Privacy Subsystem implementation for customfield_checkbox.
*
* @package customfield_checkbox
* @copyright 2018 Daniel Neis Araujo <danielneis@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace customfield_checkbox\privacy;
use core_customfield\data_controller;
use core_customfield\privacy\customfield_provider;
use core_privacy\local\request\writer;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for customfield_checkbox implementing null_provider.
*
* @copyright 2018 Daniel Neis Araujo <danielneis@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider, customfield_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
/**
* Preprocesses data object that is going to be exported
*
* @param data_controller $data
* @param \stdClass $exportdata
* @param array $subcontext
*/
public static function export_customfield_data(data_controller $data, \stdClass $exportdata, array $subcontext) {
writer::with_context($data->get_context())->export_data($subcontext, $exportdata);
}
/**
* Allows plugins to delete everything they store related to the data (usually files)
*
* @param string $dataidstest
* @param array $params
* @param array $contextids
* @return mixed|void
*/
public static function before_delete_data(string $dataidstest, array $params, array $contextids) {
}
/**
* Allows plugins to delete everything they store related to the field configuration (usually files)
*
* @param string $fieldidstest
* @param array $params
* @param array $contextids
*/
public static function before_delete_fields(string $fieldidstest, array $params, array $contextids) {
}
}
@@ -0,0 +1,30 @@
<?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/>.
/**
* Customfield checkbox plugin
* @package customfield_checkbox
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['checkedbydefault'] = 'Checked by default';
$string['errorconfigunique'] = 'The checkbox field cannot be defined as unique.';
$string['pluginname'] = 'Checkbox';
$string['privacy:metadata'] = 'The Checkbox field type plugin doesn\'t store any personal data; it uses tables defined in core.';
$string['specificsettings'] = 'Checkbox field settings';
@@ -0,0 +1,78 @@
@customfield @customfield_checkbox @javascript
Feature: Managers can manage course custom fields checkbox
In order to have additional data on the course
As a manager
I need to create, edit, remove and sort custom fields
Background:
Given the following "custom field categories" exist:
| name | component | area | itemid |
| Category for test | core_course | course | 0 |
And I log in as "admin"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
Scenario: Create a custom course checkbox field
When I click on "Add a new custom field" "link"
And I click on "Checkbox" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I click on "Save changes" "button" in the "Adding a new Checkbox" "dialogue"
Then I should see "Test field"
And I log out
Scenario: Edit a custom course checkbox field
When I click on "Add a new custom field" "link"
And I click on "Checkbox" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I click on "Save changes" "button" in the "Adding a new Checkbox" "dialogue"
And I click on "Edit" "link" in the "Test field" "table_row"
And I set the following fields to these values:
| Name | Edited field |
And I click on "Save changes" "button" in the "Updating Test field" "dialogue"
Then I should see "Edited field"
And I should not see "Test field"
Scenario: Delete a custom course checkbox field
When I click on "Add a new custom field" "link"
And I click on "Checkbox" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I click on "Save changes" "button" in the "Adding a new Checkbox" "dialogue"
And I click on "Delete" "link" in the "Test field" "table_row"
And I click on "Yes" "button" in the "Confirm" "dialogue"
Then I should not see "Test field"
And I log out
Scenario: A checkbox checked by default must be shown on listing but allow uncheck that will keep showing
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | Example 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
When I click on "Add a new custom field" "link"
And I click on "Checkbox" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
| Checked by default | Yes |
And I click on "Save changes" "button" in the "Adding a new Checkbox" "dialogue"
And I log out
And I log in as "teacher1"
And I am on site homepage
Then I should see "Test field: Yes"
When I am on "Course 1" course homepage
And I navigate to "Settings" in current page administration
And I expand all fieldsets
And I set the field "Test field" to ""
And I press "Save and display"
And I am on site homepage
Then I should see "Test field: No"
And I log out
@@ -0,0 +1,172 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace customfield_checkbox;
use core_customfield_generator;
use core_customfield_test_instance_form;
/**
* Functional test for customfield_checkbox
*
* @package customfield_checkbox
* @copyright 2019 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class plugin_test extends \advanced_testcase {
/** @var \stdClass[] */
private $courses = [];
/** @var \core_customfield\category_controller */
private $cfcat;
/** @var \core_customfield\field_controller[] */
private $cfields;
/** @var \core_customfield\data_controller[] */
private $cfdata;
/**
* Tests set up.
*/
public function setUp(): void {
$this->resetAfterTest();
$this->cfcat = $this->get_generator()->create_category();
$this->cfields[1] = $this->get_generator()->create_field(
['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield1', 'type' => 'checkbox']);
$this->cfields[2] = $this->get_generator()->create_field(
['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield2', 'type' => 'checkbox',
'configdata' => ['required' => 1]]);
$this->cfields[3] = $this->get_generator()->create_field(
['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield3', 'type' => 'checkbox',
'configdata' => ['checkbydefault' => 1]]);
$this->courses[1] = $this->getDataGenerator()->create_course();
$this->courses[2] = $this->getDataGenerator()->create_course();
$this->courses[3] = $this->getDataGenerator()->create_course();
$this->cfdata[1] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[1]->id, 1);
$this->cfdata[2] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[2]->id, 1);
$this->setUser($this->getDataGenerator()->create_user());
}
/**
* Get generator
* @return core_customfield_generator
*/
protected function get_generator(): core_customfield_generator {
return $this->getDataGenerator()->get_plugin_generator('core_customfield');
}
/**
* Test for initialising field and data controllers
*/
public function test_initialise(): void {
$f = \core_customfield\field_controller::create($this->cfields[1]->get('id'));
$this->assertTrue($f instanceof field_controller);
$f = \core_customfield\field_controller::create(0, (object)['type' => 'checkbox'], $this->cfcat);
$this->assertTrue($f instanceof field_controller);
$d = \core_customfield\data_controller::create($this->cfdata[1]->get('id'));
$this->assertTrue($d instanceof data_controller);
$d = \core_customfield\data_controller::create(0, null, $this->cfields[1]);
$this->assertTrue($d instanceof data_controller);
}
/**
* Test for configuration form functions
*
* Create a configuration form and submit it with the same values as in the field
*/
public function test_config_form(): void {
$this->setAdminUser();
$submitdata = (array)$this->cfields[1]->to_record();
$submitdata['configdata'] = $this->cfields[1]->get('configdata');
$submitdata = \core_customfield\field_config_form::mock_ajax_submit($submitdata);
$form = new \core_customfield\field_config_form(null, null, 'post', '', null, true,
$submitdata, true);
$form->set_data_for_dynamic_submission();
$this->assertTrue($form->is_validated());
$form->process_dynamic_submission();
// Try submitting with 'unique values' checked.
$submitdata['configdata']['uniquevalues'] = 1;
$submitdata = \core_customfield\field_config_form::mock_ajax_submit($submitdata);
$form = new \core_customfield\field_config_form(null, null, 'post', '', null, true,
$submitdata, true);
$form->set_data_for_dynamic_submission();
$this->assertFalse($form->is_validated());
}
/**
* Test for instance form functions
*/
public function test_instance_form(): void {
global $CFG;
require_once($CFG->dirroot . '/customfield/tests/fixtures/test_instance_form.php');
$this->setAdminUser();
$handler = $this->cfcat->get_handler();
// First try to submit without required field.
$submitdata = (array)$this->courses[1];
core_customfield_test_instance_form::mock_submit($submitdata, []);
$form = new core_customfield_test_instance_form('POST',
['handler' => $handler, 'instance' => $this->courses[1]]);
$this->assertFalse($form->is_validated());
// Now with required field.
$submitdata['customfield_myfield2'] = 1;
core_customfield_test_instance_form::mock_submit($submitdata, []);
$form = new core_customfield_test_instance_form('POST',
['handler' => $handler, 'instance' => $this->courses[1]]);
$this->assertTrue($form->is_validated());
$data = $form->get_data();
$this->assertNotEmpty($data->customfield_myfield1);
$this->assertNotEmpty($data->customfield_myfield2);
$handler->instance_form_save($data);
}
/**
* Test for data_controller::get_value and export_value
*/
public function test_get_export_value(): void {
$this->assertEquals(1, $this->cfdata[1]->get_value());
$this->assertEquals('Yes', $this->cfdata[1]->export_value());
// Field without data.
$d = \core_customfield\data_controller::create(0, null, $this->cfields[2]);
$this->assertEquals(0, $d->get_value());
$this->assertEquals('No', $d->export_value());
// Field without data that is checked by default.
$d = \core_customfield\data_controller::create(0, null, $this->cfields[3]);
$this->assertEquals(1, $d->get_value());
$this->assertEquals('Yes', $d->export_value());
}
/**
* Deleting fields and data
*/
public function test_delete(): void {
$this->cfcat->get_handler()->delete_all();
}
}
+28
View File
@@ -0,0 +1,28 @@
<?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/>.
/**
* Customfield checkbox plugin
* @package customfield_checkbox
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'customfield_checkbox';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
@@ -0,0 +1,147 @@
<?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/>.
/**
* Customfield date plugin
*
* @package customfield_date
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace customfield_date;
use core_customfield\api;
defined('MOODLE_INTERNAL') || die;
/**
* Class data
*
* @package customfield_date
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class data_controller extends \core_customfield\data_controller {
/**
* Return the name of the field where the information is stored
* @return string
*/
public function datafield(): string {
return 'intvalue';
}
/**
* Add fields for editing data of a date field on a context.
*
* @param \MoodleQuickForm $mform
*/
public function instance_form_definition(\MoodleQuickForm $mform) {
$field = $this->get_field();
// Get the current calendar in use - see MDL-18375.
$calendartype = \core_calendar\type_factory::get_calendar_instance();
$config = $field->get('configdata');
// Always set the form element to "optional", even when it's required. Otherwise it defaults to the
// current date and is easy to miss.
$attributes = ['optional' => true];
if (!empty($config['mindate'])) {
$attributes['startyear'] = $calendartype->timestamp_to_date_array($config['mindate'])['year'];
}
if (!empty($config['maxdate'])) {
$attributes['stopyear'] = $calendartype->timestamp_to_date_array($config['maxdate'])['year'];
}
if (empty($config['includetime'])) {
$element = 'date_selector';
} else {
$element = 'date_time_selector';
}
$elementname = $this->get_form_element_name();
$mform->addElement($element, $elementname, $this->get_field()->get_formatted_name(), $attributes);
$mform->setType($elementname, PARAM_INT);
$mform->setDefault($elementname, time());
if ($field->get_configdata_property('required')) {
$mform->addRule($elementname, null, 'required', null, 'client');
}
}
/**
* Validates data for this field.
*
* @param array $data
* @param array $files
* @return array
*/
public function instance_form_validation(array $data, array $files): array {
$errors = parent::instance_form_validation($data, $files);
$elementname = $this->get_form_element_name();
if (!empty($data[$elementname])) {
// Compare the date with min/max values, trim the date to the minute or to the day (depending on inludetime setting).
$includetime = $this->get_field()->get_configdata_property('includetime');
$machineformat = $includetime ? '%Y-%m-%d %H:%M' : '%Y-%m-%d';
$humanformat = $includetime ? get_string('strftimedatetimeshort') : get_string('strftimedatefullshort');
$value = userdate($data[$elementname], $machineformat, 99, false, false);
$mindate = $this->get_field()->get_configdata_property('mindate');
$maxdate = $this->get_field()->get_configdata_property('maxdate');
if ($mindate && userdate($mindate, $machineformat, 99, false, false) > $value) {
$errors[$elementname] = get_string('errormindate', 'customfield_date', userdate($mindate, $humanformat));
}
if ($maxdate && userdate($maxdate, $machineformat, 99, false, false) < $value) {
$errors[$elementname] = get_string('errormaxdate', 'customfield_date', userdate($maxdate, $humanformat));
}
}
return $errors;
}
/**
* Returns the default value as it would be stored in the database (not in human-readable format).
*
* @return mixed
*/
public function get_default_value() {
return 0;
}
/**
* Returns value in a human-readable format
*
* @return mixed|null value or null if empty
*/
public function export_value() {
$value = $this->get_value();
if ($this->is_empty($value)) {
return null;
}
// Check if time needs to be included.
if ($this->get_field()->get_configdata_property('includetime')) {
$format = get_string('strftimedaydatetime', 'langconfig');
} else {
$format = get_string('strftimedate', 'langconfig');
}
return userdate($value, $format);
}
}
@@ -0,0 +1,130 @@
<?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/>.
/**
* Customfield date plugin
*
* @package customfield_date
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace customfield_date;
defined('MOODLE_INTERNAL') || die;
/**
* Class field
*
* @package customfield_date
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class field_controller extends \core_customfield\field_controller {
/**
* Type of plugin data
*/
const TYPE = 'date';
/**
* Validate the data from the config form.
*
* @param array $data
* @param array $files
* @return array associative array of error messages
*/
public function config_form_validation(array $data, $files = array()): array {
$errors = array();
// Make sure the start year is not greater than the end year.
if (!empty($data['configdata']['mindate']) && !empty($data['configdata']['maxdate']) &&
$data['configdata']['mindate'] > $data['configdata']['maxdate']) {
$errors['configdata[mindate]'] = get_string('mindateaftermax', 'customfield_date');
}
return $errors;
}
/**
* Add fields for editing a date field.
*
* @param \MoodleQuickForm $mform
*/
public function config_form_definition(\MoodleQuickForm $mform) {
$config = $this->get('configdata');
// Add elements.
$mform->addElement('header', 'header_specificsettings', get_string('specificsettings', 'customfield_date'));
$mform->setExpanded('header_specificsettings', true);
$mform->addElement('advcheckbox', 'configdata[includetime]', get_string('includetime', 'customfield_date'));
$mform->addElement('date_time_selector', 'configdata[mindate]', get_string('mindate', 'customfield_date'),
['optional' => true]);
$mform->addElement('date_time_selector', 'configdata[maxdate]', get_string('maxdate', 'customfield_date'),
['optional' => true]);
$mform->hideIf('configdata[maxdate][hour]', 'configdata[includetime]');
$mform->hideIf('configdata[maxdate][minute]', 'configdata[includetime]');
$mform->hideIf('configdata[mindate][hour]', 'configdata[includetime]');
$mform->hideIf('configdata[mindate][minute]', 'configdata[includetime]');
}
/**
* Does this custom field type support being used as part of the block_myoverview
* custom field grouping?
* @return bool
*/
public function supports_course_grouping(): bool {
return true;
}
/**
* If this field supports course grouping, then this function needs overriding to
* return the formatted values for this.
* @param array $values the used values that need formatting
* @return array
*/
public function course_grouping_format_values($values): array {
$format = get_string('strftimedate', 'langconfig');
$ret = [];
foreach ($values as $value) {
if ($value) {
$ret[$value] = userdate($value, $format);
}
}
if (!$ret) {
return []; // If the only dates found are 0, then do not show any options.
}
$ret[BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY] = get_string('nocustomvalue', 'block_myoverview',
$this->get_formatted_name());
return $ret;
}
/**
* Convert given value into appropriate timestamp
*
* @param string $value
* @return int
*/
public function parse_value(string $value) {
$timestamp = strtotime($value);
// If we have a valid, positive timestamp then return it.
return $timestamp > 0 ? $timestamp : 0;
}
}
@@ -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/>.
/**
* Privacy Subsystem implementation for customfield_date.
*
* @package customfield_date
* @copyright 2018 Daniel Neis Araujo <danielneis@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace customfield_date\privacy;
use core_customfield\data_controller;
use core_customfield\privacy\customfield_provider;
use core_privacy\local\request\writer;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for customfield_date implementing null_provider.
*
* @copyright 2018 Daniel Neis Araujo <danielneis@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider, customfield_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
/**
* Preprocesses data object that is going to be exported
*
* @param data_controller $data
* @param \stdClass $exportdata
* @param array $subcontext
*/
public static function export_customfield_data(data_controller $data, \stdClass $exportdata, array $subcontext) {
$context = $data->get_context();
// For date field we want to use PrivacyAPI date format instead of export_value().
$exportdata->value = \core_privacy\local\request\transform::datetime($data->get_value());
writer::with_context($context)
->export_data($subcontext, $exportdata);
}
/**
* Allows plugins to delete everything they store related to the data (usually files)
*
* @param string $dataidstest
* @param array $params
* @param array $contextids
* @return mixed|void
*/
public static function before_delete_data(string $dataidstest, array $params, array $contextids) {
}
/**
* Allows plugins to delete everything they store related to the field configuration (usually files)
*
* @param string $fieldidstest
* @param array $params
* @param array $contextids
*/
public static function before_delete_fields(string $fieldidstest, array $params, array $contextids) {
}
}
@@ -0,0 +1,35 @@
<?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/>.
/**
* Customfields date plugin
*
* @package customfield_date
* @copyright 2018 David Matamoros
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['errormaxdate'] = 'Please enter a date no later than {$a}.';
$string['errormindate'] = 'Please enter a date on or after {$a}.';
$string['includetime'] = 'Include time';
$string['maxdate'] = 'Maximum value';
$string['mindate'] = 'Minimum value';
$string['mindateaftermax'] = 'The minimum value cannot be bigger than the maximum value.';
$string['pluginname'] = 'Date and time';
$string['privacy:metadata'] = 'The Date and time field type plugin doesn\'t store any personal data; it uses tables defined in core.';
$string['specificsettings'] = 'Date and time field settings';
+35
View File
@@ -0,0 +1,35 @@
<?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/>.
/**
* Customfield date plugin
*
* @package customfield_date
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Get icon mapping for font-awesome.
*/
function customfield_date_get_fontawesome_icon_map() {
return [
'customfield_date:checked' => 'fa-check-square-o',
'customfield_date:notchecked' => 'fa-square-o',
];
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" preserveAspectRatio="xMinYMid meet">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(60%,60%,60%);fill-opacity:1;" d="M 13.144531 8.304688 L 13.144531 11.144531 C 13.144531 11.851562 12.890625 12.457031 12.386719 12.960938 C 11.886719 13.460938 11.28125 13.714844 10.570312 13.714844 L 3.144531 13.714844 C 2.433594 13.714844 1.828125 13.460938 1.324219 12.960938 C 0.824219 12.457031 0.570312 11.851562 0.570312 11.144531 L 0.570312 3.714844 C 0.570312 3.007812 0.824219 2.398438 1.324219 1.898438 C 1.828125 1.394531 2.433594 1.144531 3.144531 1.144531 L 10.570312 1.144531 C 10.945312 1.144531 11.292969 1.21875 11.617188 1.367188 C 11.707031 1.40625 11.757812 1.476562 11.777344 1.570312 C 11.792969 1.671875 11.769531 1.757812 11.695312 1.832031 L 11.257812 2.269531 C 11.199219 2.328125 11.132812 2.355469 11.054688 2.355469 C 11.035156 2.355469 11.007812 2.351562 10.972656 2.339844 C 10.835938 2.304688 10.703125 2.285156 10.570312 2.285156 L 3.144531 2.285156 C 2.75 2.285156 2.414062 2.425781 2.132812 2.707031 C 1.855469 2.984375 1.714844 3.320312 1.714844 3.714844 L 1.714844 11.144531 C 1.714844 11.535156 1.855469 11.871094 2.132812 12.152344 C 2.414062 12.429688 2.75 12.570312 3.144531 12.570312 L 10.570312 12.570312 C 10.964844 12.570312 11.300781 12.429688 11.582031 12.152344 C 11.859375 11.871094 12 11.535156 12 11.144531 L 12 8.875 C 12 8.796875 12.027344 8.730469 12.082031 8.679688 L 12.652344 8.105469 C 12.710938 8.046875 12.78125 8.019531 12.855469 8.019531 C 12.894531 8.019531 12.929688 8.027344 12.964844 8.042969 C 13.082031 8.09375 13.144531 8.179688 13.144531 8.304688 Z M 15.207031 3.9375 L 7.9375 11.207031 C 7.792969 11.347656 7.625 11.417969 7.429688 11.417969 C 7.230469 11.417969 7.0625 11.347656 6.917969 11.207031 L 3.082031 7.367188 C 2.9375 7.222656 2.867188 7.054688 2.867188 6.855469 C 2.867188 6.660156 2.9375 6.492188 3.082031 6.347656 L 4.0625 5.367188 C 4.207031 5.222656 4.375 5.152344 4.570312 5.152344 C 4.769531 5.152344 4.9375 5.222656 5.082031 5.367188 L 7.429688 7.714844 L 13.207031 1.9375 C 13.347656 1.792969 13.519531 1.722656 13.714844 1.722656 C 13.910156 1.722656 14.082031 1.792969 14.222656 1.9375 L 15.207031 2.917969 C 15.347656 3.0625 15.417969 3.230469 15.417969 3.429688 C 15.417969 3.625 15.347656 3.792969 15.207031 3.9375 Z M 15.207031 3.9375 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" preserveAspectRatio="xMinYMid meet">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(60%,60%,60%);fill-opacity:1;" d="M 11.714844 2.285156 L 4.285156 2.285156 C 3.894531 2.285156 3.554688 2.425781 3.277344 2.707031 C 2.996094 2.984375 2.855469 3.320312 2.855469 3.714844 L 2.855469 11.144531 C 2.855469 11.535156 2.996094 11.871094 3.277344 12.152344 C 3.554688 12.429688 3.894531 12.570312 4.285156 12.570312 L 11.714844 12.570312 C 12.105469 12.570312 12.445312 12.429688 12.722656 12.152344 C 13.003906 11.871094 13.144531 11.535156 13.144531 11.144531 L 13.144531 3.714844 C 13.144531 3.320312 13.003906 2.984375 12.722656 2.707031 C 12.445312 2.425781 12.105469 2.285156 11.714844 2.285156 Z M 14.285156 3.714844 L 14.285156 11.144531 C 14.285156 11.851562 14.035156 12.457031 13.53125 12.960938 C 13.027344 13.460938 12.421875 13.714844 11.714844 13.714844 L 4.285156 13.714844 C 3.578125 13.714844 2.972656 13.460938 2.46875 12.960938 C 1.964844 12.457031 1.714844 11.851562 1.714844 11.144531 L 1.714844 3.714844 C 1.714844 3.007812 1.964844 2.398438 2.46875 1.898438 C 2.972656 1.394531 3.578125 1.144531 4.285156 1.144531 L 11.714844 1.144531 C 12.421875 1.144531 13.027344 1.394531 13.53125 1.898438 C 14.035156 2.398438 14.285156 3.007812 14.285156 3.714844 Z M 14.285156 3.714844 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,102 @@
@customfield @customfield_date @javascript
Feature: Managers can manage course custom fields date
In order to have additional data on the course
As a manager
I need to create, edit, remove and sort custom fields
Background:
Given the following "custom field categories" exist:
| name | component | area | itemid |
| Category for test | core_course | course | 0 |
And I log in as "admin"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
Scenario: Create a custom course date field
When I click on "Add a new custom field" "link"
And I click on "Date and time" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I click on "Save changes" "button" in the "Adding a new Date and time" "dialogue"
Then I should see "Test field"
And I log out
Scenario: Edit a custom course date field
When I click on "Add a new custom field" "link"
And I click on "Date and time" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I click on "Save changes" "button" in the "Adding a new Date and time" "dialogue"
And I click on "[data-role='editfield']" "css_element"
And I set the following fields to these values:
| Name | Edited field |
And I click on "Save changes" "button" in the "Updating Test field" "dialogue"
Then I should see "Edited field"
And I log out
Scenario: Delete a custom course date field
When I click on "Add a new custom field" "link"
And I click on "Date and time" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I click on "Save changes" "button" in the "Adding a new Date and time" "dialogue"
And I click on "[data-role='deletefield']" "css_element"
And I click on "Yes" "button" in the "Confirm" "dialogue"
Then I should not see "Test field"
And I log out
Scenario: A date field makerd to include time must show those fields on course form
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | Example 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
When I click on "Add a new custom field" "link"
And I click on "Date and time" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
| Include time | 1 |
And I click on "Save changes" "button" in the "Adding a new Date and time" "dialogue"
And I log out
Then I log in as "teacher1"
When I am on site homepage
When I am on "Course 1" course homepage
And I navigate to "Settings" in current page administration
And I expand all fieldsets
Then "#id_customfield_testfield_hour" "css_element" should be visible
Then "#id_customfield_testfield_minute" "css_element" should be visible
And I log out
Scenario: A date field makerd to not include time must not show those fields on course form
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | Example 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
When I click on "Add a new custom field" "link"
And I click on "Date and time" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
| Include time | |
And I click on "Save changes" "button" in the "Adding a new Date and time" "dialogue"
And I log out
Then I log in as "teacher1"
When I am on site homepage
When I am on "Course 1" course homepage
And I navigate to "Settings" in current page administration
And I expand all fieldsets
Then "#id_customfield_testfield_hour" "css_element" should not be visible
Then "#id_customfield_testfield_minute" "css_element" should not be visible
And I log out
@@ -0,0 +1,202 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace customfield_date;
use core_customfield_generator;
use core_customfield_test_instance_form;
/**
* Functional test for customfield_date
*
* @package customfield_date
* @copyright 2019 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class plugin_test extends \advanced_testcase {
/** @var stdClass[] */
private $courses = [];
/** @var \core_customfield\category_controller */
private $cfcat;
/** @var \core_customfield\field_controller[] */
private $cfields;
/** @var \core_customfield\data_controller[] */
private $cfdata;
/**
* Tests set up.
*/
public function setUp(): void {
$this->resetAfterTest();
$this->cfcat = $this->get_generator()->create_category();
$this->cfields[1] = $this->get_generator()->create_field(
['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield1', 'type' => 'date']);
$this->cfields[2] = $this->get_generator()->create_field(
['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield2', 'type' => 'date',
'configdata' => ['required' => 1, 'includetime' => 0, 'mindate' => 946684800, 'maxdate' => 1893456000]]);
$this->courses[1] = $this->getDataGenerator()->create_course();
$this->courses[2] = $this->getDataGenerator()->create_course();
$this->courses[3] = $this->getDataGenerator()->create_course();
$this->cfdata[1] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[1]->id, 1546300800);
$this->cfdata[2] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[2]->id, 1546300800);
$this->setUser($this->getDataGenerator()->create_user());
}
/**
* Get generator
* @return core_customfield_generator
*/
protected function get_generator(): core_customfield_generator {
return $this->getDataGenerator()->get_plugin_generator('core_customfield');
}
/**
* Test for initialising field and data controllers
*/
public function test_initialise(): void {
$f = \core_customfield\field_controller::create($this->cfields[1]->get('id'));
$this->assertTrue($f instanceof field_controller);
$f = \core_customfield\field_controller::create(0, (object)['type' => 'date'], $this->cfcat);
$this->assertTrue($f instanceof field_controller);
$d = \core_customfield\data_controller::create($this->cfdata[1]->get('id'));
$this->assertTrue($d instanceof data_controller);
$d = \core_customfield\data_controller::create(0, null, $this->cfields[1]);
$this->assertTrue($d instanceof data_controller);
}
/**
* Test for configuration form functions
*
* Create a configuration form and submit it with the same values as in the field
*/
public function test_config_form(): void {
$this->setAdminUser();
$submitdata = (array)$this->cfields[1]->to_record();
$submitdata['configdata'] = $this->cfields[1]->get('configdata');
$submitdata = \core_customfield\field_config_form::mock_ajax_submit($submitdata);
$form = new \core_customfield\field_config_form(null, null, 'post', '', null, true,
$submitdata, true);
$form->set_data_for_dynamic_submission();
$this->assertTrue($form->is_validated());
$form->process_dynamic_submission();
}
/**
* Test for instance form functions
*/
public function test_instance_form(): void {
global $CFG;
require_once($CFG->dirroot . '/customfield/tests/fixtures/test_instance_form.php');
$this->setAdminUser();
$handler = $this->cfcat->get_handler();
// First try to submit without required field.
$submitdata = (array)$this->courses[1];
core_customfield_test_instance_form::mock_submit($submitdata, []);
$form = new core_customfield_test_instance_form('POST',
['handler' => $handler, 'instance' => $this->courses[1]]);
$this->assertFalse($form->is_validated());
// Now with required field.
$submitdata['customfield_myfield2'] = time();
core_customfield_test_instance_form::mock_submit($submitdata, []);
$form = new core_customfield_test_instance_form('POST',
['handler' => $handler, 'instance' => $this->courses[1]]);
$this->assertTrue($form->is_validated());
$data = $form->get_data();
$this->assertEmpty($data->customfield_myfield1);
$this->assertNotEmpty($data->customfield_myfield2);
$handler->instance_form_save($data);
}
/**
* Test for min/max date validation
*/
public function test_instance_form_validation(): void {
$this->setAdminUser();
$handler = $this->cfcat->get_handler();
$submitdata = (array)$this->courses[1];
$data = data_controller::create(0, null, $this->cfields[2]);
// Submit with date less than mindate.
$submitdata['customfield_myfield2'] = 915148800;
$this->assertNotEmpty($data->instance_form_validation($submitdata, []));
// Submit with date more than maxdate.
$submitdata['customfield_myfield2'] = 1893557000;
$this->assertNotEmpty($data->instance_form_validation($submitdata, []));
}
/**
* Test for data_controller::get_value and export_value
*/
public function test_get_export_value(): void {
$this->assertEquals(1546300800, $this->cfdata[1]->get_value());
$this->assertStringMatchesFormat('%a 1 January 2019%a', $this->cfdata[1]->export_value());
// Field without data.
$d = \core_customfield\data_controller::create(0, null, $this->cfields[2]);
$this->assertEquals(0, $d->get_value());
$this->assertEquals(null, $d->export_value());
}
/**
* Data provider for {@see test_parse_value}
*
* @return array
*/
public function parse_value_provider(): array {
return [
// Valid times.
['2019-10-01', strtotime('2019-10-01')],
['2019-10-01 14:00', strtotime('2019-10-01 14:00')],
// Invalid times.
['ZZZZZ', 0],
['202-04-01', 0],
['2019-15-15', 0],
];
}
/**
* Test field parse_value method
*
* @param string $value
* @param int $expected
* @return void
*
* @dataProvider parse_value_provider
*/
public function test_parse_value(string $value, int $expected): void {
$this->assertSame($expected, $this->cfields[1]->parse_value($value));
}
/**
* Deleting fields and data
*/
public function test_delete(): void {
$this->cfcat->get_handler()->delete_all();
}
}
+30
View File
@@ -0,0 +1,30 @@
<?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/>.
/**
* Customfield date plugin
*
* @package customfield_date
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'customfield_date';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
@@ -0,0 +1,121 @@
<?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/>.
/**
* Select plugin data controller
*
* @package customfield_select
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace customfield_select;
defined('MOODLE_INTERNAL') || die;
/**
* Class data
*
* @package customfield_select
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class data_controller extends \core_customfield\data_controller {
/**
* Return the name of the field where the information is stored
* @return string
*/
public function datafield(): string {
return 'intvalue';
}
/**
* Returns the default value as it would be stored in the database (not in human-readable format).
*
* @return mixed
*/
public function get_default_value() {
$defaultvalue = $this->get_field()->get_configdata_property('defaultvalue');
if ('' . $defaultvalue !== '') {
$key = array_search($defaultvalue, $this->get_field()->get_options());
if ($key !== false) {
return $key;
}
}
return 0;
}
/**
* Add fields for editing a textarea field.
*
* @param \MoodleQuickForm $mform
*/
public function instance_form_definition(\MoodleQuickForm $mform) {
$field = $this->get_field();
$config = $field->get('configdata');
$options = $field->get_options();
$elementname = $this->get_form_element_name();
$mform->addElement('select', $elementname, $this->get_field()->get_formatted_name(), $options);
if (($defaultkey = array_search($config['defaultvalue'], $options)) !== false) {
$mform->setDefault($elementname, $defaultkey);
}
if ($field->get_configdata_property('required')) {
$mform->addRule($elementname, null, 'required', null, 'client');
}
}
/**
* Validates data for this field.
*
* @param array $data
* @param array $files
* @return array
*/
public function instance_form_validation(array $data, array $files): array {
$errors = parent::instance_form_validation($data, $files);
if ($this->get_field()->get_configdata_property('required')) {
// Standard required rule does not work on select element.
$elementname = $this->get_form_element_name();
if (empty($data[$elementname])) {
$errors[$elementname] = get_string('err_required', 'form');
}
}
return $errors;
}
/**
* Returns value in a human-readable format
*
* @return mixed|null value or null if empty
*/
public function export_value() {
$value = $this->get_value();
if ($this->is_empty($value)) {
return null;
}
$options = $this->get_field()->get_options();
if (array_key_exists($value, $options)) {
return $options[$value];
}
return null;
}
}
@@ -0,0 +1,135 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace customfield_select;
use coding_exception;
/**
* Class field
*
* @package customfield_select
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class field_controller extends \core_customfield\field_controller {
/**
* Customfield type
*/
const TYPE = 'select';
/**
* Add fields for editing a select field.
*
* @param \MoodleQuickForm $mform
*/
public function config_form_definition(\MoodleQuickForm $mform) {
$mform->addElement('header', 'header_specificsettings', get_string('specificsettings', 'customfield_select'));
$mform->setExpanded('header_specificsettings', true);
$mform->addElement('textarea', 'configdata[options]', get_string('menuoptions', 'customfield_select'));
$mform->setType('configdata[options]', PARAM_TEXT);
$mform->addElement('text', 'configdata[defaultvalue]', get_string('defaultvalue', 'core_customfield'), 'size="50"');
$mform->setType('configdata[defaultvalue]', PARAM_TEXT);
}
/**
* @deprecated since Moodle 3.10 - MDL-68569 please use $field->get_options
*/
public static function get_options_array(): void {
throw new coding_exception('get_options_array() is deprecated, please use $field->get_options() instead');
}
/**
* Return configured field options
*
* @return array
*/
public function get_options(): array {
$optionconfig = $this->get_configdata_property('options');
if ($optionconfig) {
$context = $this->get_handler()->get_configuration_context();
$options = array_map(
fn(string $option) => format_string($option, true, ['context' => $context]),
preg_split("/\s*\n\s*/", trim($optionconfig), -1, PREG_SPLIT_NO_EMPTY),
);
} else {
$options = array();
}
return array_merge([''], $options);
}
/**
* Validate the data from the config form.
* Sub classes must reimplement it.
*
* @param array $data from the add/edit profile field form
* @param array $files
* @return array associative array of error messages
*/
public function config_form_validation(array $data, $files = array()): array {
$options = preg_split("/\s*\n\s*/", trim($data['configdata']['options']));
$errors = [];
if (!$options || count($options) < 2) {
$errors['configdata[options]'] = get_string('errornotenoughoptions', 'customfield_select');
} else if (!empty($data['configdata']['defaultvalue'])) {
$defaultkey = array_search($data['configdata']['defaultvalue'], $options);
if ($defaultkey === false) {
$errors['configdata[defaultvalue]'] = get_string('errordefaultvaluenotinlist', 'customfield_select');
}
}
return $errors;
}
/**
* Does this custom field type support being used as part of the block_myoverview
* custom field grouping?
* @return bool
*/
public function supports_course_grouping(): bool {
return true;
}
/**
* If this field supports course grouping, then this function needs overriding to
* return the formatted values for this.
* @param array $values the used values that need formatting
* @return array
*/
public function course_grouping_format_values($values): array {
$options = $this->get_options();
$ret = [];
foreach ($values as $value) {
if (isset($options[$value])) {
$ret[$value] = $options[$value];
}
}
$ret[BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY] = get_string('nocustomvalue', 'block_myoverview',
$this->get_formatted_name());
return $ret;
}
/**
* Locate the value parameter in the field options array, and return it's index
*
* @param string $value
* @return int
*/
public function parse_value(string $value) {
return (int) array_search($value, $this->get_options());
}
}
@@ -0,0 +1,83 @@
<?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/>.
/**
* Privacy Subsystem implementation for customfield_select.
*
* @package customfield_select
* @copyright 2018 Daniel Neis Araujo <danielneis@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace customfield_select\privacy;
use core_customfield\data_controller;
use core_customfield\privacy\customfield_provider;
use core_privacy\local\request\writer;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for customfield_select implementing null_provider.
*
* @copyright 2018 Daniel Neis Araujo <danielneis@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider, customfield_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
/**
* Preprocesses data object that is going to be exported
*
* @param data_controller $data
* @param \stdClass $exportdata
* @param array $subcontext
*/
public static function export_customfield_data(data_controller $data, \stdClass $exportdata, array $subcontext) {
$context = $data->get_context();
$exportdata->value = $data->export_value();
writer::with_context($context)
->export_data($subcontext, $exportdata);
}
/**
* Allows plugins to delete everything they store related to the data (usually files)
*
* @param string $dataidstest
* @param array $params
* @param array $contextids
* @return mixed|void
*/
public static function before_delete_data(string $dataidstest, array $params, array $contextids) {
}
/**
* Allows plugins to delete everything they store related to the field configuration (usually files)
*
* @param string $fieldidstest
* @param array $params
* @param array $contextids
*/
public static function before_delete_fields(string $fieldidstest, array $params, array $contextids) {
}
}
@@ -0,0 +1,33 @@
<?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/>.
/**
* Customfield text field plugin strings
*
* @package customfield_select
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['errordefaultvaluenotinlist'] = 'The default value must be one of the options from the list above.';
$string['errornotenoughoptions'] = 'Please provide at least two options, with each on a new line.';
$string['invalidoption'] = 'Invalid option selected';
$string['menuoptions'] = 'Menu options (one per line)';
$string['pluginname'] = 'Dropdown menu';
$string['privacy:metadata'] = 'The Dropdown menu field type plugin doesn\'t store any personal data; it uses tables defined in core.';
$string['specificsettings'] = 'Dropdown menu field settings';
@@ -0,0 +1,85 @@
@customfield @customfield_select @javascript
Feature: Managers can manage course custom fields select
In order to have additional data on the course
As a manager
I need to create, edit, remove and sort custom fields
Background:
Given the following "custom field categories" exist:
| name | component | area | itemid |
| Category for test | core_course | course | 0 |
And I log in as "admin"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
Scenario: Create a custom course select field
When I click on "Add a new custom field" "link"
And I click on "Dropdown menu" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I set the field "Menu options (one per line)" to multiline:
"""
a
b
"""
And I click on "Save changes" "button" in the "Adding a new Dropdown menu" "dialogue"
Then I should see "Test field"
And I log out
Scenario: Edit a custom course select field
When I click on "Add a new custom field" "link"
And I click on "Dropdown menu" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I set the field "Menu options (one per line)" to multiline:
"""
a
b
"""
And I click on "Save changes" "button" in the "Adding a new Dropdown menu" "dialogue"
And I click on "Edit" "link" in the "Test field" "table_row"
And I set the following fields to these values:
| Name | Edited field |
And I click on "Save changes" "button" in the "Updating Test field" "dialogue"
Then I should see "Edited field"
And I should not see "Test field"
And I log out
Scenario: Delete a custom course select field
When I click on "Add a new custom field" "link"
And I click on "Dropdown menu" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I set the field "Menu options (one per line)" to multiline:
"""
a
b
"""
And I click on "Save changes" "button" in the "Adding a new Dropdown menu" "dialogue"
And I click on "Delete" "link" in the "Test field" "table_row"
And I click on "Yes" "button" in the "Confirm" "dialogue"
Then I should not see "Test field"
And I log out
Scenario: Validation of custom course select field configuration
When I click on "Add a new custom field" "link"
And I click on "Dropdown menu" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I click on "Save changes" "button" in the "Adding a new Dropdown menu" "dialogue"
And I should see "Please provide at least two options, with each on a new line." in the "Menu options (one per line)" "form_row"
And I set the field "Menu options (one per line)" to multiline:
"""
a
b
"""
And I set the field "Default value" to "c"
And I click on "Save changes" "button" in the "Adding a new Dropdown menu" "dialogue"
And I should see "The default value must be one of the options from the list above" in the "Default value" "form_row"
And I set the field "Default value" to "b"
And I click on "Save changes" "button" in the "Adding a new Dropdown menu" "dialogue"
And "testfield" "text" should exist in the "Test field" "table_row"
And I log out
@@ -0,0 +1,225 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace customfield_select;
use core_customfield_generator;
use core_customfield_test_instance_form;
use stdClass;
/**
* Functional test for customfield_select
*
* @package customfield_select
* @covers \customfield_select\data_controller
* @covers \customfield_select\field_controller
* @copyright 2019 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class plugin_test extends \advanced_testcase {
/** @var stdClass[] */
private $courses = [];
/** @var \core_customfield\category_controller */
private $cfcat;
/** @var \core_customfield\field_controller[] */
private $cfields;
/** @var \core_customfield\data_controller[] */
private $cfdata;
/**
* Tests set up.
*/
public function setUp(): void {
$this->resetAfterTest();
$this->cfcat = $this->get_generator()->create_category();
$this->cfields[1] = $this->get_generator()->create_field(
['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield1', 'type' => 'select',
'configdata' => ['options' => "a\nb\nc"]]);
$this->cfields[2] = $this->get_generator()->create_field(
['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield2', 'type' => 'select',
'configdata' => ['required' => 1, 'options' => "a\nb\nc"]]);
$this->cfields[3] = $this->get_generator()->create_field(
['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield3', 'type' => 'select',
'configdata' => ['defaultvalue' => 'b', 'options' => "a\nb\nc"]]);
$this->courses[1] = $this->getDataGenerator()->create_course();
$this->courses[2] = $this->getDataGenerator()->create_course();
$this->courses[3] = $this->getDataGenerator()->create_course();
$this->cfdata[1] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[1]->id, 1);
$this->cfdata[2] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[2]->id, 1);
$this->setUser($this->getDataGenerator()->create_user());
}
/**
* Get generator
* @return core_customfield_generator
*/
protected function get_generator(): core_customfield_generator {
return $this->getDataGenerator()->get_plugin_generator('core_customfield');
}
/**
* Test for initialising field and data controllers
*/
public function test_initialise(): void {
$f = \core_customfield\field_controller::create($this->cfields[1]->get('id'));
$this->assertTrue($f instanceof field_controller);
$f = \core_customfield\field_controller::create(0, (object)['type' => 'select'], $this->cfcat);
$this->assertTrue($f instanceof field_controller);
$d = \core_customfield\data_controller::create($this->cfdata[1]->get('id'));
$this->assertTrue($d instanceof data_controller);
$d = \core_customfield\data_controller::create(0, null, $this->cfields[1]);
$this->assertTrue($d instanceof data_controller);
}
/**
* Test for configuration form functions
*
* Create a configuration form and submit it with the same values as in the field
*/
public function test_config_form(): void {
$this->setAdminUser();
$submitdata = (array)$this->cfields[1]->to_record();
$submitdata['configdata'] = $this->cfields[1]->get('configdata');
$submitdata = \core_customfield\field_config_form::mock_ajax_submit($submitdata);
$form = new \core_customfield\field_config_form(null, null, 'post', '', null, true,
$submitdata, true);
$form->set_data_for_dynamic_submission();
$this->assertTrue($form->is_validated());
$form->process_dynamic_submission();
}
/**
* Test for instance form functions
*/
public function test_instance_form(): void {
global $CFG;
require_once($CFG->dirroot . '/customfield/tests/fixtures/test_instance_form.php');
$this->setAdminUser();
$handler = $this->cfcat->get_handler();
// First try to submit without required field.
$submitdata = (array)$this->courses[1];
core_customfield_test_instance_form::mock_submit($submitdata, []);
$form = new core_customfield_test_instance_form('POST',
['handler' => $handler, 'instance' => $this->courses[1]]);
$this->assertFalse($form->is_validated());
// Now with required field.
$submitdata['customfield_myfield2'] = 1;
core_customfield_test_instance_form::mock_submit($submitdata, []);
$form = new core_customfield_test_instance_form('POST',
['handler' => $handler, 'instance' => $this->courses[1]]);
$this->assertTrue($form->is_validated());
$data = $form->get_data();
$this->assertNotEmpty($data->customfield_myfield1);
$this->assertNotEmpty($data->customfield_myfield2);
$handler->instance_form_save($data);
}
/**
* Test for data_controller::get_value and export_value
*/
public function test_get_export_value(): void {
$this->assertEquals(1, $this->cfdata[1]->get_value());
$this->assertEquals('a', $this->cfdata[1]->export_value());
// Field without data but with a default value.
$d = \core_customfield\data_controller::create(0, null, $this->cfields[3]);
$this->assertEquals(2, $d->get_value());
$this->assertEquals('b', $d->export_value());
}
/**
* Test getting field options, formatted
*/
public function test_get_options(): void {
filter_set_global_state('multilang', TEXTFILTER_ON);
filter_set_applies_to_strings('multilang', true);
$field = $this->get_generator()->create_field([
'categoryid' => $this->cfcat->get('id'),
'type' => 'select',
'shortname' => 'myselect',
'configdata' => [
'options' => <<<EOF
<span lang="en" class="multilang">Beginner</span><span lang="es" class="multilang">Novato</span>
<span lang="en" class="multilang">Intermediate</span><span lang="es" class="multilang">Intermedio</span>
<span lang="en" class="multilang">Advanced</span><span lang="es" class="multilang">Avanzado</span>
EOF,
],
]);
$this->assertEquals([
'',
'Beginner',
'Intermediate',
'Advanced',
], $field->get_options());
}
/**
* Data provider for {@see test_parse_value}
*
* @return array
*/
public static function parse_value_provider(): array {
return [
['Red', 1],
['Blue', 2],
['Green', 3],
['Mauve', 0],
];
}
/**
* Test field parse_value method
*
* @param string $value
* @param int $expected
*
* @dataProvider parse_value_provider
*/
public function test_parse_value(string $value, int $expected): void {
$field = $this->get_generator()->create_field([
'categoryid' => $this->cfcat->get('id'),
'type' => 'select',
'shortname' => 'myselect',
'configdata' => [
'options' => "Red\nBlue\nGreen",
],
]);
$this->assertSame($expected, $field->parse_value($value));
}
/**
* Deleting fields and data
*/
public function test_delete(): void {
$this->cfcat->get_handler()->delete_all();
}
}
+29
View File
@@ -0,0 +1,29 @@
<?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/>.
/**
* Customfield Select Type
*
* @package customfield_select
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'customfield_select';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
@@ -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/>.
/**
* Customfields text field plugin
*
* @package customfield_text
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace customfield_text;
defined('MOODLE_INTERNAL') || die;
use core_customfield\api;
/**
* Class data
*
* @package customfield_text
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class data_controller extends \core_customfield\data_controller {
/**
* Return the name of the field where the information is stored
* @return string
*/
public function datafield(): string {
return 'charvalue';
}
/**
* Add fields for editing a text field.
*
* @param \MoodleQuickForm $mform
*/
public function instance_form_definition(\MoodleQuickForm $mform) {
$field = $this->get_field();
$config = $field->get('configdata');
$type = $config['ispassword'] ? 'password' : 'text';
$elementname = $this->get_form_element_name();
$mform->addElement($type, $elementname, $this->get_field()->get_formatted_name(), 'size=' . (int)$config['displaysize']);
$mform->setType($elementname, PARAM_TEXT);
if (!empty($config['defaultvalue'])) {
$mform->setDefault($elementname, $config['defaultvalue']);
}
if ($field->get_configdata_property('required')) {
$mform->addRule($elementname, null, 'required', null, 'client');
}
}
/**
* Validates data for this field.
*
* @param array $data
* @param array $files
* @return array
*/
public function instance_form_validation(array $data, array $files): array {
$errors = parent::instance_form_validation($data, $files);
$maxlength = $this->get_field()->get_configdata_property('maxlength');
$elementname = $this->get_form_element_name();
if (($maxlength > 0) && ($maxlength < \core_text::strlen($data[$elementname]))) {
$errors[$elementname] = get_string('errormaxlength', 'customfield_text', $maxlength);
}
return $errors;
}
/**
* Returns the default value as it would be stored in the database (not in human-readable format).
*
* @return mixed
*/
public function get_default_value() {
return $this->get_field()->get_configdata_property('defaultvalue');
}
/**
* Returns value in a human-readable format
*
* @return mixed|null value or null if empty
*/
public function export_value() {
$value = parent::export_value();
if ($value === null) {
return null;
}
$link = $this->get_field()->get_configdata_property('link');
if ($link) {
$linktarget = $this->get_field()->get_configdata_property('linktarget');
$url = str_replace('$$', urlencode($this->get_value()), $link);
$attributes = $linktarget ? ['target' => $linktarget] : [];
$value = \html_writer::link($url, $value, $attributes);
}
return $value;
}
}
@@ -0,0 +1,152 @@
<?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/>.
/**
* Customfields text plugin
*
* @package customfield_text
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace customfield_text;
defined('MOODLE_INTERNAL') || die;
/**
* Class field
*
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @package customfield_text
*/
class field_controller extends \core_customfield\field_controller {
/**
* Plugin type text
*/
const TYPE = 'text';
/**
* Add fields for editing a text field.
*
* @param \MoodleQuickForm $mform
*/
public function config_form_definition(\MoodleQuickForm $mform) {
$mform->addElement('header', 'header_specificsettings', get_string('specificsettings', 'customfield_text'));
$mform->setExpanded('header_specificsettings', true);
$mform->addElement('text', 'configdata[defaultvalue]', get_string('defaultvalue', 'core_customfield'),
['size' => 50]);
$mform->setType('configdata[defaultvalue]', PARAM_TEXT);
$mform->addElement('text', 'configdata[displaysize]', get_string('displaysize', 'customfield_text'), ['size' => 6]);
$mform->setType('configdata[displaysize]', PARAM_INT);
if (!$this->get_configdata_property('displaysize')) {
$mform->setDefault('configdata[displaysize]', 50);
}
$mform->addRule('configdata[displaysize]', null, 'numeric', null, 'client');
$mform->addElement('text', 'configdata[maxlength]', get_string('maxlength', 'customfield_text'), ['size' => 6]);
$mform->setType('configdata[maxlength]', PARAM_INT);
if (!$this->get_configdata_property('maxlength')) {
$mform->setDefault('configdata[maxlength]', 1333);
}
$mform->addRule('configdata[maxlength]', null, 'numeric', null, 'client');
$mform->addElement('selectyesno', 'configdata[ispassword]', get_string('ispassword', 'customfield_text'));
$mform->setType('configdata[ispassword]', PARAM_INT);
$mform->addElement('text', 'configdata[link]', get_string('islink', 'customfield_text'), ['size' => 50]);
$mform->setType('configdata[link]', PARAM_RAW_TRIMMED);
$mform->addHelpButton('configdata[link]', 'islink', 'customfield_text');
$mform->disabledIf('configdata[link]', 'configdata[ispassword]', 'eq', 1);
$linkstargetoptions = array(
'' => get_string('none', 'customfield_text'),
'_blank' => get_string('newwindow', 'customfield_text'),
'_self' => get_string('sameframe', 'customfield_text'),
'_top' => get_string('samewindow', 'customfield_text')
);
$mform->addElement('select', 'configdata[linktarget]', get_string('linktarget', 'customfield_text'),
$linkstargetoptions);
$mform->disabledIf('configdata[linktarget]', 'configdata[link]', 'eq', '');
}
/**
* Validate the data on the field configuration form
*
* @param array $data from the add/edit profile field form
* @param array $files
* @return array associative array of error messages
*/
public function config_form_validation(array $data, $files = array()): array {
global $CFG;
$errors = parent::config_form_validation($data, $files);
$maxlength = (int)$data['configdata']['maxlength'];
if ($maxlength < 1 || $maxlength > 1333) {
$errors['configdata[maxlength]'] = get_string('errorconfigmaxlen', 'customfield_text');
}
$displaysize = (int)$data['configdata']['displaysize'];
if ($displaysize < 1 || $displaysize > 200) {
$errors['configdata[displaysize]'] = get_string('errorconfigdisplaysize', 'customfield_text');
}
if (isset($data['configdata']['link'])) {
$link = $data['configdata']['link'];
if (strlen($link)) {
require_once($CFG->dirroot . '/lib/validateurlsyntax.php');
if (strpos($link, '$$') === false) {
$errors['configdata[link]'] = get_string('errorconfiglinkplaceholder', 'customfield_text');
} else if (!validateUrlSyntax(str_replace('$$', 'XYZ', $link), 's+H?S?F-E-u-P-a?I?p?f?q?r?')) {
// This validation is more strict than PARAM_URL - it requires the protocol and it must be either http or https.
$errors['configdata[link]'] = get_string('errorconfiglinksyntax', 'customfield_text');
}
}
}
return $errors;
}
/**
* Does this custom field type support being used as part of the block_myoverview
* custom field grouping?
* @return bool
*/
public function supports_course_grouping(): bool {
return true;
}
/**
* If this field supports course grouping, then this function needs overriding to
* return the formatted values for this.
* @param array $values the used values that need formatting
* @return array
*/
public function course_grouping_format_values($values): array {
$ret = [];
foreach ($values as $value) {
$ret[$value] = format_string($value);
}
$ret[BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY] = get_string('nocustomvalue', 'block_myoverview',
$this->get_formatted_name());
return $ret;
}
}
@@ -0,0 +1,84 @@
<?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/>.
/**
* Privacy Subsystem implementation for customfield_text.
*
* @package customfield_text
* @copyright 2018 Daniel Neis Araujo <danielneis@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace customfield_text\privacy;
use core_customfield\data_controller;
use core_customfield\privacy\customfield_provider;
use core_privacy\local\request\writer;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for customfield_text implementing null_provider.
*
* @copyright 2018 Daniel Neis Araujo <danielneis@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider, customfield_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
/**
* Preprocesses data object that is going to be exported
*
* @param data_controller $data
* @param \stdClass $exportdata
* @param array $subcontext
*/
public static function export_customfield_data(data_controller $data, \stdClass $exportdata, array $subcontext) {
$context = $data->get_context();
// For text fields we want to apply format_string even to raw value to avoid CSS.
$exportdata->{$data->datafield()} = $data->export_value();
writer::with_context($context)
->export_data($subcontext, $exportdata);
}
/**
* Allows plugins to delete everything they store related to the data (usually files)
*
* @param string $dataidstest
* @param array $params
* @param array $contextids
* @return mixed|void
*/
public static function before_delete_data(string $dataidstest, array $params, array $contextids) {
}
/**
* Allows plugins to delete everything they store related to the field configuration (usually files)
*
* @param string $fieldidstest
* @param array $params
* @param array $contextids
*/
public static function before_delete_fields(string $fieldidstest, array $params, array $contextids) {
}
}
@@ -0,0 +1,44 @@
<?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/>.
/**
* Customfield text plugin
*
* @package customfield_text
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['displaysize'] = 'Form input size';
$string['errorconfigdisplaysize'] = 'The form input size must be between 1 and 200 characters.';
$string['errorconfiglinkplaceholder'] = 'The link must contain a placeholder $$.';
$string['errorconfiglinksyntax'] = 'The link must be a valid URL starting with either http:// or https://.';
$string['errorconfigmaxlen'] = 'The maximum number of characters allowed must be between 1 and 1333.';
$string['errormaxlength'] = 'The maximum number of characters allowed in this field is {$a}.';
$string['islink'] = 'Link field';
$string['islink_help'] = 'To transform the text into a link, enter a URL containing $$ as a placeholder, where $$ will be replaced with the text. For example, to transform a Twitter ID to a link, enter https://twitter.com/$$.';
$string['ispassword'] = 'Password field';
$string['linktarget'] = 'Link target';
$string['maxlength'] = 'Maximum number of characters';
$string['newwindow'] = 'New window';
$string['none'] = 'None';
$string['pluginname'] = 'Short text';
$string['privacy:metadata'] = 'The Short text field type plugin doesn\'t store any personal data; it uses tables defined in core.';
$string['sameframe'] = 'Same frame';
$string['samewindow'] = 'Same window';
$string['specificsettings'] = 'Short text field settings';
@@ -0,0 +1,140 @@
@customfield @customfield_text @javascript
Feature: Managers can manage course custom fields text
In order to have additional data on the course
As a manager
I need to create, edit, remove and sort custom fields
Background:
Given the following "custom field categories" exist:
| name | component | area | itemid |
| Category for test | core_course | course | 0 |
And I log in as "admin"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
Scenario: Create a custom course text field
When I click on "Add a new custom field" "link"
And I click on "Short text" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
Then I should see "Test field"
And I log out
Scenario: Edit a custom course text field
When I click on "Add a new custom field" "link"
And I click on "Short text" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I click on "Edit" "link" in the "Test field" "table_row"
And I set the following fields to these values:
| Name | Edited field |
And I click on "Save changes" "button" in the "Updating Test field" "dialogue"
Then I should see "Edited field"
And I navigate to "Reports > Logs" in site administration
And I press "Get these logs"
And I log out
Scenario: Delete a custom course text field
When I click on "Add a new custom field" "link"
And I click on "Short text" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I click on "Delete" "link" in the "Test field" "table_row"
And I click on "Yes" "button" in the "Confirm" "dialogue"
And I wait until the page is ready
And I wait until "Test field" "text" does not exist
Then I should not see "Test field"
And I log out
Scenario: A text field with a link setting must show link on course listing
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | Example 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And I navigate to "Courses > Default settings > Course custom fields" in site administration
And I click on "Add a new custom field" "link"
And I click on "Short text" "link"
And I set the following fields to these values:
| Name | See more on website |
| Short name | testfield |
| Visible to | Everyone |
| Link | https://www.moodle.org/$$ |
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I log out
Then I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Settings" in current page administration
And I set the following fields to these values:
| See more on website | course/view.php?id=35 |
And I press "Save and display"
And I am on site homepage
Then I should see "course/view.php?id=35" in the ".customfields-container .customfieldvalue a" "css_element"
Then I should see "See more on website" in the ".customfields-container .customfieldname" "css_element"
Scenario: A text field with a max length must validate it on course edit form
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | Example 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And I navigate to "Courses > Default settings > Course custom fields" in site administration
And I click on "Add a new custom field" "link"
And I click on "Short text" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
| Maximum number of characters | 3 |
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I log out
Then I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Settings" in current page administration
And I set the following fields to these values:
| Test field | 1234 |
And I press "Save and display"
Then I should see "The maximum number of characters allowed in this field is 3."
Scenario: A text field with a default value must be shown on listing but allow empty values that will not be shown
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | Example 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And I navigate to "Courses > Default settings > Course custom fields" in site administration
And I click on "Add a new custom field" "link"
And I click on "Short text" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
| Default value | testdefault |
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I log out
Then I log in as "teacher1"
When I am on site homepage
Then I should see "Test field: testdefault"
When I am on "Course 1" course homepage
And I navigate to "Settings" in current page administration
Then the "value" attribute of "#id_customfield_testfield" "css_element" should contain "testdefault"
When I set the following fields to these values:
| Test field | |
And I press "Save and display"
And I am on site homepage
And I should not see "Test field"
@@ -0,0 +1,169 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace customfield_text;
use core_customfield_generator;
use core_customfield_test_instance_form;
/**
* Functional test for customfield_text
*
* @package customfield_text
* @copyright 2019 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class plugin_test extends \advanced_testcase {
/** @var stdClass[] */
private $courses = [];
/** @var \core_customfield\category_controller */
private $cfcat;
/** @var \core_customfield\field_controller[] */
private $cfields;
/** @var \core_customfield\data_controller[] */
private $cfdata;
/**
* Tests set up.
*/
public function setUp(): void {
$this->resetAfterTest();
$this->cfcat = $this->get_generator()->create_category();
$this->cfields[1] = $this->get_generator()->create_field(
['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield1', 'type' => 'text',
'configdata' => ['maxlength' => 30, 'displaysize' => 50], 'description' => null]);
$this->cfields[2] = $this->get_generator()->create_field(
['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield2', 'type' => 'text',
'configdata' => ['required' => 1, 'maxlength' => 30, 'displaysize' => 50]]);
$this->cfields[3] = $this->get_generator()->create_field(
['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield3', 'type' => 'text',
'configdata' => ['defaultvalue' => 'Defvalue', 'maxlength' => 30, 'displaysize' => 50]]);
$this->cfields[4] = $this->get_generator()->create_field(
['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield4', 'type' => 'text',
'configdata' => ['link' => 'https://twitter.com/$$', 'maxlength' => 30, 'displaysize' => 50]]);
$this->courses[1] = $this->getDataGenerator()->create_course();
$this->courses[2] = $this->getDataGenerator()->create_course();
$this->courses[3] = $this->getDataGenerator()->create_course();
$this->cfdata[1] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[1]->id,
'Value1');
$this->cfdata[2] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[2]->id,
'Value2');
$this->setUser($this->getDataGenerator()->create_user());
}
/**
* Get generator
* @return core_customfield_generator
*/
protected function get_generator(): core_customfield_generator {
return $this->getDataGenerator()->get_plugin_generator('core_customfield');
}
/**
* Test for initialising field and data controllers
*/
public function test_initialise(): void {
$f = \core_customfield\field_controller::create($this->cfields[1]->get('id'));
$this->assertTrue($f instanceof field_controller);
$f = \core_customfield\field_controller::create(0, (object)['type' => 'text'], $this->cfcat);
$this->assertTrue($f instanceof field_controller);
$d = \core_customfield\data_controller::create($this->cfdata[1]->get('id'));
$this->assertTrue($d instanceof data_controller);
$d = \core_customfield\data_controller::create(0, null, $this->cfields[1]);
$this->assertTrue($d instanceof data_controller);
}
/**
* Test for configuration form functions
*
* Create a configuration form and submit it with the same values as in the field
*/
public function test_config_form(): void {
$this->setAdminUser();
$submitdata = (array)$this->cfields[1]->to_record();
$submitdata['configdata'] = $this->cfields[1]->get('configdata');
$submitdata = \core_customfield\field_config_form::mock_ajax_submit($submitdata);
$form = new \core_customfield\field_config_form(null, null, 'post', '', null, true,
$submitdata, true);
$form->set_data_for_dynamic_submission();
$this->assertTrue($form->is_validated());
$form->process_dynamic_submission();
}
/**
* Test for instance form functions
*/
public function test_instance_form(): void {
global $CFG;
require_once($CFG->dirroot . '/customfield/tests/fixtures/test_instance_form.php');
$this->setAdminUser();
$handler = $this->cfcat->get_handler();
// First try to submit without required field.
$submitdata = (array)$this->courses[1];
core_customfield_test_instance_form::mock_submit($submitdata, []);
$form = new core_customfield_test_instance_form('POST',
['handler' => $handler, 'instance' => $this->courses[1]]);
$this->assertFalse($form->is_validated());
// Now with required field.
$submitdata['customfield_myfield2'] = 'Some text';
core_customfield_test_instance_form::mock_submit($submitdata, []);
$form = new core_customfield_test_instance_form('POST',
['handler' => $handler, 'instance' => $this->courses[1]]);
$this->assertTrue($form->is_validated());
$data = $form->get_data();
$this->assertNotEmpty($data->customfield_myfield1);
$this->assertNotEmpty($data->customfield_myfield2);
$handler->instance_form_save($data);
}
/**
* Test for data_controller::get_value and export_value
*/
public function test_get_export_value(): void {
$this->assertEquals('Value1', $this->cfdata[1]->get_value());
$this->assertEquals('Value1', $this->cfdata[1]->export_value());
// Field without data but with a default value.
$d = \core_customfield\data_controller::create(0, null, $this->cfields[3]);
$this->assertEquals('Defvalue', $d->get_value());
$this->assertEquals('Defvalue', $d->export_value());
// Field with a link.
$d = $this->get_generator()->add_instance_data($this->cfields[4], $this->courses[1]->id, 'mynickname');
$this->assertEquals('mynickname', $d->get_value());
$this->assertEquals('<a href="https://twitter.com/mynickname">mynickname</a>', $d->export_value());
}
/**
* Deleting fields and data
*/
public function test_delete(): void {
$this->cfcat->get_handler()->delete_all();
}
}
+29
View File
@@ -0,0 +1,29 @@
<?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/>.
/**
* Customfield text plugin
*
* @package customfield_text
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'customfield_text';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
@@ -0,0 +1,261 @@
<?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/>.
/**
* Customfields textarea plugin
*
* @package customfield_textarea
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace customfield_textarea;
use backup_nested_element;
defined('MOODLE_INTERNAL') || die;
/**
* Class data
*
* @package customfield_textarea
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class data_controller extends \core_customfield\data_controller {
/**
* Return the name of the field where the information is stored
* @return string
*/
public function datafield(): string {
return 'value';
}
/**
* Options for the editor
*
* @return array
*/
protected function value_editor_options() {
/** @var field_controller $field */
$field = $this->get_field();
return $field->value_editor_options($this->get('id') ? $this->get_context() : null);
}
/**
* Returns the name of the field to be used on HTML forms.
*
* @return string
*/
public function get_form_element_name(): string {
return parent::get_form_element_name() . '_editor';
}
/**
* Add fields for editing a textarea field.
*
* @param \MoodleQuickForm $mform
*/
public function instance_form_definition(\MoodleQuickForm $mform) {
$field = $this->get_field();
$desceditoroptions = $this->value_editor_options();
$elementname = $this->get_form_element_name();
$mform->addElement('editor', $elementname, $this->get_field()->get_formatted_name(), null, $desceditoroptions);
if ($field->get_configdata_property('required')) {
$mform->addRule($elementname, null, 'required', null, 'client');
}
}
/**
* Saves the data coming from form
*
* @param \stdClass $datanew data coming from the form
*/
public function instance_form_save(\stdClass $datanew) {
$fieldname = $this->get_form_element_name();
if (!property_exists($datanew, $fieldname)) {
return;
}
// Normalise form data, for cases it's come from an external source.
$fromform = $datanew->$fieldname;
if (!is_array($fromform)) {
$fromform = ['text' => $fromform];
$fromform['format'] = $this->get('id') ? $this->get('valueformat') :
$this->get_field()->get_configdata_property('defaultvalueformat');
}
if (!$this->get('id')) {
$this->data->set('value', '');
$this->data->set('valueformat', FORMAT_MOODLE);
$this->data->set('valuetrust', false);
$this->save();
}
if (array_key_exists('text', $fromform)) {
$textoptions = $this->value_editor_options();
$context = $textoptions['context'];
$data = (object) ['field_editor' => $fromform];
$data = file_postupdate_standard_editor($data, 'field', $textoptions, $context,
'customfield_textarea', 'value', $this->get('id'));
$this->data->set('value', $data->field);
$this->data->set('valueformat', $data->fieldformat);
$this->data->set('valuetrust', trusttext_trusted($context));
$this->save();
}
}
/**
* Prepares the custom field data related to the object to pass to mform->set_data() and adds them to it
*
* This function must be called before calling $form->set_data($object);
*
* @param \stdClass $instance the entity that has custom fields, if 'id' attribute is present the custom
* fields for this entity will be added, otherwise the default values will be added.
*/
public function instance_form_before_set_data(\stdClass $instance) {
$textoptions = $this->value_editor_options();
$context = $textoptions['context'];
if ($this->get('id')) {
$text = $this->get('value');
$format = $this->get('valueformat');
$temp = (object) ['field' => $text, 'fieldformat' => $format, 'fieldtrust' => trusttext_trusted($context)];
file_prepare_standard_editor($temp, 'field', $textoptions, $context, 'customfield_textarea',
'value', $this->get('id'));
$value = $temp->field_editor;
} else {
$text = $this->get_field()->get_configdata_property('defaultvalue');
$format = $this->get_field()->get_configdata_property('defaultvalueformat');
$temp = (object) ['field' => $text, 'fieldformat' => $format, 'fieldtrust' => trusttext_trusted($context)];
file_prepare_standard_editor($temp, 'field', $textoptions, $context, 'customfield_textarea',
'defaultvalue', $this->get_field()->get('id'));
$value = $temp->field_editor;
}
$instance->{$this->get_form_element_name()} = $value;
}
/**
* Checks if the value is empty, overriding the base method to ensure it's the "text" element of our value being compared
*
* @param string|string[] $value
* @return bool
*/
protected function is_empty($value): bool {
if (is_array($value)) {
$value = $value['text'];
}
return html_is_blank($value);
}
/**
* Checks if the value is unique, overriding the base method to ensure it's the "text" element of our value being compared
*
* @param mixed $value
* @return bool
*/
protected function is_unique($value): bool {
return parent::is_unique($value['text']);
}
/**
* Delete data
*
* @return bool
*/
public function delete() {
get_file_storage()->delete_area_files($this->get('contextid'), 'customfield_textarea',
'value', $this->get('id'));
return parent::delete();
}
/**
* Returns the default value as it would be stored in the database (not in human-readable format).
*
* @return mixed
*/
public function get_default_value() {
return $this->get_field()->get_configdata_property('defaultvalue');
}
/**
* Implement the backup callback for the custom field element.
* This includes any embedded files in the custom field element.
*
* @param \backup_nested_element $customfieldelement The custom field element to be backed up.
*/
public function backup_define_structure(backup_nested_element $customfieldelement): void {
$annotations = $customfieldelement->get_file_annotations();
if (!isset($annotations['customfield_textarea']['value'])) {
$customfieldelement->annotate_files('customfield_textarea', 'value', 'id');
}
}
/**
* Implement the restore callback for the custom field element.
* This includes restoring any embedded files in the custom field element.
*
* @param \restore_structure_step $step The restore step instance.
* @param int $newid The new ID for the custom field data after restore.
* @param int $oldid The original ID of the custom field data before backup.
*/
public function restore_define_structure(\restore_structure_step $step, int $newid, int $oldid): void {
if (!$step->get_mappingid('customfield_data', $oldid)) {
$step->set_mapping('customfield_data', $oldid, $newid, true);
$step->add_related_files('customfield_textarea', 'value', 'customfield_data');
}
}
/**
* Returns value in a human-readable format
*
* @return mixed|null value or null if empty
*/
public function export_value() {
global $CFG;
require_once($CFG->libdir . '/filelib.php');
$value = $this->get_value();
if ($this->is_empty($value)) {
return null;
}
if ($dataid = $this->get('id')) {
$context = $this->get_context();
$processed = file_rewrite_pluginfile_urls($value, 'pluginfile.php',
$context->id, 'customfield_textarea', 'value', $dataid);
$value = format_text($processed, $this->get('valueformat'), [
'context' => $context,
'trusted' => $this->get('valuetrust'),
]);
} else {
$field = $this->get_field();
$context = $field->get_handler()->get_configuration_context();
$processed = file_rewrite_pluginfile_urls($value, 'pluginfile.php',
$context->id, 'customfield_textarea', 'defaultvalue', $field->get('id'));
$value = format_text($processed, $field->get_configdata_property('defaultvalueformat'), [
'context' => $context,
'trusted' => true,
]);
}
return $value;
}
}
@@ -0,0 +1,152 @@
<?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/>.
/**
* Customfield textarea plugin
*
* @package customfield_textarea
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace customfield_textarea;
defined('MOODLE_INTERNAL') || die;
/**
* Class field
*
* @package customfield_textarea
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class field_controller extends \core_customfield\field_controller {
/**
* Const type
*/
const TYPE = 'textarea';
/**
* Before delete bulk actions
*/
public function delete(): bool {
global $DB;
$fs = get_file_storage();
// Delete files in the defaultvalue.
$fs->delete_area_files($this->get_handler()->get_configuration_context()->id, 'customfield_textarea',
'defaultvalue', $this->get('id'));
// Delete files in the data. We can not use $fs->delete_area_files_select() because context may be different.
$params = ['component' => 'customfield_textarea', 'filearea' => 'value', 'fieldid' => $this->get('id')];
$where = "component = :component AND filearea = :filearea
AND itemid IN (SELECT cfd.id FROM {customfield_data} cfd WHERE cfd.fieldid = :fieldid)";
$filerecords = $DB->get_recordset_select('files', $where, $params);
foreach ($filerecords as $filerecord) {
$fs->get_file_instance($filerecord)->delete();
}
$filerecords->close();
// Delete data and field.
return parent::delete();
}
/**
* Prepare the field data to set in the configuration form
*
* Necessary if some preprocessing required for editor or filemanager fields
*
* @param \stdClass $formdata
*/
public function prepare_for_config_form(\stdClass $formdata) {
if (!empty($formdata->configdata['defaultvalue'])) {
$textoptions = $this->value_editor_options();
$context = $textoptions['context'];
$record = new \stdClass();
$record->defaultvalue = $formdata->configdata['defaultvalue'];
$record->defaultvalueformat = $formdata->configdata['defaultvalueformat'];
file_prepare_standard_editor($record, 'defaultvalue', $textoptions, $context,
'customfield_textarea', 'defaultvalue', $formdata->id);
$formdata->configdata['defaultvalue_editor'] = $record->defaultvalue_editor;
}
}
/**
* Add fields for editing a textarea field.
*
* @param \MoodleQuickForm $mform
*/
public function config_form_definition(\MoodleQuickForm $mform) {
$mform->addElement('header', 'header_specificsettings', get_string('specificsettings', 'customfield_textarea'));
$mform->setExpanded('header_specificsettings', true);
$desceditoroptions = $this->value_editor_options();
$mform->addElement('editor', 'configdata[defaultvalue_editor]', get_string('defaultvalue', 'core_customfield'),
null, $desceditoroptions);
}
/**
* Options for editor
*
* @param \context|null $context context if known, otherwise configuration context will be used
* @return array
*/
public function value_editor_options(\context $context = null) {
global $CFG;
require_once($CFG->libdir.'/formslib.php');
if (!$context) {
$context = $this->get_handler()->get_configuration_context();
}
return [
'context' => $context,
'trusttext' => true,
'maxfiles' => EDITOR_UNLIMITED_FILES,
'maxbytes' => $CFG->maxbytes,
];
}
/**
* Saves the field configuration
*/
public function save() {
$configdata = $this->get('configdata');
if (!array_key_exists('defaultvalue_editor', $configdata)) {
$this->field->save();
return;
}
if (!$this->get('id')) {
$this->field->save();
}
// Store files.
$textoptions = $this->value_editor_options();
$tempvalue = (object) ['defaultvalue_editor' => $configdata['defaultvalue_editor']];
$tempvalue = file_postupdate_standard_editor($tempvalue, 'defaultvalue', $textoptions, $textoptions['context'],
'customfield_textarea', 'defaultvalue', $this->get('id'));
$configdata['defaultvalue'] = $tempvalue->defaultvalue;
$configdata['defaultvalueformat'] = $tempvalue->defaultvalueformat;
unset($configdata['defaultvalue_editor']);
$this->field->set('configdata', json_encode($configdata));
$this->field->save();
}
}
@@ -0,0 +1,97 @@
<?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/>.
/**
* Privacy Subsystem implementation for customfield_textarea.
*
* @package customfield_textarea
* @copyright 2018 Daniel Neis Araujo <danielneis@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace customfield_textarea\privacy;
use core_customfield\data_controller;
use core_customfield\privacy\customfield_provider;
use core_privacy\local\request\writer;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for customfield_textarea implementing null_provider.
*
* @copyright 2018 Daniel Neis Araujo <danielneis@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider, customfield_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
/**
* Preprocesses data object that is going to be exported
*
* @param data_controller $data
* @param \stdClass $exportdata
* @param array $subcontext
*/
public static function export_customfield_data(data_controller $data, \stdClass $exportdata, array $subcontext) {
$context = $data->get_context();
$exportdata->value = writer::with_context($context)
->rewrite_pluginfile_urls($subcontext, 'customfield_textarea', 'value',
$exportdata->id, $exportdata->value);
writer::with_context($context)
->export_data($subcontext, $exportdata)
->export_area_files($subcontext, 'customfield_textarea', 'value', $exportdata->id);
}
/**
* Allows plugins to delete everything they store related to the data (usually files)
*
* @param string $dataidstest
* @param array $params
* @param array $contextids
* @return mixed|void
*/
public static function before_delete_data(string $dataidstest, array $params, array $contextids) {
$fs = get_file_storage();
foreach ($contextids as $contextid) {
$fs->delete_area_files_select($contextid, 'customfield_textarea', 'value', $dataidstest, $params);
}
}
/**
* Allows plugins to delete everything they store related to the field configuration (usually files)
*
* The implementation should not delete data or anything related to the data, since "before_delete_data" is
* invoked separately.
*
* @param string $fieldidstest
* @param array $params
* @param array $contextids
*/
public static function before_delete_fields(string $fieldidstest, array $params, array $contextids) {
$fs = get_file_storage();
foreach ($contextids as $contextid) {
$fs->delete_area_files_select($contextid, 'customfield_textarea', 'defaultvalue', $fieldidstest, $params);
}
}
}
@@ -0,0 +1,29 @@
<?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/>.
/**
* Customfield textarea plugin
*
* @package customfield_textarea
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['pluginname'] = 'Text area';
$string['privacy:metadata'] = 'The Text area field type plugin doesn\'t store any personal data; it uses tables defined in core.';
$string['specificsettings'] = 'Text area field settings';
+76
View File
@@ -0,0 +1,76 @@
<?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/>.
/**
* Callbacks
*
* @package customfield_textarea
* @copyright 2018 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
/**
* Serve the files from the customfield_textarea file areas
*
* @param stdClass $course the course object
* @param stdClass $cm the course module object
* @param context $context the context
* @param string $filearea the name of the file area
* @param array $args extra arguments (itemid, path)
* @param bool $forcedownload whether or not force download
* @param array $options additional options affecting the file serving
* @return bool false if the file not found, just send the file otherwise and do not return
*/
function customfield_textarea_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
global $DB;
$itemid = array_shift($args);
if ($filearea === 'value') {
// Value of the data, itemid = id in data table.
$datarecord = $DB->get_record(\core_customfield\data::TABLE, ['id' => $itemid], '*', MUST_EXIST);
$field = \core_customfield\field_controller::create($datarecord->fieldid);
$data = \core_customfield\data_controller::create(0, $datarecord, $field);
$handler = $field->get_handler();
if ($field->get('type') !== 'textarea' || !$handler->can_view($field, $data->get('instanceid'))
|| $data->get_context()->id != $context->id) {
send_file_not_found();
}
} else if ($filearea === 'defaultvalue') {
// Default value of the field, itemid = id in the field table.
$field = \core_customfield\field_controller::create($itemid);
$handler = $field->get_handler();
if ($field->get('type') !== 'textarea' || $handler->get_configuration_context()->id != $context->id) {
send_file_not_found();
}
} else {
send_file_not_found();
}
$filename = array_pop($args); // The last item in the $args array.
$filepath = '/' . ($args ? implode('/', $args) . '/' : '');
// Retrieve the file from the Files API.
$fs = get_file_storage();
$file = $fs->get_file($context->id, 'customfield_textarea', $filearea, $itemid, $filepath, $filename);
if (!$file) {
send_file_not_found();
}
// We can now send the file back to the browser - in this case with a cache lifetime of 1 day and no filtering.
send_stored_file($file, DAYSECS, 0, $forcedownload, $options);
}
@@ -0,0 +1,81 @@
@customfield @customfield_textarea @javascript @editor_tiny
Feature: Default value for the textarea custom field can contain images
In order to see images on custom fields
As a manager
I need to be able to add images to the default value
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher | Teacher | 1 | teacher1@example.com |
| manager | Manager | 1 | manager1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher | C1 | editingteacher |
And the following "system role assigns" exist:
| user | course | role |
| manager | Acceptance test site | manager |
And the following "custom field categories" exist:
| name | component | area | itemid |
| Category for test | core_course | course | 0 |
And the following "user private files" exist:
| user | filepath |
| admin | lib/tests/fixtures/gd-logo.png |
And I log in as "admin"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
And I click on "Add a new custom field" "link"
And I click on "Text area" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
# Embed the image into Default value.
And I click on "Image" "button" in the "Default value" "form_row"
And I click on "Browse repositories" "button"
And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
And I click on "gd-logo.png" "link"
And I click on "Select this file" "button"
And I set the field "How would you describe this image to someone who can't see it?" to "Example"
And I click on "Save" "button" in the "Image details" "dialogue"
And I click on "Save changes" "button" in the "Adding a new Text area" "dialogue"
And I log out
Scenario: For the courses that existed before the custom field was created the default value is displayed
When I am on site homepage
Then the image at "//*[contains(@class, 'frontpage-course-list-all')]//*[contains(@class, 'customfield_textarea')]//img[contains(@src, 'pluginfile.php') and contains(@src, '/customfield_textarea/defaultvalue/') and @alt='Example']" "xpath_element" should be identical to "lib/tests/fixtures/gd-logo.png"
Scenario: Teacher will see textarea default value when editing a course created before custom field was created
# Teacher will see the image when editing existing course.
When I log in as "teacher"
And I am on "Course 1" course homepage
And I navigate to "Settings" in current page administration
And I expand all fieldsets
And I switch to the "Test field" TinyMCE editor iframe
Then "//img[contains(@src, 'draftfile.php') and contains(@src, '/gd-logo.png') and @alt='Example']" "xpath_element" should exist
And I switch to the main frame
# Save the course without changing the default value.
And I press "Save and display"
And I log out
# Now the same image is displayed as "value" and not as "defaultvalue".
And I am on site homepage
Then "//img[contains(@src, '/customfield_textarea/defaultvalue/')]" "xpath_element" should not exist
And the image at "//*[contains(@class, 'frontpage-course-list-all')]//*[contains(@class, 'customfield_textarea')]//img[contains(@src, 'pluginfile.php') and contains(@src, '/customfield_textarea/value/') and @alt='Example']" "xpath_element" should be identical to "lib/tests/fixtures/gd-logo.png"
Scenario: Manager can create a course and the default value for textarea custom field will apply.
When I log in as "manager"
And I go to the courses management page
And I click on "Create new course" "link" in the "#course-listing" "css_element"
And I set the following fields to these values:
| Course full name | Course 2 |
| Course short name | C2 |
And I expand all fieldsets
And I switch to the "Test field" TinyMCE editor iframe
Then "//img[contains(@src, 'draftfile.php') and contains(@src, '/gd-logo.png') and @alt='Example']" "xpath_element" should exist
And I switch to the main frame
And I press "Save and display"
And I log out
# Now the same image is displayed as "value" and not as "defaultvalue".
And I am on site homepage
Then the image at "//*[contains(@class, 'frontpage-course-list-all')]//*[contains(@class, 'customfield_textarea')]//img[contains(@src, 'pluginfile.php') and contains(@src, '/customfield_textarea/value/') and @alt='Example']" "xpath_element" should be identical to "lib/tests/fixtures/gd-logo.png"
@@ -0,0 +1,49 @@
@customfield @customfield_textarea @javascript
Feature: Managers can manage course custom fields textarea
In order to have additional data on the course
As a manager
I need to create, edit, remove and sort custom fields
Background:
Given the following "custom field categories" exist:
| name | component | area | itemid |
| Category for test | core_course | course | 0 |
And I log in as "admin"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
Scenario: Create a custom course textarea field
When I click on "Add a new custom field" "link"
And I click on "Text area" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I click on "Save changes" "button" in the "Adding a new Text area" "dialogue"
Then I should see "Test field"
And I log out
Scenario: Edit a custom course textarea field
When I click on "Add a new custom field" "link"
And I click on "Text area" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I click on "Save changes" "button" in the "Adding a new Text area" "dialogue"
And I click on "Edit" "link" in the "Test field" "table_row"
And I set the following fields to these values:
| Name | Edited field |
And I click on "Save changes" "button" in the "Updating Test field" "dialogue"
Then I should see "Edited field"
And I should not see "Test field"
And I log out
Scenario: Delete a custom course textarea field
When I click on "Add a new custom field" "link"
And I click on "Text area" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I click on "Save changes" "button" in the "Adding a new Text area" "dialogue"
And I click on "Delete" "link" in the "Test field" "table_row"
And I click on "Yes" "button" in the "Confirm" "dialogue"
Then I should not see "Test field"
And I log out
@@ -0,0 +1,327 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace customfield_textarea;
use core_customfield_generator;
use core_customfield_test_instance_form;
use context_user;
use context_course;
use context_system;
/**
* Functional test for customfield_textarea
*
* @package customfield_textarea
* @copyright 2019 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \customfield_textarea\field_controller
* @covers \customfield_textarea\data_controller
*/
class plugin_test extends \advanced_testcase {
/** @var \stdClass[] */
private $courses = [];
/** @var \core_customfield\category_controller */
private $cfcat;
/** @var \core_customfield\field_controller[] */
private $cfields;
/** @var \core_customfield\data_controller[] */
private $cfdata;
/**
* Tests set up.
*/
public function setUp(): void {
$this->resetAfterTest();
$this->cfcat = $this->get_generator()->create_category();
$this->cfields[1] = $this->get_generator()->create_field(
['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield1', 'type' => 'textarea']);
$this->cfields[2] = $this->get_generator()->create_field(
['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield2', 'type' => 'textarea',
'configdata' => ['required' => 1]]);
$this->cfields[3] = $this->get_generator()->create_field(
['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield3', 'type' => 'textarea',
'configdata' => ['defaultvalue' => 'Value3', 'defaultvalueformat' => FORMAT_MOODLE]]);
$this->courses[1] = $this->getDataGenerator()->create_course();
$this->courses[2] = $this->getDataGenerator()->create_course();
$this->courses[3] = $this->getDataGenerator()->create_course();
$this->cfdata[1] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[1]->id,
['text' => 'Value1', 'format' => FORMAT_MOODLE]);
$this->cfdata[2] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[2]->id,
['text' => '<br />', 'format' => FORMAT_MOODLE]);
$this->setUser($this->getDataGenerator()->create_user());
}
/**
* Get generator
* @return core_customfield_generator
*/
protected function get_generator(): core_customfield_generator {
return $this->getDataGenerator()->get_plugin_generator('core_customfield');
}
/**
* Test for initialising field and data controllers
*/
public function test_initialise(): void {
$f = \core_customfield\field_controller::create($this->cfields[1]->get('id'));
$this->assertTrue($f instanceof field_controller);
$f = \core_customfield\field_controller::create(0, (object)['type' => 'textarea'], $this->cfcat);
$this->assertTrue($f instanceof field_controller);
$d = \core_customfield\data_controller::create($this->cfdata[1]->get('id'));
$this->assertTrue($d instanceof data_controller);
$d = \core_customfield\data_controller::create(0, null, $this->cfields[1]);
$this->assertTrue($d instanceof data_controller);
}
/**
* Test for configuration form functions
*
* Create a configuration form and submit it with the same values as in the field
*/
public function test_config_form(): void {
$this->setAdminUser();
$submitdata = (array)$this->cfields[3]->to_record();
$submitdata['configdata'] = $this->cfields[3]->get('configdata');
$submitdata = \core_customfield\field_config_form::mock_ajax_submit($submitdata);
$form = new \core_customfield\field_config_form(null, null, 'post', '', null, true,
$submitdata, true);
$form->set_data_for_dynamic_submission();
$this->assertTrue($form->is_validated());
$form->process_dynamic_submission();
}
/**
* Test for instance form functions
*/
public function test_instance_form(): void {
global $CFG;
require_once($CFG->dirroot . '/customfield/tests/fixtures/test_instance_form.php');
$this->setAdminUser();
$handler = $this->cfcat->get_handler();
// First try to submit without required field.
$submitdata = (array)$this->courses[1];
core_customfield_test_instance_form::mock_submit($submitdata, []);
$form = new core_customfield_test_instance_form('POST',
['handler' => $handler, 'instance' => $this->courses[1]]);
$this->assertFalse($form->is_validated());
// Now with required field.
$submitdata['customfield_myfield2_editor'] = ['text' => 'Some text', 'format' => FORMAT_HTML];
core_customfield_test_instance_form::mock_submit($submitdata, []);
$form = new core_customfield_test_instance_form('POST',
['handler' => $handler, 'instance' => $this->courses[1]]);
$this->assertTrue($form->is_validated());
$data = $form->get_data();
$this->assertNotEmpty($data->customfield_myfield1_editor);
$this->assertNotEmpty($data->customfield_myfield2_editor);
$handler->instance_form_save($data);
}
/**
* Test that instance form save empties the field content for blank values
*/
public function test_instance_form_save_clear(): void {
global $CFG;
require_once("{$CFG->dirroot}/customfield/tests/fixtures/test_instance_form.php");
$this->setAdminUser();
$handler = $this->cfcat->get_handler();
// Set our custom field to a known value.
$submitdata = (array) $this->courses[1] + [
'customfield_myfield1_editor' => ['text' => 'I can see it in your eyes', 'format' => FORMAT_HTML],
'customfield_myfield2_editor' => ['text' => 'I can see it in your smile', 'format' => FORMAT_HTML],
];
core_customfield_test_instance_form::mock_submit($submitdata, []);
$form = new core_customfield_test_instance_form('post', ['handler' => $handler, 'instance' => $this->courses[1]]);
$handler->instance_form_save($form->get_data());
$this->assertEquals($submitdata['customfield_myfield1_editor']['text'],
\core_customfield\data_controller::create($this->cfdata[1]->get('id'))->export_value());
// Now empty our non-required field.
$submitdata['customfield_myfield1_editor']['text'] = '';
core_customfield_test_instance_form::mock_submit($submitdata, []);
$form = new core_customfield_test_instance_form('post', ['handler' => $handler, 'instance' => $this->courses[1]]);
$handler->instance_form_save($form->get_data());
$this->assertNull(\core_customfield\data_controller::create($this->cfdata[1]->get('id'))->export_value());
}
/**
* Test for data_controller::get_value and export_value
*/
public function test_get_export_value(): void {
$this->assertEquals('Value1', $this->cfdata[1]->get_value());
$this->assertEquals('<div class="text_to_html">Value1</div>', $this->cfdata[1]->export_value());
// Field with empty data.
$this->assertNull($this->cfdata[2]->export_value());
// Field without data but with a default value.
$d = \core_customfield\data_controller::create(0, null, $this->cfields[3]);
$this->assertEquals('Value3', $d->get_value());
$this->assertEquals('<div class="text_to_html">Value3</div>', $d->export_value());
}
/**
* Deleting fields and data
*/
public function test_delete(): void {
$this->cfcat->get_handler()->delete_all();
}
/**
* Test embedded file backup and restore.
*
* @covers \customfield_textarea\data_controller::backup_define_structure
* @covers \customfield_textarea\data_controller::backup_restore_structure
*/
public function test_embedded_file_backup_and_restore(): void {
global $CFG, $USER, $DB;
require_once($CFG->dirroot . '/customfield/tests/fixtures/test_instance_form.php');
$this->setAdminUser();
$handler = $this->cfcat->get_handler();
// Create a file.
$fs = get_file_storage();
$filerecord = [
'contextid' => context_user::instance($USER->id)->id,
'component' => 'user',
'filearea' => 'draft',
'itemid' => file_get_unused_draft_itemid(),
'filepath' => '/',
'filename' => 'mytextfile.txt',
];
$fs->create_file_from_string($filerecord, 'Some text contents');
// Add the file to the custom field.
$submitdata = (array) $this->courses[1];
$submitdata['customfield_myfield1_editor'] = [
'text' => 'Here is a file: @@PLUGINFILE@@/mytextfile.txt',
'format' => FORMAT_HTML,
'itemid' => $filerecord['itemid'],
];
// Set the required field and submit.
$submitdata['customfield_myfield2_editor'] = ['text' => 'Some text', 'format' => FORMAT_HTML];
core_customfield_test_instance_form::mock_submit($submitdata, []);
$form = new core_customfield_test_instance_form('POST',
['handler' => $handler, 'instance' => $this->courses[1]]);
$this->assertTrue($form->is_validated());
$data = $form->get_data();
$this->assertNotEmpty($data->customfield_myfield1_editor);
$this->assertNotEmpty($data->customfield_myfield2_editor);
$handler->instance_form_save($data);
// Check if the draft file exists.
$context = context_course::instance($this->courses[1]->id);
$file = $fs->get_file($filerecord['contextid'], $filerecord['component'], $filerecord['filearea'], $filerecord['itemid'],
$filerecord['filepath'], $filerecord['filename']);
$this->assertNotEmpty($file);
// Check if the permanent file exists.
$file = $fs->get_file($context->id, 'customfield_textarea', 'value', $this->cfdata[1]->get('id'), '/', 'mytextfile.txt');
$this->assertNotEmpty($file);
// Backup and restore the course.
$backupid = $this->backup($this->courses[1]);
$newcourseid = $this->restore($backupid, $this->courses[1], '_copy');
$newcontext = context_course::instance($newcourseid);
$newcfdata = $DB->get_record('customfield_data', ['instanceid' => $newcourseid, 'fieldid' => $this->cfields[1]->get('id')]);
// Check if the permanent file exists in the new course after restore.
$file = $fs->get_file($newcontext->id, 'customfield_textarea', 'value', $newcfdata->id, '/', 'mytextfile.txt');
$this->assertNotEmpty($file);
}
/**
* Backs a course up to temp directory.
*
* @param \stdClass $course Course object to backup
* @return string ID of backup
*/
protected function backup($course): string {
global $USER, $CFG;
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
// Turn off file logging, otherwise it can't delete the file (Windows).
$CFG->backup_file_logger_level = \backup::LOG_NONE;
// Do backup with default settings. MODE_IMPORT means it will just
// create the directory and not zip it.
$bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id,
\backup::FORMAT_MOODLE, \backup::INTERACTIVE_NO, \backup::MODE_IMPORT,
$USER->id);
$bc->get_plan()->get_setting('users')->set_status(\backup_setting::NOT_LOCKED);
$bc->get_plan()->get_setting('users')->set_value(true);
$bc->get_plan()->get_setting('logs')->set_value(true);
$backupid = $bc->get_backupid();
$bc->execute_plan();
$bc->destroy();
return $backupid;
}
/**
* Restores a course from temp directory.
*
* @param string $backupid Backup id
* @param \stdClass $course Original course object
* @param string $suffix Suffix to add after original course shortname and fullname
* @return int New course id
* @throws \restore_controller_exception
*/
protected function restore(string $backupid, $course, string $suffix): int {
global $USER, $CFG;
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
// Do restore to new course with default settings.
$newcourseid = \restore_dbops::create_new_course(
$course->fullname . $suffix, $course->shortname . $suffix, $course->category);
$rc = new \restore_controller($backupid, $newcourseid,
\backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
\backup::TARGET_NEW_COURSE);
$rc->get_plan()->get_setting('logs')->set_value(true);
$rc->get_plan()->get_setting('users')->set_value(true);
$this->assertTrue($rc->execute_precheck());
$rc->execute_plan();
$rc->destroy();
return $newcourseid;
}
}
+29
View File
@@ -0,0 +1,29 @@
<?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/>.
/**
* Customfield text area plugin
*
* @package customfield_textarea
* @copyright 2018 David Matamoros <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'customfield_textarea';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
+5
View File
@@ -0,0 +1,5 @@
This files describes API changes in /customfield/field/* - customfield field types,
information provided here is intended especially for developers.
=== 3.8 ===
* supports_course_grouping() and course_grouping_format_values() functions added to support use of custom fields in block_myoverview
+86
View File
@@ -0,0 +1,86 @@
<?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/>.
/**
* Callbacks
*
* @package core_customfield
* @copyright 2018 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core_external\external_api;
defined('MOODLE_INTERNAL') || die;
/**
* Edit customfield elements inplace
*
* @param string $itemtype
* @param int $itemid
* @param string $newvalue
* @return \core\output\inplace_editable
*/
function core_customfield_inplace_editable($itemtype, $itemid, $newvalue) {
if ($itemtype === 'category') {
$category = core_customfield\category_controller::create($itemid);
$handler = $category->get_handler();
external_api::validate_context($handler->get_configuration_context());
if (!$handler->can_configure()) {
throw new moodle_exception('nopermissionconfigure', 'core_customfield');
}
$newvalue = clean_param($newvalue, PARAM_TEXT);
$handler->rename_category($category, $newvalue);
return \core_customfield\api::get_category_inplace_editable($category, true);
}
}
/**
* Serve the files from the core_customfield file areas
*
* @param stdClass $course the course object
* @param stdClass $cm the course module object
* @param context $context the context
* @param string $filearea the name of the file area
* @param array $args extra arguments (itemid, path)
* @param bool $forcedownload whether or not force download
* @param array $options additional options affecting the file serving
* @return bool false if the file not found, just send the file otherwise and do not return
*/
function core_customfield_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
if ($filearea !== 'description') {
return false;
}
$itemid = array_shift($args);
$filename = array_pop($args); // The last item in the $args array.
$field = \core_customfield\field_controller::create($itemid);
$handler = $field->get_handler();
if ($handler->get_configuration_context()->id != $context->id) {
return false;
}
// Retrieve the file from the Files API.
$fs = get_file_storage();
$file = $fs->get_file($context->id, 'core_customfield', $filearea, $itemid, '/', $filename);
if (!$file) {
return false; // The file does not exist.
}
// We can now send the file back to the browser - in this case with a cache lifetime of 1 day and no filtering.
send_stored_file($file, DAYSECS, 0, $forcedownload, $options);
}
+33
View File
@@ -0,0 +1,33 @@
{{!
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/>.
}}
{{!
@template core_customfield/field_data
Example context (json):
{
"hasvalue": 1,
"fieldtype" : "text",
"fieldname" : "Nick name",
"fieldshortname" : "nickname",
"fieldvalue" : "Star Lord"
}
}}
{{#hasvalue}}
<div class="customfield customfield_{{type}} customfield_{{shortname}}">
<span class="customfieldname">{{{name}}}</span><span class="customfieldseparator">: </span><span class="customfieldvalue">{{{value}}}</span>
</div>
{{/hasvalue}}
+132
View File
@@ -0,0 +1,132 @@
{{!
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/>.
}}
{{!
@template core_customfield/list
Moodle list template.
The purpose of this template is to render a list.
Classes required for JS:
* none
Data attributes required for JS:
* data-component
* data-area
* data-itemid
Context variables required for this template:
* attributes Array of name / value pairs.
Example context (json):
{
"component": "core_nonexisting",
"area": "course",
"itemid": 0,
"usescategories": 1,
"categories": [
{ "id": "0",
"nameeditable": "Other fields",
"addfieldmenu": "Add field",
"fields": [
{ "id": 0, "name": "Field name", "shortname": "shortname", "type": "Text" },
{ "id": 0, "name": "Another field", "shortname": "checkme", "type": "Checkbox" }
]
},
{ "id": "00",
"nameeditable": "Empty category",
"addfieldmenu": "Add field",
"fields": [] }
],
"singleselect" : "select"
}
}}
{{{alert}}}
<div data-region="list-page" id="customfield_catlist" data-component="{{component}}" data-area="{{area}}" data-itemid="{{itemid}}">
<div class="row">
<div class="col align-self-end">
{{#usescategories}}
<a tabindex="0" role="button" class="btn btn-secondary float-right" data-role="addnewcategory">{{#str}}addnewcategory, core_customfield{{/str}}</a>
{{/usescategories}}
</div>
</div>
{{^categories}}
{{{nocategories}}}
{{/categories}}
<div class="categorieslist">
{{#categories}}
<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">
{{#usescategories}}
<h3>
<span class="movecategory icon-size-3">{{> core/drag_handle}}</span>
{{{nameeditable}}}
<a class="icon-size-3" href="#" data-role="deletecategory" data-id="{{id}}">
{{#pix}} t/delete, core, {{#str}} delete, moodle {{/str}} {{/pix}}
</a>
</h3>
{{/usescategories}}
</div>
<div class="col-auto text-right">
{{{addfieldmenu}}}
</div>
</div>
<div>
<table class="generaltable fullwidth fieldslist">
<thead>
<tr>
<th scope="col" class="col-5">{{#str}} customfield, core_customfield {{/str}}</th>
<th scope="col" class="col-3">{{#str}} shortname, core_customfield {{/str}}</th>
<th scope="col" class="col-2">{{#str}} type, core_customfield {{/str}}</th>
<th scope="col" class="col-2 text-right">{{#str}} action, core_customfield {{/str}}</th>
</tr>
</thead>
<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-3">{{{shortname}}}</td>
<td class="col-2">{{{type}}}</td>
<td class="col-2 text-right">
<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>
</td>
</tr>
{{/fields}}
{{^fields}}
{{> core_customfield/nofields }}
{{/fields}}
</tbody>
</table>
</div>
</div>
{{/categories}}
</div>
</div>
{{#js}}
require(['core_customfield/form'], function(s) {
s.init();
});
{{/js}}
+39
View File
@@ -0,0 +1,39 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_customfield/nofields
Moodle list template.
The purpose of this template is to render the nofields tbody.
Classes required for JS:
* none
Data attributes required for JS:
* data-component
* data-area
* data-itemid
Context variables required for this template:
* attributes Array of name / value pairs.
Example context (json):
{
}
}}
<tr class="nofields"><td colspan="4">{{# str }} therearenofields, core_customfield {{/ str }}</td></tr>
+254
View File
@@ -0,0 +1,254 @@
<?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;
/**
* Functional test for class \core_customfield\api
*
* @package core_customfield
* @category test
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class api_test extends \advanced_testcase {
/**
* Get generator.
*
* @return core_customfield_generator
*/
protected function get_generator(): \core_customfield_generator {
return $this->getDataGenerator()->get_plugin_generator('core_customfield');
}
/**
* Help to assert that the given property in an array of object has the expected value
*
* @param array $expected
* @param array $array array of objects with "get($property)" method
* @param string $propertyname
*/
protected function assert_property_in_array($expected, $array, $propertyname) {
$this->assertEquals($expected, array_values(array_map(function($a) use ($propertyname) {
return $a->get($propertyname);
}, $array)));
}
/**
* Tests for \core_customfield\api::move_category() behaviour.
*
* This replicates what is happening when categories are moved
* in the interface using drag-drop.
*/
public function test_move_category(): void {
$this->resetAfterTest();
// Create the categories.
$params = ['component' => 'core_course', 'area' => 'course', 'itemid' => 0];
$id0 = $this->get_generator()->create_category($params)->get('id');
$id1 = $this->get_generator()->create_category($params)->get('id');
$id2 = $this->get_generator()->create_category($params)->get('id');
$id3 = $this->get_generator()->create_category($params)->get('id');
$id4 = $this->get_generator()->create_category($params)->get('id');
$id5 = $this->get_generator()->create_category($params)->get('id');
// Check order after re-fetch.
$categories = api::get_categories_with_fields($params['component'], $params['area'], $params['itemid']);
$this->assertEquals([$id0, $id1, $id2, $id3, $id4, $id5], array_keys($categories));
$this->assert_property_in_array([0, 1, 2, 3, 4, 5], $categories, 'sortorder');
// Move up 1 position.
api::move_category(category_controller::create($id3), $id2);
$categories = api::get_categories_with_fields($params['component'], $params['area'], $params['itemid']);
$this->assertEquals([$id0, $id1, $id3, $id2, $id4, $id5], array_keys($categories));
$this->assert_property_in_array([0, 1, 2, 3, 4, 5], $categories, 'sortorder');
// Move down 1 position.
api::move_category(category_controller::create($id2), $id3);
$categories = api::get_categories_with_fields($params['component'], $params['area'], $params['itemid']);
$this->assertEquals([$id0, $id1, $id2, $id3, $id4, $id5], array_keys($categories));
$this->assert_property_in_array([0, 1, 2, 3, 4, 5], $categories, 'sortorder');
// Move up 2 positions.
api::move_category(category_controller::create($id4), $id2);
$categories = api::get_categories_with_fields($params['component'], $params['area'], $params['itemid']);
$this->assertEquals([$id0, $id1, $id4, $id2, $id3, $id5], array_keys($categories));
$this->assert_property_in_array([0, 1, 2, 3, 4, 5], $categories, 'sortorder');
// Move down 2 positions.
api::move_category(category_controller::create($id4), $id5);
$categories = api::get_categories_with_fields($params['component'], $params['area'], $params['itemid']);
$this->assertEquals([$id0, $id1, $id2, $id3, $id4, $id5], array_keys($categories));
$this->assert_property_in_array([0, 1, 2, 3, 4, 5], $categories, 'sortorder');
// Move to the end of the list.
api::move_category(category_controller::create($id2));
$categories = api::get_categories_with_fields($params['component'], $params['area'], $params['itemid']);
$this->assertEquals([$id0, $id1, $id3, $id4, $id5, $id2], array_keys($categories));
$this->assert_property_in_array([0, 1, 2, 3, 4, 5], $categories, 'sortorder');
}
/**
* Tests for \core_customfield\api::get_categories_with_fields() behaviour.
*/
public function test_get_categories_with_fields(): void {
$this->resetAfterTest();
// Create the categories.
$options = [
'component' => 'core_course',
'area' => 'course',
'itemid' => 0,
'contextid' => \context_system::instance()->id
];
$category0 = $this->get_generator()->create_category(['name' => 'aaaa'] + $options);
$category1 = $this->get_generator()->create_category(['name' => 'bbbb'] + $options);
$category2 = $this->get_generator()->create_category(['name' => 'cccc'] + $options);
$category3 = $this->get_generator()->create_category(['name' => 'dddd'] + $options);
$category4 = $this->get_generator()->create_category(['name' => 'eeee'] + $options);
$category5 = $this->get_generator()->create_category(['name' => 'ffff'] + $options);
// Let's test counts.
$this->assertCount(6, api::get_categories_with_fields($options['component'], $options['area'], $options['itemid']));
api::delete_category($category5);
$this->assertCount(5, api::get_categories_with_fields($options['component'], $options['area'], $options['itemid']));
api::delete_category($category4);
$this->assertCount(4, api::get_categories_with_fields($options['component'], $options['area'], $options['itemid']));
api::delete_category($category3);
$this->assertCount(3, api::get_categories_with_fields($options['component'], $options['area'], $options['itemid']));
api::delete_category($category2);
$this->assertCount(2, api::get_categories_with_fields($options['component'], $options['area'], $options['itemid']));
api::delete_category($category1);
$this->assertCount(1, api::get_categories_with_fields($options['component'], $options['area'], $options['itemid']));
api::delete_category($category0);
$this->assertCount(0, api::get_categories_with_fields($options['component'], $options['area'], $options['itemid']));
}
/**
* Test for functions api::save_category() and rename_category)
*/
public function test_save_category(): void {
$this->resetAfterTest();
$params = ['component' => 'core_course', 'area' => 'course', 'itemid' => 0, 'name' => 'Cat1',
'contextid' => \context_system::instance()->id];
$c1 = category_controller::create(0, (object)$params);
api::save_category($c1);
$this->assertNotEmpty($c1->get('id'));
$c1 = category_controller::create($c1->get('id'));
$expected = $params + ['sortorder' => 0, 'id' => $c1->get('id'), 'description' => '', 'descriptionformat' => 0];
$actual = array_intersect_key((array)$c1->to_record(), $expected); // Ignore timecreated, timemodified.
ksort($expected);
ksort($actual);
$this->assertEquals($expected, $actual);
// Create new category and check that the sortorder will be 1.
$params['name'] = 'Cat2';
$c2 = category_controller::create(0, (object)$params);
api::save_category($c2);
$this->assertNotEmpty($c2->get('id'));
$this->assertEquals(1, $c2->get('sortorder'));
$c2 = category_controller::create($c2->get('id'));
$this->assertEquals(1, $c2->get('sortorder'));
// Rename a category.
$c1->set('name', 'Cat3');
$c1->save();
$c1 = category_controller::create($c1->get('id'));
$this->assertEquals('Cat3', $c1->get('name'));
}
/**
* Test for function handler::create_category
*/
public function test_create_category(): void {
$this->resetAfterTest();
$handler = \core_course\customfield\course_handler::create();
$c1id = $handler->create_category();
$c1 = $handler->get_categories_with_fields()[$c1id];
$this->assertEquals('Other fields', $c1->get('name'));
$this->assertEquals($handler->get_component(), $c1->get('component'));
$this->assertEquals($handler->get_area(), $c1->get('area'));
$this->assertEquals($handler->get_itemid(), $c1->get('itemid'));
$this->assertEquals($handler->get_configuration_context()->id, $c1->get('contextid'));
// 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];
$this->assertEquals('Other fields 1', $c2->get('name'));
$this->assertEquals('Other fields 2', $c3->get('name'));
}
/**
* Tests for \core_customfield\api::delete_category() behaviour.
*/
public function test_delete_category_with_fields(): void {
$this->resetAfterTest();
global $DB;
// Create two categories with fields and data.
$options = [
'component' => 'core_course',
'area' => 'course',
'itemid' => 0,
'contextid' => \context_system::instance()->id
];
$lpg = $this->get_generator();
$course = $this->getDataGenerator()->create_course();
$dataparams = ['instanceid' => $course->id, 'contextid' => \context_course::instance($course->id)->id];
$category0 = $lpg->create_category($options);
$category1 = $lpg->create_category($options);
for ($i = 0; $i < 6; $i++) {
$f = $lpg->create_field(['categoryid' => $category0->get('id')]);
\core_customfield\data_controller::create(0, (object)$dataparams, $f)->save();
$f = $lpg->create_field(['categoryid' => $category1->get('id')]);
\core_customfield\data_controller::create(0, (object)$dataparams, $f)->save();
}
// Check that each category have fields and store ids for future checks.
list($category0, $category1) = array_values(api::get_categories_with_fields($options['component'],
$options['area'], $options['itemid']));
$category0fieldsids = array_keys($category0->get_fields());
$category1fieldsids = array_keys($category1->get_fields());
// There are 6 records in field table and 6 records in data table for each category.
list($sql, $p) = $DB->get_in_or_equal($category0fieldsids);
$this->assertCount(6, $DB->get_records_select(\core_customfield\field::TABLE, 'id '.$sql, $p));
$this->assertCount(6, $DB->get_records_select(\core_customfield\data::TABLE, 'fieldid '.$sql, $p));
list($sql, $p) = $DB->get_in_or_equal($category1fieldsids);
$this->assertCount(6, $DB->get_records_select(\core_customfield\field::TABLE, 'id '.$sql, $p));
$this->assertCount(6, $DB->get_records_select(\core_customfield\data::TABLE, 'fieldid '.$sql, $p));
// Delete one category.
$this->assertTrue($category0->get_handler()->delete_category($category0));
// Check that the category fields and data were deleted.
list($sql, $p) = $DB->get_in_or_equal($category0fieldsids);
$this->assertEmpty($DB->get_records_select(\core_customfield\field::TABLE, 'id '.$sql, $p));
$this->assertEmpty($DB->get_records_select(\core_customfield\data::TABLE, 'fieldid '.$sql, $p));
// Check that fields and data for the other category remain.
list($sql, $p) = $DB->get_in_or_equal($category1fieldsids);
$this->assertCount(6, $DB->get_records_select(\core_customfield\field::TABLE, 'id '.$sql, $p));
$this->assertCount(6, $DB->get_records_select(\core_customfield\data::TABLE, 'fieldid '.$sql, $p));
}
}
@@ -0,0 +1,104 @@
@core @core_course @core_customfield @javascript
Feature: Managers can manage categories for course custom fields
In order to have additional data on the course
As a manager
I need to create, edit, remove and sort custom field's categories
Scenario: Create a category for custom course fields
Given I log in as "admin"
When I navigate to "Courses > Default settings > Course 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 navigate to "Reports > Logs" in site administration
And I press "Get these logs"
Scenario: Edit a category name for custom course fields
Given the following "custom field categories" exist:
| name | component | area | itemid |
| Category for test | core_course | course | 0 |
And I log in as "admin"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
And I set the field "Edit category name" in the "//div[contains(@class,'categoryinstance') and contains(.,'Category for test')]" "xpath_element" to "Good fields"
Then I should not see "Category for test" in the "#customfield_catlist" "css_element"
And "New value for Category for test" "field" should not exist
And I should see "Good fields" in the "#customfield_catlist" "css_element"
And I navigate to "Reports > Logs" in site administration
And I press "Get these logs"
Scenario: Delete a category for custom course fields
Given the following "custom field categories" exist:
| name | component | area | itemid |
| Category for test | core_course | course | 0 |
And the following "custom fields" exist:
| name | category | type | shortname |
| Field 1 | Category for test | text | f1 |
And I log in as "admin"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
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 "Test category" "text" does not exist
Then I should not see "Test category" in the "#customfield_catlist" "css_element"
And I navigate to "Reports > Logs" in site administration
And I press "Get these logs"
Scenario: Move field in the course custom fields to another category
Given the following "custom field categories" exist:
| name | component | area | itemid |
| Category1 | core_course | course | 0 |
| Category2 | core_course | course | 0 |
| Category3 | core_course | course | 0 |
And the following "custom fields" exist:
| name | category | type | shortname |
| Field1 | Category1 | text | f1 |
| Field2 | Category2 | text | f2 |
When I log in as "admin"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
Then "Field1" "text" should appear after "Category1" "text"
And "Category2" "text" should appear after "Field1" "text"
And "Field2" "text" should appear after "Category2" "text"
And "Category3" "text" should appear after "Field2" "text"
And I press "Move \"Field1\""
And I follow "To the top of category Category2"
And "Category2" "text" should appear after "Category1" "text"
And "Field1" "text" should appear after "Category2" "text"
And "Field2" "text" should appear after "Field1" "text"
And "Category3" "text" should appear after "Field2" "text"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
And "Category2" "text" should appear after "Category1" "text"
And "Field1" "text" should appear after "Category2" "text"
And "Field2" "text" should appear after "Field1" "text"
And "Category3" "text" should appear after "Field2" "text"
And I press "Move \"Field1\""
And I follow "After field Field2"
And "Field1" "text" should appear after "Field2" "text"
Scenario: Reorder course custom field categories
Given the following "custom field categories" exist:
| name | component | area | itemid |
| Category1 | core_course | course | 0 |
| Category2 | core_course | course | 0 |
| Category3 | core_course | course | 0 |
And the following "custom fields" exist:
| name | category | type | shortname |
| Field1 | Category1 | text | f1 |
When I log in as "admin"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
Then "Field1" "text" should appear after "Category1" "text"
And "Category2" "text" should appear after "Field1" "text"
And "Category3" "text" should appear after "Category2" "text"
And I press "Move \"Category2\""
And I follow "After \"Category3\""
And "Field1" "text" should appear after "Category1" "text"
And "Category3" "text" should appear after "Field1" "text"
And "Category2" "text" should appear after "Category3" "text"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
And "Field1" "text" should appear after "Category1" "text"
And "Category3" "text" should appear after "Field1" "text"
And "Category2" "text" should appear after "Category3" "text"
And I press "Move \"Category2\""
And I follow "After \"Category1\""
And "Field1" "text" should appear after "Category1" "text"
And "Category2" "text" should appear after "Field1" "text"
And "Category3" "text" should appear after "Category2" "text"
@@ -0,0 +1,114 @@
@core @core_course @core_customfield @javascript
Feature: Teachers can edit course custom fields
In order to have additional data on the course
As a teacher
I need to edit data for custom fields
Background:
Given the following "custom field categories" exist:
| name | component | area | itemid |
| Category for test | core_course | course | 0 |
And the following "custom fields" exist:
| name | category | type | shortname | description | configdata |
| Field 1 | Category for test | text | f1 | d1 | |
| Field 2 | Category for test | textarea | f2 | d2 | |
| Field 3 | Category for test | checkbox | f3 | d3 | |
| Field 4 | Category for test | date | f4 | d4 | |
| Field 5 | Category for test | select | f5 | d5 | {"options":"a\nb\nc"} |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "user private files" exist:
| user | filepath |
| admin | lib/tests/fixtures/gd-logo.png |
Scenario: Display custom fields on course edit form
When I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Settings" in current page administration
And I expand all fieldsets
Then I should see "Category for test"
And I should see "Field 1"
And I should see "Field 2"
And I should see "Field 3"
And I should see "Field 4"
And I should see "Field 5"
And I log out
Scenario: Create a course with custom fields from the management interface
When I log in as "admin"
And I go to the courses management page
And I should see the "Categories" management page
And I click on category "Category 1" in the management interface
And I should see the "Course categories and courses" management page
And I click on "Create new course" "link" in the "#course-listing" "css_element"
And I set the following fields to these values:
| Course full name | Course 2 |
| Course short name | C2 |
| Field 1 | testcontent1 |
| Field 2 | testcontent2 |
| Field 3 | 1 |
| customfield_f4[enabled] | 1 |
| customfield_f4[day] | 1 |
| customfield_f4[month] | January |
| customfield_f4[year] | 2019 |
| Field 5 | b |
And I press "Save and display"
And I navigate to "Settings" in current page administration
And the following fields match these values:
| Course full name | Course 2 |
| Course short name | C2 |
| Field 1 | testcontent1 |
| Field 2 | testcontent2 |
| Field 3 | 1 |
| customfield_f4[day] | 1 |
| customfield_f4[month] | January |
| customfield_f4[year] | 2019 |
| Field 5 | b |
And I log out
@javascript @editor_tiny
Scenario: Use images in the custom field description
When I log in as "admin"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
And I click on "Edit" "link" in the "Field 1" "table_row"
And I click on "Image" "button" in the "Description" "form_row"
And I click on "Browse repositories" "button"
And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
And I click on "gd-logo.png" "link"
And I click on "Select this file" "button"
And I set the field "How would you describe this image to someone who can't see it?" to "Example"
And I click on "Save" "button" in the "Image details" "dialogue"
And I click on "Save changes" "button" in the "Updating Field 1" "dialogue"
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Settings" in current page administration
And I expand all fieldsets
Then the image at "//div[contains(@class, 'fitem')][contains(., 'Field 1')]/following-sibling::div[1]//img[contains(@src, 'pluginfile.php') and contains(@src, '/core_customfield/description/') and @alt='Example']" "xpath_element" should be identical to "lib/tests/fixtures/gd-logo.png"
And I log out
@javascript
Scenario: Custom field short name must be present and unique
When I log in as "admin"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
And I click on "Add a new custom field" "link"
And I click on "Short text" "link"
And I set the following fields to these values:
| Name | Test field |
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
Then I should see "You must supply a value here" in the "Short name" "form_row"
And I set the field "Short name" to "short name"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
Then I should see "The short name can only contain alphanumeric lowercase characters and underscores (_)." in the "Short name" "form_row"
And I set the field "Short name" to "f1"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
Then I should see "Short name already exists" in the "Short name" "form_row"
And I click on "Cancel" "button" in the "Adding a new Short text" "dialogue"
And I log out
@@ -0,0 +1,58 @@
@core @core_course @core_customfield @javascript
Feature: Requiredness The course custom fields can be mandatory or not
In order to make users required to fill a custom field
As a manager
I can change the requiredness of the fields
Background:
Given the following "custom field categories" exist:
| name | component | area | itemid |
| Category for test | core_course | course | 0 |
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
Scenario: A required course custom field must be filled when editing course settings
When I log in as "admin"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
And I click on "Add a new custom field" "link"
And I click on "Short text" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
| Required | Yes |
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Settings" in current page administration
And I press "Save and display"
Then I should see "You must supply a value here"
And I set the field "Test field" to "some value"
And I press "Save and display"
And I should not see "This field is required"
And I log out
Scenario: A course custom field that is not required may not be filled
When I log in as "admin"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
And I click on "Add a new custom field" "link"
And I click on "Short text" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
| Required | No |
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Settings" in current page administration
And I press "Save and display"
Then I should see "Course 1"
And I should see "New section"
@@ -0,0 +1,74 @@
@core @core_course @core_customfield @javascript
Feature: Uniqueness The course custom fields can be mandatory or not
In order to make users required to fill a custom field
As a manager
I can change the uniqueness of the fields
Background:
Given the following "custom field categories" exist:
| name | component | area | itemid |
| Category for test | core_course | course | 0 |
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
| Course 2 | C2 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| teacher1 | C2 | editingteacher |
When I log in as "admin"
And I navigate to "Courses > Default settings > Course custom fields" in site administration
And I click on "Add a new custom field" "link"
And I click on "Short text" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
| Unique data | Yes |
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I log out
Scenario: A course custom field with unique data must not allow same data in same field in different courses
When I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Settings" in current page administration
And I set the following fields to these values:
| Test field | testcontent |
And I press "Save and display"
And I am on "Course 2" course homepage
And I navigate to "Settings" in current page administration
And I set the following fields to these values:
| Test field | testcontent |
And I press "Save and display"
Then I should see "This value is already used"
Scenario: A course custom field with unique data must not compare with itself
When I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Settings" in current page administration
And I set the following fields to these values:
| Test field | testcontent |
And I press "Save and display"
And I am on "Course 1" course homepage
And I navigate to "Settings" in current page administration
And I set the following fields to these values:
| Test field | testcontent |
And I press "Save and display"
Then I should not see "This value is already used"
And I should see "New section"
Scenario: A course custom field with unique data must allow empty data
When I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Settings" in current page administration
And I set the following fields to these values:
| Test field | |
And I press "Save and display"
And I am on "Course 2" course homepage
And I navigate to "Settings" in current page administration
And I set the following fields to these values:
| Test field | |
And I press "Save and display"
Then I should not see "This value is already used"
@@ -0,0 +1,275 @@
<?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_customfield_generator;
/**
* Functional test for class \core_customfield\category_controller.
*
* @package core_customfield
* @category test
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core_customfield\category_controller
*/
class category_controller_test extends \advanced_testcase {
/**
* Get generator.
*
* @return core_customfield_generator
*/
protected function get_generator(): core_customfield_generator {
return $this->getDataGenerator()->get_plugin_generator('core_customfield');
}
/**
* Test for the field_controller::__construct function.
*/
public function test_constructor(): void {
$this->resetAfterTest();
$c = category_controller::create(0, (object)['component' => 'core_course', 'area' => 'course', 'itemid' => 0]);
$handler = $c->get_handler();
$this->assertTrue($c instanceof category_controller);
$cat = $this->get_generator()->create_category();
$c = category_controller::create($cat->get('id'));
$this->assertTrue($c instanceof category_controller);
$c = category_controller::create($cat->get('id'), null, $handler);
$this->assertTrue($c instanceof category_controller);
$c = category_controller::create(0, $cat->to_record());
$this->assertTrue($c instanceof category_controller);
$c = category_controller::create(0, $cat->to_record(), $handler);
$this->assertTrue($c instanceof category_controller);
}
/**
* Test creation of category instance from pre-defined object
*/
public function test_constructor_from_record(): void {
$this->resetAfterTest();
// Create field object that matches the persistent/schema definition.
$category = category_controller::create(0, (object) [
'name' => 'Test',
'description' => null,
'descriptionformat' => null,
'component' => 'core_course',
'area' => 'course',
'itemid' => 0,
'sortorder' => null,
]);
// Saving the category will validate the persistent internally.
$category->save();
$this->assertInstanceOf(category_controller::class, $category);
}
/**
* Test for function \core_customfield\field_controller::create() in case of wrong parameters
*/
public function test_constructor_errors(): void {
global $DB;
$this->resetAfterTest();
$cat = $this->get_generator()->create_category();
$catrecord = $cat->to_record();
// Both id and record give warning.
$c = category_controller::create($catrecord->id, $catrecord);
$debugging = $this->getDebuggingMessages();
$this->assertEquals(1, count($debugging));
$this->assertEquals('Too many parameters, either id need to be specified or a record, but not both.',
$debugging[0]->message);
$this->resetDebugging();
$this->assertTrue($c instanceof category_controller);
// Retrieve non-existing data.
try {
category_controller::create($catrecord->id + 1);
$this->fail('Expected exception');
} catch (\moodle_exception $e) {
$this->assertEquals('Category not found', $e->getMessage());
$this->assertEquals(\moodle_exception::class, get_class($e));
}
// Missing required elements.
try {
category_controller::create(0, (object)['area' => 'course', 'itemid' => 0]);
$this->fail('Expected exception');
} catch (\coding_exception $e) {
$this->assertEquals('Coding error detected, it must be fixed by a programmer: Not enough parameters ' .
'to initialise category_controller - unknown component', $e->getMessage());
$this->assertEquals(\coding_exception::class, get_class($e));
}
// Missing required elements.
try {
category_controller::create(0, (object)['component' => 'core_course', 'itemid' => 0]);
$this->fail('Expected exception');
} catch (\coding_exception $e) {
$this->assertEquals('Coding error detected, it must be fixed by a programmer: Not enough parameters ' .
'to initialise category_controller - unknown area', $e->getMessage());
$this->assertEquals(\coding_exception::class, get_class($e));
}
// Missing required elements.
try {
category_controller::create(0, (object)['component' => 'core_course', 'area' => 'course']);
$this->fail('Expected exception');
} catch (\coding_exception $e) {
$this->assertEquals('Coding error detected, it must be fixed by a programmer: Not enough parameters ' .
'to initialise category_controller - unknown itemid', $e->getMessage());
$this->assertEquals(\coding_exception::class, get_class($e));
}
$handler = \core_course\customfield\course_handler::create();
// Missing required elements.
try {
category_controller::create(0, (object)['component' => 'x', 'area' => 'course', 'itemid' => 0], $handler);
$this->fail('Expected exception');
} catch (\coding_exception $e) {
$this->assertEquals('Coding error detected, it must be fixed by a programmer: Component of the handler ' .
'does not match the one from the record', $e->getMessage());
$this->assertEquals(\coding_exception::class, get_class($e));
}
try {
category_controller::create(0, (object)['component' => 'core_course', 'area' => 'x', 'itemid' => 0], $handler);
$this->fail('Expected exception');
} catch (\coding_exception $e) {
$this->assertEquals('Coding error detected, it must be fixed by a programmer: Area of the handler ' .
'does not match the one from the record', $e->getMessage());
$this->assertEquals(\coding_exception::class, get_class($e));
}
try {
category_controller::create(0, (object)['component' => 'core_course', 'area' => 'course', 'itemid' => 1], $handler);
$this->fail('Expected exception');
} catch (\coding_exception $e) {
$this->assertEquals('Coding error detected, it must be fixed by a programmer: Itemid of the ' .
'handler does not match the one from the record', $e->getMessage());
$this->assertEquals(\coding_exception::class, get_class($e));
}
try {
$user = $this->getDataGenerator()->create_user();
category_controller::create(0, (object)['component' => 'core_course', 'area' => 'course', 'itemid' => 0,
'contextid' => \context_user::instance($user->id)->id], $handler);
$this->fail('Expected exception');
} catch (\coding_exception $e) {
$this->assertEquals('Coding error detected, it must be fixed by a programmer: Context of the ' .
'handler does not match the one from the record', $e->getMessage());
$this->assertEquals(\coding_exception::class, get_class($e));
}
}
/**
* Tests for behaviour of:
* \core_customfield\category_controller::save()
* \core_customfield\category_controller::get()
*/
public function test_create_category(): void {
$this->resetAfterTest();
// Create the category.
$lpg = $this->get_generator();
$categorydata = new \stdClass();
$categorydata->name = 'Category1';
$categorydata->component = 'core_course';
$categorydata->area = 'course';
$categorydata->itemid = 0;
$categorydata->contextid = \context_system::instance()->id;
$category = category_controller::create(0, $categorydata);
$category->save();
$this->assertNotEmpty($category->get('id'));
// Confirm record exists.
$this->assertTrue(\core_customfield\category::record_exists($category->get('id')));
// Confirm that base data was inserted correctly.
$category = category_controller::create($category->get('id'));
$this->assertSame($category->get('name'), $categorydata->name);
$this->assertSame($category->get('component'), $categorydata->component);
$this->assertSame($category->get('area'), $categorydata->area);
$this->assertSame((int)$category->get('itemid'), $categorydata->itemid);
}
/**
* Tests for \core_customfield\category_controller::set() behaviour.
*/
public function test_rename_category(): void {
$this->resetAfterTest();
// Create the category.
$params = ['component' => 'core_course', 'area' => 'course', 'itemid' => 0, 'name' => 'Cat1',
'contextid' => \context_system::instance()->id];
$c1 = category_controller::create(0, (object)$params);
$c1->save();
$this->assertNotEmpty($c1->get('id'));
// Checking new name are correct updated.
$category = category_controller::create($c1->get('id'));
$category->set('name', 'Cat2');
$this->assertSame('Cat2', $category->get('name'));
// Checking new name are correct updated after save.
$category->save();
$category = category_controller::create($c1->get('id'));
$this->assertSame('Cat2', $category->get('name'));
}
/**
* Tests for \core_customfield\category_controller::delete() behaviour.
*/
public function test_delete_category(): void {
$this->resetAfterTest();
// Create the category.
$lpg = $this->get_generator();
$category0 = $lpg->create_category();
$id0 = $category0->get('id');
$category1 = $lpg->create_category();
$id1 = $category1->get('id');
$category2 = $lpg->create_category();
$id2 = $category2->get('id');
// Confirm that exist in the database.
$this->assertTrue(\core_customfield\category::record_exists($id0));
// Delete and confirm that is deleted.
$category0->delete();
$this->assertFalse(\core_customfield\category::record_exists($id0));
// Confirm correct order after delete.
// Check order after re-fetch.
$category1 = category_controller::create($id1);
$category2 = category_controller::create($id2);
$this->assertSame((int) $category1->get('sortorder'), 1);
$this->assertSame((int) $category2->get('sortorder'), 2);
}
}
+180
View File
@@ -0,0 +1,180 @@
<?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_customfield_generator;
use customfield_checkbox;
use customfield_date;
use customfield_select;
use customfield_text;
use customfield_textarea;
/**
* Functional test for class data_controller.
*
* @package core_customfield
* @category test
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class data_controller_test extends \advanced_testcase {
/**
* Get generator.
*
* @return core_customfield_generator
*/
protected function get_generator(): core_customfield_generator {
return $this->getDataGenerator()->get_plugin_generator('core_customfield');
}
/**
* Test for function data_controller::create()
*/
public function test_constructor(): void {
global $DB;
$this->resetAfterTest();
// Create a course, fields category and fields.
$course = $this->getDataGenerator()->create_course();
$category0 = $this->get_generator()->create_category(['name' => 'aaaa']);
// Add fields to this category.
$fielddata = new \stdClass();
$fielddata->categoryid = $category0->get('id');
$fielddata->configdata = "{\"required\":\"0\",\"uniquevalues\":\"0\",\"locked\":\"0\",\"visibility\":\"0\",
\"defaultvalue\":\"\",\"displaysize\":0,\"maxlength\":0,\"ispassword\":\"0\",
\"link\":\"\",\"linktarget\":\"\"}";
$fielddata->type = 'checkbox';
$field0 = $this->get_generator()->create_field($fielddata);
$fielddata->type = 'date';
$field1 = $this->get_generator()->create_field($fielddata);
$fielddata->type = 'select';
$field2 = $this->get_generator()->create_field($fielddata);
$fielddata->type = 'text';
$field3 = $this->get_generator()->create_field($fielddata);
$fielddata->type = 'textarea';
$field4 = $this->get_generator()->create_field($fielddata);
$params = ['instanceid' => $course->id, 'contextid' => \context_course::instance($course->id)->id];
// Generate new data_controller records for these fields, specifying field controller or fieldid or both.
$data0 = data_controller::create(0, (object)$params, $field0);
$this->assertInstanceOf(customfield_checkbox\data_controller::class, $data0);
$data1 = data_controller::create(0,
(object)($params + ['fieldid' => $field1->get('id')]), $field1);
$this->assertInstanceOf(customfield_date\data_controller::class, $data1);
$data2 = data_controller::create(0,
(object)($params + ['fieldid' => $field2->get('id')]));
$this->assertInstanceOf(customfield_select\data_controller::class, $data2);
$data3 = data_controller::create(0, (object)$params, $field3);
$this->assertInstanceOf(customfield_text\data_controller::class, $data3);
$data4 = data_controller::create(0, (object)$params, $field4);
$this->assertInstanceOf(customfield_textarea\data_controller::class, $data4);
// Save data so we can have ids.
$data0->save();
$data1->save();
$data2->save();
$data3->save();
$data4->save();
// Retrieve data by id.
$this->assertInstanceOf(customfield_checkbox\data_controller::class, data_controller::create($data0->get('id')));
$this->assertInstanceOf(customfield_date\data_controller::class, data_controller::create($data1->get('id')));
// Retrieve data by id and field.
$this->assertInstanceOf(customfield_select\data_controller::class,
data_controller::create($data2->get('id'), null, $field2));
// Retrieve data by record without field.
$datarecord = $DB->get_record(\core_customfield\data::TABLE, ['id' => $data3->get('id')], '*', MUST_EXIST);
$this->assertInstanceOf(customfield_text\data_controller::class, data_controller::create(0, $datarecord));
// Retrieve data by record with field.
$datarecord = $DB->get_record(\core_customfield\data::TABLE, ['id' => $data4->get('id')], '*', MUST_EXIST);
$this->assertInstanceOf(customfield_textarea\data_controller::class, data_controller::create(0, $datarecord, $field4));
}
/**
* Test for function \core_customfield\field_controller::create() in case of wrong parameters
*/
public function test_constructor_errors(): void {
global $DB;
$this->resetAfterTest();
// Create a category, field and data.
$category = $this->get_generator()->create_category();
$field = $this->get_generator()->create_field(['categoryid' => $category->get('id')]);
$course = $this->getDataGenerator()->create_course();
$data = data_controller::create(0, (object)['instanceid' => $course->id,
'contextid' => \context_course::instance($course->id)->id], $field);
$data->save();
$datarecord = $DB->get_record(\core_customfield\data::TABLE, ['id' => $data->get('id')], '*', MUST_EXIST);
// Both id and record give warning.
$d = data_controller::create($datarecord->id, $datarecord);
$debugging = $this->getDebuggingMessages();
$this->assertEquals(1, count($debugging));
$this->assertEquals('Too many parameters, either id need to be specified or a record, but not both.',
$debugging[0]->message);
$this->resetDebugging();
$this->assertInstanceOf(customfield_text\data_controller::class, $d);
// Retrieve non-existing data.
try {
data_controller::create($datarecord->id + 1);
$this->fail('Expected exception');
} catch (\dml_missing_record_exception $e) {
$this->assertStringMatchesFormat('Can\'t find data record in database table customfield_data%a', $e->getMessage());
$this->assertEquals(\dml_missing_record_exception::class, get_class($e));
}
// Missing field id.
try {
data_controller::create(0, (object)['instanceid' => $course->id]);
$this->fail('Expected exception');
} catch (\coding_exception $e) {
$this->assertEquals('Coding error detected, it must be fixed by a programmer: Not enough parameters to ' .
'initialise data_controller - unknown field', $e->getMessage());
$this->assertEquals(\coding_exception::class, get_class($e));
}
// Mismatching field id.
try {
data_controller::create(0, (object)['instanceid' => $course->id, 'fieldid' => $field->get('id') + 1], $field);
$this->fail('Expected exception');
} catch (\coding_exception $e) {
$this->assertEquals('Coding error detected, it must be fixed by a programmer: Field id from the record ' .
'does not match field from the parameter', $e->getMessage());
$this->assertEquals(\coding_exception::class, get_class($e));
}
// Nonexisting class.
try {
$field->set('type', 'invalid');
data_controller::create(0, (object)['instanceid' => $course->id], $field);
$this->fail('Expected exception');
} catch (\moodle_exception $e) {
$this->assertEquals('Field type invalid not found', $e->getMessage());
$this->assertEquals(\moodle_exception::class, get_class($e));
}
}
}
+270
View File
@@ -0,0 +1,270 @@
<?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_customfield_generator;
use customfield_checkbox;
use customfield_date;
use customfield_select;
use customfield_text;
use customfield_textarea;
/**
* Functional test for class \core_customfield\field_controller.
*
* @package core_customfield
* @category test
* @copyright 2018 Ruslan Kabalin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core_customfield\field_controller
*/
class field_controller_test extends \advanced_testcase {
/**
* Get generator.
*
* @return core_customfield_generator
*/
protected function get_generator(): core_customfield_generator {
return $this->getDataGenerator()->get_plugin_generator('core_customfield');
}
/**
* Test for function \core_customfield\field_controller::create()
*/
public function test_constructor(): void {
global $DB;
$this->resetAfterTest();
// Create the category.
$category0 = $this->get_generator()->create_category();
// Initiate objects without id, try with the category object or with category id or with both.
$field0 = field_controller::create(0, (object)['type' => 'checkbox'], $category0);
$this->assertInstanceOf(customfield_checkbox\field_controller::class, $field0);
$field1 = field_controller::create(0, (object)['type' => 'date', 'categoryid' => $category0->get('id')]);
$this->assertInstanceOf(customfield_date\field_controller::class, $field1);
$field2 = field_controller::create(0, (object)['type' => 'select', 'categoryid' => $category0->get('id')], $category0);
$this->assertInstanceOf(customfield_select\field_controller::class, $field2);
$field3 = field_controller::create(0, (object)['type' => 'text'], $category0);
$this->assertInstanceOf(customfield_text\field_controller::class, $field3);
$field4 = field_controller::create(0, (object)['type' => 'textarea'], $category0);
$this->assertInstanceOf(customfield_textarea\field_controller::class, $field4);
// Save fields to the db so we have ids.
\core_customfield\api::save_field_configuration($field0, (object)['name' => 'a', 'shortname' => 'a']);
\core_customfield\api::save_field_configuration($field1, (object)['name' => 'b', 'shortname' => 'b']);
\core_customfield\api::save_field_configuration($field2, (object)['name' => 'c', 'shortname' => 'c']);
\core_customfield\api::save_field_configuration($field3, (object)['name' => 'd', 'shortname' => 'd']);
\core_customfield\api::save_field_configuration($field4, (object)['name' => 'e', 'shortname' => 'e']);
// Retrieve fields by id.
$this->assertInstanceOf(customfield_checkbox\field_controller::class, field_controller::create($field0->get('id')));
$this->assertInstanceOf(customfield_date\field_controller::class, field_controller::create($field1->get('id')));
// Retrieve field by id and category.
$this->assertInstanceOf(customfield_select\field_controller::class,
field_controller::create($field2->get('id'), null, $category0));
// Retrieve fields by record without category.
$fieldrecord = $DB->get_record(\core_customfield\field::TABLE, ['id' => $field3->get('id')], '*', MUST_EXIST);
$this->assertInstanceOf(customfield_text\field_controller::class, field_controller::create(0, $fieldrecord));
// Retrieve fields by record with category.
$fieldrecord = $DB->get_record(\core_customfield\field::TABLE, ['id' => $field4->get('id')], '*', MUST_EXIST);
$this->assertInstanceOf(customfield_textarea\field_controller::class,
field_controller::create(0, $fieldrecord, $category0));
}
/**
* Test creation of field instance from pre-defined object
*/
public function test_constructor_from_record(): void {
$this->resetAfterTest();
// Create field object that matches the persistent/schema definition.
$category = $this->get_generator()->create_category();
$field = field_controller::create(0, (object) [
'name' => 'Test',
'shortname' => 'test',
'type' => 'text',
'description' => null,
'descriptionformat' => null,
'sortorder' => null,
'configdata' => null,
], $category);
// Saving the field will validate the persistent internally.
$field->save();
$this->assertInstanceOf(\customfield_text\field_controller::class, $field);
}
/**
* Test for function \core_customfield\field_controller::create() in case of wrong parameters
*/
public function test_constructor_errors(): void {
global $DB;
$this->resetAfterTest();
// Create a category and a field.
$category = $this->get_generator()->create_category();
$field = $this->get_generator()->create_field(['categoryid' => $category->get('id')]);
$fieldrecord = $DB->get_record(\core_customfield\field::TABLE, ['id' => $field->get('id')], '*', MUST_EXIST);
// Both id and record give warning.
$field = field_controller::create($fieldrecord->id, $fieldrecord);
$debugging = $this->getDebuggingMessages();
$this->assertEquals(1, count($debugging));
$this->assertEquals('Too many parameters, either id need to be specified or a record, but not both.',
$debugging[0]->message);
$this->resetDebugging();
$this->assertInstanceOf(customfield_text\field_controller::class, $field);
// Retrieve non-existing field.
try {
field_controller::create($fieldrecord->id + 1);
$this->fail('Expected exception');
} catch (\moodle_exception $e) {
$this->assertEquals('Field not found', $e->getMessage());
$this->assertEquals(\moodle_exception::class, get_class($e));
}
// Retrieve without id and without type.
try {
field_controller::create(0, (object)['name' => 'a'], $category);
$this->fail('Expected exception');
} catch (\coding_exception $e) {
$this->assertEquals('Coding error detected, it must be fixed by a programmer: Not enough parameters to ' .
'initialise field_controller - unknown field type', $e->getMessage());
$this->assertEquals(\coding_exception::class, get_class($e));
}
// Missing category id.
try {
field_controller::create(0, (object)['type' => 'text']);
$this->fail('Expected exception');
} catch (\coding_exception $e) {
$this->assertEquals('Coding error detected, it must be fixed by a programmer: Not enough parameters ' .
'to initialise field_controller - unknown category', $e->getMessage());
$this->assertEquals(\coding_exception::class, get_class($e));
}
// Mismatching category id.
try {
field_controller::create(0, (object)['type' => 'text', 'categoryid' => $category->get('id') + 1], $category);
$this->fail('Expected exception');
} catch (\coding_exception $e) {
$this->assertEquals('Coding error detected, it must be fixed by a programmer: Category of the field ' .
'does not match category from the parameter', $e->getMessage());
$this->assertEquals(\coding_exception::class, get_class($e));
}
// Non-existing type.
try {
field_controller::create(0, (object)['type' => 'nonexisting'], $category);
$this->fail('Expected exception');
} catch (\moodle_exception $e) {
$this->assertEquals('Field type nonexisting not found', $e->getMessage());
$this->assertEquals(\moodle_exception::class, get_class($e));
}
}
/**
* Tests for behaviour of:
* \core_customfield\field_controller::save()
* \core_customfield\field_controller::get()
* \core_customfield\field_controller::get_category()
*/
public function test_create_field(): void {
global $DB;
$this->resetAfterTest();
$lpg = $this->get_generator();
$category = $lpg->create_category();
$fields = $DB->get_records(\core_customfield\field::TABLE, ['categoryid' => $category->get('id')]);
$this->assertCount(0, $fields);
// Create field.
$fielddata = new \stdClass();
$fielddata->name = 'Field';
$fielddata->shortname = 'field';
$fielddata->type = 'text';
$fielddata->categoryid = $category->get('id');
$field = field_controller::create(0, $fielddata);
$field->save();
$fields = $DB->get_records(\core_customfield\field::TABLE, ['categoryid' => $category->get('id')]);
$this->assertCount(1, $fields);
$this->assertTrue(\core_customfield\field::record_exists($field->get('id')));
$this->assertInstanceOf(\customfield_text\field_controller::class, $field);
$this->assertSame($field->get('name'), $fielddata->name);
$this->assertSame($field->get('type'), $fielddata->type);
$this->assertEquals($field->get_category()->get('id'), $category->get('id'));
}
/**
* Tests for \core_customfield\field_controller::delete() behaviour.
*/
public function test_delete_field(): void {
global $DB;
$this->resetAfterTest();
$lpg = $this->get_generator();
$category = $lpg->create_category();
$fields = $DB->get_records(\core_customfield\field::TABLE, ['categoryid' => $category->get('id')]);
$this->assertCount(0, $fields);
// Create field using generator.
$field1 = $lpg->create_field(array('categoryid' => $category->get('id')));
$field2 = $lpg->create_field(array('categoryid' => $category->get('id')));
$fields = $DB->get_records(\core_customfield\field::TABLE, ['categoryid' => $category->get('id')]);
$this->assertCount(2, $fields);
// Delete fields.
$this->assertTrue($field1->delete());
$this->assertTrue($field2->delete());
// Check that the fields have been deleted.
$fields = $DB->get_records(\core_customfield\field::TABLE, ['categoryid' => $category->get('id')]);
$this->assertCount(0, $fields);
$this->assertFalse(\core_customfield\field::record_exists($field1->get('id')));
$this->assertFalse(\core_customfield\field::record_exists($field2->get('id')));
}
/**
* Tests for \core_customfield\field_controller::get_configdata_property() behaviour.
*/
public function test_get_configdata_property(): void {
$this->resetAfterTest();
$lpg = $this->get_generator();
$category = $lpg->create_category();
$configdata = ['a' => 'b', 'c' => ['d', 'e']];
$field = field_controller::create(0, (object)['type' => 'text',
'configdata' => json_encode($configdata), 'shortname' => 'a', 'name' => 'a'], $category);
$field->save();
// Retrieve field and check configdata.
$field = field_controller::create($field->get('id'));
$this->assertEquals($configdata, $field->get('configdata'));
$this->assertEquals('b', $field->get_configdata_property('a'));
$this->assertEquals(['d', 'e'], $field->get_configdata_property('c'));
$this->assertEquals(null, $field->get_configdata_property('x'));
}
}
+79
View File
@@ -0,0 +1,79 @@
<?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/>.
/**
* Class core_customfield_test_instance_form
*
* @package core_customfield
* @copyright 2019 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . '/formslib.php');
/**
* Class core_customfield_test_instance_form
*
* @package core_customfield
* @copyright 2019 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_customfield_test_instance_form extends moodleform {
/** @var \core_customfield\handler */
protected $handler;
/** @var stdClass */
protected $instance;
/**
* Form definition
*/
public function definition() {
$this->handler = $this->_customdata['handler'];
$this->instance = $this->_customdata['instance'];
$this->_form->addElement('hidden', 'id');
$this->_form->setType('id', PARAM_INT);
$this->handler->instance_form_definition($this->_form, $this->instance->id);
$this->add_action_buttons();
$this->handler->instance_form_before_set_data($this->instance);
$this->set_data($this->instance);
}
/**
* Definition after data
*/
public function definition_after_data() {
$this->handler->instance_form_definition_after_data($this->_form, $this->instance->id);
}
/**
* Form validation
*
* @param array $data
* @param array $files
* @return array
*/
public function validation($data, $files) {
return $this->handler->instance_form_validation($data, $files);
}
}
+164
View File
@@ -0,0 +1,164 @@
<?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/>.
/**
* Customfield data generator.
*
* @package core_customfield
* @category test
* @copyright 2018 Ruslan Kabalin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
use \core_customfield\category_controller;
use \core_customfield\field_controller;
use \core_customfield\api;
/**
* Customfield data generator class.
*
* @package core_customfield
* @category test
* @copyright 2018 Ruslan Kabalin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_customfield_generator extends component_generator_base {
/** @var int Number of created categories. */
protected $categorycount = 0;
/** @var int Number of created fields. */
protected $fieldcount = 0;
/**
* Create a new category.
*
* @param array|stdClass $record
* @return category_controller
*/
public function create_category($record = null) {
$this->categorycount++;
$i = $this->categorycount;
$record = (object) $record;
if (!isset($record->name)) {
$record->name = "Category $i";
}
if (!isset($record->component)) {
$record->component = 'core_course';
}
if (!isset($record->area)) {
$record->area = 'course';
}
if (!isset($record->itemid)) {
$record->itemid = 0;
}
$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];
}
/**
* Create a new field.
*
* @param array|stdClass $record
* @return field_controller
*/
public function create_field($record): field_controller {
$this->fieldcount++;
$i = $this->fieldcount;
$record = (object) $record;
if (empty($record->categoryid)) {
throw new coding_exception('The categoryid value is required.');
}
$category = category_controller::create($record->categoryid);
$handler = $category->get_handler();
if (!isset($record->name)) {
$record->name = "Field $i";
}
if (!isset($record->shortname)) {
$record->shortname = "fld$i";
}
if (!property_exists($record, 'description')) {
$record->description = "Field $i description";
}
if (!isset($record->descriptionformat)) {
$record->descriptionformat = FORMAT_HTML;
}
if (!isset($record->type)) {
$record->type = 'text';
}
if (!isset($record->sortorder)) {
$record->sortorder = 0;
}
if (empty($record->configdata)) {
$configdata = [];
} else if (is_array($record->configdata)) {
$configdata = $record->configdata;
} else {
$configdata = @json_decode($record->configdata, true);
$configdata = $configdata ?? [];
}
$configdata += [
'required' => 0,
'uniquevalues' => 0,
'locked' => 0,
'visibility' => 2,
'defaultvalue' => '',
'defaultvalueformat' => FORMAT_MOODLE,
'displaysize' => 0,
'maxlength' => 0,
'ispassword' => 0,
'link' => '',
'linktarget' => '',
'checkbydefault' => 0,
'startyear' => 2000,
'endyear' => 3000,
'includetime' => 1,
];
$record->configdata = json_encode($configdata);
$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')];
}
/**
* Adds instance data for one field
*
* @param field_controller $field
* @param int $instanceid
* @param mixed $value
* @return \core_customfield\data_controller
*/
public function add_instance_data(field_controller $field, int $instanceid, $value): \core_customfield\data_controller {
$data = \core_customfield\data_controller::create(0, (object)['instanceid' => $instanceid], $field);
$data->set('contextid', $data->get_context()->id);
$rc = new ReflectionClass(get_class($data));
$rcm = $rc->getMethod('get_form_element_name');
$formelementname = $rcm->invokeArgs($data, []);
$record = (object)[$formelementname => $value];
$data->instance_form_save($record);
return $data;
}
}
+105
View File
@@ -0,0 +1,105 @@
<?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;
/**
* core_customfield test data generator testcase.
*
* @package core_customfield
* @category test
* @copyright 2018 Ruslan Kabalin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class generator_test extends \advanced_testcase {
/**
* Get generator
* @return core_customfield_generator
*/
protected function get_generator(): \core_customfield_generator {
return $this->getDataGenerator()->get_plugin_generator('core_customfield');
}
/**
* Test creating category
*/
public function test_create_category(): void {
$this->resetAfterTest(true);
$lpg = $this->get_generator();
$category = $lpg->create_category();
$this->assertInstanceOf('\core_customfield\category_controller', $category);
$this->assertTrue(\core_customfield\category::record_exists($category->get('id')));
}
/**
* Test creating field
*/
public function test_create_field(): void {
$this->resetAfterTest(true);
$lpg = $this->get_generator();
$category = $lpg->create_category();
$field = $lpg->create_field(['categoryid' => $category->get('id')]);
$this->assertInstanceOf('\core_customfield\field_controller', $field);
$this->assertTrue(\core_customfield\field::record_exists($field->get('id')));
$category = \core_customfield\category_controller::create($category->get('id'));
$category = \core_customfield\api::get_categories_with_fields($category->get('component'),
$category->get('area'), $category->get('itemid'))[$category->get('id')];
$this->assertCount(1, $category->get_fields());
}
/**
* Test for function add_instance_data()
*/
public function test_add_instance_data(): void {
$this->resetAfterTest(true);
$lpg = $this->get_generator();
$c1 = $lpg->create_category();
$course1 = $this->getDataGenerator()->create_course();
$f11 = $this->get_generator()->create_field(['categoryid' => $c1->get('id'), 'type' => 'checkbox']);
$f12 = $this->get_generator()->create_field(['categoryid' => $c1->get('id'), 'type' => 'date']);
$f13 = $this->get_generator()->create_field(['categoryid' => $c1->get('id'),
'type' => 'select', 'configdata' => ['options' => "a\nb\nc"]]);
$f14 = $this->get_generator()->create_field(['categoryid' => $c1->get('id'), 'type' => 'text']);
$f15 = $this->get_generator()->create_field(['categoryid' => $c1->get('id'), 'type' => 'textarea']);
$this->get_generator()->add_instance_data($f11, $course1->id, 1);
$this->get_generator()->add_instance_data($f12, $course1->id, 1546300800);
$this->get_generator()->add_instance_data($f13, $course1->id, 2);
$this->get_generator()->add_instance_data($f14, $course1->id, 'Hello');
$this->get_generator()->add_instance_data($f15, $course1->id, ['text' => '<p>Hi there</p>', 'format' => FORMAT_HTML]);
$handler = $c1->get_handler();
list($data1, $data2, $data3, $data4, $data5) = array_values($handler->get_instance_data($course1->id));
$this->assertNotEmpty($data1->get('id'));
$this->assertEquals(1, $data1->get_value());
$this->assertNotEmpty($data2->get('id'));
$this->assertEquals(1546300800, $data2->get_value());
$this->assertNotEmpty($data3->get('id'));
$this->assertEquals(2, $data3->get_value());
$this->assertNotEmpty($data4->get('id'));
$this->assertEquals('Hello', $data4->get_value());
$this->assertNotEmpty($data5->get('id'));
$this->assertEquals('<p>Hi there</p>', $data5->get_value());
}
}
+51
View File
@@ -0,0 +1,51 @@
<?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;
use advanced_testcase;
use core_course\customfield\course_handler;
use moodle_exception;
/**
* Unit tests for the abstract custom fields handler
*
* @package core_customfield
* @covers \core_customfield\handler
* @copyright 2023 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class handler_test extends advanced_testcase {
/**
* Test retrieving handler for given component/area
*/
public function test_get_handler(): void {
$handler = handler::get_handler('core_course', 'course');
$this->assertInstanceOf(course_handler::class, $handler);
}
/**
* Test retrieving handler for invalid component/area
*/
public function test_get_handler_invalid(): void {
$this->expectException(moodle_exception::class);
$this->expectExceptionMessage('Unable to find handler for custom fields for component core_blimey and area test');
handler::get_handler('core_blimey', 'test');
}
}
+290
View File
@@ -0,0 +1,290 @@
<?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/>.
/**
* Class provider_test
*
* @package core_customfield
* @copyright 2019 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\privacy;
defined('MOODLE_INTERNAL') || die();
use core_privacy\tests\provider_testcase;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\writer;
use core_customfield\privacy\provider;
/**
* Class provider_test
*
* @package core_customfield
* @copyright 2019 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends provider_testcase {
/**
* Generate data.
*
* @return array
*/
protected function generate_test_data(): array {
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_customfield');
$cfcats[1] = $generator->create_category();
$cfcats[2] = $generator->create_category();
$cffields[11] = $generator->create_field(
['categoryid' => $cfcats[1]->get('id'), 'type' => 'checkbox']);
$cffields[12] = $generator->create_field(
['categoryid' => $cfcats[1]->get('id'), 'type' => 'date']);
$cffields[13] = $generator->create_field(
['categoryid' => $cfcats[1]->get('id'),
'type' => 'select', 'configdata' => ['options' => "a\nb\nc"]]);
$cffields[14] = $generator->create_field(
['categoryid' => $cfcats[1]->get('id'), 'type' => 'text']);
$cffields[15] = $generator->create_field(
['categoryid' => $cfcats[1]->get('id'), 'type' => 'textarea']);
$cffields[21] = $generator->create_field(
['categoryid' => $cfcats[2]->get('id')]);
$cffields[22] = $generator->create_field(
['categoryid' => $cfcats[2]->get('id')]);
$courses[1] = $this->getDataGenerator()->create_course();
$courses[2] = $this->getDataGenerator()->create_course();
$courses[3] = $this->getDataGenerator()->create_course();
$generator->add_instance_data($cffields[11], $courses[1]->id, 1);
$generator->add_instance_data($cffields[12], $courses[1]->id, 1546300800);
$generator->add_instance_data($cffields[13], $courses[1]->id, 2);
$generator->add_instance_data($cffields[14], $courses[1]->id, 'Hello1');
$generator->add_instance_data($cffields[15], $courses[1]->id,
['text' => '<p>Hi there</p>', 'format' => FORMAT_HTML]);
$generator->add_instance_data($cffields[21], $courses[1]->id, 'hihi1');
$generator->add_instance_data($cffields[14], $courses[2]->id, 'Hello2');
$generator->add_instance_data($cffields[21], $courses[2]->id, 'hihi2');
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
return [
'user' => $user,
'cfcats' => $cfcats,
'cffields' => $cffields,
'courses' => $courses,
];
}
/**
* Test for provider::get_metadata()
*/
public function test_get_metadata(): void {
$collection = new \core_privacy\local\metadata\collection('core_customfield');
$collection = provider::get_metadata($collection);
$this->assertNotEmpty($collection);
}
/**
* Test for provider::get_customfields_data_contexts
*/
public function test_get_customfields_data_contexts(): void {
global $DB;
[
'cffields' => $cffields,
'cfcats' => $cfcats,
'courses' => $courses,
] = $this->generate_test_data();
list($sql, $params) = $DB->get_in_or_equal([$courses[1]->id, $courses[2]->id], SQL_PARAMS_NAMED);
$r = provider::get_customfields_data_contexts('core_course', 'course', '=0',
$sql, $params);
$this->assertEqualsCanonicalizing([\context_course::instance($courses[1]->id)->id,
\context_course::instance($courses[2]->id)->id],
$r->get_contextids());
}
/**
* Test for provider::get_customfields_configuration_contexts()
*/
public function test_get_customfields_configuration_contexts(): void {
$this->generate_test_data();
$r = provider::get_customfields_configuration_contexts('core_course', 'course');
$this->assertEquals([\context_system::instance()->id], $r->get_contextids());
}
/**
* Test for provider::export_customfields_data()
*/
public function test_export_customfields_data(): void {
global $USER, $DB;
$this->resetAfterTest();
[
'cffields' => $cffields,
'cfcats' => $cfcats,
'courses' => $courses,
] = $this->generate_test_data();
// Hack one of the fields so it has an invalid field type.
$invalidfieldid = $cffields[21]->get('id');
$DB->update_record('customfield_field', ['id' => $invalidfieldid, 'type' => 'invalid']);
$context = \context_course::instance($courses[1]->id);
$contextlist = new approved_contextlist($USER, 'core_customfield', [$context->id]);
provider::export_customfields_data($contextlist, 'core_course', 'course', '=0', '=:i', ['i' => $courses[1]->id]);
/** @var core_privacy\tests\request\content_writer $writer */
$writer = writer::with_context($context);
// Make sure that all and only data for the course1 was exported.
// There is no way to fetch all data from writer as array so we need to fetch one-by-one for each data id.
$invaldfieldischecked = false;
foreach ($DB->get_records('customfield_data', []) as $dbrecord) {
$data = $writer->get_data(['Custom fields data', $dbrecord->id]);
if ($dbrecord->instanceid == $courses[1]->id) {
$this->assertEquals($dbrecord->fieldid, $data->fieldid);
$this->assertNotEmpty($data->fieldtype);
$this->assertNotEmpty($data->fieldshortname);
$this->assertNotEmpty($data->fieldname);
$invaldfieldischecked = $invaldfieldischecked ?: ($data->fieldid == $invalidfieldid);
} else {
$this->assertEmpty($data);
}
}
// Make sure field with was checked in this test.
$this->assertTrue($invaldfieldischecked);
}
/**
* Test for provider::delete_customfields_data()
*/
public function test_delete_customfields_data(): void {
global $USER, $DB;
$this->resetAfterTest();
[
'cffields' => $cffields,
'cfcats' => $cfcats,
'courses' => $courses,
] = $this->generate_test_data();
$approvedcontexts = new approved_contextlist($USER, 'core_course', [\context_course::instance($courses[1]->id)->id]);
provider::delete_customfields_data($approvedcontexts, 'core_course', 'course');
$this->assertEmpty($DB->get_records('customfield_data', ['instanceid' => $courses[1]->id]));
$this->assertNotEmpty($DB->get_records('customfield_data', ['instanceid' => $courses[2]->id]));
}
/**
* Test for provider::delete_customfields_configuration()
*/
public function test_delete_customfields_configuration(): void {
global $USER, $DB;
$this->resetAfterTest();
[
'cffields' => $cffields,
'cfcats' => $cfcats,
'courses' => $courses,
] = $this->generate_test_data();
// Remember the list of fields in the category 2 before we delete it.
$catid1 = $cfcats[1]->get('id');
$catid2 = $cfcats[2]->get('id');
$fids2 = $DB->get_fieldset_select('customfield_field', 'id', 'categoryid=?', [$catid2]);
$this->assertNotEmpty($fids2);
list($fsql, $fparams) = $DB->get_in_or_equal($fids2, SQL_PARAMS_NAMED);
$this->assertNotEmpty($DB->get_records_select('customfield_data', 'fieldid ' . $fsql, $fparams));
// A little hack here, modify customfields configuration so they have different itemids.
$DB->update_record('customfield_category', ['id' => $catid2, 'itemid' => 1]);
$contextlist = new approved_contextlist($USER, 'core_course', [\context_system::instance()->id]);
provider::delete_customfields_configuration($contextlist, 'core_course', 'course', '=:i', ['i' => 1]);
// Make sure everything for category $catid2 is gone but present for $catid1.
$this->assertEmpty($DB->get_records('customfield_category', ['id' => $catid2]));
$this->assertEmpty($DB->get_records_select('customfield_field', 'id ' . $fsql, $fparams));
$this->assertEmpty($DB->get_records_select('customfield_data', 'fieldid ' . $fsql, $fparams));
$this->assertNotEmpty($DB->get_records('customfield_category', ['id' => $catid1]));
$fids1 = $DB->get_fieldset_select('customfield_field', 'id', 'categoryid=?', [$catid1]);
list($fsql1, $fparams1) = $DB->get_in_or_equal($fids1, SQL_PARAMS_NAMED);
$this->assertNotEmpty($DB->get_records_select('customfield_field', 'id ' . $fsql1, $fparams1));
$this->assertNotEmpty($DB->get_records_select('customfield_data', 'fieldid ' . $fsql1, $fparams1));
}
/**
* Test for provider::delete_customfields_configuration_for_context()
*/
public function test_delete_customfields_configuration_for_context(): void {
global $USER, $DB;
$this->resetAfterTest();
[
'cffields' => $cffields,
'cfcats' => $cfcats,
'courses' => $courses,
] = $this->generate_test_data();
// Remember the list of fields in the category 2 before we delete it.
$catid1 = $cfcats[1]->get('id');
$catid2 = $cfcats[2]->get('id');
$fids2 = $DB->get_fieldset_select('customfield_field', 'id', 'categoryid=?', [$catid2]);
$this->assertNotEmpty($fids2);
list($fsql, $fparams) = $DB->get_in_or_equal($fids2, SQL_PARAMS_NAMED);
$this->assertNotEmpty($DB->get_records_select('customfield_data', 'fieldid ' . $fsql, $fparams));
// A little hack here, modify customfields configuration so they have different contexts.
$context = \context_user::instance($USER->id);
$DB->update_record('customfield_category', ['id' => $catid2, 'contextid' => $context->id]);
provider::delete_customfields_configuration_for_context('core_course', 'course', $context);
// Make sure everything for category $catid2 is gone but present for $catid1.
$this->assertEmpty($DB->get_records('customfield_category', ['id' => $catid2]));
$this->assertEmpty($DB->get_records_select('customfield_field', 'id ' . $fsql, $fparams));
$this->assertEmpty($DB->get_records_select('customfield_data', 'fieldid ' . $fsql, $fparams));
$this->assertNotEmpty($DB->get_records('customfield_category', ['id' => $catid1]));
$fids1 = $DB->get_fieldset_select('customfield_field', 'id', 'categoryid=?', [$catid1]);
list($fsql1, $fparams1) = $DB->get_in_or_equal($fids1, SQL_PARAMS_NAMED);
$this->assertNotEmpty($DB->get_records_select('customfield_field', 'id ' . $fsql1, $fparams1));
$this->assertNotEmpty($DB->get_records_select('customfield_data', 'fieldid ' . $fsql1, $fparams1));
}
/**
* Test for provider::delete_customfields_data_for_context()
*/
public function test_delete_customfields_data_for_context(): void {
global $DB;
$this->resetAfterTest();
[
'cffields' => $cffields,
'cfcats' => $cfcats,
'courses' => $courses,
] = $this->generate_test_data();
provider::delete_customfields_data_for_context('core_course', 'course',
\context_course::instance($courses[1]->id));
$fids2 = $DB->get_fieldset_select('customfield_field', 'id', '1=1', []);
list($fsql, $fparams) = $DB->get_in_or_equal($fids2, SQL_PARAMS_NAMED);
$fparams['course1'] = $courses[1]->id;
$fparams['course2'] = $courses[2]->id;
$this->assertEmpty($DB->get_records_select('customfield_data', 'instanceid = :course1 AND fieldid ' . $fsql, $fparams));
$this->assertNotEmpty($DB->get_records_select('customfield_data', 'instanceid = :course2 AND fieldid ' . $fsql, $fparams));
}
}
+30
View File
@@ -0,0 +1,30 @@
This files describes API changes in /customfield/*,
Information provided here is intended especially for developers.
=== 4.4.4 ===
* The field controller `get_formatted_name()` method now accepts an optional `$escape` parameter to define whether to
escape the returned name
=== 4.4.2 ===
* The customfield_select field controller `get_options` method now returns each option pre-formatted
=== 4.4 ===
* Enhance the handling of files embedded within textarea custom fields in the course backup and restore functionalities.
These files are now properly backed up when a course is backed up, and restored when the course is restored.
Other types of custom fields can also benefit from this new API. See MDL-79151 for more information.
`\core_customfield\handler::restore_instance_data_from_backup()` now conditionally returns a data_controller id and
there are two new methods, `\core_customfield\data_controller::backup_define_structure()` and
`\core_customfield\data_controller::restore_define_structure()`.
=== 4.3 ===
* Field categories are now consistently of type `PARAM_TEXT`, ensure instance `get_formatted_name()` helper is used
during output
=== 4.2 ===
* The `\customfield_select\field_controller::get_options_array` method, deprecated since 3.10, has been removed, use
the field instance `get_options` method instead
=== 3.11 ===
* Methods \core_customfield\handler::get_field_config_form() and \core_customfield\handler::setup_edit_page() are no
longer used. Components that define custom fields areas do not need to implement them. Field edit form opens in
the modal now.