diff --git a/admin/customfields.php b/admin/customfields.php new file mode 100644 index 00000000000..2f7681eb312 --- /dev/null +++ b/admin/customfields.php @@ -0,0 +1,62 @@ +. + +/** + * Allows the admin to enable, disable and uninstall custom fields + * + * @package core_admin + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../config.php'); +require_once($CFG->libdir.'/adminlib.php'); + +$action = required_param('action', PARAM_ALPHANUMEXT); +$customfieldname = required_param('field', PARAM_PLUGIN); + +$syscontext = context_system::instance(); +$PAGE->set_url('/admin/customfields.php'); +$PAGE->set_context($syscontext); + +require_login(); +require_capability('moodle/site:config', $syscontext); +require_sesskey(); + +$return = new moodle_url('/admin/settings.php', array('section' => 'managecustomfields')); + +$customfieldplugins = core_plugin_manager::instance()->get_plugins_of_type('customfield'); +$sortorder = array_flip(array_keys($customfieldplugins)); + +if (!isset($customfieldplugins[$customfieldname])) { + print_error('customfieldnotfound', 'error', $return, $customfieldname); +} + +switch ($action) { + case 'disable': + if ($customfieldplugins[$customfieldname]->is_enabled()) { + set_config('disabled', 1, 'customfield_'. $customfieldname); + core_plugin_manager::reset_caches(); + } + break; + case 'enable': + if (!$customfieldplugins[$customfieldname]->is_enabled()) { + unset_config('disabled', 'customfield_'. $customfieldname); + core_plugin_manager::reset_caches(); + } + break; +} +redirect($return); diff --git a/admin/settings/plugins.php b/admin/settings/plugins.php index 9a2202f158c..72aa6baaf68 100644 --- a/admin/settings/plugins.php +++ b/admin/settings/plugins.php @@ -59,6 +59,18 @@ if ($hassiteconfig) { $plugin->load_settings($ADMIN, 'formatsettings', $hassiteconfig); } + // Custom fields. + $ADMIN->add('modules', new admin_category('customfieldsettings', new lang_string('customfields', 'core_customfield'))); + $temp = new admin_settingpage('managecustomfields', new lang_string('managecustomfields', 'core_admin')); + $temp->add(new admin_setting_managecustomfields()); + $ADMIN->add('customfieldsettings', $temp); + $plugins = core_plugin_manager::instance()->get_plugins_of_type('customfield'); + core_collator::asort_objects_by_property($plugins, 'displayname'); + foreach ($plugins as $plugin) { + /** @var \core\plugininfo\customfield $plugin */ + $plugin->load_settings($ADMIN, 'customfieldsettings', $hassiteconfig); + } + // blocks $ADMIN->add('modules', new admin_category('blocksettings', new lang_string('blocks'))); $ADMIN->add('blocksettings', new admin_page_manageblocks()); diff --git a/customfield/classes/event/category_created.php b/customfield/classes/event/category_created.php new file mode 100644 index 00000000000..b5234e03f3f --- /dev/null +++ b/customfield/classes/event/category_created.php @@ -0,0 +1,84 @@ +. + +/** + * Custom field category created event. + * + * @package core_customfield + * @copyright 2018 Daniel Neis Araujo + * @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 + * @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'."; + } +} diff --git a/customfield/classes/event/category_updated.php b/customfield/classes/event/category_updated.php new file mode 100644 index 00000000000..2006c12344a --- /dev/null +++ b/customfield/classes/event/category_updated.php @@ -0,0 +1,84 @@ +. + +/** + * Custom field category updated event. + * + * @package core_customfield + * @copyright 2018 Daniel Neis Araujo + * @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 + * @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'."; + } +} diff --git a/customfield/classes/event/field_created.php b/customfield/classes/event/field_created.php new file mode 100644 index 00000000000..cdb016787dd --- /dev/null +++ b/customfield/classes/event/field_created.php @@ -0,0 +1,87 @@ +. + +/** + * Custom field created event. + * + * @package core_customfield + * @copyright 2018 Daniel Neis Araujo + * @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 + * @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'."; + } +} diff --git a/customfield/classes/event/field_updated.php b/customfield/classes/event/field_updated.php new file mode 100644 index 00000000000..02a17f2e605 --- /dev/null +++ b/customfield/classes/event/field_updated.php @@ -0,0 +1,87 @@ +. + +/** + * Custom field updated event. + * + * @package core_customfield + * @copyright 2018 Daniel Neis Araujo + * @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 + * @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'."; + } +} diff --git a/customfield/classes/output/field_data.php b/customfield/classes/output/field_data.php new file mode 100644 index 00000000000..8f515a975de --- /dev/null +++ b/customfield/classes/output/field_data.php @@ -0,0 +1,114 @@ +. + +/** + * core_customfield field value renderable. + * + * @package core_customfield + * @copyright 2018 Daniel Neis Araujo + * @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 + * @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') + ]; + } +} diff --git a/customfield/field/checkbox/classes/data_controller.php b/customfield/field/checkbox/classes/data_controller.php new file mode 100644 index 00000000000..be134697385 --- /dev/null +++ b/customfield/field/checkbox/classes/data_controller.php @@ -0,0 +1,84 @@ +. + +/** + * Customfield Checkbox plugin + * + * @package customfield_checkbox + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customfield_checkbox; + +use core_customfield\api; +use core_customfield\output\field_data; + +defined('MOODLE_INTERNAL') || die; + +/** + * Class data + * + * @package customfield_checkbox + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class data_controller extends \core_customfield\data_controller { + + /** + * Return the name of the field where the information is stored + * @return string + */ + public function datafield() : string { + return 'intvalue'; + } + + /** + * Add fields for editing a checkbox field. + * + * @param \MoodleQuickForm $mform + */ + public function instance_form_definition(\MoodleQuickForm $mform) { + $field = $this->get_field(); + $config = $field->get('configdata'); + $elementname = $this->get_form_element_name(); + $mform->addElement('advcheckbox', $elementname, $this->get_field()->get_formatted_name()); + $mform->setDefault($elementname, $config['checkbydefault']); + $mform->setType($elementname, PARAM_BOOL); + if ($field->get_configdata_property('required')) { + $mform->addRule($elementname, null, 'required', null, 'client'); + } + } + + /** + * Returns the default value as it would be stored in the database (not in human-readable format). + * + * @return mixed + */ + public function get_default_value() { + return $this->get_field()->get_configdata_property('checkbydefault') ? 1 : 0; + } + + /** + * Returns value in a human-readable format + * + * @return mixed|null value or null if empty + */ + public function export_value() { + $value = $this->get_value(); + return $value ? get_string('yes') : get_string('no'); + } +} diff --git a/customfield/field/checkbox/classes/privacy/provider.php b/customfield/field/checkbox/classes/privacy/provider.php new file mode 100644 index 00000000000..f892009dcc7 --- /dev/null +++ b/customfield/field/checkbox/classes/privacy/provider.php @@ -0,0 +1,81 @@ +. + +/** + * Privacy Subsystem implementation for customfield_checkbox. + * + * @package customfield_checkbox + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace customfield_checkbox\privacy; + +use core_customfield\data_controller; +use core_customfield\privacy\customfield_provider; +use core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customfield_checkbox implementing null_provider. + * + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider, customfield_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } + + /** + * Preprocesses data object that is going to be exported + * + * @param data_controller $data + * @param \stdClass $exportdata + * @param array $subcontext + */ + public static function export_customfield_data(data_controller $data, \stdClass $exportdata, array $subcontext) { + writer::with_context($data->get_context())->export_data($subcontext, $exportdata); + } + + /** + * Allows plugins to delete everything they store related to the data (usually files) + * + * @param string $dataidstest + * @param array $params + * @param array $contextids + * @return mixed|void + */ + public static function before_delete_data(string $dataidstest, array $params, array $contextids) { + } + + /** + * Allows plugins to delete everything they store related to the field configuration (usually files) + * + * @param string $fieldidstest + * @param array $params + * @param array $contextids + */ + public static function before_delete_fields(string $fieldidstest, array $params, array $contextids) { + } +} diff --git a/customfield/field/checkbox/tests/behat/field.feature b/customfield/field/checkbox/tests/behat/field.feature new file mode 100644 index 00000000000..e26a2feba5a --- /dev/null +++ b/customfield/field/checkbox/tests/behat/field.feature @@ -0,0 +1,81 @@ +@customfield @customfield_checkbox +Feature: Managers can manage course custom fields checkbox + In order to have additional data on the course + As a manager + I need to create, edit, remove and sort custom fields + + Background: + Given the following "custom field categories" exist: + | name | component | area | itemid | + | Category for test | core_course | course | 0 | + And I log in as "admin" + And I navigate to "Courses > Course custom fields" in site administration + + Scenario: Create a custom course checkbox field + When I click on "Add a new custom field" "link" + And I click on "Checkbox" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + And I press "Save changes" + Then I should see "Test field" + And I log out + + Scenario: Edit a custom course checkbox field + When I click on "Add a new custom field" "link" + And I click on "Checkbox" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + And I press "Save changes" + And I click on "Edit" "link" in the "Test field" "table_row" + And I set the following fields to these values: + | Name | Edited field | + And I press "Save changes" + Then I should see "Edited field" + And I should not see "Test field" + And I log out + + @javascript + Scenario: Delete a custom course checkbox field + When I click on "Add a new custom field" "link" + And I click on "Checkbox" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + And I press "Save changes" + And I click on "Delete" "link" in the "Test field" "table_row" + And I click on "Yes" "button" in the "Confirm" "dialogue" + Then I should not see "Test field" + And I log out + + @javascript + Scenario: A checkbox checked by default must be shown on listing but allow uncheck that will keep showing + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | Example 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | topics | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + When I click on "Add a new custom field" "link" + And I click on "Checkbox" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + | Checked by default | Yes | + And I press "Save changes" + And I log out + And I log in as "teacher1" + And I am on site homepage + Then I should see "Test field: Yes" + When I am on "Course 1" course homepage + And I navigate to "Edit settings" in current page administration + And I expand all fieldsets + And I set the field "Test field" to "" + And I press "Save and display" + And I am on site homepage + Then I should see "Test field: No" + And I log out diff --git a/customfield/field/date/classes/data_controller.php b/customfield/field/date/classes/data_controller.php new file mode 100644 index 00000000000..3419d2562e0 --- /dev/null +++ b/customfield/field/date/classes/data_controller.php @@ -0,0 +1,147 @@ +. + +/** + * Customfield date plugin + * + * @package customfield_date + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customfield_date; + +use core_customfield\api; + +defined('MOODLE_INTERNAL') || die; + +/** + * Class data + * + * @package customfield_date + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class data_controller extends \core_customfield\data_controller { + + /** + * Return the name of the field where the information is stored + * @return string + */ + public function datafield() : string { + return 'intvalue'; + } + + /** + * Add fields for editing data of a date field on a context. + * + * @param \MoodleQuickForm $mform + */ + public function instance_form_definition(\MoodleQuickForm $mform) { + $field = $this->get_field(); + // Get the current calendar in use - see MDL-18375. + $calendartype = \core_calendar\type_factory::get_calendar_instance(); + + $config = $field->get('configdata'); + + // Always set the form element to "optional", even when it's required. Otherwise it defaults to the + // current date and is easy to miss. + $attributes = ['optional' => true]; + + if (!empty($config['mindate'])) { + $attributes['startyear'] = $calendartype->timestamp_to_date_array($config['mindate'])['year']; + } + + if (!empty($config['maxdate'])) { + $attributes['stopyear'] = $calendartype->timestamp_to_date_array($config['maxdate'])['year']; + } + + if (empty($config['includetime'])) { + $element = 'date_selector'; + } else { + $element = 'date_time_selector'; + } + $elementname = $this->get_form_element_name(); + $mform->addElement($element, $elementname, $this->get_field()->get_formatted_name(), $attributes); + $mform->setType($elementname, PARAM_INT); + $mform->setDefault($elementname, time()); + if ($field->get_configdata_property('required')) { + $mform->addRule($elementname, null, 'required', null, 'client'); + } + } + + /** + * Validates data for this field. + * + * @param array $data + * @param array $files + * @return array + */ + public function instance_form_validation(array $data, array $files): array { + $errors = parent::instance_form_validation($data, $files); + + $elementname = $this->get_form_element_name(); + if (!empty($data[$elementname])) { + // Compare the date with min/max values, trim the date to the minute or to the day (depending on inludetime setting). + $includetime = $this->get_field()->get_configdata_property('includetime'); + $machineformat = $includetime ? '%Y-%m-%d %H:%M' : '%Y-%m-%d'; + $humanformat = $includetime ? get_string('strftimedatetimeshort') : get_string('strftimedatefullshort'); + $value = userdate($data[$elementname], $machineformat, 99, false, false); + $mindate = $this->get_field()->get_configdata_property('mindate'); + $maxdate = $this->get_field()->get_configdata_property('maxdate'); + + if ($mindate && userdate($mindate, $machineformat, 99, false, false) > $value) { + $errors[$elementname] = get_string('errormindate', 'customfield_date', userdate($mindate, $humanformat)); + } + if ($maxdate && userdate($maxdate, $machineformat, 99, false, false) < $value) { + $errors[$elementname] = get_string('errormaxdate', 'customfield_date', userdate($maxdate, $humanformat)); + } + } + + return $errors; + } + + /** + * Returns the default value as it would be stored in the database (not in human-readable format). + * + * @return mixed + */ + public function get_default_value() { + return 0; + } + + /** + * Returns value in a human-readable format + * + * @return mixed|null value or null if empty + */ + public function export_value() { + $value = $this->get_value(); + + if ($this->is_empty($value)) { + return null; + } + + // Check if time needs to be included. + if ($this->get_field()->get_configdata_property('includetime')) { + $format = get_string('strftimedaydatetime', 'langconfig'); + } else { + $format = get_string('strftimedate', 'langconfig'); + } + + return userdate($value, $format); + } +} diff --git a/customfield/field/date/classes/privacy/provider.php b/customfield/field/date/classes/privacy/provider.php new file mode 100644 index 00000000000..b9605a5020b --- /dev/null +++ b/customfield/field/date/classes/privacy/provider.php @@ -0,0 +1,85 @@ +. +/** + * Privacy Subsystem implementation for customfield_date. + * + * @package customfield_date + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customfield_date\privacy; + +use core_customfield\data_controller; +use core_customfield\privacy\customfield_provider; +use core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customfield_date implementing null_provider. + * + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider, customfield_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason(): string { + return 'privacy:metadata'; + } + + /** + * Preprocesses data object that is going to be exported + * + * @param data_controller $data + * @param \stdClass $exportdata + * @param array $subcontext + */ + public static function export_customfield_data(data_controller $data, \stdClass $exportdata, array $subcontext) { + $context = $data->get_context(); + // For date field we want to use PrivacyAPI date format instead of export_value(). + $exportdata->value = \core_privacy\local\request\transform::datetime($data->get_value()); + writer::with_context($context) + ->export_data($subcontext, $exportdata); + } + + /** + * Allows plugins to delete everything they store related to the data (usually files) + * + * @param string $dataidstest + * @param array $params + * @param array $contextids + * @return mixed|void + */ + public static function before_delete_data(string $dataidstest, array $params, array $contextids) { + } + + /** + * Allows plugins to delete everything they store related to the field configuration (usually files) + * + * @param string $fieldidstest + * @param array $params + * @param array $contextids + */ + public static function before_delete_fields(string $fieldidstest, array $params, array $contextids) { + } +} diff --git a/customfield/field/date/lib.php b/customfield/field/date/lib.php new file mode 100644 index 00000000000..00a7d3cc0bb --- /dev/null +++ b/customfield/field/date/lib.php @@ -0,0 +1,35 @@ +. + +/** + * Customfield date plugin + * + * @package customfield_date + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Get icon mapping for font-awesome. + */ +function customfield_date_get_fontawesome_icon_map() { + return [ + 'customfield_date:checked' => 'fa-check-square-o', + 'customfield_date:notchecked' => 'fa-square-o', + ]; +} diff --git a/customfield/field/date/tests/behat/field.feature b/customfield/field/date/tests/behat/field.feature new file mode 100644 index 00000000000..a897294a6b2 --- /dev/null +++ b/customfield/field/date/tests/behat/field.feature @@ -0,0 +1,105 @@ +@customfield @customfield_date +Feature: Managers can manage course custom fields date + In order to have additional data on the course + As a manager + I need to create, edit, remove and sort custom fields + + Background: + Given the following "custom field categories" exist: + | name | component | area | itemid | + | Category for test | core_course | course | 0 | + And I log in as "admin" + And I navigate to "Courses > Course custom fields" in site administration + + Scenario: Create a custom course date field + When I click on "Add a new custom field" "link" + And I click on "Date and time" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + And I press "Save changes" + Then I should see "Test field" + And I log out + + Scenario: Edit a custom course date field + When I click on "Add a new custom field" "link" + And I click on "Date and time" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + And I press "Save changes" + And I click on "[data-role='editfield']" "css_element" + And I set the following fields to these values: + | Name | Edited field | + And I press "Save changes" + Then I should see "Edited field" + And I log out + + @javascript + Scenario: Delete a custom course date field + When I click on "Add a new custom field" "link" + And I click on "Date and time" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + And I press "Save changes" + And I click on "[data-role='deletefield']" "css_element" + And I click on "Yes" "button" in the "Confirm" "dialogue" + Then I should not see "Test field" + And I log out + + @javascript + Scenario: A date field makerd to include time must show those fields on course form + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | Example 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | topics | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + When I click on "Add a new custom field" "link" + And I click on "Date and time" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + | Include time | 1 | + And I press "Save changes" + And I log out + Then I log in as "teacher1" + When I am on site homepage + When I am on "Course 1" course homepage + And I navigate to "Edit settings" in current page administration + And I expand all fieldsets + Then "#id_customfield_testfield_hour" "css_element" should be visible + Then "#id_customfield_testfield_minute" "css_element" should be visible + And I log out + + @javascript + Scenario: A date field makerd to not include time must not show those fields on course form + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | Example 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | topics | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + When I click on "Add a new custom field" "link" + And I click on "Date and time" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + | Include time | | + And I press "Save changes" + And I log out + Then I log in as "teacher1" + When I am on site homepage + When I am on "Course 1" course homepage + And I navigate to "Edit settings" in current page administration + And I expand all fieldsets + Then "#id_customfield_testfield_hour" "css_element" should not be visible + Then "#id_customfield_testfield_minute" "css_element" should not be visible + And I log out \ No newline at end of file diff --git a/customfield/field/select/classes/data_controller.php b/customfield/field/select/classes/data_controller.php new file mode 100644 index 00000000000..4280429f0cd --- /dev/null +++ b/customfield/field/select/classes/data_controller.php @@ -0,0 +1,131 @@ +. + +/** + * Select plugin data controller + * + * @package customfield_select + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customfield_select; + +use core_customfield\api; + +defined('MOODLE_INTERNAL') || die; + +/** + * Class data + * + * @package customfield_select + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class data_controller extends \core_customfield\data_controller { + + /** + * Return the name of the field where the information is stored + * @return string + */ + public function datafield() : string { + return 'intvalue'; + } + + /** + * Returns the default value as it would be stored in the database (not in human-readable format). + * + * @return mixed + */ + public function get_default_value() { + $defaultvalue = $this->get_field()->get_configdata_property('defaultvalue'); + if ('' . $defaultvalue !== '') { + $options = field_controller::get_options_array($this->get_field()); + $key = array_search($defaultvalue, $options); + if ($key !== false) { + return $key; + } + } + return 0; + } + + /** + * Add fields for editing a textarea field. + * + * @param \MoodleQuickForm $mform + */ + public function instance_form_definition(\MoodleQuickForm $mform) { + $field = $this->get_field(); + $config = $field->get('configdata'); + $options = field_controller::get_options_array($field); + $formattedoptions = array(); + $context = $this->get_field()->get_handler()->get_configuration_context(); + foreach ($options as $key => $option) { + // Multilang formatting with filters. + $formattedoptions[$key] = format_string($option, true, ['context' => $context]); + } + + $elementname = $this->get_form_element_name(); + $mform->addElement('select', $elementname, $this->get_field()->get_formatted_name(), $formattedoptions); + + if (($defaultkey = array_search($config['defaultvalue'], $options)) !== false) { + $mform->setDefault($elementname, $defaultkey); + } + if ($field->get_configdata_property('required')) { + $mform->addRule($elementname, null, 'required', null, 'client'); + } + } + + /** + * Validates data for this field. + * + * @param array $data + * @param array $files + * @return array + */ + public function instance_form_validation(array $data, array $files): array { + $errors = parent::instance_form_validation($data, $files); + if ($this->get_field()->get_configdata_property('required')) { + // Standard required rule does not work on select element. + $elementname = $this->get_form_element_name(); + if (empty($data[$elementname])) { + $errors[$elementname] = get_string('err_required', 'form'); + } + } + return $errors; + } + + /** + * Returns value in a human-readable format + * + * @return mixed|null value or null if empty + */ + public function export_value() { + $value = $this->get_value(); + + if ($this->is_empty($value)) { + return null; + } + + $options = field_controller::get_options_array($this->get_field()); + if (array_key_exists($value, $options)) { + return format_string($options[$value], true, + ['context' => $this->get_field()->get_handler()->get_configuration_context()]); + } + + return null; + } +} diff --git a/customfield/field/select/classes/privacy/provider.php b/customfield/field/select/classes/privacy/provider.php new file mode 100644 index 00000000000..e1c55473b79 --- /dev/null +++ b/customfield/field/select/classes/privacy/provider.php @@ -0,0 +1,83 @@ +. +/** + * Privacy Subsystem implementation for customfield_select. + * + * @package customfield_select + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace customfield_select\privacy; + +use core_customfield\data_controller; +use core_customfield\privacy\customfield_provider; +use core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customfield_select implementing null_provider. + * + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider, customfield_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } + + /** + * Preprocesses data object that is going to be exported + * + * @param data_controller $data + * @param \stdClass $exportdata + * @param array $subcontext + */ + public static function export_customfield_data(data_controller $data, \stdClass $exportdata, array $subcontext) { + $context = $data->get_context(); + $exportdata->value = $data->export_value(); + writer::with_context($context) + ->export_data($subcontext, $exportdata); + } + + /** + * Allows plugins to delete everything they store related to the data (usually files) + * + * @param string $dataidstest + * @param array $params + * @param array $contextids + * @return mixed|void + */ + public static function before_delete_data(string $dataidstest, array $params, array $contextids) { + } + + /** + * Allows plugins to delete everything they store related to the field configuration (usually files) + * + * @param string $fieldidstest + * @param array $params + * @param array $contextids + */ + public static function before_delete_fields(string $fieldidstest, array $params, array $contextids) { + } +} diff --git a/customfield/field/select/tests/behat/field.feature b/customfield/field/select/tests/behat/field.feature new file mode 100644 index 00000000000..6c43de12de6 --- /dev/null +++ b/customfield/field/select/tests/behat/field.feature @@ -0,0 +1,86 @@ +@customfield @customfield_select +Feature: Managers can manage course custom fields select + In order to have additional data on the course + As a manager + I need to create, edit, remove and sort custom fields + + Background: + Given the following "custom field categories" exist: + | name | component | area | itemid | + | Category for test | core_course | course | 0 | + And I log in as "admin" + And I navigate to "Courses > Course custom fields" in site administration + + Scenario: Create a custom course select field + When I click on "Add a new custom field" "link" + And I click on "Dropdown menu" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + And I set the field "Menu options (one per line)" to multiline: + """ + a + b + """ + And I press "Save changes" + Then I should see "Test field" + And I log out + + Scenario: Edit a custom course select field + When I click on "Add a new custom field" "link" + And I click on "Dropdown menu" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + And I set the field "Menu options (one per line)" to multiline: + """ + a + b + """ + And I press "Save changes" + And I click on "Edit" "link" in the "Test field" "table_row" + And I set the following fields to these values: + | Name | Edited field | + And I press "Save changes" + Then I should see "Edited field" + And I should not see "Test field" + And I log out + + @javascript + Scenario: Delete a custom course select field + When I click on "Add a new custom field" "link" + And I click on "Dropdown menu" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + And I set the field "Menu options (one per line)" to multiline: + """ + a + b + """ + And I press "Save changes" + And I click on "Delete" "link" in the "Test field" "table_row" + And I click on "Yes" "button" in the "Confirm" "dialogue" + Then I should not see "Test field" + And I log out + + Scenario: Validation of custom course select field configuration + When I click on "Add a new custom field" "link" + And I click on "Dropdown menu" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + And I press "Save changes" + And I should see "Please provide at least two options separated with a newline" in the "Menu options (one per line)" "form_row" + And I set the field "Menu options (one per line)" to multiline: + """ + a + b + """ + And I set the field "Default value" to "c" + And I press "Save changes" + And I should see "Default value must be one of the options from the list above" in the "Default value" "form_row" + And I set the field "Default value" to "b" + And I press "Save changes" + And "testfield" "text" should exist in the "Test field" "table_row" + And I log out diff --git a/customfield/field/text/classes/data_controller.php b/customfield/field/text/classes/data_controller.php new file mode 100644 index 00000000000..a55f57031f5 --- /dev/null +++ b/customfield/field/text/classes/data_controller.php @@ -0,0 +1,116 @@ +. + +/** + * Customfields text field plugin + * + * @package customfield_text + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customfield_text; + +defined('MOODLE_INTERNAL') || die; + +use core_customfield\api; + +/** + * Class data + * + * @package customfield_text + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class data_controller extends \core_customfield\data_controller { + + /** + * Return the name of the field where the information is stored + * @return string + */ + public function datafield() : string { + return 'charvalue'; + } + + /** + * Add fields for editing a text field. + * + * @param \MoodleQuickForm $mform + */ + public function instance_form_definition(\MoodleQuickForm $mform) { + $field = $this->get_field(); + $config = $field->get('configdata'); + $type = $config['ispassword'] ? 'password' : 'text'; + $elementname = $this->get_form_element_name(); + $mform->addElement($type, $elementname, $this->get_field()->get_formatted_name(), 'size=' . (int)$config['displaysize']); + $mform->setType($elementname, PARAM_TEXT); + if (!empty($config['defaultvalue'])) { + $mform->setDefault($elementname, $config['defaultvalue']); + } + if ($field->get_configdata_property('required')) { + $mform->addRule($elementname, null, 'required', null, 'client'); + } + } + + /** + * Validates data for this field. + * + * @param array $data + * @param array $files + * @return array + */ + public function instance_form_validation(array $data, array $files): array { + + $errors = parent::instance_form_validation($data, $files); + $maxlength = $this->get_field()->get_configdata_property('maxlength'); + $elementname = $this->get_form_element_name(); + if (($maxlength > 0) && ($maxlength < \core_text::strlen($data[$elementname]))) { + $errors[$elementname] = get_string('errormaxlength', 'customfield_text', $maxlength); + } + return $errors; + } + + /** + * Returns the default value as it would be stored in the database (not in human-readable format). + * + * @return mixed + */ + public function get_default_value() { + return $this->get_field()->get_configdata_property('defaultvalue'); + } + + /** + * Returns value in a human-readable format + * + * @return mixed|null value or null if empty + */ + public function export_value() { + $value = parent::export_value(); + if ($value === null) { + return null; + } + + $link = $this->get_field()->get_configdata_property('link'); + if ($link) { + $linktarget = $this->get_field()->get_configdata_property('linktarget'); + $url = str_replace('$$', urlencode($this->get_value()), $link); + $attributes = $linktarget ? ['target' => $linktarget] : []; + $value = \html_writer::link($url, $value, $attributes); + } + + return $value; + } +} diff --git a/customfield/field/text/classes/privacy/provider.php b/customfield/field/text/classes/privacy/provider.php new file mode 100644 index 00000000000..59371459fd8 --- /dev/null +++ b/customfield/field/text/classes/privacy/provider.php @@ -0,0 +1,84 @@ +. +/** + * Privacy Subsystem implementation for customfield_text. + * + * @package customfield_text + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace customfield_text\privacy; + +use core_customfield\data_controller; +use core_customfield\privacy\customfield_provider; +use core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customfield_text implementing null_provider. + * + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider, customfield_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } + + /** + * Preprocesses data object that is going to be exported + * + * @param data_controller $data + * @param \stdClass $exportdata + * @param array $subcontext + */ + public static function export_customfield_data(data_controller $data, \stdClass $exportdata, array $subcontext) { + $context = $data->get_context(); + // For text fields we want to apply format_string even to raw value to avoid CSS. + $exportdata->{$data->datafield()} = $data->export_value(); + writer::with_context($context) + ->export_data($subcontext, $exportdata); + } + + /** + * Allows plugins to delete everything they store related to the data (usually files) + * + * @param string $dataidstest + * @param array $params + * @param array $contextids + * @return mixed|void + */ + public static function before_delete_data(string $dataidstest, array $params, array $contextids) { + } + + /** + * Allows plugins to delete everything they store related to the field configuration (usually files) + * + * @param string $fieldidstest + * @param array $params + * @param array $contextids + */ + public static function before_delete_fields(string $fieldidstest, array $params, array $contextids) { + } +} diff --git a/customfield/field/text/tests/behat/field.feature b/customfield/field/text/tests/behat/field.feature new file mode 100644 index 00000000000..e1bed824608 --- /dev/null +++ b/customfield/field/text/tests/behat/field.feature @@ -0,0 +1,141 @@ +@customfield @customfield_text +Feature: Managers can manage course custom fields text + In order to have additional data on the course + As a manager + I need to create, edit, remove and sort custom fields + + Background: + Given the following "custom field categories" exist: + | name | component | area | itemid | + | Category for test | core_course | course | 0 | + And I log in as "admin" + And I navigate to "Courses > Course custom fields" in site administration + + Scenario: Create a custom course text field + When I click on "Add a new custom field" "link" + And I click on "Text field" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + And I press "Save changes" + Then I should see "Test field" + And I log out + + Scenario: Edit a custom course text field + When I click on "Add a new custom field" "link" + And I click on "Text field" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + And I press "Save changes" + And I click on "Edit" "link" in the "Test field" "table_row" + And I set the following fields to these values: + | Name | Edited field | + And I press "Save changes" + Then I should see "Edited field" + And I navigate to "Reports > Logs" in site administration + And I press "Get these logs" + And I log out + + @javascript + Scenario: Delete a custom course text field + When I click on "Add a new custom field" "link" + And I click on "Text field" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + And I press "Save changes" + And I click on "Delete" "link" in the "Test field" "table_row" + And I click on "Yes" "button" in the "Confirm" "dialogue" + And I wait until the page is ready + And I wait until "Test field" "text" does not exist + Then I should not see "Test field" + And I log out + + Scenario: A text field with a link setting must show link on course listing + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | Example 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | topics | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And I navigate to "Courses > Course custom fields" in site administration + And I click on "Add a new custom field" "link" + And I click on "Text field" "link" + And I set the following fields to these values: + | Name | See more on website | + | Short name | testfield | + | Visible to | Everyone | + | Link | https://www.moodle.org/$$ | + And I press "Save changes" + And I log out + Then I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | See more on website | course/view.php?id=35 | + And I press "Save and display" + And I am on site homepage + Then I should see "course/view.php?id=35" in the ".customfields-container .customfieldvalue a" "css_element" + Then I should see "See more on website" in the ".customfields-container .customfieldname" "css_element" + + Scenario: A text field with a max length must validate it on course edit form + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | Example 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | topics | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And I navigate to "Courses > Course custom fields" in site administration + And I click on "Add a new custom field" "link" + And I click on "Text field" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + | Maximum length | 3 | + And I press "Save changes" + And I log out + Then I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | Test field | 1234 | + And I press "Save and display" + Then I should see "This field maximum length is 3" + + Scenario: A text field with a default value must be shown on listing but allow empty values that will not be shown + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | Example 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | topics | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And I navigate to "Courses > Course custom fields" in site administration + And I click on "Add a new custom field" "link" + And I click on "Text field" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + | Default value | testdefault | + And I press "Save changes" + And I log out + Then I log in as "teacher1" + When I am on site homepage + Then I should see "Test field: testdefault" + When I am on "Course 1" course homepage + And I navigate to "Edit settings" in current page administration + Then the "value" attribute of "#id_customfield_testfield" "css_element" should contain "testdefault" + When I set the following fields to these values: + | Test field | | + And I press "Save and display" + And I am on site homepage + And I should not see "Test field" diff --git a/customfield/field/textarea/classes/data_controller.php b/customfield/field/textarea/classes/data_controller.php new file mode 100644 index 00000000000..943dcba5432 --- /dev/null +++ b/customfield/field/textarea/classes/data_controller.php @@ -0,0 +1,186 @@ +. + +/** + * Customfields textarea plugin + * + * @package customfield_textarea + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customfield_textarea; + +defined('MOODLE_INTERNAL') || die; + +/** + * Class data + * + * @package customfield_textarea + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class data_controller extends \core_customfield\data_controller { + + /** + * Return the name of the field where the information is stored + * @return string + */ + public function datafield() : string { + return 'value'; + } + + /** + * Options for the editor + * + * @return array + */ + protected function value_editor_options() { + /** @var field_controller $field */ + $field = $this->get_field(); + return $field->value_editor_options($this->get('id') ? $this->get_context() : null); + } + + /** + * Returns the name of the field to be used on HTML forms. + * + * @return string + */ + protected function get_form_element_name(): string { + return parent::get_form_element_name() . '_editor'; + } + + /** + * Add fields for editing a textarea field. + * + * @param \MoodleQuickForm $mform + */ + public function instance_form_definition(\MoodleQuickForm $mform) { + $field = $this->get_field(); + $desceditoroptions = $this->value_editor_options(); + $elementname = $this->get_form_element_name(); + $mform->addElement('editor', $elementname, $this->get_field()->get_formatted_name(), null, $desceditoroptions); + if ($field->get_configdata_property('required')) { + $mform->addRule($elementname, null, 'required', null, 'client'); + } + } + + /** + * Saves the data coming from form + * + * @param \stdClass $datanew data coming from the form + */ + public function instance_form_save(\stdClass $datanew) { + $fieldname = $this->get_form_element_name(); + if (!property_exists($datanew, $fieldname)) { + return; + } + $fromform = $datanew->$fieldname; + + if (!$this->get('id')) { + $this->data->set('value', ''); + $this->data->set('valueformat', FORMAT_MOODLE); + $this->save(); + } + + if ($fromform['text']) { + $textoptions = $this->value_editor_options(); + $data = (object) ['field_editor' => $fromform]; + $data = file_postupdate_standard_editor($data, 'field', $textoptions, $textoptions['context'], + 'customfield_textarea', 'value', $this->get('id')); + $this->data->set('value', $data->field); + $this->data->set('valueformat', $data->fieldformat); + + $this->save(); + } + } + + /** + * Prepares the custom field data related to the object to pass to mform->set_data() and adds them to it + * + * This function must be called before calling $form->set_data($object); + * + * @param \stdClass $instance the entity that has custom fields, if 'id' attribute is present the custom + * fields for this entity will be added, otherwise the default values will be added. + */ + public function instance_form_before_set_data(\stdClass $instance) { + $textoptions = $this->value_editor_options(); + if ($this->get('id')) { + $text = $this->get('value'); + $format = $this->get('valueformat'); + $temp = (object)['field' => $text, 'fieldformat' => $format]; + file_prepare_standard_editor($temp, 'field', $textoptions, $textoptions['context'], 'customfield_textarea', + 'value', $this->get('id')); + $value = $temp->field_editor; + } else { + $text = $this->get_field()->get_configdata_property('defaultvalue'); + $format = $this->get_field()->get_configdata_property('defaultvalueformat'); + $temp = (object)['field' => $text, 'fieldformat' => $format]; + file_prepare_standard_editor($temp, 'field', $textoptions, $textoptions['context'], 'customfield_textarea', + 'defaultvalue', $this->get_field()->get('id')); + $value = $temp->field_editor; + } + $instance->{$this->get_form_element_name()} = $value; + } + + /** + * Delete data + * + * @return bool + */ + public function delete() { + get_file_storage()->delete_area_files($this->get('contextid'), 'customfield_textarea', + 'value', $this->get('id')); + return parent::delete(); + } + + /** + * Returns the default value as it would be stored in the database (not in human-readable format). + * + * @return mixed + */ + public function get_default_value() { + return $this->get_field()->get_configdata_property('defaultvalue'); + } + + /** + * Returns value in a human-readable format + * + * @return mixed|null value or null if empty + */ + public function export_value() { + $value = $this->get_value(); + if ($this->is_empty($value)) { + return null; + } + + if ($dataid = $this->get('id')) { + $context = $this->get_context(); + $processed = file_rewrite_pluginfile_urls($value, 'pluginfile.php', + $context->id, 'customfield_textarea', 'value', $dataid); + $value = format_text($processed, $this->get('valueformat'), ['context' => $context]); + } else { + $fieldid = $this->get_field()->get('id'); + $configcontext = $this->get_field()->get_handler()->get_configuration_context(); + $processed = file_rewrite_pluginfile_urls($value, 'pluginfile.php', + $configcontext->id, 'customfield_textarea', 'defaultvalue', $fieldid); + $valueformat = $this->get_field()->get_configdata_property('defaultvalueformat'); + $value = format_text($processed, $valueformat, ['context' => $configcontext]); + } + + return $value; + } +} diff --git a/customfield/field/textarea/classes/privacy/provider.php b/customfield/field/textarea/classes/privacy/provider.php new file mode 100644 index 00000000000..f8924c90c9c --- /dev/null +++ b/customfield/field/textarea/classes/privacy/provider.php @@ -0,0 +1,97 @@ +. +/** + * Privacy Subsystem implementation for customfield_textarea. + * + * @package customfield_textarea + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace customfield_textarea\privacy; + +use core_customfield\data_controller; +use core_customfield\privacy\customfield_provider; +use core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customfield_textarea implementing null_provider. + * + * @copyright 2018 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider, customfield_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } + + /** + * Preprocesses data object that is going to be exported + * + * @param data_controller $data + * @param \stdClass $exportdata + * @param array $subcontext + */ + public static function export_customfield_data(data_controller $data, \stdClass $exportdata, array $subcontext) { + $context = $data->get_context(); + $exportdata->value = writer::with_context($context) + ->rewrite_pluginfile_urls($subcontext, 'customfield_textarea', 'value', + $exportdata->id, $exportdata->value); + writer::with_context($context) + ->export_data($subcontext, $exportdata) + ->export_area_files($subcontext, 'customfield_textarea', 'value', $exportdata->id); + } + + /** + * Allows plugins to delete everything they store related to the data (usually files) + * + * @param string $dataidstest + * @param array $params + * @param array $contextids + * @return mixed|void + */ + public static function before_delete_data(string $dataidstest, array $params, array $contextids) { + $fs = get_file_storage(); + foreach ($contextids as $contextid) { + $fs->delete_area_files_select($contextid, 'customfield_textarea', 'value', $dataidstest, $params); + } + } + + /** + * Allows plugins to delete everything they store related to the field configuration (usually files) + * + * The implementation should not delete data or anything related to the data, since "before_delete_data" is + * invoked separately. + * + * @param string $fieldidstest + * @param array $params + * @param array $contextids + */ + public static function before_delete_fields(string $fieldidstest, array $params, array $contextids) { + $fs = get_file_storage(); + foreach ($contextids as $contextid) { + $fs->delete_area_files_select($contextid, 'customfield_textarea', 'defaultvalue', $fieldidstest, $params); + } + } +} diff --git a/customfield/field/textarea/tests/behat/default_value.feature b/customfield/field/textarea/tests/behat/default_value.feature new file mode 100644 index 00000000000..2f55ed3311b --- /dev/null +++ b/customfield/field/textarea/tests/behat/default_value.feature @@ -0,0 +1,80 @@ +@customfield @customfield_textarea @javascript +Feature: Default value for the textarea custom field can contain images + In order to see images on custom fields + As a manager + I need to be able to add images to the default value + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher | Teacher | 1 | teacher1@example.com | + | manager | Manager | 1 | manager1@example.com | + And the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | topics | + And the following "course enrolments" exist: + | user | course | role | + | teacher | C1 | editingteacher | + And the following "system role assigns" exist: + | user | course | role | + | manager | Acceptance test site | manager | + And the following "custom field categories" exist: + | name | component | area | itemid | + | Category for test | core_course | course | 0 | + # Upload an image into the private files. + And I log in as "admin" + And I follow "Manage private files" + And I upload "lib/tests/fixtures/gd-logo.png" file to "Files" filemanager + And I click on "Save changes" "button" + And I navigate to "Courses > Course custom fields" in site administration + And I click on "Add a new custom field" "link" + And I click on "Text area" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + | Default value | v | + # Embed the image into Default value. + And I select the text in the "Default value" Atto editor + And I click on "Insert or edit image" "button" in the "//*[@data-fieldtype='editor']/*[descendant::*[@id='id_configdata_defaultvalue_editoreditable']]" "xpath_element" + And I click on "Browse repositories..." "button" + And I click on "Private files" "link" in the ".fp-repo-area" "css_element" + And I click on "gd-logo.png" "link" + And I click on "Select this file" "button" + And I set the field "Describe this image for someone who cannot see it" to "Example" + And I click on "Save image" "button" + And I press "Save changes" + And I log out + + Scenario: For the courses that existed before the custom field was created the default value is displayed + When I am on site homepage + Then the image at "//*[contains(@class, 'frontpage-course-list-all')]//*[contains(@class, 'customfield_textarea')]//img[contains(@src, 'pluginfile.php') and contains(@src, '/customfield_textarea/defaultvalue/') and @alt='Example']" "xpath_element" should be identical to "lib/tests/fixtures/gd-logo.png" + + Scenario: Teacher will see textarea default value when editing a course created before custom field was created + # Teacher will see the image when editing existing course. + And I log in as "teacher" + And I am on "Course 1" course homepage + And I navigate to "Edit settings" in current page administration + And I expand all fieldsets + Then "//*[@id='id_customfield_testfield_editoreditable']//img[contains(@src, 'draftfile.php') and contains(@src, '/gd-logo.png') and @alt='Example']" "xpath_element" should exist + # Save the course without changing the default value. + And I press "Save and display" + And I log out + # Now the same image is displayed as "value" and not as "defaultvalue". + And I am on site homepage + And "//img[contains(@src, '/customfield_textarea/defaultvalue/')]" "xpath_element" should not exist + And the image at "//*[contains(@class, 'frontpage-course-list-all')]//*[contains(@class, 'customfield_textarea')]//img[contains(@src, 'pluginfile.php') and contains(@src, '/customfield_textarea/value/') and @alt='Example']" "xpath_element" should be identical to "lib/tests/fixtures/gd-logo.png" + + Scenario: Manager can create a course and the default value for textarea custom field will apply. + When I log in as "manager" + And I go to the courses management page + And I click on "Create new course" "link" in the "#course-listing" "css_element" + And I set the following fields to these values: + | Course full name | Course 2 | + | Course short name | C2 | + And I expand all fieldsets + Then "//*[@id='id_customfield_testfield_editoreditable']//img[contains(@src, 'draftfile.php') and contains(@src, '/gd-logo.png') and @alt='Example']" "xpath_element" should exist + And I press "Save and display" + And I log out + # Now the same image is displayed as "value" and not as "defaultvalue". + And I am on site homepage + And the image at "//*[contains(@class, 'frontpage-course-list-all')]//*[contains(@class, 'customfield_textarea')]//img[contains(@src, 'pluginfile.php') and contains(@src, '/customfield_textarea/value/') and @alt='Example']" "xpath_element" should be identical to "lib/tests/fixtures/gd-logo.png" diff --git a/customfield/field/textarea/tests/behat/field.feature b/customfield/field/textarea/tests/behat/field.feature new file mode 100644 index 00000000000..70936012c0a --- /dev/null +++ b/customfield/field/textarea/tests/behat/field.feature @@ -0,0 +1,50 @@ +@customfield @customfield_textarea +Feature: Managers can manage course custom fields textarea + In order to have additional data on the course + As a manager + I need to create, edit, remove and sort custom fields + + Background: + Given the following "custom field categories" exist: + | name | component | area | itemid | + | Category for test | core_course | course | 0 | + And I log in as "admin" + And I navigate to "Courses > Course custom fields" in site administration + + Scenario: Create a custom course textarea field + When I click on "Add a new custom field" "link" + And I click on "Text area" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + And I press "Save changes" + Then I should see "Test field" + And I log out + + Scenario: Edit a custom course textarea field + When I click on "Add a new custom field" "link" + And I click on "Text area" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + And I press "Save changes" + And I click on "Edit" "link" in the "Test field" "table_row" + And I set the following fields to these values: + | Name | Edited field | + And I press "Save changes" + Then I should see "Edited field" + And I should not see "Test field" + And I log out + + @javascript + Scenario: Delete a custom course textarea field + When I click on "Add a new custom field" "link" + And I click on "Text area" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + And I press "Save changes" + And I click on "Delete" "link" in the "Test field" "table_row" + And I click on "Yes" "button" in the "Confirm" "dialogue" + Then I should not see "Test field" + And I log out diff --git a/customfield/templates/field_data.mustache b/customfield/templates/field_data.mustache new file mode 100644 index 00000000000..94d323a940c --- /dev/null +++ b/customfield/templates/field_data.mustache @@ -0,0 +1,33 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . + }} +{{! + @template core_customfield/display_field + + Example context (json): + { + "hasvalue": 1, + "fieldtype" : "text", + "fieldname" : "Nick name", + "fieldshortname" : "nickname", + "fieldvalue" : "Star Lord" + } +}} +{{#hasvalue}} +
+ {{{name}}}: {{{value}}} +
+{{/hasvalue}} diff --git a/customfield/tests/behat/edit_categories.feature b/customfield/tests/behat/edit_categories.feature new file mode 100644 index 00000000000..5fb4802a866 --- /dev/null +++ b/customfield/tests/behat/edit_categories.feature @@ -0,0 +1,111 @@ +@core @core_course @core_customfield @javascript +Feature: Managers can manage categories for course custom fields + In order to have additional data on the course + As a manager + I need to create, edit, remove and sort custom field's categories + + Scenario: Create a category for custom course fields + Given I log in as "admin" + When I navigate to "Courses > Course custom fields" in site administration + And I press "Add a new category" + And I wait until the page is ready + Then I should see "Other fields" in the "#customfield_catlist" "css_element" + And I navigate to "Reports > Logs" in site administration + And I press "Get these logs" + And I log out + + Scenario: Edit a category name for custom course fields + Given the following "custom field categories" exist: + | name | component | area | itemid | + | Category for test | core_course | course | 0 | + And I log in as "admin" + And I navigate to "Courses > Course custom fields" in site administration + And I click on "Edit category name" "link" in the "//div[contains(@class,'categoryinstance') and contains(.,'Category for test')]" "xpath_element" + And I set the field "New value for Category for test" to "Good fields" + And I press key "13" in the field "New value for Category for test" + Then I should not see "Category for test" in the "#customfield_catlist" "css_element" + And "New value for Category for test" "field" should not exist + And I should see "Good fields" in the "#customfield_catlist" "css_element" + And I navigate to "Reports > Logs" in site administration + And I press "Get these logs" + And I log out + + Scenario: Delete a category for custom course fields + Given the following "custom field categories" exist: + | name | component | area | itemid | + | Category for test | core_course | course | 0 | + And the following "custom fields" exist: + | name | category | type | shortname | + | Field 1 | Category for test | text | f1 | + And I log in as "admin" + And I navigate to "Courses > Course custom fields" in site administration + And I click on "[data-role='deletecategory']" "css_element" + And I click on "Yes" "button" in the "Confirm" "dialogue" + And I wait until the page is ready + And I wait until "Test category" "text" does not exist + Then I should not see "Test category" in the "#customfield_catlist" "css_element" + And I navigate to "Reports > Logs" in site administration + And I press "Get these logs" + And I log out + + Scenario: Move field in the course custom fields to another category + Given the following "custom field categories" exist: + | name | component | area | itemid | + | Category1 | core_course | course | 0 | + | Category2 | core_course | course | 0 | + | Category3 | core_course | course | 0 | + And the following "custom fields" exist: + | name | category | type | shortname | + | Field1 | Category1 | text | f1 | + | Field2 | Category2 | text | f2 | + When I log in as "admin" + And I navigate to "Courses > Course custom fields" in site administration + Then "Field1" "text" should appear after "Category1" "text" + And "Category2" "text" should appear after "Field1" "text" + And "Field2" "text" should appear after "Category2" "text" + And "Category3" "text" should appear after "Field2" "text" + And I press "Move \"Field1\"" + And I follow "To the top of category Category2" + And "Category2" "text" should appear after "Category1" "text" + And "Field1" "text" should appear after "Category2" "text" + And "Field2" "text" should appear after "Field1" "text" + And "Category3" "text" should appear after "Field2" "text" + And I navigate to "Courses > Course custom fields" in site administration + And "Category2" "text" should appear after "Category1" "text" + And "Field1" "text" should appear after "Category2" "text" + And "Field2" "text" should appear after "Field1" "text" + And "Category3" "text" should appear after "Field2" "text" + And I press "Move \"Field1\"" + And I follow "After field Field2" + And "Field1" "text" should appear after "Field2" "text" + And I log out + + Scenario: Reorder course custom field categories + Given the following "custom field categories" exist: + | name | component | area | itemid | + | Category1 | core_course | course | 0 | + | Category2 | core_course | course | 0 | + | Category3 | core_course | course | 0 | + And the following "custom fields" exist: + | name | category | type | shortname | + | Field1 | Category1 | text | f1 | + When I log in as "admin" + And I navigate to "Courses > Course custom fields" in site administration + Then "Field1" "text" should appear after "Category1" "text" + And "Category2" "text" should appear after "Field1" "text" + And "Category3" "text" should appear after "Category2" "text" + And I press "Move \"Category2\"" + And I follow "After \"Category3\"" + And "Field1" "text" should appear after "Category1" "text" + And "Category3" "text" should appear after "Field1" "text" + And "Category2" "text" should appear after "Category3" "text" + And I navigate to "Courses > Course custom fields" in site administration + And "Field1" "text" should appear after "Category1" "text" + And "Category3" "text" should appear after "Field1" "text" + And "Category2" "text" should appear after "Category3" "text" + And I press "Move \"Category2\"" + And I follow "After \"Category1\"" + And "Field1" "text" should appear after "Category1" "text" + And "Category2" "text" should appear after "Field1" "text" + And "Category3" "text" should appear after "Category2" "text" + And I log out diff --git a/customfield/tests/behat/edit_fields_settings.feature b/customfield/tests/behat/edit_fields_settings.feature new file mode 100644 index 00000000000..6d9f96c31ac --- /dev/null +++ b/customfield/tests/behat/edit_fields_settings.feature @@ -0,0 +1,115 @@ +@core @core_course @core_customfield +Feature: Teachers can edit course custom fields + In order to have additional data on the course + As a teacher + I need to edit data for custom fields + + Background: + Given the following "custom field categories" exist: + | name | component | area | itemid | + | Category for test | core_course | course | 0 | + And the following "custom fields" exist: + | name | category | type | shortname | description | configdata | + | Field 1 | Category for test | text | f1 | d1 | | + | Field 2 | Category for test | textarea | f2 | d2 | | + | Field 3 | Category for test | checkbox | f3 | d3 | | + | Field 4 | Category for test | date | f4 | d4 | | + | Field 5 | Category for test | select | f5 | d5 | {"options":"a\nb\nc"} | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | topics | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + + Scenario: Display custom fields on course edit form + When I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Edit settings" in current page administration + And I expand all fieldsets + Then I should see "Category for test" + And I should see "Field 1" + And I should see "Field 2" + And I should see "Field 3" + And I should see "Field 4" + And I should see "Field 5" + And I log out + + Scenario: Create a course with custom fields from the management interface + When I log in as "admin" + And I go to the courses management page + And I should see the "Categories" management page + And I click on category "Miscellaneous" in the management interface + And I should see the "Course categories and courses" management page + And I click on "Create new course" "link" in the "#course-listing" "css_element" + And I set the following fields to these values: + | Course full name | Course 2 | + | Course short name | C2 | + | Field 1 | testcontent1 | + | Field 2 | testcontent2 | + | Field 3 | 1 | + | customfield_f4[enabled] | 1 | + | customfield_f4[day] | 1 | + | customfield_f4[month] | January | + | customfield_f4[year] | 2019 | + | Field 5 | b | + And I press "Save and display" + And I press "Proceed to course content" + And I navigate to "Edit settings" in current page administration + And the following fields match these values: + | Course full name | Course 2 | + | Course short name | C2 | + | Field 1 | testcontent1 | + | Field 2 | testcontent2 | + | Field 3 | 1 | + | customfield_f4[day] | 1 | + | customfield_f4[month] | January | + | customfield_f4[year] | 2019 | + | Field 5 | b | + And I log out + + @javascript + Scenario: Use images in the custom field description + When I log in as "admin" + And I follow "Manage private files" + And I upload "lib/tests/fixtures/gd-logo.png" file to "Files" filemanager + And I click on "Save changes" "button" + And I navigate to "Courses > Course custom fields" in site administration + And I click on "Edit" "link" in the "Field 1" "table_row" + And I select the text in the "Description" Atto editor + And I click on "Insert or edit image" "button" in the "//*[@data-fieldtype='editor']/*[descendant::*[@id='id_description_editoreditable']]" "xpath_element" + And I click on "Browse repositories..." "button" + And I click on "Private files" "link" in the ".fp-repo-area" "css_element" + And I click on "gd-logo.png" "link" + And I click on "Select this file" "button" + And I set the field "Describe this image for someone who cannot see it" to "Example" + And I click on "Save image" "button" + And I press "Save changes" + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Edit settings" in current page administration + And I expand all fieldsets + Then the image at "//div[contains(@class, 'fitem')][contains(., 'Field 1')]/following-sibling::div[1]//img[contains(@src, 'pluginfile.php') and contains(@src, '/core_customfield/description/') and @alt='Example']" "xpath_element" should be identical to "lib/tests/fixtures/gd-logo.png" + And I log out + + @javascript + Scenario: Custom field short name must be present and unique + When I log in as "admin" + And I navigate to "Courses > Course custom fields" in site administration + And I click on "Add a new custom field" "link" + And I click on "Text field" "link" + And I set the following fields to these values: + | Name | Test field | + And I press "Save changes" + Then I should see "You must supply a value here" in the "Short name" "form_row" + And I set the field "Short name" to "short name" + And I press "Save changes" + And I should see "Short name can only contain lowercase latin letters, digits and an underscore sign" in the "Short name" "form_row" + And I set the field "Short name" to "f1" + And I press "Save changes" + And I should see "Short name already exists" in the "Short name" "form_row" + And I log out diff --git a/customfield/tests/behat/required_field.feature b/customfield/tests/behat/required_field.feature new file mode 100644 index 00000000000..d0aea9662fe --- /dev/null +++ b/customfield/tests/behat/required_field.feature @@ -0,0 +1,58 @@ +@core @core_course @core_customfield +Feature: Requiredness The course custom fields can be mandatory or not + In order to make users required to fill a custom field + As a manager + I can change the requiredness of the fields + + Background: + Given the following "custom field categories" exist: + | name | component | area | itemid | + | Category for test | core_course | course | 0 | + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | topics | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + + Scenario: A required course custom field must be filled when editing course settings + When I log in as "admin" + And I navigate to "Courses > Course custom fields" in site administration + And I click on "Add a new custom field" "link" + And I click on "Text field" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + | Required | Yes | + And I press "Save changes" + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Edit settings" in current page administration + And I press "Save and display" + Then I should see "You must supply a value here" + And I set the field "Test field" to "some value" + And I press "Save and display" + And I should not see "This field is required" + And I log out + + Scenario: A course custom field that is not required may not be filled + When I log in as "admin" + And I navigate to "Courses > Course custom fields" in site administration + And I click on "Add a new custom field" "link" + And I click on "Text field" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + | Required | No | + And I press "Save changes" + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Edit settings" in current page administration + And I press "Save and display" + Then I should see "Course 1" + And I should see "Topic 1" diff --git a/customfield/tests/behat/unique_field.feature b/customfield/tests/behat/unique_field.feature new file mode 100644 index 00000000000..8fe7063f108 --- /dev/null +++ b/customfield/tests/behat/unique_field.feature @@ -0,0 +1,74 @@ +@core @core_course @core_customfield +Feature: Uniqueness The course custom fields can be mandatory or not + In order to make users required to fill a custom field + As a manager + I can change the uniqueness of the fields + + Background: + Given the following "custom field categories" exist: + | name | component | area | itemid | + | Category for test | core_course | course | 0 | + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | topics | + | Course 2 | C2 | topics | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | teacher1 | C2 | editingteacher | + When I log in as "admin" + And I navigate to "Courses > Course custom fields" in site administration + And I click on "Add a new custom field" "link" + And I click on "Text field" "link" + And I set the following fields to these values: + | Name | Test field | + | Short name | testfield | + | Unique data | Yes | + And I press "Save changes" + And I log out + + Scenario: A course custom field with unique data must not allow same data in same field in different courses + When I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | Test field | testcontent | + And I press "Save and display" + And I am on "Course 2" course homepage + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | Test field | testcontent | + And I press "Save and display" + Then I should see "This value is already used" + + Scenario: A course custom field with unique data must not compare with itself + When I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | Test field | testcontent | + And I press "Save and display" + And I am on "Course 1" course homepage + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | Test field | testcontent | + And I press "Save and display" + Then I should not see "This value is already used" + And I should see "Topic 1" + + Scenario: A course custom field with unique data must allow empty data + When I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | Test field | | + And I press "Save and display" + And I am on "Course 2" course homepage + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | Test field | | + And I press "Save and display" + Then I should not see "This value is already used" \ No newline at end of file diff --git a/lang/en/plugin.php b/lang/en/plugin.php index 78c661b80bc..2f3322b846e 100644 --- a/lang/en/plugin.php +++ b/lang/en/plugin.php @@ -121,6 +121,8 @@ $string['type_cachestore'] = 'Cache store'; $string['type_cachestore_plural'] = 'Cache stores'; $string['type_calendartype'] = 'Calendar type'; $string['type_calendartype_plural'] = 'Calendar types'; +$string['type_customfield'] = 'Custom field'; +$string['type_customfield_plural'] = 'Custom fields'; $string['type_coursereport'] = 'Course report'; $string['type_coursereport_plural'] = 'Course reports'; $string['type_dataformat'] = 'Data format'; diff --git a/lib/adminlib.php b/lib/adminlib.php index d15b8d288a6..1693dde0533 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -7260,6 +7260,125 @@ class admin_setting_manageformats extends admin_setting { } } +/** + * Custom fields manager. Allows to enable/disable custom fields and jump to settings. + */ +class admin_setting_managecustomfields extends admin_setting { + + /** + * Calls parent::__construct with specific arguments + */ + public function __construct() { + $this->nosave = true; + parent::__construct('customfieldsui', new lang_string('managecustomfields', 'core_admin'), '', ''); + } + + /** + * Always returns true + * + * @return true + */ + public function get_setting() { + return true; + } + + /** + * Always returns true + * + * @return true + */ + public function get_defaultsetting() { + return true; + } + + /** + * Always returns '' and doesn't write anything + * + * @param mixed $data string or array, must not be NULL + * @return string Always returns '' + */ + public function write_setting($data) { + // Do not write any setting. + return ''; + } + + /** + * Search to find if Query is related to format plugin + * + * @param string $query The string to search for + * @return bool true for related false for not + */ + public function is_related($query) { + if (parent::is_related($query)) { + return true; + } + $formats = core_plugin_manager::instance()->get_plugins_of_type('customfield'); + foreach ($formats as $format) { + if (strpos($format->component, $query) !== false || + strpos(core_text::strtolower($format->displayname), $query) !== false) { + return true; + } + } + return false; + } + + /** + * Return XHTML to display control + * + * @param mixed $data Unused + * @param string $query + * @return string highlight + */ + public function output_html($data, $query='') { + global $CFG, $OUTPUT; + $return = ''; + $return = $OUTPUT->heading(new lang_string('customfields'), 3, 'main'); + $return .= $OUTPUT->box_start('generalbox customfieldsui'); + + $fields = core_plugin_manager::instance()->get_plugins_of_type('customfield'); + + $txt = get_strings(array('settings', 'name', 'enable', 'disable', 'up', 'down')); + $txt->uninstall = get_string('uninstallplugin', 'core_admin'); + $txt->updown = "$txt->up/$txt->down"; + + $table = new html_table(); + $table->head = array($txt->name, $txt->enable, $txt->uninstall, $txt->settings); + $table->align = array('left', 'center', 'center', 'center'); + $table->attributes['class'] = 'managecustomfieldtable generaltable admintable'; + $table->data = array(); + + $spacer = $OUTPUT->pix_icon('spacer', '', 'moodle', array('class' => 'iconsmall')); + foreach ($fields as $field) { + $url = new moodle_url('/admin/customfields.php', + array('sesskey' => sesskey(), 'field' => $field->name)); + + if ($field->is_enabled()) { + $strfieldname = $field->displayname; + $hideshow = html_writer::link($url->out(false, array('action' => 'disable')), + $OUTPUT->pix_icon('t/hide', $txt->disable, 'moodle', array('class' => 'iconsmall'))); + } else { + $strfieldname = $field->displayname; + $class = 'dimmed_text'; + $hideshow = html_writer::link($url->out(false, array('action' => 'enable')), + $OUTPUT->pix_icon('t/show', $txt->enable, 'moodle', array('class' => 'iconsmall'))); + } + $settings = ''; + if ($field->get_settings_url()) { + $settings = html_writer::link($field->get_settings_url(), $txt->settings); + } + $uninstall = ''; + if ($uninstallurl = core_plugin_manager::instance()->get_uninstall_url('customfield_'.$field->name, 'manage')) { + $uninstall = html_writer::link($uninstallurl, $txt->uninstall); + } + $row = new html_table_row(array($strfieldname, $hideshow, $uninstall, $settings)); + $table->data[] = $row; + } + $return .= html_writer::table($table); + $return .= $OUTPUT->box_end(); + return highlight($query, $return); + } +} + /** * Data formats manager. Allow reorder and to enable/disable data formats and jump to settings * diff --git a/lib/classes/component.php b/lib/classes/component.php index 11ce5a6d552..de199dce524 100644 --- a/lib/classes/component.php +++ b/lib/classes/component.php @@ -436,6 +436,7 @@ $cache = '.var_export($cache, true).'; 'countries' => null, 'course' => $CFG->dirroot.'/course', 'currencies' => null, + 'customfield' => $CFG->dirroot.'/customfield', 'dbtransfer' => null, 'debug' => null, 'editor' => $CFG->dirroot.'/lib/editor', @@ -504,6 +505,7 @@ $cache = '.var_export($cache, true).'; 'mod' => $CFG->dirroot.'/mod', 'auth' => $CFG->dirroot.'/auth', 'calendartype' => $CFG->dirroot.'/calendar/type', + 'customfield' => $CFG->dirroot.'/customfield/field', 'enrol' => $CFG->dirroot.'/enrol', 'message' => $CFG->dirroot.'/message/output', 'block' => $CFG->dirroot.'/blocks', diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index a934008fcae..82423d9f282 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1740,6 +1740,10 @@ class core_plugin_manager { 'gregorian' ), + 'customfield' => array( + 'checkbox', 'date', 'select', 'text', 'textarea' + ), + 'coursereport' => array( // Deprecated! ), diff --git a/lib/testing/generator/data_generator.php b/lib/testing/generator/data_generator.php index 961d669d91c..1c2b54f7d3b 100644 --- a/lib/testing/generator/data_generator.php +++ b/lib/testing/generator/data_generator.php @@ -426,6 +426,12 @@ EOD; $record['numsections'] = get_config('moodlecourse', 'numsections'); } + if (!empty($record['customfields'])) { + foreach($record['customfields'] as $field) { + $record['customfield_'.$field['shortname']] = $field['value']; + } + } + $course = create_course((object)$record); context_course::instance($course->id); @@ -1173,6 +1179,31 @@ EOD; return $event->properties(); } + /** + * Create a new course custom field category with the given name. + * + * @param array $data Array with data['name'] of category + * @return \core_customfield\category_controller The created category + */ + public function create_custom_field_category($data): \core_customfield\category_controller { + return $this->get_plugin_generator('core_customfield')->create_category($data); + } + + /** + * Create a new custom field + * + * @param array $data Array with 'name', 'shortname' and 'type' of the field + * @return \core_customfield\field_controller The created field + */ + public function create_custom_field($data): \core_customfield\field_controller { + global $DB; + if (empty($data['categoryid']) && !empty($data['category'])) { + $data['categoryid'] = $DB->get_field('customfield_category', 'id', ['name' => $data['category']]); + unset($data['category']); + } + return $this->get_plugin_generator('core_customfield')->create_field($data); + } + /** * Create a new user, and enrol them in the specified course as the supplied role. * diff --git a/lib/tests/behat/behat_data_generators.php b/lib/tests/behat/behat_data_generators.php index d0bdb8eee7c..22d8759bc25 100644 --- a/lib/tests/behat/behat_data_generators.php +++ b/lib/tests/behat/behat_data_generators.php @@ -92,6 +92,16 @@ class behat_data_generators extends behat_base { 'required' => array('user', 'course', 'role'), 'switchids' => array('user' => 'userid', 'course' => 'courseid', 'role' => 'roleid') ), + 'custom field categories' => array( + 'datagenerator' => 'custom_field_category', + 'required' => array('name', 'component', 'area', 'itemid'), + 'switchids' => array() + ), + 'custom fields' => array( + 'datagenerator' => 'custom_field', + 'required' => array('name', 'category', 'type', 'shortname'), + 'switchids' => array() + ), 'permission overrides' => array( 'datagenerator' => 'permission_override', 'required' => array('capability', 'permission', 'role', 'contextlevel', 'reference'),