MDL-57898 core_customfield: Custom fields API

This commit is part of work on Custom fields API,
to minimize commit history in moodle core the work of a team of developers was split
into several commits with different authors but the authorship of individual
lines of code may be different from the commit author.
This commit is contained in:
David Matamoros 2019-01-11 12:12:18 +01:00 committed by Marina Glancy
parent 1eeb465a0c
commit 0446fee7a9
15 changed files with 3007 additions and 0 deletions

415
customfield/classes/api.php Normal file
View File

@ -0,0 +1,415 @@
<?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', format_string($category->get('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;
}
}

View File

@ -0,0 +1,134 @@
<?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;
global $CFG;
require_once($CFG->libdir . '/formslib.php');
/**
* 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 \moodleform {
/**
* Class definition
*
* @throws \coding_exception
*/
public function definition() {
global $PAGE;
$mform = $this->_form;
$field = $this->_customdata['field'];
if (!($field && $field instanceof field_controller)) {
throw new \coding_exception('Field must be passed in customdata');
}
$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->add_action_buttons(true);
}
/**
* Field data validation
*
* @param array $data
* @param array $files
* @return array
*/
public function validation($data, $files = array()) {
global $DB;
$errors = array();
/** @var field_controller $field */
$field = $this->_customdata['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;
}
}

View File

@ -0,0 +1,840 @@
<?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 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
*/
protected final 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', (object)$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;
}
/**
* The form to create or edit a field
*
* @param field_controller $field
* @return field_config_form
*/
public function get_field_config_form(field_controller $field): field_config_form {
$form = new field_config_form(null, ['field' => $field]);
$form->set_data(api::prepare_field_for_config_form($field));
return $form;
}
/**
* 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);
}
/**
* 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')];
}
}
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
*/
public function instance_form_definition(\MoodleQuickForm $mform, int $instanceid = 0) {
$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) {
$mform->addElement('header', 'category_' . $categoryid,
format_string($data->get_field()->get_category()->get('name')));
$lastcategoryid = $categoryid;
}
$data->instance_form_definition($mform);
$field = $data->get_field()->to_record();
if (strlen($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
*/
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();
}
}
/**
* Set up page customfield/edit.php
*
* Handler should override this method and set page context
*
* @param field_controller $field
* @return string page heading
*/
public function setup_edit_page(field_controller $field): string {
global $PAGE;
// Page context.
$context = $this->get_configuration_context();
if ($context->contextlevel == CONTEXT_MODULE) {
list($course, $cm) = get_course_and_cm_from_cmid($context->instanceid, '', $context->get_course_context()->instanceid);
require_login($course, false, $cm);
} else if ($context->contextlevel == CONTEXT_COURSE) {
require_login($context->instanceid, false);
} else {
$PAGE->set_context(null); // This will set to system context only if the context was not set before.
if ($PAGE->context->id != $context->id) {
// In case of user or block context level this method must be overridden.
debugging('Handler must override setup_edit_page() and set the page context before calling parent method.',
DEBUG_DEVELOPER);
}
}
// Set up url and title.
if ($field->get('id')) {
$field = $this->validate_field($field);
} else {
$this->validate_category($field->get_category());
}
$url = new \moodle_url('/customfield/edit.php',
['id' => $field->get('id'), 'type' => $field->get('type'), 'categoryid' => $field->get('categoryid')]);
$PAGE->set_url($url);
$typestr = get_string('pluginname', 'customfield_' . $field->get('type'));
if ($field->get('id')) {
$title = get_string('editingfield', 'core_customfield',
$field->get_formatted_name());
} else {
$title = get_string('addingnewcustomfield', 'core_customfield', $typestr);
}
$PAGE->set_title($title);
return $title;
}
}

View File

