first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+442
View File
@@ -0,0 +1,442 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Api customfield package
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield;
use core\output\inplace_editable;
use core_customfield\event\category_created;
use core_customfield\event\category_deleted;
use core_customfield\event\category_updated;
use core_customfield\event\field_created;
use core_customfield\event\field_deleted;
use core_customfield\event\field_updated;
defined('MOODLE_INTERNAL') || die;
/**
* Class api
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class api {
/**
* For the given instance and list of fields fields retrieves data associated with them
*
* @param field_controller[] $fields list of fields indexed by field id
* @param int $instanceid
* @param bool $adddefaults
* @return data_controller[] array of data_controller objects indexed by fieldid. All fields are present,
* some data_controller objects may have 'id', some not
* If ($adddefaults): All fieldids are present, some data_controller objects may have 'id', some not.
* If (!$adddefaults): Only fieldids with data are present, all data_controller objects have 'id'.
*/
public static function get_instance_fields_data(array $fields, int $instanceid, bool $adddefaults = true): array {
return self::get_instances_fields_data($fields, [$instanceid], $adddefaults)[$instanceid];
}
/**
* For given list of instances and fields retrieves data associated with them
*
* @param field_controller[] $fields list of fields indexed by field id
* @param int[] $instanceids
* @param bool $adddefaults
* @return data_controller[][] 2-dimension array, first index is instanceid, second index is fieldid.
* If ($adddefaults): All instanceids and all fieldids are present, some data_controller objects may have 'id', some not.
* If (!$adddefaults): All instanceids are present but only fieldids with data are present, all
* data_controller objects have 'id'.
*/
public static function get_instances_fields_data(array $fields, array $instanceids, bool $adddefaults = true): array {
global $DB;
// Create the results array where instances and fields order is the same as in the input arrays.
$result = array_fill_keys($instanceids, array_fill_keys(array_keys($fields), null));
if (empty($instanceids) || empty($fields)) {
return $result;
}
// Retrieve all existing data.
list($sqlfields, $params) = $DB->get_in_or_equal(array_keys($fields), SQL_PARAMS_NAMED, 'fld');
list($sqlinstances, $iparams) = $DB->get_in_or_equal($instanceids, SQL_PARAMS_NAMED, 'ins');
$sql = "SELECT d.*
FROM {customfield_field} f
JOIN {customfield_data} d ON (f.id = d.fieldid AND d.instanceid {$sqlinstances})
WHERE f.id {$sqlfields}";
$fieldsdata = $DB->get_recordset_sql($sql, $params + $iparams);
foreach ($fieldsdata as $data) {
$result[$data->instanceid][$data->fieldid] = data_controller::create(0, $data, $fields[$data->fieldid]);
}
$fieldsdata->close();
if ($adddefaults) {
// Add default data where it was not retrieved.
foreach ($instanceids as $instanceid) {
foreach ($fields as $fieldid => $field) {
if ($result[$instanceid][$fieldid] === null) {
$result[$instanceid][$fieldid] =
data_controller::create(0, (object)['instanceid' => $instanceid], $field);
}
}
}
} else {
// Remove null-placeholders for data that was not retrieved.
foreach ($instanceids as $instanceid) {
$result[$instanceid] = array_filter($result[$instanceid]);
}
}
return $result;
}
/**
* Retrieve a list of all available custom field types
*
* @return array a list of the fieldtypes suitable to use in a select statement
*/
public static function get_available_field_types() {
$fieldtypes = array();
$plugins = \core\plugininfo\customfield::get_enabled_plugins();
foreach ($plugins as $type => $unused) {
$fieldtypes[$type] = get_string('pluginname', 'customfield_' . $type);
}
asort($fieldtypes);
return $fieldtypes;
}
/**
* Updates or creates a field with data that came from a form
*
* @param field_controller $field
* @param \stdClass $formdata
*/
public static function save_field_configuration(field_controller $field, \stdClass $formdata) {
foreach ($formdata as $key => $value) {
if ($key === 'configdata' && is_array($formdata->configdata)) {
$field->set($key, json_encode($value));
} else if ($key === 'id' || ($key === 'type' && $field->get('id'))) {
continue;
} else if (field::has_property($key)) {
$field->set($key, $value);
}
}
$isnewfield = empty($field->get('id'));
// Process files in description.
if (isset($formdata->description_editor)) {
if (!$field->get('id')) {
// We need 'id' field to store files used in description.
$field->save();
}
$data = (object) ['description_editor' => $formdata->description_editor];
$textoptions = $field->get_handler()->get_description_text_options();
$data = file_postupdate_standard_editor($data, 'description', $textoptions, $textoptions['context'],
'core_customfield', 'description', $field->get('id'));
$field->set('description', $data->description);
$field->set('descriptionformat', $data->descriptionformat);
}
// Save the field.
$field->save();
if ($isnewfield) {
// Move to the end of the category.
self::move_field($field, $field->get('categoryid'));
}
if ($isnewfield) {
field_created::create_from_object($field)->trigger();
} else {
field_updated::create_from_object($field)->trigger();
}
}
/**
* Change fields sort order, move field to another category
*
* @param field_controller $field field that needs to be moved
* @param int $categoryid category that needs to be moved
* @param int $beforeid id of the category this category needs to be moved before, 0 to move to the end
*/
public static function move_field(field_controller $field, int $categoryid, int $beforeid = 0) {
global $DB;
if ($field->get('categoryid') != $categoryid) {
// Move field to another category. Validate that this category exists and belongs to the same component/area/itemid.
$category = $field->get_category();
$DB->get_record(category::TABLE, [
'component' => $category->get('component'),
'area' => $category->get('area'),
'itemid' => $category->get('itemid'),
'id' => $categoryid], 'id', MUST_EXIST);
$field->set('categoryid', $categoryid);
$field->save();
field_updated::create_from_object($field)->trigger();
}
// Reorder fields in the target category.
$records = $DB->get_records(field::TABLE, ['categoryid' => $categoryid], 'sortorder, id', '*');
$id = $field->get('id');
$fieldsids = array_values(array_diff(array_keys($records), [$id]));
$idx = $beforeid ? array_search($beforeid, $fieldsids) : false;
if ($idx === false) {
// Set as the last field.
$fieldsids = array_merge($fieldsids, [$id]);
} else {
// Set before field with id $beforeid.
$fieldsids = array_merge(array_slice($fieldsids, 0, $idx), [$id], array_slice($fieldsids, $idx));
}
foreach (array_values($fieldsids) as $idx => $fieldid) {
// Use persistent class to update the sortorder for each field that needs updating.
if ($records[$fieldid]->sortorder != $idx) {
$f = ($fieldid == $id) ? $field : new field(0, $records[$fieldid]);
$f->set('sortorder', $idx);
$f->save();
}
}
}
/**
* Delete a field
*
* @param field_controller $field
*/
public static function delete_field_configuration(field_controller $field): bool {
$event = field_deleted::create_from_object($field);
get_file_storage()->delete_area_files($field->get_handler()->get_configuration_context()->id, 'core_customfield',
'description', $field->get('id'));
$result = $field->delete();
$event->trigger();
return $result;
}
/**
* Returns an object for inplace editable
*
* @param category_controller $category category that needs to be moved
* @param bool $editable
* @return inplace_editable
*/
public static function get_category_inplace_editable(category_controller $category, bool $editable = true): inplace_editable {
return new inplace_editable('core_customfield',
'category',
$category->get('id'),
$editable,
$category->get_formatted_name(),
$category->get('name'),
get_string('editcategoryname', 'core_customfield'),
get_string('newvaluefor', 'core_form', $category->get_formatted_name())
);
}
/**
* Reorder categories, move given category before another category
*
* @param category_controller $category category that needs to be moved
* @param int $beforeid id of the category this category needs to be moved before, 0 to move to the end
*/
public static function move_category(category_controller $category, int $beforeid = 0) {
global $DB;
$records = $DB->get_records(category::TABLE, [
'component' => $category->get('component'),
'area' => $category->get('area'),
'itemid' => $category->get('itemid')
], 'sortorder, id', '*');
$id = $category->get('id');
$categoriesids = array_values(array_diff(array_keys($records), [$id]));
$idx = $beforeid ? array_search($beforeid, $categoriesids) : false;
if ($idx === false) {
// Set as the last category.
$categoriesids = array_merge($categoriesids, [$id]);
} else {
// Set before category with id $beforeid.
$categoriesids = array_merge(array_slice($categoriesids, 0, $idx), [$id], array_slice($categoriesids, $idx));
}
foreach (array_values($categoriesids) as $idx => $categoryid) {
// Use persistent class to update the sortorder for each category that needs updating.
if ($records[$categoryid]->sortorder != $idx) {
$c = ($categoryid == $id) ? $category : category_controller::create(0, $records[$categoryid]);
$c->set('sortorder', $idx);
$c->save();
}
}
}
/**
* Insert or update custom field category
*
* @param category_controller $category
*/
public static function save_category(category_controller $category) {
$isnewcategory = empty($category->get('id'));
$category->save();
if ($isnewcategory) {
// Move to the end.
self::move_category($category);
category_created::create_from_object($category)->trigger();
} else {
category_updated::create_from_object($category)->trigger();
}
}
/**
* Delete a custom field category
*
* @param category_controller $category
* @return bool
*/
public static function delete_category(category_controller $category): bool {
$event = category_deleted::create_from_object($category);
// Delete all fields.
foreach ($category->get_fields() as $field) {
self::delete_field_configuration($field);
}
$result = $category->delete();
$event->trigger();
return $result;
}
/**
* Returns a list of categories with their related fields.
*
* @param string $component
* @param string $area
* @param int $itemid
* @return category_controller[]
*/
public static function get_categories_with_fields(string $component, string $area, int $itemid): array {
global $DB;
$categories = [];
$options = [
'component' => $component,
'area' => $area,
'itemid' => $itemid
];
$plugins = \core\plugininfo\customfield::get_enabled_plugins();
list($sqlfields, $params) = $DB->get_in_or_equal(array_keys($plugins), SQL_PARAMS_NAMED, 'param', true, null);
$fields = 'f.*, ' . join(', ', array_map(function($field) {
return "c.$field AS category_$field";
}, array_diff(array_keys(category::properties_definition()), ['usermodified', 'timemodified'])));
$sql = "SELECT $fields
FROM {customfield_category} c
LEFT JOIN {customfield_field} f ON c.id = f.categoryid AND f.type $sqlfields
WHERE c.component = :component AND c.area = :area AND c.itemid = :itemid
ORDER BY c.sortorder, f.sortorder";
$fieldsdata = $DB->get_recordset_sql($sql, $options + $params);
foreach ($fieldsdata as $data) {
if (!array_key_exists($data->category_id, $categories)) {
$categoryobj = new \stdClass();
foreach ($data as $key => $value) {
if (preg_match('/^category_(.*)$/', $key, $matches)) {
$categoryobj->{$matches[1]} = $value;
}
}
$category = category_controller::create(0, $categoryobj);
$categories[$categoryobj->id] = $category;
} else {
$category = $categories[$data->categoryid];
}
if ($data->id) {
$fieldobj = new \stdClass();
foreach ($data as $key => $value) {
if (!preg_match('/^category_/', $key)) {
$fieldobj->{$key} = $value;
}
}
$field = field_controller::create(0, $fieldobj, $category);
}
}
$fieldsdata->close();
return $categories;
}
/**
* Prepares the object to pass to field configuration form set_data() method
*
* @param field_controller $field
* @return \stdClass
*/
public static function prepare_field_for_config_form(field_controller $field): \stdClass {
if ($field->get('id')) {
$formdata = $field->to_record();
$formdata->configdata = $field->get('configdata');
// Preprocess the description.
$textoptions = $field->get_handler()->get_description_text_options();
file_prepare_standard_editor($formdata, 'description', $textoptions, $textoptions['context'], 'core_customfield',
'description', $formdata->id);
} else {
$formdata = (object)['categoryid' => $field->get('categoryid'), 'type' => $field->get('type'), 'configdata' => []];
}
// Allow field to do more preprocessing (usually for editor or filemanager elements).
$field->prepare_for_config_form($formdata);
return $formdata;
}
/**
* Get a list of the course custom fields that support course grouping in
* block_myoverview
* @return array $shortname => $name
*/
public static function get_fields_supporting_course_grouping() {
global $DB;
$sql = "
SELECT f.*
FROM {customfield_field} f
JOIN {customfield_category} cat ON cat.id = f.categoryid
WHERE cat.component = 'core_course' AND cat.area = 'course'
ORDER BY f.name
";
$ret = [];
$fields = $DB->get_records_sql($sql);
foreach ($fields as $field) {
$inst = field_controller::create(0, $field);
$isvisible = $inst->get_configdata_property('visibility') == \core_course\customfield\course_handler::VISIBLETOALL;
// Only visible fields to everybody supporting course grouping will be displayed.
if ($inst->supports_course_grouping() && $isvisible) {
$ret[$inst->get('shortname')] = $inst->get('name');
}
}
return $ret;
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_customfield;
use core\persistent;
/**
* Customfield category persistent class
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class category extends persistent {
/**
* Database table.
*/
const TABLE = 'customfield_category';
/**
* Return the definition of the properties of this model.
*
* @return array
*/
protected static function define_properties(): array {
return array(
'name' => [
'type' => PARAM_TEXT,
],
'description' => [
'type' => PARAM_RAW,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'descriptionformat' => [
'type' => PARAM_INT,
'default' => FORMAT_MOODLE,
'optional' => true,
'null' => NULL_ALLOWED,
],
'component' => [
'type' => PARAM_COMPONENT
],
'area' => [
'type' => PARAM_COMPONENT
],
'itemid' => [
'type' => PARAM_INT,
'optional' => true,
'default' => 0
],
'contextid' => [
'type' => PARAM_INT,
'optional' => false
],
'sortorder' => [
'type' => PARAM_INT,
'optional' => true,
'default' => -1,
'null' => NULL_ALLOWED,
],
);
}
}
+231
View File
@@ -0,0 +1,231 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Customfield catecory controller class
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield;
defined('MOODLE_INTERNAL') || die;
/**
* Class category
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class category_controller {
/**
* Category persistent
*
* @var category
*/
protected $category;
/**
* @var field_controller[]
*/
protected $fields = [];
/** @var handler */
protected $handler;
/**
* category constructor.
*
* This class is not abstract, however the constructor was made protected to be consistent with
* field_controller and data_controller
*
* @param int $id
* @param \stdClass|null $record
*/
protected function __construct(int $id = 0, \stdClass $record = null) {
$this->category = new category($id, $record);
}
/**
* Creates an instance of category_controller
*
* Either $id or $record or $handler need to be specified
* If handler is known pass it to constructor to avoid retrieving it later
* Component, area and itemid must not conflict with the ones in handler
*
* @param int $id
* @param \stdClass|null $record
* @param handler|null $handler
* @return category_controller
* @throws \moodle_exception
* @throws \coding_exception
*/
public static function create(int $id, \stdClass $record = null, handler $handler = null): category_controller {
global $DB;
if ($id && $record) {
// This warning really should be in persistent as well.
debugging('Too many parameters, either id need to be specified or a record, but not both.',
DEBUG_DEVELOPER);
}
if ($id) {
if (!$record = $DB->get_record(category::TABLE, array('id' => $id), '*', IGNORE_MISSING)) {
throw new \moodle_exception('categorynotfound', 'core_customfield');
}
}
if (empty($record->component)) {
if (!$handler) {
throw new \coding_exception('Not enough parameters to initialise category_controller - unknown component');
}
$record->component = $handler->get_component();
}
if (empty($record->area)) {
if (!$handler) {
throw new \coding_exception('Not enough parameters to initialise category_controller - unknown area');
}
$record->area = $handler->get_area();
}
if (!isset($record->itemid)) {
if (!$handler) {
throw new \coding_exception('Not enough parameters to initialise category_controller - unknown itemid');
}
$record->itemid = $handler->get_itemid();
}
$category = new self(0, $record);
if (!$category->get('contextid')) {
// If contextid was not present in the record we can find it out from the handler.
$handlernew = $handler ?? $category->get_handler();
$category->set('contextid', $handlernew->get_configuration_context()->id);
}
if ($handler) {
$category->set_handler($handler);
}
return $category;
}
/**
* Persistent getter parser.
*
* @param string $property
* @return mixed
*/
final public function get($property) {
return $this->category->get($property);
}
/**
* Persistent setter parser.
*
* @param string $property
* @param mixed $value
*/
final public function set($property, $value) {
return $this->category->set($property, $value);
}
/**
* Persistent delete parser.
*
* @return bool
*/
final public function delete() {
return $this->category->delete();
}
/**
* Persistent save parser.
*
* @return void
*/
final public function save() {
$this->category->save();
}
/**
* Return an array of field objects associated with this category.
*
* @return field_controller[]
*/
public function get_fields() {
return $this->fields;
}
/**
* Adds a child field
*
* @param field_controller $field
*/
public function add_field(field_controller $field) {
$this->fields[$field->get('id')] = $field;
}
/**
* Gets a handler, if not known retrieve it
*
* @return handler
*/
public function get_handler(): handler {
if ($this->handler === null) {
$this->handler = handler::get_handler($this->get('component'), $this->get('area'), $this->get('itemid'));
}
return $this->handler;
}
/**
* Allows to set handler so we don't need to retrieve it later
*
* @param handler $handler
* @throws \coding_exception
*/
public function set_handler(handler $handler) {
// Make sure there are no conflicts.
if ($this->get('component') !== $handler->get_component()) {
throw new \coding_exception('Component of the handler does not match the one from the record');
}
if ($this->get('area') !== $handler->get_area()) {
throw new \coding_exception('Area of the handler does not match the one from the record');
}
if ($this->get('itemid') != $handler->get_itemid()) {
throw new \coding_exception('Itemid of the handler does not match the one from the record');
}
if ($this->get('contextid') != $handler->get_configuration_context()->id) {
throw new \coding_exception('Context of the handler does not match the one from the record');
}
$this->handler = $handler;
}
/**
* Persistent to_record parser.
*
* @return \stdClass
*/
final public function to_record() {
return $this->category->to_record();
}
/**
* Returns the category name formatted according to configuration context.
*
* @return string
*/
public function get_formatted_name(): string {
$context = $this->get_handler()->get_configuration_context();
return format_string($this->get('name'), true, ['context' => $context]);
}
}
+113
View File
@@ -0,0 +1,113 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Data persistent class
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield;
use core\persistent;
defined('MOODLE_INTERNAL') || die;
/**
* Class data
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class data extends persistent {
/**
* Database data.
*/
const TABLE = 'customfield_data';
/**
* Return the definition of the properties of this model.
*
* @return array
*/
protected static function define_properties(): array {
return array(
'fieldid' => [
'type' => PARAM_INT,
'optional' => false,
'null' => NULL_NOT_ALLOWED
],
'instanceid' => [
'type' => PARAM_INT,
'optional' => false,
'null' => NULL_NOT_ALLOWED
],
'intvalue' => [
'type' => PARAM_INT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'decvalue' => [
'type' => PARAM_FLOAT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'charvalue' => [
'type' => PARAM_TEXT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'shortcharvalue' => [
'type' => PARAM_TEXT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
// Mandatory field.
'value' => [
'type' => PARAM_RAW,
'null' => NULL_NOT_ALLOWED,
'default' => ''
],
// Mandatory field.
'valueformat' => [
'type' => PARAM_INT,
'null' => NULL_NOT_ALLOWED,
'default' => FORMAT_MOODLE,
'optional' => true
],
'valuetrust' => [
'type' => PARAM_BOOL,
'null' => NULL_NOT_ALLOWED,
'default' => false,
'optional' => true,
],
'contextid' => [
'type' => PARAM_INT,
'optional' => false,
'null' => NULL_NOT_ALLOWED
]
);
}
}
+395
View File
@@ -0,0 +1,395 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Customfield component data controller abstract class
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield;
use backup_nested_element;
use core_customfield\output\field_data;
defined('MOODLE_INTERNAL') || die;
/**
* Base class for custom fields data controllers
*
* This class is a wrapper around the persistent data class that allows to define
* how the element behaves in the instance edit forms.
*
* Custom field plugins must define a class
* \{pluginname}\data_controller extends \core_customfield\data_controller
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class data_controller {
/**
* Data persistent
*
* @var data
*/
protected $data;
/**
* Field that this data belongs to.
*
* @var field_controller
*/
protected $field;
/**
* data_controller constructor.
*
* @param int $id
* @param \stdClass|null $record
*/
public function __construct(int $id, \stdClass $record) {
$this->data = new data($id, $record);
}
/**
* Creates an instance of data_controller
*
* Parameters $id, $record and $field can complement each other but not conflict.
* If $id is not specified, fieldid must be present either in $record or in $field.
* If $id is not specified, instanceid must be present in $record
*
* No DB queries are performed if both $record and $field are specified.
* @param int $id
* @param \stdClass|null $record
* @param field_controller|null $field
* @return data_controller
* @throws \coding_exception
* @throws \moodle_exception
*/
public static function create(int $id, \stdClass $record = null, field_controller $field = null): data_controller {
global $DB;
if ($id && $record) {
// This warning really should be in persistent as well.
debugging('Too many parameters, either id need to be specified or a record, but not both.',
DEBUG_DEVELOPER);
}
if ($id) {
$record = $DB->get_record(data::TABLE, array('id' => $id), '*', MUST_EXIST);
} else if (!$record) {
$record = new \stdClass();
}
if (!$field && empty($record->fieldid)) {
throw new \coding_exception('Not enough parameters to initialise data_controller - unknown field');
}
if (!$field) {
$field = field_controller::create($record->fieldid);
}
if (empty($record->fieldid)) {
$record->fieldid = $field->get('id');
}
if ($field->get('id') != $record->fieldid) {
throw new \coding_exception('Field id from the record does not match field from the parameter');
}
$type = $field->get('type');
$customfieldtype = "\\customfield_{$type}\\data_controller";
if (!class_exists($customfieldtype) || !is_subclass_of($customfieldtype, self::class)) {
throw new \moodle_exception('errorfieldtypenotfound', 'core_customfield', '', s($type));
}
$datacontroller = new $customfieldtype(0, $record);
$datacontroller->field = $field;
return $datacontroller;
}
/**
* Returns the name of the field to be used on HTML forms.
*
* @return string
*/
public function get_form_element_name(): string {
return 'customfield_' . $this->get_field()->get('shortname');
}
/**
* Persistent getter parser.
*
* @param string $property
* @return mixed
*/
final public function get($property) {
return $this->data->get($property);
}
/**
* Persistent setter parser.
*
* @param string $property
* @param mixed $value
* @return data
*/
final public function set($property, $value) {
return $this->data->set($property, $value);
}
/**
* Return the name of the field in the db table {customfield_data} where the data is stored
*
* Must be one of the following:
* intvalue - can store integer values, this field is indexed
* decvalue - can store decimal values
* shortcharvalue - can store character values up to 255 characters long, this field is indexed
* charvalue - can store character values up to 1333 characters long, this field is not indexed but
* full text search is faster than on field 'value'
* value - can store character values of unlimited length ("text" field in the db)
*
* @return string
*/
abstract public function datafield(): string;
/**
* Delete data. Element can override it if related information needs to be deleted as well (such as files)
*
* @return bool
*/
public function delete() {
return $this->data->delete();
}
/**
* Persistent save parser.
*
* @return void
*/
public function save() {
$this->data->save();
}
/**
* Field associated with this data
*
* @return field_controller
*/
public function get_field(): field_controller {
return $this->field;
}
/**
* Saves the data coming from form
*
* @param \stdClass $datanew data coming from the form
*/
public function instance_form_save(\stdClass $datanew) {
$elementname = $this->get_form_element_name();
if (!property_exists($datanew, $elementname)) {
return;
}
$value = $datanew->$elementname;
$this->data->set($this->datafield(), $value);
$this->data->set('value', $value);
$this->save();
}
/**
* Prepares the custom field data related to the object to pass to mform->set_data() and adds them to it
*
* This function must be called before calling $form->set_data($object);
*
* @param \stdClass $instance the instance that has custom fields, if 'id' attribute is present the custom
* fields for this instance will be added, otherwise the default values will be added.
*/
public function instance_form_before_set_data(\stdClass $instance) {
$instance->{$this->get_form_element_name()} = $this->get_value();
}
/**
* Checks if the value is empty
*
* @param mixed $value
* @return bool
*/
protected function is_empty($value): bool {
if ($this->datafield() === 'value' || $this->datafield() === 'charvalue' || $this->datafield() === 'shortcharvalue') {
return '' . $value === '';
}
return empty($value);
}
/**
* Checks if the value is unique
*
* @param mixed $value
* @return bool
*/
protected function is_unique($value): bool {
global $DB;
// Ensure the "value" datafield can be safely compared across all databases.
$datafield = $this->datafield();
if ($datafield === 'value') {
$datafield = $DB->sql_cast_to_char($datafield);
}
$where = "fieldid = ? AND {$datafield} = ?";
$params = [$this->get_field()->get('id'), $value];
if ($this->get('id')) {
$where .= ' AND id <> ?';
$params[] = $this->get('id');
}
return !$DB->record_exists_select('customfield_data', $where, $params);
}
/**
* Called from instance edit form in validation()
*
* @param array $data
* @param array $files
* @return array array of errors
*/
public function instance_form_validation(array $data, array $files): array {
$errors = [];
$elementname = $this->get_form_element_name();
if ($this->get_field()->get_configdata_property('uniquevalues') == 1) {
$value = $data[$elementname];
if (!$this->is_empty($value) && !$this->is_unique($value)) {
$errors[$elementname] = get_string('erroruniquevalues', 'core_customfield');
}
}
return $errors;
}
/**
* Called from instance edit form in definition_after_data()
*
* @param \MoodleQuickForm $mform
*/
public function instance_form_definition_after_data(\MoodleQuickForm $mform) {
}
/**
* Used by handlers to display data on various places.
*
* @return string
*/
public function display(): string {
global $PAGE;
$output = $PAGE->get_renderer('core_customfield');
return $output->render(new field_data($this));
}
/**
* Returns the default value as it would be stored in the database (not in human-readable format).
*
* @return mixed
*/
abstract public function get_default_value();
/**
* Returns the value as it is stored in the database or default value if data record is not present
*
* @return mixed
*/
public function get_value() {
if (!$this->get('id')) {
return $this->get_default_value();
}
return $this->get($this->datafield());
}
/**
* Return the context of the field
*
* @return \context
*/
public function get_context(): \context {
if ($this->get('contextid')) {
return \context::instance_by_id($this->get('contextid'));
} else if ($this->get('instanceid')) {
return $this->get_field()->get_handler()->get_instance_context($this->get('instanceid'));
} else {
// Context is not yet known (for example, entity is not yet created).
return \context_system::instance();
}
}
/**
* Add a field to the instance edit form.
*
* @param \MoodleQuickForm $mform
*/
abstract public function instance_form_definition(\MoodleQuickForm $mform);
/**
* Returns value in a human-readable format or default value if data record is not present
*
* This is the default implementation that most likely needs to be overridden
*
* @return mixed|null value or null if empty
*/
public function export_value() {
$value = $this->get_value();
if ($this->is_empty($value)) {
return null;
}
if ($this->datafield() === 'intvalue') {
return (int)$value;
} else if ($this->datafield() === 'decvalue') {
return (float)$value;
} else if ($this->datafield() === 'value') {
return format_text($value, $this->get('valueformat'), [
'context' => $this->get_context(),
'trusted' => $this->get('valuetrust'),
]);
} else {
return format_string($value, true, ['context' => $this->get_context()]);
}
}
/**
* Callback for backup, allowing custom fields to add additional data to the backup.
* It is not an abstract method for backward compatibility reasons.
*
* @param \backup_nested_element $customfieldelement The custom field element to be backed up.
*/
public function backup_define_structure(backup_nested_element $customfieldelement): void {
}
/**
* Callback for restore, allowing custom fields to restore additional data from the backup.
* It is not an abstract method for backward compatibility reasons.
*
* @param \restore_structure_step $step The restore step instance.
* @param int $newid The new ID for the custom field data after restore.
* @param int $oldid The original ID of the custom field data before backup.
*/
public function restore_define_structure(\restore_structure_step $step, int $newid, int $oldid): void {
}
/**
* Persistent to_record parser.
*
* @return \stdClass
*/
final public function to_record() {
return $this->data->to_record();
}
}
@@ -0,0 +1,84 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Custom field category created event.
*
* @package core_customfield
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\event;
use core_customfield\category_controller;
defined('MOODLE_INTERNAL') || die();
/**
* Custom field category created event class.
*
* @package core_customfield
* @since Moodle 3.6
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class category_created extends \core\event\base {
/**
* Initialise the event data.
*/
protected function init() {
$this->data['objecttable'] = 'customfield_category';
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Creates an instance from a category controller object
*
* @param category_controller $category
* @return category_created
*/
public static function create_from_object(category_controller $category): category_created {
$eventparams = [
'objectid' => $category->get('id'),
'context' => $category->get_handler()->get_configuration_context(),
'other' => ['name' => $category->get('name')]
];
$event = self::create($eventparams);
$event->add_record_snapshot($event->objecttable, $category->to_record());
return $event;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventcategorycreated', 'core_customfield');
}
/**
* Returns non-localised description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' created the category with id '$this->objectid'.";
}
}
@@ -0,0 +1,84 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Custom field category created event.
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\event;
use core_customfield\category_controller;
defined('MOODLE_INTERNAL') || die();
/**
* Custom field category created event class.
*
* @package core_customfield
* @since Moodle 3.6
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class category_deleted extends \core\event\base {
/**
* Initialise the event data.
*/
protected function init() {
$this->data['objecttable'] = 'customfield_category';
$this->data['crud'] = 'd';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Creates an instance from a category controller object
*
* @param category_controller $category
* @return category_deleted
*/
public static function create_from_object(category_controller $category): category_deleted {
$eventparams = [
'objectid' => $category->get('id'),
'context' => $category->get_handler()->get_configuration_context(),
'other' => ['name' => $category->get('name')]
];
$event = self::create($eventparams);
$event->add_record_snapshot($event->objecttable, $category->to_record());
return $event;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventcategorydeleted', 'core_customfield');
}
/**
* Returns non-localised description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' deleted the category with id '$this->objectid'.";
}
}
@@ -0,0 +1,84 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Custom field category updated event.
*
* @package core_customfield
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\event;
use core_customfield\category_controller;
defined('MOODLE_INTERNAL') || die();
/**
* Custom field category updated event class.
*
* @package core_customfield
* @since Moodle 3.6
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class category_updated extends \core\event\base {
/**
* Initialise the event data.
*/
protected function init() {
$this->data['objecttable'] = 'customfield_category';
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Creates an instance from a category controller object
*
* @param category_controller $category
* @return category_updated
*/
public static function create_from_object(category_controller $category): category_updated {
$eventparams = [
'objectid' => $category->get('id'),
'context' => $category->get_handler()->get_configuration_context(),
'other' => ['name' => $category->get('name')]
];
$event = self::create($eventparams);
$event->add_record_snapshot($event->objecttable, $category->to_record());
return $event;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventcategoryupdated', 'core_customfield');
}
/**
* Returns non-localised description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' updated the category with id '$this->objectid'.";
}
}
@@ -0,0 +1,87 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Custom field created event.
*
* @package core_customfield
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\event;
use core_customfield\field_controller;
defined('MOODLE_INTERNAL') || die();
/**
* Custom field created event class.
*
* @package core_customfield
* @since Moodle 3.6
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class field_created extends \core\event\base {
/**
* Initialise the event data.
*/
protected function init() {
$this->data['objecttable'] = 'customfield_field';
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Creates an instance from a field controller object
*
* @param field_controller $field
* @return field_created
*/
public static function create_from_object(field_controller $field): field_created {
$eventparams = [
'objectid' => $field->get('id'),
'context' => $field->get_handler()->get_configuration_context(),
'other' => [
'shortname' => $field->get('shortname'),
'name' => $field->get('name')
]
];
$event = self::create($eventparams);
$event->add_record_snapshot($event->objecttable, $field->to_record());
return $event;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventfieldcreated', 'core_customfield');
}
/**
* Returns non-localised description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' created the field with id '$this->objectid'.";
}
}
@@ -0,0 +1,87 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Custom field updated event.
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\event;
use core_customfield\field_controller;
defined('MOODLE_INTERNAL') || die();
/**
* Custom field updated event class.
*
* @package core_customfield
* @since Moodle 3.6
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class field_deleted extends \core\event\base {
/**
* Initialise the event data.
*/
protected function init() {
$this->data['objecttable'] = 'customfield_field';
$this->data['crud'] = 'd';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Creates an instance from a field controller object
*
* @param field_controller $field
* @return field_deleted
*/
public static function create_from_object(field_controller $field): field_deleted {
$eventparams = [
'objectid' => $field->get('id'),
'context' => $field->get_handler()->get_configuration_context(),
'other' => [
'shortname' => $field->get('shortname'),
'name' => $field->get('name')
]
];
$event = self::create($eventparams);
$event->add_record_snapshot($event->objecttable, $field->to_record());
return $event;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventfielddeleted', 'core_customfield');
}
/**
* Returns non-localised description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' deleted the field with id '$this->objectid'.";
}
}
@@ -0,0 +1,87 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Custom field updated event.
*
* @package core_customfield
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\event;
use core_customfield\field_controller;
defined('MOODLE_INTERNAL') || die();
/**
* Custom field updated event class.
*
* @package core_customfield
* @since Moodle 3.6
* @copyright 2018 Daniel Neis Araujo <daniel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class field_updated extends \core\event\base {
/**
* Initialise the event data.
*/
protected function init() {
$this->data['objecttable'] = 'customfield_field';
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Creates an instance from a field controller object
*
* @param field_controller $field
* @return field_updated
*/
public static function create_from_object(field_controller $field): field_updated {
$eventparams = [
'objectid' => $field->get('id'),
'context' => $field->get_handler()->get_configuration_context(),
'other' => [
'shortname' => $field->get('shortname'),
'name' => $field->get('name')
]
];
$event = self::create($eventparams);
$event->add_record_snapshot($event->objecttable, $field->to_record());
return $event;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventfieldupdated', 'core_customfield');
}
/**
* Returns non-localised description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' updated the field with id '$this->objectid'.";
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_customfield;
use core\persistent;
/**
* Customfield field persistent class
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class field extends persistent {
/**
* Database table.
*/
const TABLE = 'customfield_field';
/**
* Return the definition of the properties of this model.
*
* @return array
*/
protected static function define_properties(): array {
return array(
'name' => [
'type' => PARAM_TEXT,
],
'shortname' => [
'type' => PARAM_TEXT,
],
'type' => [
'type' => PARAM_PLUGIN,
],
'description' => [
'type' => PARAM_RAW,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'descriptionformat' => [
'type' => PARAM_INT,
'default' => FORMAT_MOODLE,
'optional' => true,
'null' => NULL_ALLOWED,
],
'sortorder' => [
'type' => PARAM_INT,
'optional' => true,
'default' => -1,
'null' => NULL_ALLOWED,
],
'categoryid' => [
'type' => PARAM_INT
],
'configdata' => [
'type' => PARAM_RAW,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
);
}
/**
* Get decoded configdata.
*
* @return array
*/
protected function get_configdata(): array {
return json_decode($this->raw_get('configdata') ?? '', true) ?? array();
}
}
+214
View File
@@ -0,0 +1,214 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Customfield package
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield;
defined('MOODLE_INTERNAL') || die;
/**
* Class field_config_form
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class field_config_form extends \core_form\dynamic_form {
/** @var field_controller */
protected $field;
/**
* Class definition
*
* @throws \coding_exception
*/
public function definition() {
$mform = $this->_form;
$field = $this->get_field();
$handler = $field->get_handler();
$mform->addElement('header', '_commonsettings', get_string('commonsettings', 'core_customfield'));
$mform->addElement('text', 'name', get_string('fieldname', 'core_customfield'), 'size="50"');
$mform->addRule('name', null, 'required', null, 'client');
$mform->setType('name', PARAM_TEXT);
// Accepted values for 'shortname' would follow [a-z0-9_] pattern,
// but we are accepting any PARAM_TEXT value here,
// and checking [a-zA-Z0-9_] pattern in validation() function to throw an error when needed.
$mform->addElement('text', 'shortname', get_string('fieldshortname', 'core_customfield'), 'size=20');
$mform->addHelpButton('shortname', 'shortname', 'core_customfield');
$mform->addRule('shortname', null, 'required', null, 'client');
$mform->setType('shortname', PARAM_TEXT);
$desceditoroptions = $handler->get_description_text_options();
$mform->addElement('editor', 'description_editor', get_string('description', 'core_customfield'), null, $desceditoroptions);
$mform->addHelpButton('description_editor', 'description', 'core_customfield');
// If field is required.
$mform->addElement('selectyesno', 'configdata[required]', get_string('isfieldrequired', 'core_customfield'));
$mform->addHelpButton('configdata[required]', 'isfieldrequired', 'core_customfield');
$mform->setType('configdata[required]', PARAM_BOOL);
// If field data is unique.
$mform->addElement('selectyesno', 'configdata[uniquevalues]', get_string('isdataunique', 'core_customfield'));
$mform->addHelpButton('configdata[uniquevalues]', 'isdataunique', 'core_customfield');
$mform->setType('configdata[uniquevalues]', PARAM_BOOL);
// Field specific settings from field type.
$field->config_form_definition($mform);
// Handler/component settings.
$handler->config_form_definition($mform);
// We add hidden fields.
$mform->addElement('hidden', 'categoryid');
$mform->setType('categoryid', PARAM_INT);
$mform->addElement('hidden', 'type');
$mform->setType('type', PARAM_COMPONENT);
$mform->addElement('hidden', 'id');
$mform->setType('id', PARAM_INT);
// This form is only used inside modal dialogues and never needs action buttons.
}
/**
* Field data validation
*
* @param array $data
* @param array $files
* @return array
*/
public function validation($data, $files = array()) {
global $DB;
$errors = array();
$field = $this->get_field();
$handler = $field->get_handler();
// Check the shortname is specified and is unique for this component-area-itemid combination.
if (!preg_match('/^[a-z0-9_]+$/', $data['shortname'])) {
// Check allowed pattern (numbers, letters and underscore).
$errors['shortname'] = get_string('invalidshortnameerror', 'core_customfield');
} else if ($DB->record_exists_sql('SELECT 1 FROM {customfield_field} f ' .
'JOIN {customfield_category} c ON c.id = f.categoryid ' .
'WHERE f.shortname = ? AND f.id <> ? AND c.component = ? AND c.area = ? AND c.itemid = ?',
[$data['shortname'], $data['id'],
$handler->get_component(), $handler->get_area(), $handler->get_itemid()])) {
$errors['shortname'] = get_string('formfieldcheckshortname', 'core_customfield');
}
$errors = array_merge($errors, $field->config_form_validation($data, $files));
return $errors;
}
/**
* Get field
*
* @return field_controller
* @throws \moodle_exception
*/
protected function get_field(): field_controller {
if ($this->field === null) {
if (!empty($this->_ajaxformdata['id'])) {
$this->field = \core_customfield\field_controller::create((int)$this->_ajaxformdata['id']);
} else if (!empty($this->_ajaxformdata['categoryid']) && !empty($this->_ajaxformdata['type'])) {
$category = \core_customfield\category_controller::create((int)$this->_ajaxformdata['categoryid']);
$type = clean_param($this->_ajaxformdata['type'], PARAM_PLUGIN);
$this->field = \core_customfield\field_controller::create(0, (object)['type' => $type], $category);
} else {
throw new \moodle_exception('fieldnotfound', 'core_customfield');
}
}
return $this->field;
}
/**
* Check if current user has access to this form, otherwise throw exception
*
* Sometimes permission check may depend on the action and/or id of the entity.
* If necessary, form data is available in $this->_ajaxformdata
*/
protected function check_access_for_dynamic_submission(): void {
$field = $this->get_field();
$handler = $field->get_handler();
if (!$handler->can_configure()) {
throw new \moodle_exception('nopermissionconfigure', 'core_customfield');
}
}
/**
* Load in existing data as form defaults
*
* Can be overridden to retrieve existing values from db by entity id and also
* to preprocess editor and filemanager elements
*
* Example:
* $this->set_data(get_entity($this->_ajaxformdata['id']));
*/
public function set_data_for_dynamic_submission(): void {
$this->set_data(api::prepare_field_for_config_form($this->get_field()));
}
/**
* Process the form submission
*
* This method can return scalar values or arrays that can be json-encoded, they will be passed to the caller JS.
*
* @return mixed
*/
public function process_dynamic_submission() {
$data = $this->get_data();
$field = $this->get_field();
$handler = $field->get_handler();
$handler->save_field_configuration($field, $data);
return null;
}
/**
* Form context
* @return \context
*/
protected function get_context_for_dynamic_submission(): \context {
return $this->get_field()->get_handler()->get_configuration_context();
}
/**
* Page url
* @return \moodle_url
*/
protected function get_page_url_for_dynamic_submission(): \moodle_url {
$field = $this->get_field();
if ($field->get('id')) {
$params = ['action' => 'editfield', 'id' => $field->get('id')];
} else {
$params = ['action' => 'addfield', 'categoryid' => $field->get('categoryid'), 'type' => $field->get('type')];
}
return new \moodle_url($field->get_handler()->get_configuration_url(), $params);
}
}
+287
View File
@@ -0,0 +1,287 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Field controller abstract class
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield;
defined('MOODLE_INTERNAL') || die;
/**
* Base class for custom fields controllers
*
* This class is a wrapper around the persistent field class that allows to define the field
* configuration
*
* Custom field plugins must define a class
* \{pluginname}\field_controller extends \core_customfield\field_controller
*
* @package core_customfield
* @copyright 2018 Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class field_controller {
/**
* Field persistent class
*
* @var field
*/
protected $field;
/**
* Category of the field.
*
* @var category_controller
*/
protected $category;
/**
* Constructor.
*
* @param int $id
* @param \stdClass|null $record
*/
public function __construct(int $id = 0, \stdClass $record = null) {
$this->field = new field($id, $record);
}
/**
* Creates an instance of field_controller
*
* Parameters $id, $record and $category can complement each other but not conflict.
* If $id is not specified, categoryid must be present either in $record or in $category.
* If $id is not specified, type must be present in $record
*
* No DB queries are performed if both $record and $category are specified.
*
* @param int $id
* @param \stdClass|null $record
* @param category_controller|null $category
* @return field_controller will return the instance of the class from the customfield element plugin
* @throws \coding_exception
* @throws \moodle_exception
*/
public static function create(int $id, \stdClass $record = null, category_controller $category = null): field_controller {
global $DB;
if ($id && $record) {
// This warning really should be in persistent as well.
debugging('Too many parameters, either id need to be specified or a record, but not both.',
DEBUG_DEVELOPER);
}
if ($id) {
if (!$record = $DB->get_record(field::TABLE, array('id' => $id), '*', IGNORE_MISSING)) {
throw new \moodle_exception('fieldnotfound', 'core_customfield');
}
}
if (empty($record->categoryid)) {
if (!$category) {
throw new \coding_exception('Not enough parameters to initialise field_controller - unknown category');
} else {
$record->categoryid = $category->get('id');
}
}
if (empty($record->type)) {
throw new \coding_exception('Not enough parameters to initialise field_controller - unknown field type');
}
$type = $record->type;
if (!$category) {
$category = category_controller::create($record->categoryid);
}
if ($category->get('id') != $record->categoryid) {
throw new \coding_exception('Category of the field does not match category from the parameter');
}
$customfieldtype = "\\customfield_{$type}\\field_controller";
if (!class_exists($customfieldtype) || !is_subclass_of($customfieldtype, self::class)) {
throw new \moodle_exception('errorfieldtypenotfound', 'core_customfield', '', s($type));
}
$fieldcontroller = new $customfieldtype(0, $record);
$fieldcontroller->category = $category;
$category->add_field($fieldcontroller);
return $fieldcontroller;
}
/**
* Perform pre-processing of field values, for example those that originate from an external source (e.g. upload course tool)
*
* Override in plugin classes as necessary
*
* @param string $value
* @return mixed
*/
public function parse_value(string $value) {
return $value;
}
/**
* Validate the data on the field configuration form
*
* Plugins can override it
*
* @param array $data from the add/edit profile field form
* @param array $files
* @return array associative array of error messages
*/
public function config_form_validation(array $data, $files = array()): array {
return array();
}
/**
* Persistent getter parser.
*
* @param string $property
* @return mixed
*/
final public function get(string $property) {
return $this->field->get($property);
}
/**
* Persistent setter parser.
*
* @param string $property
* @param mixed $value
* @return field
*/
final public function set($property, $value) {
return $this->field->set($property, $value);
}
/**
* Delete a field and all associated data
*
* Plugins may override it if it is necessary to delete related data (such as files)
*
* Not that the delete() method from data_controller is not called here.
*
* @return bool
*/
public function delete(): bool {
global $DB;
$DB->delete_records('customfield_data', ['fieldid' => $this->get('id')]);
return $this->field->delete();
}
/**
* Save or update the persistent class to database.
*
* @return void
*/
public function save() {
$this->field->save();
}
/**
* Persistent to_record parser.
*
* @return \stdClass
*/
final public function to_record() {
return $this->field->to_record();
}
/**
* Get the category associated with this field
*
* @return category_controller
*/
final public function get_category(): category_controller {
return $this->category;
}
/**
* Get configdata property.
*
* @param string $property name of the property
* @return mixed
*/
public function get_configdata_property(string $property) {
$configdata = $this->field->get('configdata');
if (!isset($configdata[$property])) {
return null;
}
return $configdata[$property];
}
/**
* Returns a handler for this field
*
* @return handler
*/
final public function get_handler(): handler {
return $this->get_category()->get_handler();
}
/**
* Prepare the field data to set in the configuration form
*
* Plugin can override if some preprocessing required for editor or filemanager fields
*
* @param \stdClass $formdata
*/
public function prepare_for_config_form(\stdClass $formdata) {
}
/**
* Add specific settings to the field configuration form, for example "default value"
*
* @param \MoodleQuickForm $mform
*/
abstract public function config_form_definition(\MoodleQuickForm $mform);
/**
* Returns the field name formatted according to configuration context.
*
* @param bool $escape
* @return string
*/
public function get_formatted_name(bool $escape = true): string {
$context = $this->get_handler()->get_configuration_context();
return format_string($this->get('name'), true, [
'context' => $context,
'escape' => $escape,
]);
}
/**
* Does this custom field type support being used as part of the block_myoverview
* custom field grouping?
* @return bool
*/
public function supports_course_grouping(): bool {
return false;
}
/**
* If this field supports course filtering, then this function needs overriding to
* return the formatted values for this.
* @param array $values the used values that need grouping
* @return array
*/
public function course_grouping_format_values($values): array {
return [];
}
}
+824
View File
@@ -0,0 +1,824 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The abstract custom fields handler
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield;
use backup_nested_element;
use core_customfield\output\field_data;
use stdClass;
defined('MOODLE_INTERNAL') || die;
/**
* Base class for custom fields handlers
*
* This handler provides callbacks for field configuration form and also allows to add the fields to the instance editing form
*
* Every plugin that wants to use custom fields must define a handler class:
* <COMPONENT_OR_PLUGIN>\customfield\<AREA>_handler extends \core_customfield\handler
*
* To initiate a class use an appropriate static method:
* - <handlerclass>::create - to create an instance of a known handler
* - \core_customfield\handler::get_handler - to create an instance of a handler for given component/area/itemid
*
* Also handler is automatically created when the following methods are called:
* - \core_customfield\api::get_field($fieldid)
* - \core_customfield\api::get_category($categoryid)
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class handler {
/**
* The component this handler handles
*
* @var string $component
*/
private $component;
/**
* The area within the component
*
* @var string $area
*/
private $area;
/**
* The id of the item within the area and component
* @var int $itemid
*/
private $itemid;
/**
* @var category_controller[]
*/
protected $categories = null;
/**
* Handler constructor.
*
* @param int $itemid
*/
final protected function __construct(int $itemid = 0) {
if (!preg_match('|^(\w+_[\w_]+)\\\\customfield\\\\([\w_]+)_handler$|', static::class, $matches)) {
throw new \coding_exception('Handler class name must have format: <PLUGIN>\\customfield\\<AREA>_handler');
}
$this->component = $matches[1];
$this->area = $matches[2];
$this->itemid = $itemid;
}
/**
* Returns an instance of the handler
*
* Some areas may choose to use singleton/caching here
*
* @param int $itemid
* @return handler
*/
public static function create(int $itemid = 0): handler {
return new static($itemid);
}
/**
* Returns an instance of handler by component/area/itemid
*
* @param string $component component name of full frankenstyle plugin name
* @param string $area name of the area (each component/plugin may define handlers for multiple areas)
* @param int $itemid item id if the area uses them (usually not used)
* @return handler
*/
public static function get_handler(string $component, string $area, int $itemid = 0): handler {
$classname = $component . '\\customfield\\' . $area . '_handler';
if (class_exists($classname) && is_subclass_of($classname, self::class)) {
return $classname::create($itemid);
}
$a = ['component' => s($component), 'area' => s($area)];
throw new \moodle_exception('unknownhandler', 'core_customfield', '', $a);
}
/**
* Get component
*
* @return string
*/
public function get_component(): string {
return $this->component;
}
/**
* Get area
*
* @return string
*/
public function get_area(): string {
return $this->area;
}
/**
* Context that should be used for new categories created by this handler
*
* @return \context
*/
abstract public function get_configuration_context(): \context;
/**
* URL for configuration of the fields on this handler.
*
* @return \moodle_url
*/
abstract public function get_configuration_url(): \moodle_url;
/**
* Context that should be used for data stored for the given record
*
* @param int $instanceid id of the instance or 0 if the instance is being created
* @return \context
*/
abstract public function get_instance_context(int $instanceid = 0): \context;
/**
* Get itemid
*
* @return int|null
*/
public function get_itemid(): int {
return $this->itemid;
}
/**
* Uses categories
*
* @return bool
*/
public function uses_categories(): bool {
return true;
}
/**
* Generates a name for the new category
*
* @param int $suffix
* @return string
*/
protected function generate_category_name($suffix = 0): string {
if ($suffix) {
return get_string('otherfieldsn', 'core_customfield', $suffix);
} else {
return get_string('otherfields', 'core_customfield');
}
}
/**
* Creates a new category and inserts it to the database
*
* @param string $name name of the category, null to generate automatically
* @return int id of the new category
*/
public function create_category(string $name = null): int {
global $DB;
$params = ['component' => $this->get_component(), 'area' => $this->get_area(), 'itemid' => $this->get_itemid()];
if (empty($name)) {
for ($suffix = 0; $suffix < 100; $suffix++) {
$name = $this->generate_category_name($suffix);
if (!$DB->record_exists(category::TABLE, $params + ['name' => $name])) {
break;
}
}
}
$category = category_controller::create(0, (object)['name' => $name], $this);
api::save_category($category);
$this->clear_configuration_cache();
return $category->get('id');
}
/**
* Validate that the given category belongs to this handler
*
* @param category_controller $category
* @return category_controller
* @throws \moodle_exception
*/
protected function validate_category(category_controller $category): category_controller {
$categories = $this->get_categories_with_fields();
if (!array_key_exists($category->get('id'), $categories)) {
throw new \moodle_exception('categorynotfound', 'core_customfield');
}
return $categories[$category->get('id')];
}
/**
* Validate that the given field belongs to this handler
*
* @param field_controller $field
* @return field_controller
* @throws \moodle_exception
*/
protected function validate_field(field_controller $field): field_controller {
if (!array_key_exists($field->get('categoryid'), $this->get_categories_with_fields())) {
throw new \moodle_exception('fieldnotfound', 'core_customfield');
}
$category = $this->get_categories_with_fields()[$field->get('categoryid')];
if (!array_key_exists($field->get('id'), $category->get_fields())) {
throw new \moodle_exception('fieldnotfound', 'core_customfield');
}
return $category->get_fields()[$field->get('id')];
}
/**
* Change name for a field category
*
* @param category_controller $category
* @param string $name
*/
public function rename_category(category_controller $category, string $name) {
$this->validate_category($category);
$category->set('name', $name);
api::save_category($category);
$this->clear_configuration_cache();
}
/**
* Change sort order of the categories
*
* @param category_controller $category category that needs to be moved
* @param int $beforeid id of the category this category needs to be moved before, 0 to move to the end
*/
public function move_category(category_controller $category, int $beforeid = 0) {
$category = $this->validate_category($category);
api::move_category($category, $beforeid);
$this->clear_configuration_cache();
}
/**
* Permanently delete category, all fields in it and all associated data
*
* @param category_controller $category
* @return bool
*/
public function delete_category(category_controller $category): bool {
$category = $this->validate_category($category);
$result = api::delete_category($category);
$this->clear_configuration_cache();
return $result;
}
/**
* Deletes all data and all fields and categories defined in this handler
*/
public function delete_all() {
$categories = $this->get_categories_with_fields();
foreach ($categories as $category) {
api::delete_category($category);
}
$this->clear_configuration_cache();
}
/**
* Permanently delete a custom field configuration and all associated data
*
* @param field_controller $field
* @return bool
*/
public function delete_field_configuration(field_controller $field): bool {
$field = $this->validate_field($field);
$result = api::delete_field_configuration($field);
$this->clear_configuration_cache();
return $result;
}
/**
* Change fields sort order, move field to another category
*
* @param field_controller $field field that needs to be moved
* @param int $categoryid category that needs to be moved
* @param int $beforeid id of the category this category needs to be moved before, 0 to move to the end
*/
public function move_field(field_controller $field, int $categoryid, int $beforeid = 0) {
$field = $this->validate_field($field);
api::move_field($field, $categoryid, $beforeid);
$this->clear_configuration_cache();
}
/**
* The current user can configure custom fields on this component.
*
* @return bool
*/
abstract public function can_configure(): bool;
/**
* The current user can edit given custom fields on the given instance
*
* Called to filter list of fields displayed on the instance edit form
*
* Capability to edit/create instance is checked separately
*
* @param field_controller $field
* @param int $instanceid id of the instance or 0 if the instance is being created
* @return bool
*/
abstract public function can_edit(field_controller $field, int $instanceid = 0): bool;
/**
* The current user can view the value of the custom field for a given custom field and instance
*
* Called to filter list of fields returned by methods get_instance_data(), get_instances_data(),
* export_instance_data(), export_instance_data_object()
*
* Access to the instance itself is checked by handler before calling these methods
*
* @param field_controller $field
* @param int $instanceid
* @return bool
*/
abstract public function can_view(field_controller $field, int $instanceid): bool;
/**
* Returns the custom field values for an individual instance
*
* The caller must check access to the instance itself before invoking this method
*
* The result is an array of data_controller objects
*
* @param int $instanceid
* @param bool $returnall return data for all fields (by default only visible fields)
* @return data_controller[] array of data_controller objects indexed by fieldid. All fields are present,
* some data_controller objects may have 'id', some not
* In the last case data_controller::get_value() and export_value() functions will return default values.
*/
public function get_instance_data(int $instanceid, bool $returnall = false): array {
$fields = $returnall ? $this->get_fields() : $this->get_visible_fields($instanceid);
return api::get_instance_fields_data($fields, $instanceid);
}
/**
* Returns the custom fields values for multiple instances
*
* The caller must check access to the instance itself before invoking this method
*
* The result is an array of data_controller objects
*
* @param int[] $instanceids
* @param bool $returnall return data for all fields (by default only visible fields)
* @return data_controller[][] 2-dimension array, first index is instanceid, second index is fieldid.
* All instanceids and all fieldids are present, some data_controller objects may have 'id', some not.
* In the last case data_controller::get_value() and export_value() functions will return default values.
*/
public function get_instances_data(array $instanceids, bool $returnall = false): array {
$result = api::get_instances_fields_data($this->get_fields(), $instanceids);
if (!$returnall) {
// Filter only by visible fields (list of visible fields may be different for each instance).
$handler = $this;
foreach ($instanceids as $instanceid) {
$result[$instanceid] = array_filter($result[$instanceid], function(data_controller $d) use ($handler) {
return $handler->can_view($d->get_field(), $d->get('instanceid'));
});
}
}
return $result;
}
/**
* Returns the custom field values for an individual instance ready to be displayed
*
* The caller must check access to the instance itself before invoking this method
*
* The result is an array of \core_customfield\output\field_data objects
*
* @param int $instanceid
* @param bool $returnall
* @return \core_customfield\output\field_data[]
*/
public function export_instance_data(int $instanceid, bool $returnall = false): array {
return array_map(function($d) {
return new field_data($d);
}, $this->get_instance_data($instanceid, $returnall));
}
/**
* Returns the custom field values for an individual instance ready to be displayed
*
* The caller must check access to the instance itself before invoking this method
*
* The result is a class where properties are fields short names and the values their export values for this instance
*
* @param int $instanceid
* @param bool $returnall
* @return stdClass
*/
public function export_instance_data_object(int $instanceid, bool $returnall = false): stdClass {
$rv = new stdClass();
foreach ($this->export_instance_data($instanceid, $returnall) as $d) {
$rv->{$d->get_shortname()} = $d->get_value();
}
return $rv;
}
/**
* Display visible custom fields.
* This is a sample implementation that can be overridden in each handler.
*
* @param data_controller[] $fieldsdata
* @return string
*/
public function display_custom_fields_data(array $fieldsdata): string {
global $PAGE;
$output = $PAGE->get_renderer('core_customfield');
$content = '';
foreach ($fieldsdata as $data) {
$fd = new field_data($data);
$content .= $output->render($fd);
}
return $content;
}
/**
* Returns array of categories, each of them contains a list of fields definitions.
*
* @return category_controller[]
*/
public function get_categories_with_fields(): array {
if ($this->categories === null) {
$this->categories = api::get_categories_with_fields($this->get_component(), $this->get_area(), $this->get_itemid());
}
$handler = $this;
array_walk($this->categories, function(category_controller $c) use ($handler) {
$c->set_handler($handler);
});
return $this->categories;
}
/**
* Clears a list of categories with corresponding fields definitions.
*/
protected function clear_configuration_cache() {
$this->categories = null;
}
/**
* Checks if current user can backup a given field
*
* Capability to backup the instance does not need to be checked here
*
* @param field_controller $field
* @param int $instanceid
* @return bool
*/
protected function can_backup(field_controller $field, int $instanceid): bool {
return $this->can_view($field, $instanceid) || $this->can_edit($field, $instanceid);
}
/**
* Run the custom field backup callback for each controller.
*
* @param int $instanceid The instance ID.
* @param \backup_nested_element $customfieldselement The custom field element to be backed up.
*/
public function backup_define_structure(int $instanceid, backup_nested_element $customfieldselement): void {
$datacontrollers = $this->get_instance_data($instanceid);
foreach ($datacontrollers as $controller) {
if ($this->can_backup($controller->get_field(), $instanceid)) {
$controller->backup_define_structure($customfieldselement);
}
}
}
/**
* Run the custom field restore callback for each controller.
*
* @param \restore_structure_step $step The restore step instance.
* @param int $newid The new ID for the custom field data after restore.
* @param int $oldid The original ID of the custom field data before backup.
*/
public function restore_define_structure(\restore_structure_step $step, int $newid, int $oldid): void {
$datacontrollers = $this->get_instance_data($newid);
foreach ($datacontrollers as $controller) {
$controller->restore_define_structure($step, $newid, $oldid);
}
}
/**
* Get raw data associated with all fields current user can view or edit
*
* @param int $instanceid
* @return array
*/
public function get_instance_data_for_backup(int $instanceid): array {
$finalfields = [];
$data = $this->get_instance_data($instanceid, true);
foreach ($data as $d) {
if ($d->get('id') && $this->can_backup($d->get_field(), $instanceid)) {
$finalfields[] = [
'id' => $d->get('id'),
'shortname' => $d->get_field()->get('shortname'),
'type' => $d->get_field()->get('type'),
'value' => $d->get_value(),
'valueformat' => $d->get('valueformat'),
'valuetrust' => $d->get('valuetrust'),
];
}
}
return $finalfields;
}
/**
* Form data definition callback.
*
* This method is called from moodleform::definition_after_data and allows to tweak
* mform with some data coming directly from the field plugin data controller.
*
* @param \MoodleQuickForm $mform
* @param int $instanceid
*/
public function instance_form_definition_after_data(\MoodleQuickForm $mform, int $instanceid = 0) {
$editablefields = $this->get_editable_fields($instanceid);
$fields = api::get_instance_fields_data($editablefields, $instanceid);
foreach ($fields as $formfield) {
$formfield->instance_form_definition_after_data($mform);
}
}
/**
* Prepares the custom fields data related to the instance to pass to mform->set_data()
*
* Example:
* $instance = $DB->get_record(...);
* // .... prepare editor, filemanager, add tags, etc.
* $handler->instance_form_before_set_data($instance);
* $form->set_data($instance);
*
* @param stdClass $instance the instance that has custom fields, if 'id' attribute is present the custom
* fields for this instance will be added, otherwise the default values will be added.
*/
public function instance_form_before_set_data(stdClass $instance) {
$instanceid = !empty($instance->id) ? $instance->id : 0;
$fields = api::get_instance_fields_data($this->get_editable_fields($instanceid), $instanceid);
foreach ($fields as $formfield) {
$formfield->instance_form_before_set_data($instance);
}
}
/**
* Saves the given data for custom fields, must be called after the instance is saved and id is present
*
* Example:
* if ($data = $form->get_data()) {
* // ... save main instance, set $data->id if instance was created.
* $handler->instance_form_save($data);
* redirect(...);
* }
*
* @param stdClass $instance data received from a form
* @param bool $isnewinstance if this is call is made during instance creation
*/
public function instance_form_save(stdClass $instance, bool $isnewinstance = false) {
if (empty($instance->id)) {
throw new \coding_exception('Caller must ensure that id is already set in data before calling this method');
}
if (!preg_grep('/^customfield_/', array_keys((array)$instance))) {
// For performance.
return;
}
$editablefields = $this->get_editable_fields($isnewinstance ? 0 : $instance->id);
$fields = api::get_instance_fields_data($editablefields, $instance->id);
foreach ($fields as $data) {
if (!$data->get('id')) {
$data->set('contextid', $this->get_instance_context($instance->id)->id);
}
$data->instance_form_save($instance);
}
}
/**
* Validates the given data for custom fields, used in moodleform validation() function
*
* Example:
* public function validation($data, $files) {
* $errors = [];
* // .... check other fields.
* $errors = array_merge($errors, $handler->instance_form_validation($data, $files));
* return $errors;
* }
*
* @param array $data
* @param array $files
* @return array validation errors
*/
public function instance_form_validation(array $data, array $files) {
$instanceid = empty($data['id']) ? 0 : $data['id'];
$editablefields = $this->get_editable_fields($instanceid);
$fields = api::get_instance_fields_data($editablefields, $instanceid);
$errors = [];
foreach ($fields as $formfield) {
$errors += $formfield->instance_form_validation($data, $files);
}
return $errors;
}
/**
* Adds custom fields to instance editing form
*
* Example:
* public function definition() {
* // ... normal instance definition, including hidden 'id' field.
* $handler->instance_form_definition($this->_form, $instanceid);
* $this->add_action_buttons();
* }
*
* @param \MoodleQuickForm $mform
* @param int $instanceid id of the instance, can be null when instance is being created
* @param string $headerlangidentifier If specified, a lang string will be used for field category headings
* @param string $headerlangcomponent
*/
public function instance_form_definition(\MoodleQuickForm $mform, int $instanceid = 0,
?string $headerlangidentifier = null, ?string $headerlangcomponent = null) {
$editablefields = $this->get_editable_fields($instanceid);
$fieldswithdata = api::get_instance_fields_data($editablefields, $instanceid);
$lastcategoryid = null;
foreach ($fieldswithdata as $data) {
$categoryid = $data->get_field()->get_category()->get('id');
if ($categoryid != $lastcategoryid) {
$categoryname = $data->get_field()->get_category()->get_formatted_name();
// Load category header lang string if specified.
if (!empty($headerlangidentifier)) {
$categoryname = get_string($headerlangidentifier, $headerlangcomponent, $categoryname);
}
$mform->addElement('header', 'category_' . $categoryid, $categoryname);
$lastcategoryid = $categoryid;
}
$data->instance_form_definition($mform);
$field = $data->get_field()->to_record();
if (strlen((string)$field->description)) {
// Add field description.
$context = $this->get_configuration_context();
$value = file_rewrite_pluginfile_urls($field->description, 'pluginfile.php',
$context->id, 'core_customfield', 'description', $field->id);
$value = format_text($value, $field->descriptionformat, ['context' => $context]);
$mform->addElement('static', 'customfield_' . $field->shortname . '_static', '', $value);
}
}
}
/**
* Get field types array
*
* @return array
*/
public function get_available_field_types(): array {
return api::get_available_field_types();
}
/**
* Options for processing embedded files in the field description.
*
* Handlers may want to extend it to disable files support and/or specify 'noclean'=>true
* Context is not necessary here
*
* @return array
*/
public function get_description_text_options(): array {
global $CFG;
require_once($CFG->libdir.'/formslib.php');
return [
'maxfiles' => EDITOR_UNLIMITED_FILES,
'maxbytes' => $CFG->maxbytes,
'context' => $this->get_configuration_context()
];
}
/**
* Save the field configuration with the data from the form
*
* @param field_controller $field
* @param stdClass $data data from the form
*/
public function save_field_configuration(field_controller $field, stdClass $data) {
if ($field->get('id')) {
$field = $this->validate_field($field);
} else {
$this->validate_category($field->get_category());
}
api::save_field_configuration($field, $data);
$this->clear_configuration_cache();
}
/**
* Creates or updates custom field data for a instanceid from backup data.
* The handlers have to override it if they support backup.
*
* @param \restore_task $task
* @param array $data
*
* @return int|void Implementations should conditionally return the ID of the created or updated record.
*/
public function restore_instance_data_from_backup(\restore_task $task, array $data) {
throw new \coding_exception('Must be implemented in the handler');
}
/**
* Returns list of fields defined for this instance as an array (not groupped by categories)
*
* Fields are sorted in the same order they would appear on the instance edit form
*
* Note that this function returns all fields in all categories regardless of whether the current user
* can view or edit data associated with them
*
* @return field_controller[]
*/
public function get_fields(): array {
$categories = $this->get_categories_with_fields();
$fields = [];
foreach ($categories as $category) {
foreach ($category->get_fields() as $field) {
$fields[$field->get('id')] = $field;
}
}
return $fields;
}
/**
* Get visible fields
*
* @param int $instanceid
* @return field_controller[]
*/
protected function get_visible_fields(int $instanceid): array {
$handler = $this;
return array_filter($this->get_fields(),
function($field) use($handler, $instanceid) {
return $handler->can_view($field, $instanceid);
}
);
}
/**
* Get editable fields
*
* @param int $instanceid
* @return field_controller[]
*/
public function get_editable_fields(int $instanceid): array {
$handler = $this;
return array_filter($this->get_fields(),
function($field) use($handler, $instanceid) {
return $handler->can_edit($field, $instanceid);
}
);
}
/**
* Allows to add custom controls to the field configuration form that will be saved in configdata
*
* @param \MoodleQuickForm $mform
*/
public function config_form_definition(\MoodleQuickForm $mform) {
}
/**
* Deletes all data related to all fields of an instance.
*
* @param int $instanceid
*/
public function delete_instance(int $instanceid) {
$fielddata = api::get_instance_fields_data($this->get_fields(), $instanceid, false);
foreach ($fielddata as $data) {
$data->delete();
}
}
}
+114
View File
@@ -0,0 +1,114 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* core_customfield field value renderable.
*
* @package core_customfield
* @copyright 2018 Daniel Neis Araujo <danielneis@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\output;
use core_customfield\data_controller;
defined('MOODLE_INTERNAL') || die;
/**
* core_customfield field value renderable class.
*
* @package core_customfield
* @copyright 2018 Daniel Neis Araujo <danielneis@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class field_data implements \renderable, \templatable {
/** @var \core_customfield\data_controller */
protected $data;
/**
* Renderable constructor.
*
* @param \core_customfield\data_controller $data
*/
public function __construct(\core_customfield\data_controller $data) {
$this->data = $data;
}
/**
* Returns the data value formatted for the output
*
* @return mixed|null
*/
public function get_value() {
return $this->data->export_value();
}
/**
* Returns the field type (checkbox, date, text, ...)
*
* @return string
*/
public function get_type(): string {
return $this->data->get_field()->get('type');
}
/**
* Returns the field short name
*
* @return string
*/
public function get_shortname(): string {
return $this->data->get_field()->get('shortname');
}
/**
* Returns the field name formatted for the output
*
* @return string
*/
public function get_name(): string {
return $this->data->get_field()->get_formatted_name();
}
/**
* Returns the data controller used to create this object if additional attributes are needed
*
* @return data_controller
*/
public function get_data_controller(): data_controller {
return $this->data;
}
/**
* Export data for using as template context.
*
* @param \renderer_base $output
* @return \stdClass
*/
public function export_for_template(\renderer_base $output) {
$value = $this->get_value();
return (object)[
'value' => $value,
'type' => $this->get_type(),
'shortname' => $this->get_shortname(),
'name' => $this->get_name(),
'hasvalue' => ($value !== null),
'instanceid' => $this->data->get('instanceid')
];
}
}
+126
View File
@@ -0,0 +1,126 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Customfield component output.
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\output;
use core_customfield\api;
use core_customfield\handler;
use renderable;
use templatable;
defined('MOODLE_INTERNAL') || die;
/**
* Class management
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class management implements renderable, templatable {
/**
* @var handler
*/
protected $handler;
/**
* @var
*/
protected $categoryid;
/**
* management constructor.
*
* @param \core_customfield\handler $handler
*/
public function __construct(\core_customfield\handler $handler) {
$this->handler = $handler;
}
/**
* Export for template
*
* @param \renderer_base $output
* @return array|object|\stdClass
*/
public function export_for_template(\renderer_base $output) {
$data = new \stdClass();
$fieldtypes = $this->handler->get_available_field_types();
$data->component = $this->handler->get_component();
$data->area = $this->handler->get_area();
$data->itemid = $this->handler->get_itemid();
$data->usescategories = $this->handler->uses_categories();
$categories = $this->handler->get_categories_with_fields();
$categoriesarray = array();
foreach ($categories as $category) {
$categoryarray = array();
$categoryarray['id'] = $category->get('id');
$categoryarray['nameeditable'] = $output->render(api::get_category_inplace_editable($category, true));
$categoryarray['movetitle'] = get_string('movecategory', 'core_customfield',
$category->get_formatted_name());
$categoryarray['fields'] = array();
foreach ($category->get_fields() as $field) {
$fieldname = $field->get_formatted_name();
$fieldarray['type'] = $fieldtypes[$field->get('type')];
$fieldarray['id'] = $field->get('id');
$fieldarray['name'] = $fieldname;
$fieldarray['shortname'] = $field->get('shortname');
$fieldarray['movetitle'] = get_string('movefield', 'core_customfield', $fieldname);
$categoryarray['fields'][] = $fieldarray;
}
$menu = new \action_menu();
$menu->set_menu_trigger(get_string('createnewcustomfield', 'core_customfield'));
foreach ($fieldtypes as $type => $fieldname) {
$action = new \action_menu_link_secondary(new \moodle_url('#'), null, $fieldname,
['data-role' => 'addfield', 'data-categoryid' => $category->get('id'), 'data-type' => $type,
'data-typename' => $fieldname]);
$menu->add($action);
}
$menu->attributes['class'] .= ' float-left mr-1';
$categoryarray['addfieldmenu'] = $output->render($menu);
$categoriesarray[] = $categoryarray;
}
$data->categories = $categoriesarray;
if (empty($data->categories)) {
$data->nocategories = get_string('nocategories', 'core_customfield');
}
return $data;
}
}
+62
View File
@@ -0,0 +1,62 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Renderer.
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\output;
defined('MOODLE_INTERNAL') || die();
use plugin_renderer_base;
/**
* Renderer class.
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class renderer extends plugin_renderer_base {
/**
* Render custom field management interface.
*
* @param \core_customfield\output\management $list
* @return string HTML
*/
protected function render_management(\core_customfield\output\management $list) {
$context = $list->export_for_template($this);
return $this->render_from_template('core_customfield/list', $context);
}
/**
* Render single custom field value
*
* @param \core_customfield\output\field_data $field
* @return string HTML
*/
protected function render_field_data(\core_customfield\output\field_data $field) {
$context = $field->export_for_template($this);
return $this->render_from_template('core_customfield/field_data', $context);
}
}
@@ -0,0 +1,84 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains interface customfield_provider
*
* @package core_customfield
* @copyright 2018 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\privacy;
use core_customfield\data_controller;
defined('MOODLE_INTERNAL') || die();
/**
* Interface customfield_provider, all customfield plugins need to implement it
*
* @package core_customfield
* @copyright 2018 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface customfield_provider extends
\core_privacy\local\request\plugin\subplugin_provider,
// The customfield plugins do not need to do anything themselves for the shared_userlist.
// This is all handled by the component core_customfield.
\core_privacy\local\request\shared_userlist_provider
{
/**
* Preprocesses data object that is going to be exported
*
* Minimum implementation:
* writer::with_context($data->get_context())->export_data($subcontext, $exportdata);
*
* @param data_controller $data
* @param \stdClass $exportdata generated object to be exported
* @param array $subcontext subcontext to use when exporting
* @return mixed
*/
public static function export_customfield_data(data_controller $data, \stdClass $exportdata, array $subcontext);
/**
* Allows plugins to delete everything they store related to the data (usually files)
*
* If plugin does not store any related files or other information, implement as an empty function
*
* @param string $dataidstest select query for data id (note that it may also return data for other field types)
* @param array $params named parameters for the select query
* @param array $contextids list of affected data contexts
* @return mixed
*/
public static function before_delete_data(string $dataidstest, array $params, array $contextids);
/**
* Allows plugins to delete everything they store related to the field configuration (usually files)
*
* The implementation should not delete data or anything related to the data, since "before_delete_data" is
* invoked separately.
*
* If plugin does not store any related files or other information, implement as an empty function
*
* @param string $fieldidstest select query for field id (note that it may also return fields of other types)
* @param array $params named parameters for the select query
* @param int[] $contextids list of affected configuration contexts
*/
public static function before_delete_fields(string $fieldidstest, array $params, array $contextids);
}
+495
View File
@@ -0,0 +1,495 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Customfield component provider class
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_customfield\privacy;
defined('MOODLE_INTERNAL') || die();
use core_customfield\data_controller;
use core_customfield\handler;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\writer;
use core_privacy\manager;
/**
* Class provider
*
* Customfields API does not directly store userid and does not perform any export or delete functionality by itself
*
* However this class defines several functions that can be utilized by components that use customfields API to
* export/delete user data.
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
// Customfield store data.
\core_privacy\local\metadata\provider,
// The customfield subsystem stores data on behalf of other components.
\core_privacy\local\request\subsystem\plugin_provider,
\core_privacy\local\request\shared_userlist_provider {
/**
* Return the fields which contain personal data.
*
* @param collection $collection a reference to the collection to use to store the metadata.
* @return collection the updated collection of metadata items.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_database_table(
'customfield_data',
[
'fieldid' => 'privacy:metadata:customfield_data:fieldid',
'instanceid' => 'privacy:metadata:customfield_data:instanceid',
'intvalue' => 'privacy:metadata:customfield_data:intvalue',
'decvalue' => 'privacy:metadata:customfield_data:decvalue',
'shortcharvalue' => 'privacy:metadata:customfield_data:shortcharvalue',
'charvalue' => 'privacy:metadata:customfield_data:charvalue',
'value' => 'privacy:metadata:customfield_data:value',
'valueformat' => 'privacy:metadata:customfield_data:valueformat',
'valuetrust' => 'privacy:metadata:customfield_data:valuetrust',
'timecreated' => 'privacy:metadata:customfield_data:timecreated',
'timemodified' => 'privacy:metadata:customfield_data:timemodified',
'contextid' => 'privacy:metadata:customfield_data:contextid',
],
'privacy:metadata:customfield_data'
);
// Link to subplugins.
$collection->add_plugintype_link('customfield', [], 'privacy:metadata:customfieldpluginsummary');
$collection->link_subsystem('core_files', 'privacy:metadata:filepurpose');
return $collection;
}
/**
* Returns contexts that have customfields data
*
* To be used in implementations of core_user_data_provider::get_contexts_for_userid
* Caller needs to transfer the $userid to the select subqueries for
* customfield_category->itemid and/or customfield_data->instanceid
*
* @param string $component
* @param string $area
* @param string $itemidstest subquery for selecting customfield_category->itemid
* @param string $instanceidstest subquery for selecting customfield_data->instanceid
* @param array $params array of named parameters
* @return contextlist
*/
public static function get_customfields_data_contexts(string $component, string $area,
string $itemidstest = 'IS NOT NULL', string $instanceidstest = 'IS NOT NULL', array $params = []): contextlist {
$sql = "SELECT d.contextid FROM {customfield_category} c
JOIN {customfield_field} f ON f.categoryid = c.id
JOIN {customfield_data} d ON d.fieldid = f.id AND d.instanceid $instanceidstest
WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest";
$contextlist = new contextlist();
$contextlist->add_from_sql($sql, self::get_params($component, $area, $params));
return $contextlist;
}
/**
* Returns contexts that have customfields configuration (categories and fields)
*
* To be used in implementations of core_user_data_provider::get_contexts_for_userid in cases when user is
* an owner of the fields configuration
* Caller needs to transfer the $userid to the select subquery for customfield_category->itemid
*
* @param string $component
* @param string $area
* @param string $itemidstest subquery for selecting customfield_category->itemid
* @param array $params array of named parameters for itemidstest subquery
* @return contextlist
*/
public static function get_customfields_configuration_contexts(string $component, string $area,
string $itemidstest = 'IS NOT NULL', array $params = []): contextlist {
$sql = "SELECT c.contextid FROM {customfield_category} c
WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest";
$params['component'] = $component;
$params['area'] = $area;
$contextlist = new contextlist();
$contextlist->add_from_sql($sql, self::get_params($component, $area, $params));
return $contextlist;
}
/**
* Exports customfields data
*
* To be used in implementations of core_user_data_provider::export_user_data
* Caller needs to transfer the $userid to the select subqueries for
* customfield_category->itemid and/or customfield_data->instanceid
*
* @param approved_contextlist $contextlist
* @param string $component
* @param string $area
* @param string $itemidstest subquery for selecting customfield_category->itemid
* @param string $instanceidstest subquery for selecting customfield_data->instanceid
* @param array $params array of named parameters for itemidstest and instanceidstest subqueries
* @param array $subcontext subcontext to use in context_writer::export_data, if null (default) the
* "Custom fields data" will be used;
* the data id will be appended to the subcontext array.
*/
public static function export_customfields_data(approved_contextlist $contextlist, string $component, string $area,
string $itemidstest = 'IS NOT NULL', string $instanceidstest = 'IS NOT NULL', array $params = [],
array $subcontext = null) {
global $DB;
// This query is very similar to api::get_instances_fields_data() but also works for multiple itemids
// and has a context filter.
list($contextidstest, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED, 'cfctx');
$sql = "SELECT d.*, f.type AS fieldtype, f.name as fieldname, f.shortname as fieldshortname, c.itemid
FROM {customfield_category} c
JOIN {customfield_field} f ON f.categoryid = c.id
JOIN {customfield_data} d ON d.fieldid = f.id AND d.instanceid $instanceidstest AND d.contextid $contextidstest
WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest
ORDER BY c.itemid, c.sortorder, f.sortorder";
$params = self::get_params($component, $area, $params) + $contextparams;
$records = $DB->get_recordset_sql($sql, $params);
if ($subcontext === null) {
$subcontext = [get_string('customfielddata', 'core_customfield')];
}
/** @var handler $handler */
$handler = null;
$fields = null;
foreach ($records as $record) {
if (!$handler || $handler->get_itemid() != $record->itemid) {
$handler = handler::get_handler($component, $area, $record->itemid);
$fields = $handler->get_fields();
}
$field = (object)['type' => $record->fieldtype, 'shortname' => $record->fieldshortname, 'name' => $record->fieldname];
unset($record->itemid, $record->fieldtype, $record->fieldshortname, $record->fieldname);
try {
$field = array_key_exists($record->fieldid, $fields) ? $fields[$record->fieldid] : null;
$data = data_controller::create(0, $record, $field);
self::export_customfield_data($data, array_merge($subcontext, [$record->id]));
} catch (\Exception $e) {
// We store some data that we can not initialise controller for. We still need to export it.
self::export_customfield_data_unknown($record, $field, array_merge($subcontext, [$record->id]));
}
}
$records->close();
}
/**
* Deletes customfields data
*
* To be used in implementations of core_user_data_provider::delete_data_for_user
* Caller needs to transfer the $userid to the select subqueries for
* customfield_category->itemid and/or customfield_data->instanceid
*
* @param approved_contextlist $contextlist
* @param string $component
* @param string $area
* @param string $itemidstest subquery for selecting customfield_category->itemid
* @param string $instanceidstest subquery for selecting customfield_data->instanceid
* @param array $params array of named parameters for itemidstest and instanceidstest subqueries
*/
public static function delete_customfields_data(approved_contextlist $contextlist, string $component, string $area,
string $itemidstest = 'IS NOT NULL', string $instanceidstest = 'IS NOT NULL', array $params = []) {
global $DB;
list($contextidstest, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED, 'cfctx');
$sql = "SELECT d.id
FROM {customfield_category} c
JOIN {customfield_field} f ON f.categoryid = c.id
JOIN {customfield_data} d ON d.fieldid = f.id AND d.instanceid $instanceidstest AND d.contextid $contextidstest
WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest";
$params = self::get_params($component, $area, $params) + $contextparams;
self::before_delete_data('IN (' . $sql . ') ', $params);
$DB->execute("DELETE FROM {customfield_data}
WHERE instanceid $instanceidstest
AND contextid $contextidstest
AND fieldid IN (SELECT f.id
FROM {customfield_category} c
JOIN {customfield_field} f ON f.categoryid = c.id
WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest)", $params);
}
/**
* Deletes customfields configuration (categories and fields) and all relevant data
*
* To be used in implementations of core_user_data_provider::delete_data_for_user in cases when user is
* an owner of the fields configuration and it is considered user information (quite unlikely situtation but we never
* know what customfields API can be used for)
*
* Caller needs to transfer the $userid to the select subquery for customfield_category->itemid
*
* @param approved_contextlist $contextlist
* @param string $component
* @param string $area
* @param string $itemidstest subquery for selecting customfield_category->itemid
* @param array $params array of named parameters for itemidstest subquery
*/
public static function delete_customfields_configuration(approved_contextlist $contextlist, string $component, string $area,
string $itemidstest = 'IS NOT NULL', array $params = []) {
global $DB;
list($contextidstest, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED, 'cfctx');
$params = self::get_params($component, $area, $params) + $contextparams;
$categoriesids = $DB->get_fieldset_sql("SELECT c.id
FROM {customfield_category} c
WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest AND c.contextid $contextidstest",
$params);
self::delete_categories($contextlist->get_contextids(), $categoriesids);
}
/**
* Deletes all customfields configuration (categories and fields) and all relevant data for the given category context
*
* To be used in implementations of core_user_data_provider::delete_data_for_all_users_in_context
*
* @param string $component
* @param string $area
* @param \context $context
*/
public static function delete_customfields_configuration_for_context(string $component, string $area, \context $context) {
global $DB;
$categoriesids = $DB->get_fieldset_sql("SELECT c.id
FROM {customfield_category} c
JOIN {context} ctx ON ctx.id = c.contextid AND ctx.path LIKE :ctxpath
WHERE c.component = :cfcomponent AND c.area = :cfarea",
self::get_params($component, $area, ['ctxpath' => $context->path]));
self::delete_categories([$context->id], $categoriesids);
}
/**
* Deletes all customfields data for the given context
*
* To be used in implementations of core_user_data_provider::delete_data_for_all_users_in_context
*
* @param string $component
* @param string $area
* @param \context $context
*/
public static function delete_customfields_data_for_context(string $component, string $area, \context $context) {
global $DB;
$sql = "SELECT d.id
FROM {customfield_category} c
JOIN {customfield_field} f ON f.categoryid = c.id
JOIN {customfield_data} d ON d.fieldid = f.id
JOIN {context} ctx ON ctx.id = d.contextid AND ctx.path LIKE :ctxpath
WHERE c.component = :cfcomponent AND c.area = :cfarea";
$params = self::get_params($component, $area, ['ctxpath' => $context->path . '%']);
self::before_delete_data('IN (' . $sql . ') ', $params);
$DB->execute("DELETE FROM {customfield_data}
WHERE fieldid IN (SELECT f.id
FROM {customfield_category} c
JOIN {customfield_field} f ON f.categoryid = c.id
WHERE c.component = :cfcomponent AND c.area = :cfarea)
AND contextid IN (SELECT id FROM {context} WHERE path LIKE :ctxpath)",
$params);
}
/**
* Checks that $params is an associative array and adds parameters for component and area
*
* @param string $component
* @param string $area
* @param array $params
* @return array
* @throws \coding_exception
*/
protected static function get_params(string $component, string $area, array $params): array {
if (!empty($params) && (array_keys($params) === range(0, count($params) - 1))) {
// Argument $params is not an associative array.
throw new \coding_exception('Argument $params must be an associative array!');
}
return $params + ['cfcomponent' => $component, 'cfarea' => $area];
}
/**
* Delete custom fields categories configurations, all their fields and data
*
* @param array $contextids
* @param array $categoriesids
*/
protected static function delete_categories(array $contextids, array $categoriesids) {
global $DB;
if (!$categoriesids) {
return;
}
list($categoryidstest, $catparams) = $DB->get_in_or_equal($categoriesids, SQL_PARAMS_NAMED, 'cfcat');
$datasql = "SELECT d.id FROM {customfield_data} d JOIN {customfield_field} f ON f.id = d.fieldid " .
"WHERE f.categoryid $categoryidstest";
$fieldsql = "SELECT f.id AS fieldid FROM {customfield_field} f WHERE f.categoryid $categoryidstest";
self::before_delete_data("IN ($datasql)", $catparams);
self::before_delete_fields($categoryidstest, $catparams);
$DB->execute('DELETE FROM {customfield_data} WHERE fieldid IN (' . $fieldsql . ')', $catparams);
$DB->execute("DELETE FROM {customfield_field} WHERE categoryid $categoryidstest", $catparams);
$DB->execute("DELETE FROM {customfield_category} WHERE id $categoryidstest", $catparams);
}
/**
* Executes callbacks from the customfield plugins to delete anything related to the data records (usually files)
*
* @param string $dataidstest
* @param array $params
*/
protected static function before_delete_data(string $dataidstest, array $params) {
global $DB;
// Find all field types and all contexts for each field type.
$records = $DB->get_recordset_sql("SELECT ff.type, dd.contextid
FROM {customfield_data} dd
JOIN {customfield_field} ff ON ff.id = dd.fieldid
WHERE dd.id $dataidstest
GROUP BY ff.type, dd.contextid",
$params);
$fieldtypes = [];
foreach ($records as $record) {
$fieldtypes += [$record->type => []];
$fieldtypes[$record->type][] = $record->contextid;
}
$records->close();
// Call plugin callbacks to delete data customfield_provider::before_delete_data().
foreach ($fieldtypes as $fieldtype => $contextids) {
$classname = manager::get_provider_classname_for_component('customfield_' . $fieldtype);
if (class_exists($classname) && is_subclass_of($classname, customfield_provider::class)) {
component_class_callback($classname, 'before_delete_data', [$dataidstest, $params, $contextids]);
}
}
}
/**
* Executes callbacks from the plugins to delete anything related to the fields (usually files)
*
* Also deletes description files
*
* @param string $categoryidstest
* @param array $params
*/
protected static function before_delete_fields(string $categoryidstest, array $params) {
global $DB;
// Find all field types and contexts.
$fieldsql = "SELECT f.id AS fieldid FROM {customfield_field} f WHERE f.categoryid $categoryidstest";
$records = $DB->get_recordset_sql("SELECT f.type, c.contextid
FROM {customfield_field} f
JOIN {customfield_category} c ON c.id = f.categoryid
WHERE c.id $categoryidstest",
$params);
$contexts = [];
$fieldtypes = [];
foreach ($records as $record) {
$contexts[$record->contextid] = $record->contextid;
$fieldtypes += [$record->type => []];
$fieldtypes[$record->type][] = $record->contextid;
}
$records->close();
// Delete description files.
foreach ($contexts as $contextid) {
get_file_storage()->delete_area_files_select($contextid, 'core_customfield', 'description',
" IN ($fieldsql) ", $params);
}
// Call plugin callbacks to delete fields customfield_provider::before_delete_fields().
foreach ($fieldtypes as $type => $contextids) {
$classname = manager::get_provider_classname_for_component('customfield_' . $type);
if (class_exists($classname) && is_subclass_of($classname, customfield_provider::class)) {
component_class_callback($classname, 'before_delete_fields',
[" IN ($fieldsql) ", $params, $contextids]);
}
}
$records->close();
}
/**
* Exports one instance of custom field data
*
* @param data_controller $data
* @param array $subcontext subcontext to pass to content_writer::export_data
*/
public static function export_customfield_data(data_controller $data, array $subcontext) {
$context = $data->get_context();
$exportdata = $data->to_record();
$exportdata->fieldtype = $data->get_field()->get('type');
$exportdata->fieldshortname = $data->get_field()->get('shortname');
$exportdata->fieldname = $data->get_field()->get_formatted_name();
$exportdata->timecreated = \core_privacy\local\request\transform::datetime($exportdata->timecreated);
$exportdata->timemodified = \core_privacy\local\request\transform::datetime($exportdata->timemodified);
unset($exportdata->contextid);
// Use the "export_value" by default for the 'value' attribute, however the plugins may override it in their callback.
$exportdata->value = $data->export_value();
$classname = manager::get_provider_classname_for_component('customfield_' . $data->get_field()->get('type'));
if (class_exists($classname) && is_subclass_of($classname, customfield_provider::class)) {
component_class_callback($classname, 'export_customfield_data', [$data, $exportdata, $subcontext]);
} else {
// Custom field plugin does not implement customfield_provider, just export default value.
writer::with_context($context)->export_data($subcontext, $exportdata);
}
}
/**
* Export data record of unknown type when we were not able to create instance of data_controller
*
* @param \stdClass $record record from db table {customfield_data}
* @param \stdClass $field field record with at least fields type, shortname, name
* @param array $subcontext
*/
protected static function export_customfield_data_unknown(\stdClass $record, \stdClass $field, array $subcontext) {
$context = \context::instance_by_id($record->contextid);
$record->fieldtype = $field->type;
$record->fieldshortname = $field->shortname;
$record->fieldname = format_string($field->name);
$record->timecreated = \core_privacy\local\request\transform::datetime($record->timecreated);
$record->timemodified = \core_privacy\local\request\transform::datetime($record->timemodified);
unset($record->contextid);
$record->value = format_text($record->value, $record->valueformat, [
'context' => $context,
'trusted' => $record->valuetrust,
]);
writer::with_context($context)->export_data($subcontext, $record);
}
}