}
+ */
+ notifyResetFormChanges() {
+ return new Promise(resolve => {
+ Y.use('event', 'moodle-core-event', 'moodle-core-formchangechecker', () => {
+ Event.notifyFormSubmitAjax(this.modal.getRoot().find('form')[0], true);
+ M.core_formchangechecker.reset_form_dirty_state();
+ resolve();
+ });
+ });
+ }
+
+ /**
+ * Wrapper for Event.notifyFormSubmitAjax that waits for the module to load
+ *
+ * We often destroy the form right after calling this function and we need to make sure that it actually
+ * completes before it, or otherwise it will try to work with a form that does not exist.
+ *
+ * @param {Boolean} skipValidation
+ * @return {Promise}
+ */
+ notifyFormSubmitAjax(skipValidation = false) {
+ return new Promise(resolve => {
+ Y.use('event', 'moodle-core-event', 'moodle-core-formchangechecker', () => {
+ Event.notifyFormSubmitAjax(this.modal.getRoot().find('form')[0], skipValidation);
+ resolve();
+ });
+ });
+ }
+
+ /**
+ * Click on a "submit" button that is marked in the form as registerNoSubmitButton()
+ *
+ * @param {Element} button button that was pressed
+ */
+ processNoSubmitButton(button) {
+ this.notifyFormSubmitAjax(true)
+ .then(() => {
+ // Add the button name to the form data and submit it.
+ let formData = this.modal.getRoot().find('form').serialize();
+ formData = formData + '&' + encodeURIComponent(button.getAttribute('name')) + '=' +
+ encodeURIComponent(button.getAttribute('value'));
+ this.modal.setBodyContent(this.getBody(formData));
+ return null;
+ })
+ .catch(null);
+ }
+
+ /**
+ * Validate form elements
+ * @return {Promise} promise that returns true if client-side validation has passed, false if there are errors
+ */
+ validateElements() {
+ return this.notifyFormSubmitAjax()
+ .then(() => {
+ // Now the change events have run, see if there are any "invalid" form fields.
+ /** @var {jQuery} list of elements with errors */
+ const invalid = this.modal.getRoot().find('[aria-invalid="true"], .error');
+
+ // If we found invalid fields, focus on the first one and do not submit via ajax.
+ if (invalid.length) {
+ invalid.first().focus();
+ return false;
+ }
+
+ return true;
+ });
+ }
+
+ /**
+ * Disable buttons during form submission
+ */
+ disableButtons() {
+ this.modal.getFooter().find('[data-action]').attr('disabled', true);
+ }
+
+ /**
+ * Enable buttons after form submission (on validation error)
+ */
+ enableButtons() {
+ this.modal.getFooter().find('[data-action]').removeAttr('disabled');
+ }
+
+ /**
+ * Submit the form via AJAX call to the core_form_dynamic_form WS
+ */
+ async submitFormAjax() {
+ // If we found invalid fields, focus on the first one and do not submit via ajax.
+ if (!await this.validateElements()) {
+ this.trigger(this.events.CLIENT_VALIDATION_ERROR, null, false);
+ return;
+ }
+ this.disableButtons();
+
+ // Convert all the form elements values to a serialised string.
+ const formData = this.modal.getRoot().find('form').serialize();
+
+ // Now we can continue...
+ Ajax.call([{
+ methodname: 'core_form_dynamic_form',
+ args: {
+ formdata: formData,
+ form: this.config.formClass
+ }
+ }])[0]
+ .then((response) => {
+ if (!response.submitted) {
+ // Form was not submitted because validation failed.
+ const promise = new Promise(
+ resolve => resolve({html: response.html, js: Fragment.processCollectedJavascript(response.javascript)}));
+ this.modal.setBodyContent(promise);
+ this.enableButtons();
+ this.trigger(this.events.SERVER_VALIDATION_ERROR);
+ } else {
+ // Form was submitted properly. Hide the modal and execute callback.
+ const data = JSON.parse(response.data);
+ const event = this.trigger(this.events.FORM_SUBMITTED, data);
+ if (!event.defaultPrevented) {
+ this.modal.hide();
+ }
+ return null;
+ }
+ return null;
+ })
+ .catch(this.onSubmitError);
+ }
+
+ /**
+ * Set the classes for the 'save' button.
+ *
+ * @method setSaveButtonClasses
+ * @param {(String)} value The 'save' button classes.
+ */
+ setSaveButtonClasses(value) {
+ const button = this.modal.getFooter().find("[data-action='save']");
+ if (!button) {
+ throw new Error("Unable to find the 'save' button");
+ }
+ button.removeClass().addClass(value);
+ }
+}
diff --git a/lib/form/classes/dynamic_form.php b/lib/form/classes/dynamic_form.php
new file mode 100644
index 00000000000..ee328457632
--- /dev/null
+++ b/lib/form/classes/dynamic_form.php
@@ -0,0 +1,146 @@
+.
+
+namespace core_form;
+
+use context;
+use moodle_url;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/formslib.php');
+
+/**
+ * Class modal
+ *
+ * Extend this class to create a form that can be used in a modal dialogue.
+ *
+ * @package core_form
+ * @copyright 2020 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class dynamic_form extends \moodleform {
+
+ /**
+ * Constructor for modal forms can not be overridden, however the same form can be used both in AJAX and normally
+ *
+ * @param string $action
+ * @param array $customdata
+ * @param string $method
+ * @param string $target
+ * @param array $attributes
+ * @param bool $editable
+ * @param array $ajaxformdata Forms submitted via ajax, must pass their data here, instead of relying on _GET and _POST.
+ * @param bool $isajaxsubmission whether the form is called from WS and it needs to validate user access and set up context
+ */
+ final public function __construct(?string $action = null,
+ ?array $customdata = null,
+ string $method = 'post',
+ string $target = '',
+ ?array $attributes = [],
+ bool $editable = true,
+ ?array $ajaxformdata = null,
+ bool $isajaxsubmission = false) {
+ global $PAGE, $CFG;
+ $this->_ajaxformdata = $ajaxformdata;
+ if ($isajaxsubmission) {
+ require_once($CFG->libdir . '/externallib.php');
+ // This form was created from the WS that needs to validate user access to it and set page context.
+ // It has to be done before calling parent constructor because elements definitions may need to use
+ // format_string functions and other methods that expect the page to be set up.
+ \external_api::validate_context($this->get_context_for_dynamic_submission());
+ $PAGE->set_url($this->get_page_url_for_dynamic_submission());
+ $this->check_access_for_dynamic_submission();
+ }
+ $attributes = ['data-random-ids' => 1] + ($attributes ?: []);
+ parent::__construct($action, $customdata, $method, $target, $attributes, $editable, $ajaxformdata);
+ }
+
+ /**
+ * Returns context where this form is used
+ *
+ * This context is validated in {@link \external_api::validate_context()}
+ *
+ * If context depends on the form data, it is available in $this->_ajaxformdata or
+ * by calling $this->optional_param()
+ *
+ * Example:
+ * $cmid = $this->optional_param('cmid', 0, PARAM_INT);
+ * return context_module::instance($cmid);
+ *
+ * @return context
+ */
+ abstract protected function get_context_for_dynamic_submission(): context;
+
+ /**
+ * Checks if current user has access to this form, otherwise throws exception
+ *
+ * Sometimes permission check may depend on the action and/or id of the entity.
+ * If necessary, form data is available in $this->_ajaxformdata or
+ * by calling $this->optional_param()
+ *
+ * Example:
+ * require_capability('dosomething', $this->get_context_for_dynamic_submission());
+ */
+ abstract protected function check_access_for_dynamic_submission(): void;
+
+ /**
+ * Process the form submission, used if form was submitted via AJAX
+ *
+ * This method can return scalar values or arrays that can be json-encoded, they will be passed to the caller JS.
+ *
+ * Submission data can be accessed as: $this->get_data()
+ *
+ * Example:
+ * $data = $this->get_data();
+ * file_postupdate_standard_filemanager($data, ....);
+ * api::save_entity($data); // Save into the DB, trigger event, etc.
+ *
+ * @return mixed
+ */
+ abstract public function process_dynamic_submission();
+
+ /**
+ * Load in existing data as form defaults
+ *
+ * Can be overridden to retrieve existing values from db by entity id and also
+ * to preprocess editor and filemanager elements
+ *
+ * Example:
+ * $id = $this->optional_param('id', 0, PARAM_INT);
+ * $data = api::get_entity($id); // For example, retrieve a row from the DB.
+ * file_prepare_standard_filemanager($data, ...);
+ * $this->set_data($data);
+ */
+ abstract public function set_data_for_dynamic_submission(): void;
+
+ /**
+ * Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX
+ *
+ * This is used in the form elements sensitive to the page url, such as Atto autosave in 'editor'
+ *
+ * If the form has arguments (such as 'id' of the element being edited), the URL should
+ * also have respective argument.
+ *
+ * Example:
+ * $id = $this->optional_param('id', 0, PARAM_INT);
+ * return new moodle_url('/my/page/where/form/is/used.php', ['id' => $id]);
+ *
+ * @return moodle_url
+ */
+ abstract protected function get_page_url_for_dynamic_submission(): moodle_url;
+}
diff --git a/lib/form/classes/external/modal.php b/lib/form/classes/external/modal.php
new file mode 100644
index 00000000000..0663123c8f1
--- /dev/null
+++ b/lib/form/classes/external/modal.php
@@ -0,0 +1,106 @@
+.
+
+namespace core_form\external;
+
+use core_search\engine_exception;
+use external_api;
+use external_function_parameters;
+use external_value;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/externallib.php');
+
+/**
+ * Implements the external functions provided by the core_form subsystem.
+ *
+ * @copyright 2020 Marina Glancy
+ * @package core_form
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class dynamic_form extends external_api {
+
+ /**
+ * Parameters for modal form
+ *
+ * @return external_function_parameters
+ */
+ public static function execute_parameters(): external_function_parameters {
+ return new external_function_parameters([
+ 'form' => new external_value(PARAM_RAW_TRIMMED, 'Form class', VALUE_REQUIRED),
+ 'formdata' => new external_value(PARAM_RAW, 'url-encoded form data', VALUE_REQUIRED),
+ ]);
+ }
+
+ /**
+ * Submit a form from a modal dialogue.
+ *
+ * @param string $formclass
+ * @param string $formdatastr
+ * @return array
+ * @throws \moodle_exception
+ */
+ public static function execute(string $formclass, string $formdatastr): array {
+ global $PAGE, $OUTPUT;
+
+ $params = self::validate_parameters(self::execute_parameters(), [
+ 'form' => $formclass,
+ 'formdata' => $formdatastr,
+ ]);
+ $formclass = $params['form'];
+ parse_str($params['formdata'], $formdata);
+
+ if (!class_exists($formclass) || !is_subclass_of($formclass, \core_form\dynamic_form::class)) {
+ // For security reason we don't throw exception "class does not exist" but rather an access exception.
+ throw new \moodle_exception('nopermissionform', 'core_form');
+ }
+
+ /** @var \core_form\dynamic_form $form */
+ $form = new $formclass(null, null, 'post', '', [], true, $formdata, true);
+ $form->set_data_for_dynamic_submission();
+ if (!$form->is_cancelled() && $form->is_submitted() && $form->is_validated()) {
+ // Form was properly submitted, process and return results of processing. No need to render it again.
+ return ['submitted' => true, 'data' => json_encode($form->process_dynamic_submission())];
+ }
+
+ // Render actual form.
+
+ // Hack alert: Forcing bootstrap_renderer to initiate moodle page.
+ $OUTPUT->header();
+
+ $PAGE->start_collecting_javascript_requirements();
+ $data = $form->render();
+ $jsfooter = $PAGE->requires->get_end_code();
+ $output = ['submitted' => false, 'html' => $data, 'javascript' => $jsfooter];
+ return $output;
+ }
+
+ /**
+ * Return for modal
+ * @return \external_single_structure
+ */
+ public static function execute_returns(): \external_single_structure {
+ return new \external_single_structure(
+ array(
+ 'submitted' => new external_value(PARAM_BOOL, 'If form was submitted and validated'),
+ 'data' => new external_value(PARAM_RAW, 'JSON-encoded return data from form processing method', VALUE_OPTIONAL),
+ 'html' => new external_value(PARAM_RAW, 'HTML fragment of the form', VALUE_OPTIONAL),
+ 'javascript' => new external_value(PARAM_RAW, 'JavaScript fragment of the form', VALUE_OPTIONAL)
+ )
+ );
+ }
+}
diff --git a/lib/form/tests/behat/fixtures/repeat_with_delete_form.php b/lib/form/tests/behat/fixtures/repeat_with_delete_form.php
new file mode 100644
index 00000000000..b63adbfd0f0
--- /dev/null
+++ b/lib/form/tests/behat/fixtures/repeat_with_delete_form.php
@@ -0,0 +1,77 @@
+.
+
+/**
+ * Test form repeat elements and delete button
+ *
+ * @copyright 2021 Marina Glancy
+ * @package core_form
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__.'/../../../../../config.php');
+
+defined('BEHAT_SITE_RUNNING') || die();
+
+global $CFG, $PAGE, $OUTPUT;
+require_once($CFG->libdir.'/formslib.php');
+$PAGE->set_url('/lib/form/tests/behat/fixtures/repeat_with_delete_form.php');
+require_login();
+$PAGE->set_context(context_system::instance());
+
+/**
+ * Class repeat_with_delete_form
+ *
+ * @copyright 2021 Marina Glancy
+ * @package core_form
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class repeat_with_delete_form extends moodleform {
+ /**
+ * Form definition
+ */
+ public function definition() {
+ $mform = $this->_form;
+ $repeatcount = $this->_customdata['repeatcount'];
+
+ $repeat = array();
+ $repeatopts = array();
+
+ $repeat[] = $mform->createElement('header', 'testheading', 'Heading {no}');
+
+ $repeat[] = $mform->createElement('text', 'testtext', 'Test text {no}');
+ $repeatopts['testtext']['default'] = 'Testing';
+ $repeatopts['testtext']['type'] = PARAM_TEXT;
+
+ $repeat[] = $mform->createElement('submit', 'deleteel', 'Delete option {no}', [], false);
+
+ $this->repeat_elements($repeat, $repeatcount, $repeatopts, 'test_repeat',
+ 'test_repeat_add', 1, 'Add repeats', true, 'deleteel');
+
+ $this->add_action_buttons();
+ }
+}
+
+$repeatcount = optional_param('test_repeat', 1, PARAM_INT);
+$form = new repeat_with_delete_form(null, array('repeatcount' => $repeatcount));
+
+echo $OUTPUT->header();
+if ($data = $form->get_data()) {
+ echo "".json_encode($data->testtext)."
";
+} else {
+ $form->display();
+}
+echo $OUTPUT->footer();
diff --git a/lib/form/tests/behat/repeat_defaults.feature b/lib/form/tests/behat/repeat_defaults.feature
index 25e0385f41a..bc27feab5ed 100644
--- a/lib/form/tests/behat/repeat_defaults.feature
+++ b/lib/form/tests/behat/repeat_defaults.feature
@@ -1,5 +1,5 @@
@core_form
-Feature: Newly created repeat elements have the correct default values
+Feature: Repeated elements in moodleforms
Scenario: Clicking button to add repeat elements creates repeat elements with the correct default values
Given I log in as "admin"
@@ -22,3 +22,27 @@ Feature: Newly created repeat elements have the correct default values
| testselectyes[1] | Yes |
| testselectno[1] | No |
| testtext[1] | Testing 123 |
+
+ Scenario: Functionality to delete an option in the repeated elements
+ Given I log in as "admin"
+ And I am on fixture page "/lib/form/tests/behat/fixtures/repeat_with_delete_form.php"
+ And I set the field "Test text 1" to "value 1"
+ When I press "Add repeats"
+ Then the following fields match these values:
+ | Test text 1 | value 1 |
+ | Test text 2 | Testing |
+ And I set the field "Test text 2" to "value 2"
+ And I press "Add repeats"
+ And the following fields match these values:
+ | Test text 1 | value 1 |
+ | Test text 2 | value 2 |
+ | Test text 3 | Testing |
+ And I set the field "Test text 3" to "value 3"
+ And I press "Delete option 2"
+ And the following fields match these values:
+ | Test text 1 | value 1 |
+ | Test text 3 | value 3 |
+ And I should not see "Test text 2"
+ And I should not see "Delete option 2"
+ And I press "Save changes"
+ And I should see "{\"0\":\"value 1\",\"2\":\"value 3\"}"
diff --git a/lib/formslib.php b/lib/formslib.php
index 879a2aa619d..5190e548d56 100644
--- a/lib/formslib.php
+++ b/lib/formslib.php
@@ -519,20 +519,58 @@ abstract class moodleform {
return $nosubmit;
}
+ /**
+ * Returns an element of multi-dimensional array given the list of keys
+ *
+ * Example:
+ * $array['a']['b']['c'] = 13;
+ * $v = $this->get_array_value_by_keys($array, ['a', 'b', 'c']);
+ *
+ * Will result it $v==13
+ *
+ * @param array $array
+ * @param array $keys
+ * @return mixed returns null if keys not present
+ */
+ protected function get_array_value_by_keys(array $array, array $keys) {
+ $value = $array;
+ foreach ($keys as $key) {
+ if (array_key_exists($key, $value)) {
+ $value = $value[$key];
+ } else {
+ return null;
+ }
+ }
+ return $value;
+ }
+
/**
* Checks if a parameter was passed in the previous form submission
*
- * @param string $name the name of the page parameter we want
+ * @param string $name the name of the page parameter we want, for example 'id' or 'element[sub][13]'
* @param mixed $default the default value to return if nothing is found
* @param string $type expected type of parameter
* @return mixed
*/
public function optional_param($name, $default, $type) {
- if (isset($this->_ajaxformdata[$name])) {
- return clean_param($this->_ajaxformdata[$name], $type);
- } else {
- return optional_param($name, $default, $type);
+ $nameparsed = [];
+ // Convert element name into a sequence of keys, for example 'element[sub][13]' -> ['element', 'sub', '13'].
+ parse_str($name . '=1', $nameparsed);
+ $keys = [];
+ while (is_array($nameparsed)) {
+ $key = key($nameparsed);
+ $keys[] = $key;
+ $nameparsed = $nameparsed[$key];
}
+
+ // Search for the element first in $this->_ajaxformdata, then in $_POST and then in $_GET.
+ if (($value = $this->get_array_value_by_keys($this->_ajaxformdata ?? [], $keys)) !== null ||
+ ($value = $this->get_array_value_by_keys($_POST, $keys)) !== null ||
+ ($value = $this->get_array_value_by_keys($_GET, $keys)) !== null) {
+ return $type == PARAM_RAW ? $value : clean_param($value, $type);
+ }
+
+ return $default;
}
/**
@@ -1099,11 +1137,14 @@ abstract class moodleform {
* @param int $addfieldsno how many fields to add at a time
* @param string $addstring name of button, {no} is replaced by no of blanks that will be added.
* @param bool $addbuttoninside if true, don't call closeHeaderBefore($addfieldsname). Default false.
+ * @param string $deletebuttonname if specified, treats the no-submit button with this name as a "delete element" button
+ * in each of the elements
* @return int no of repeats of element in this page
*/
- function repeat_elements($elementobjs, $repeats, $options, $repeathiddenname,
- $addfieldsname, $addfieldsno=5, $addstring=null, $addbuttoninside=false){
- if ($addstring===null){
+ public function repeat_elements($elementobjs, $repeats, $options, $repeathiddenname,
+ $addfieldsname, $addfieldsno = 5, $addstring = null, $addbuttoninside = false,
+ $deletebuttonname = '') {
+ if ($addstring === null) {
$addstring = get_string('addfields', 'form', $addfieldsno);
} else {
$addstring = str_ireplace('{no}', $addfieldsno, $addstring);
@@ -1121,7 +1162,18 @@ abstract class moodleform {
//value not to be overridden by submitted value
$mform->setConstants(array($repeathiddenname=>$repeats));
$namecloned = array();
+ $no = 1;
for ($i = 0; $i < $repeats; $i++) {
+ if ($deletebuttonname) {
+ $mform->registerNoSubmitButton($deletebuttonname . "[$i]");
+ $isdeleted = $this->optional_param($deletebuttonname . "[$i]", false, PARAM_RAW) ||
+ $this->optional_param($deletebuttonname . "-hidden[$i]", false, PARAM_RAW);
+ if ($isdeleted) {
+ $mform->addElement('hidden', $deletebuttonname . "-hidden[$i]", 1);
+ $mform->setType($deletebuttonname . "-hidden[$i]", PARAM_INT);
+ continue;
+ }
+ }
foreach ($elementobjs as $elementobj){
$elementclone = fullclone($elementobj);
$this->repeat_elements_fix_clone($i, $elementclone, $namecloned);
@@ -1130,7 +1182,13 @@ abstract class moodleform {
foreach ($elementclone->getElements() as $el) {
$this->repeat_elements_fix_clone($i, $el, $namecloned);
}
- $elementclone->setLabel(str_replace('{no}', $i + 1, $elementclone->getLabel()));
+ $elementclone->setLabel(str_replace('{no}', $no, $elementclone->getLabel()));
+ } else if ($elementobj instanceof \HTML_QuickForm_submit && $elementobj->getName() == $deletebuttonname) {
+ // Mark the "Delete" button as no-submit.
+ $onclick = $elementclone->getAttribute('onclick');
+ $skip = 'skipClientValidation = true;';
+ $onclick = ($onclick !== null) ? $skip . ' ' . $onclick : $skip;
+ $elementclone->updateAttributes(['data-skip-validation' => 1, 'data-no-submit' => 1, 'onclick' => $onclick]);
}
// Mark newly created elements, so they know not to look for any submitted data.
@@ -1139,6 +1197,7 @@ abstract class moodleform {
}
$mform->addElement($elementclone);
+ $no++;
}
}
for ($i=0; $i<$repeats; $i++) {
@@ -1161,24 +1220,22 @@ abstract class moodleform {
call_user_func_array(array(&$mform, 'addHelpButton'), $params);
break;
case 'disabledif' :
- foreach ($namecloned as $num => $name){
- if ($params[0] == $name){
- $params[0] = $params[0]."[$i]";
- break;
- }
- }
- $params = array_merge(array($realelementname), $params);
- call_user_func_array(array(&$mform, 'disabledIf'), $params);
- break;
case 'hideif' :
+ $pos = strpos($params[0], '[');
+ $ending = '';
+ if ($pos !== false) {
+ $ending = substr($params[0], $pos);
+ $params[0] = substr($params[0], 0, $pos);
+ }
foreach ($namecloned as $num => $name){
if ($params[0] == $name){
- $params[0] = $params[0]."[$i]";
+ $params[0] = $params[0] . "[$i]" . $ending;
break;
}
}
$params = array_merge(array($realelementname), $params);
- call_user_func_array(array(&$mform, 'hideIf'), $params);
+ $function = ($option === 'disabledif') ? 'disabledIf' : 'hideIf';
+ call_user_func_array(array(&$mform, $function), $params);
break;
case 'rule' :
if (is_string($params)){
@@ -1203,7 +1260,7 @@ abstract class moodleform {
}
}
}
- $mform->addElement('submit', $addfieldsname, $addstring);
+ $mform->addElement('submit', $addfieldsname, $addstring, [], false);
if (!$addbuttoninside) {
$mform->closeHeaderBefore($addfieldsname);
@@ -1432,6 +1489,40 @@ abstract class moodleform {
}
}
+ /**
+ * Used by tests to simulate submitted form data submission via AJAX.
+ *
+ * For form fields where no data is submitted the default for that field as set by set_data or setDefault will be passed to
+ * get_data.
+ *
+ * This method sets $_POST or $_GET and $_FILES with the data supplied. Our unit test code empties all these
+ * global arrays after each test.
+ *
+ * @param array $simulatedsubmitteddata An associative array of form values (same format as $_POST).
+ * @param array $simulatedsubmittedfiles An associative array of files uploaded (same format as $_FILES). Can be omitted.
+ * @param string $method 'post' or 'get', defaults to 'post'.
+ * @param null $formidentifier the default is to use the class name for this class but you may need to provide
+ * a different value here for some forms that are used more than once on the
+ * same page.
+ * @return array array to pass to form constructor as $ajaxdata
+ */
+ public static function mock_ajax_submit($simulatedsubmitteddata, $simulatedsubmittedfiles = array(), $method = 'post',
+ $formidentifier = null) {
+ $_FILES = $simulatedsubmittedfiles;
+ if ($formidentifier === null) {
+ $formidentifier = get_called_class();
+ $formidentifier = str_replace('\\', '_', $formidentifier); // See MDL-56233 for more information.
+ }
+ $simulatedsubmitteddata['_qf__'.$formidentifier] = 1;
+ $simulatedsubmitteddata['sesskey'] = sesskey();
+ if (strtolower($method) === 'get') {
+ $_GET = ['sesskey' => sesskey()];
+ } else {
+ $_POST = ['sesskey' => sesskey()];
+ }
+ return $simulatedsubmitteddata;
+ }
+
/**
* Used by tests to generate valid submit keys for moodle forms that are
* submitted with ajax data.
diff --git a/lib/upgrade.txt b/lib/upgrade.txt
index 6d4e6119861..6eb57758737 100644
--- a/lib/upgrade.txt
+++ b/lib/upgrade.txt
@@ -24,6 +24,8 @@ information provided here is intended especially for developers.
* emoji-data has been upgraded to 6.0.0.
* The final deprecation of /message/defaultoutputs.php file and admin_page_defaultmessageoutputs.
All their settings moved to admin/message.php (see MDL-64495). Please use admin_page_managemessageoutputs class instead.
+* Added new class, AMD modules and WS that allow displaying forms in modal popups or load and submit in AJAX requests.
+ See https://docs.moodle.org/dev/Modal_and_AJAX_forms for more details.
=== 3.10 ===
* PHPUnit has been upgraded to 8.5. That comes with a few changes:
diff --git a/version.php b/version.php
index 2aa5e104ef7..11323a45a90 100644
--- a/version.php
+++ b/version.php
@@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
-$version = 2021021100.00; // 20201109 = branching date YYYYMMDD - do not modify!
+$version = 2021021100.01; // 20201109 = branching date YYYYMMDD - do not modify!
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '3.11dev (Build: 20210211)';// Human-friendly version name