@ -0,0 +1,132 @@
<?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);
$fieldarray['editfieldurl'] = (new \moodle_url('/customfield/edit.php', [
'id' => $fieldarray['id'],
]))->out(false);
$categoryarray['fields'][] = $fieldarray;
}
$menu = new \action_menu();
$menu->set_alignment(\action_menu::BL, \action_menu::BL);
$menu->set_menu_trigger(get_string('createnewcustomfield', 'core_customfield'));
$baseaddfieldurl = new \moodle_url('/customfield/edit.php',
array('action' => 'editfield', 'categoryid' => $category->get('id')));
foreach ($fieldtypes as $type => $fieldname) {
$addfieldurl = new \moodle_url($baseaddfieldurl, array('type' => $type));
$action = new \action_menu_link_secondary($addfieldurl, null, $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;
}
}

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);
}
}

View File

@ -0,0 +1,486 @@
<?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;
use Horde\Socket\Client\Exception;
/**
* 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 \core_privacy\local\metadata\provider, \core_privacy\local\request\subsystem\plugin_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',
'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]);
writer::with_context($context)->export_data($subcontext, $record);
}
}

61
customfield/edit.php Normal file
View File

@ -0,0 +1,61 @@
<?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/>.
/**
* Edit configuration of a custom field
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../config.php');
require_once($CFG->libdir . '/adminlib.php');
$id = optional_param('id', 0, PARAM_INT);
$categoryid = optional_param('categoryid', 0, PARAM_INT);
$type = optional_param('type', null, PARAM_COMPONENT);
if ($id) {
$field = \core_customfield\field_controller::create($id);
} else if ($categoryid && $type) {
$category = \core_customfield\category_controller::create($categoryid);
$field = \core_customfield\field_controller::create(0, (object)['type' => $type], $category);
} else {
print_error('fieldnotfound', 'core_customfield');
}
$handler = $field->get_handler();
require_login();
if (!$handler->can_configure()) {
print_error('nopermissionconfigure', 'core_customfield');
}
$title = $handler->setup_edit_page($field);
$mform = $handler->get_field_config_form($field);
if ($mform->is_cancelled()) {
redirect($handler->get_configuration_url());
} else if ($data = $mform->get_data()) {
$handler->save_field_configuration($field, $data);
redirect($handler->get_configuration_url());
}
echo $OUTPUT->header();
echo $OUTPUT->heading($title);
$mform->display();
echo $OUTPUT->footer();

296
customfield/externallib.php Normal file
View File

@ -0,0 +1,296 @@
<?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/>.
/**
* 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
*/
defined('MOODLE_INTERNAL') || die;
require_once($CFG->libdir . "/externallib.php");
/**
* Class core_customfield_external
*
* @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_INT, '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'),
'editfieldurl' => new external_value(PARAM_URL, 'edit field url'),
'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() {
}
}

View File

@ -0,0 +1,71 @@
<?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;
}
}

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/>.
/**
* 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]');
}
}

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/>.
/**
* 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 date no later than {$a}';
$string['errormindate'] = 'Please enter date on or after {$a}';
$string['includetime'] = 'Include time';
$string['maxdate'] = 'Maximum value';
$string['mindate'] = 'Minimum value';
$string['mindateaftermax'] = 'The minimum value can not be bigger than the maximum value';
$string['pluginname'] = 'Date and time';
$string['privacy:metadata'] = 'Date and time field type plugin does not store any personal data, it uses tables defined in core';
$string['specificsettings'] = 'Settings for the date and time field';

View File

@ -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/>.
/**
* 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
*/
namespace customfield_select;
defined('MOODLE_INTERNAL') || die;
/**
* 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);
}
/**
* Returns the options available as an array.
*
* @param \core_customfield\field_controller $field
* @return array
*/
public static function get_options_array(\core_customfield\field_controller $field): array {
if ($field->get_configdata_property('options')) {
$options = preg_split("/\s*\n\s*/", trim($field->get_configdata_property('options')));
} 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;
}
}

View File

@ -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/>.
/**
* 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);
$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);
$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');
}
$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('errorconfigdisplaysize', 'customfield_text');
}
}
return $errors;
}
}

View File

@ -0,0 +1,145 @@
<?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 ['maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $CFG->maxbytes, 'context' => $context];
}
/**
* 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();
}
}

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 = 2018120300;
$plugin->requires = 2018122000;