Merge branch 'MDL-64554-master' of git://github.com/andrewnicols/moodle

This commit is contained in:
Andrew Nicols 2021-02-18 10:21:55 +08:00
commit d6e50d14d2
60 changed files with 1920 additions and 461 deletions

View File

@ -61,8 +61,14 @@ class block_private_files extends block_base {
$this->content->text = $renderer->private_files_tree();
if (has_capability('moodle/user:manageownfiles', $this->context)) {
$this->content->footer = html_writer::link(
new moodle_url('/user/files.php', array('returnurl' => $this->page->url->out())),
get_string('privatefilesmanage') . '...');
new moodle_url('/user/files.php'),
get_string('privatefilesmanage') . '...',
['data-action' => 'manageprivatefiles']);
$this->page->requires->js_call_amd(
'core_user/private_files',
'initModal',
['[data-action=manageprivatefiles]', \core_user\form\private_files::class]
);
}
}

View File

@ -238,20 +238,4 @@ class course_handler extends \core_customfield\handler {
}
}
}
/**
* Set up page customfield/edit.php
*
* @param field_controller $field
* @return string page heading
*/
public function setup_edit_page(field_controller $field) : string {
global $CFG, $PAGE;
require_once($CFG->libdir.'/adminlib.php');
$title = parent::setup_edit_page($field);
admin_externalpage_setup('course_customfield');
$PAGE->navbar->add($title);
return $title;
}
}

View File

@ -1,5 +1,5 @@
@core @core_course @core_customfield
Feature: Fields locked control where they are displayed
@core @core_course @core_customfield @javascript
Feature: Fields locked control who is able to edit it
In order to display custom fields on course listing
As a manager
I can change the visibility of the fields
@ -19,12 +19,40 @@ Feature: Fields locked control where they are displayed
| user | course | role |
| teacher1 | C1 | editingteacher |
Scenario: Display course custom fields on homepage
Scenario: Editing locked and not locked custom fields
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 "Short text" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
| Locked | No |
| Name | Test field1 |
| Short name | testfield1 |
| Locked | No |
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I click on "Add a new custom field" "link"
And I click on "Short text" "link"
And I set the following fields to these values:
| Name | Test field2 |
| Short name | testfield2 |
| Locked | Yes |
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
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 field1 | testcontent1 |
| Test field2 | testcontent2 |
And I press "Save and display"
And I am on site homepage
Then I should see "Test field1: testcontent1"
And I should see "Test field2: testcontent2"
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
And the field "Test field1" matches value "testcontent1"
And I should not see "Test field2"
And I press "Save and display"
And I am on site homepage
And I should see "Test field1: testcontent1"
And I should see "Test field2: testcontent2"

View File

@ -1,4 +1,4 @@
@core @core_course @core_customfield
@core @core_course @core_customfield @javascript
Feature: The visibility of fields control where they are displayed
In order to display custom fields on course listing
As a manager
@ -28,7 +28,7 @@ Feature: The visibility of fields control where they are displayed
| Name | Test field |
| Short name | testfield |
| Visible to | Everyone |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I log out
Then I log in as "teacher1"
And I am on "Course 1" course homepage
@ -48,7 +48,7 @@ Feature: The visibility of fields control where they are displayed
| Name | Test field |
| Short name | testfield |
| Visible to | Nobody |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I log out
When I log in as "teacher1"
And I am on "Course 1" course homepage
@ -68,7 +68,7 @@ Feature: The visibility of fields control where they are displayed
| Name | Test field |
| Short name | testfield |
| Visible to | Teachers |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I log out
When I log in as "teacher1"
And I am on "Course 1" course homepage

View File

@ -1,2 +1,2 @@
define ("core_customfield/form",["jquery","core/str","core/notification","core/ajax","core/templates","core/sortable_list","core/inplace_editable"],function(a,b,c,d,e,f){var g=function(f,g,h,i,j){b.get_strings([{key:"confirm"},{key:"confirmdelete"+g,component:"core_customfield"},{key:"yes"},{key:"no"}]).done(function(b){c.confirm(b[0],b[1],b[2],b[3],function(){var b="field"===g?"core_customfield_delete_field":"core_customfield_delete_category";d.call([{methodname:b,args:{id:f}},{methodname:"core_customfield_reload_template",args:{component:h,area:i,itemid:j}}])[1].then(function(a){return e.render("core_customfield/list",a)}).then(function(b,c){e.replaceNode(a("[data-region=\"list-page\"]"),b,c);return null}).fail(c.exception)})}).fail(c.exception)},h=function(b,f,g){var h=d.call([{methodname:"core_customfield_create_category",args:{component:b,area:f,itemid:g}},{methodname:"core_customfield_reload_template",args:{component:b,area:f,itemid:g}}]),i;h[0].then(function(a){i=a;return null}).fail(c.exception);h[1].then(function(a){return e.render("core_customfield/list",a)}).then(function(b,c){e.replaceNode(a("[data-region=\"list-page\"]"),b,c);window.location.href="#category-"+i;return null}).fail(c.exception)};return{init:function init(){var e=a("#customfield_catlist"),i=e.attr("data-component"),j=e.attr("data-area"),k=e.attr("data-itemid");a("[data-role=deletefield]").on("click",function(b){g(a(this).attr("data-id"),"field",i,j,k);b.preventDefault()});a("[data-role=deletecategory]").on("click",function(b){g(a(this).attr("data-id"),"category",i,j,k);b.preventDefault()});a("[data-role=addnewcategory]").on("click",function(){h(i,j,k)});var l=function(a){return a.closest("[data-category-id]").find("[data-inplaceeditable][data-itemtype=category][data-component=core_customfield]").attr("data-value")},m=new f(a("#customfield_catlist .categorieslist"),{moveHandlerSelector:".movecategory [data-drag-type=move]"});m.getElementName=function(b){return a.Deferred().resolve(l(b))};a("[data-category-id]").on("sortablelist-drop",function(a,b){if(b.positionChanged){var e=d.call([{methodname:"core_customfield_move_category",args:{id:b.element.data("category-id"),beforeid:b.targetNextElement.data("category-id")}}]);e[0].fail(c.exception)}a.stopPropagation()});var n=new f(a("#customfield_catlist .fieldslist tbody"),{moveHandlerSelector:".movefield [data-drag-type=move]"});n.getDestinationName=function(c,d){if(!d.length){return b.get_string("totopofcategory","customfield",l(c))}else if(d.attr("data-field-name")){return b.get_string("afterfield","customfield",d.attr("data-field-name"))}else{return a.Deferred().resolve("")}};a("[data-field-name]").on("sortablelist-drop",function(a,b){a.stopPropagation();if(b.positionChanged){var e=d.call([{methodname:"core_customfield_move_field",args:{id:b.element.data("field-id"),beforeid:b.targetNextElement.data("field-id"),categoryid:+b.targetList.closest("[data-category-id]").attr("data-category-id")}}]);e[0].fail(c.exception)}}).on("sortablelist-drag",function(d){d.stopPropagation();b.get_string("therearenofields","core_customfield").then(function(b){a("#customfield_catlist .categorieslist").children().each(function(){var c=a(this).find(a(".field")),d=a(this).find(a(".nofields"));if(!c.length&&!d.length){a(this).find("tbody").append("<tr class=\"nofields\"><td colspan=\"5\">"+b+"</td></tr>")}if(c.length&&d.length){d.remove()}});return null}).fail(c.exception)});a("[data-category-id], [data-field-name]").on("sortablelist-dragstart",function(b,c){setTimeout(function(){a(".sortable-list-is-dragged").width(c.element.width())},501)})}}});
define ("core_customfield/form",["jquery","core/str","core/notification","core/ajax","core/templates","core/sortable_list","core_form/modalform","core/inplace_editable"],function(a,b,c,d,e,f,g){var h=function(f,g,h,i,j){b.get_strings([{key:"confirm"},{key:"confirmdelete"+g,component:"core_customfield"},{key:"yes"},{key:"no"}]).done(function(b){c.confirm(b[0],b[1],b[2],b[3],function(){var b="field"===g?"core_customfield_delete_field":"core_customfield_delete_category";d.call([{methodname:b,args:{id:f}},{methodname:"core_customfield_reload_template",args:{component:h,area:i,itemid:j}}])[1].then(function(a){return e.render("core_customfield/list",a)}).then(function(b,c){e.replaceNode(a("[data-region=\"list-page\"]"),b,c);return null}).fail(c.exception)})}).fail(c.exception)},i=function(b,f,g){var h=d.call([{methodname:"core_customfield_create_category",args:{component:b,area:f,itemid:g}},{methodname:"core_customfield_reload_template",args:{component:b,area:f,itemid:g}}]),i;h[0].then(function(a){i=a;return null}).fail(c.exception);h[1].then(function(a){return e.render("core_customfield/list",a)}).then(function(b,c){e.replaceNode(a("[data-region=\"list-page\"]"),b,c);window.location.href="#category-"+i;return null}).fail(c.exception)},j=function(a){var c=a.closest(".action-menu").querySelector(".dropdown-toggle"),d=new g({formClass:"core_customfield\\field_config_form",args:{categoryid:a.getAttribute("data-categoryid"),type:a.getAttribute("data-type")},modalConfig:{title:b.get_string("addingnewcustomfield","core_customfield",a.getAttribute("data-typename"))},returnFocus:c});d.addEventListener(d.events.FORM_SUBMITTED,function(){return window.location.reload()});d.show()},k=function(a){var c=new g({formClass:"core_customfield\\field_config_form",args:{id:a.getAttribute("data-id")},modalConfig:{title:b.get_string("editingfield","core_customfield",a.getAttribute("data-name"))},returnFocus:a});c.addEventListener(c.events.FORM_SUBMITTED,function(){return window.location.reload()});c.show()};return{init:function init(){var e=a("#customfield_catlist"),g=e.attr("data-component"),l=e.attr("data-area"),m=e.attr("data-itemid");a("[data-role=deletefield]").on("click",function(b){h(a(this).attr("data-id"),"field",g,l,m);b.preventDefault()});a("[data-role=deletecategory]").on("click",function(b){h(a(this).attr("data-id"),"category",g,l,m);b.preventDefault()});a("[data-role=addnewcategory]").on("click",function(){i(g,l,m)});a("[data-role=addfield]").on("click",function(a){j(a.currentTarget);a.preventDefault()});a("[data-role=editfield]").on("click",function(a){k(a.currentTarget);a.preventDefault()});var n=function(a){return a.closest("[data-category-id]").find("[data-inplaceeditable][data-itemtype=category][data-component=core_customfield]").attr("data-value")},o=new f(a("#customfield_catlist .categorieslist"),{moveHandlerSelector:".movecategory [data-drag-type=move]"});o.getElementName=function(b){return a.Deferred().resolve(n(b))};a("[data-category-id]").on("sortablelist-drop",function(a,b){if(b.positionChanged){var e=d.call([{methodname:"core_customfield_move_category",args:{id:b.element.data("category-id"),beforeid:b.targetNextElement.data("category-id")}}]);e[0].fail(c.exception)}a.stopPropagation()});var p=new f(a("#customfield_catlist .fieldslist tbody"),{moveHandlerSelector:".movefield [data-drag-type=move]"});p.getDestinationName=function(c,d){if(!d.length){return b.get_string("totopofcategory","customfield",n(c))}else if(d.attr("data-field-name")){return b.get_string("afterfield","customfield",d.attr("data-field-name"))}else{return a.Deferred().resolve("")}};a("[data-field-name]").on("sortablelist-drop",function(a,b){a.stopPropagation();if(b.positionChanged){var e=d.call([{methodname:"core_customfield_move_field",args:{id:b.element.data("field-id"),beforeid:b.targetNextElement.data("field-id"),categoryid:+b.targetList.closest("[data-category-id]").attr("data-category-id")}}]);e[0].fail(c.exception)}}).on("sortablelist-drag",function(d){d.stopPropagation();b.get_string("therearenofields","core_customfield").then(function(b){a("#customfield_catlist .categorieslist").children().each(function(){var c=a(this).find(a(".field")),d=a(this).find(a(".nofields"));if(!c.length&&!d.length){a(this).find("tbody").append("<tr class=\"nofields\"><td colspan=\"5\">"+b+"</td></tr>")}if(c.length&&d.length){d.remove()}});return null}).fail(c.exception)});a("[data-category-id], [data-field-name]").on("sortablelist-dragstart",function(b,c){setTimeout(function(){a(".sortable-list-is-dragged").width(c.element.width())},501)})}}});
//# sourceMappingURL=form.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -21,9 +21,10 @@
* @copyright 2018 Toni Barbera
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/str', 'core/notification', 'core/ajax', 'core/templates', 'core/sortable_list', 'core/inplace_editable'],
define(['jquery', 'core/str', 'core/notification', 'core/ajax', 'core/templates', 'core/sortable_list',
'core_form/modalform', 'core/inplace_editable'],
function(
$, Str, Notification, Ajax, Templates, SortableList) {
$, Str, Notification, Ajax, Templates, SortableList, ModalForm) {
/**
* Display confirmation dialogue
@ -84,6 +85,48 @@ define(['jquery', 'core/str', 'core/notification', 'core/ajax', 'core/templates'
}).fail(Notification.exception);
};
/**
* Create new custom field
*
* @param {HTMLElement} element
*/
var createNewField = function(element) {
const returnFocus = element.closest(".action-menu").querySelector(".dropdown-toggle");
const form = new ModalForm({
formClass: "core_customfield\\field_config_form",
args: {
categoryid: element.getAttribute('data-categoryid'),
type: element.getAttribute('data-type'),
},
modalConfig: {
title: Str.get_string('addingnewcustomfield', 'core_customfield', element.getAttribute('data-typename')),
},
returnFocus,
});
form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload());
form.show();
};
/**
* Edit custom field
*
* @param {HTMLElement} element
*/
var editField = function(element) {
const form = new ModalForm({
formClass: "core_customfield\\field_config_form",
args: {
id: element.getAttribute('data-id'),
},
modalConfig: {
title: Str.get_string('editingfield', 'core_customfield', element.getAttribute('data-name')),
},
returnFocus: element,
});
form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload());
form.show();
};
return {
/**
* Initialise the custom fields manager
@ -104,6 +147,14 @@ define(['jquery', 'core/str', 'core/notification', 'core/ajax', 'core/templates'
$('[data-role=addnewcategory]').on('click', function() {
createNewCategory(component, area, itemid);
});
$('[data-role=addfield]').on('click', function(e) {
createNewField(e.currentTarget);
e.preventDefault();
});
$('[data-role=editfield]').on('click', function(e) {
editField(e.currentTarget);
e.preventDefault();
});
var categoryName = function(element) {
return element

View File

@ -26,9 +26,6 @@ namespace core_customfield;
defined('MOODLE_INTERNAL') || die;
global $CFG;
require_once($CFG->libdir . '/formslib.php');
/**
* Class field_config_form
*
@ -36,7 +33,10 @@ require_once($CFG->libdir . '/formslib.php');
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class field_config_form extends \moodleform {
class field_config_form extends \core_form\dynamic_form {
/** @var field_controller */
protected $field;
/**
* Class definition
@ -44,13 +44,9 @@ class field_config_form extends \moodleform {
* @throws \coding_exception
*/
public function definition() {
global $PAGE;
$mform = $this->_form;
$field = $this->_customdata['field'];
if (!($field && $field instanceof field_controller)) {
throw new \coding_exception('Field must be passed in customdata');
}
$field = $this->get_field();
$handler = $field->get_handler();
$mform->addElement('header', '_commonsettings', get_string('commonsettings', 'core_customfield'));
@ -97,7 +93,7 @@ class field_config_form extends \moodleform {
$mform->addElement('hidden', 'id');
$mform->setType('id', PARAM_INT);
$this->add_action_buttons(true);
// This form is only used inside modal dialogues and never needs action buttons.
}
/**
@ -111,8 +107,7 @@ class field_config_form extends \moodleform {
global $DB;
$errors = array();
/** @var field_controller $field */
$field = $this->_customdata['field'];
$field = $this->get_field();
$handler = $field->get_handler();
// Check the shortname is specified and is unique for this component-area-itemid combination.
@ -131,4 +126,89 @@ class field_config_form extends \moodleform {
return $errors;
}
/**
* Get field
*
* @return field_controller
* @throws \moodle_exception
*/
protected function get_field(): field_controller {
if ($this->field === null) {
if (!empty($this->_ajaxformdata['id'])) {
$this->field = \core_customfield\field_controller::create((int)$this->_ajaxformdata['id']);
} else if (!empty($this->_ajaxformdata['categoryid']) && !empty($this->_ajaxformdata['type'])) {
$category = \core_customfield\category_controller::create((int)$this->_ajaxformdata['categoryid']);
$type = clean_param($this->_ajaxformdata['type'], PARAM_PLUGIN);
$this->field = \core_customfield\field_controller::create(0, (object)['type' => $type], $category);
} else {
throw new \moodle_exception('fieldnotfound', 'core_customfield');
}
}
return $this->field;
}
/**
* Check if current user has access to this form, otherwise throw exception
*
* Sometimes permission check may depend on the action and/or id of the entity.
* If necessary, form data is available in $this->_ajaxformdata
*/
protected function check_access_for_dynamic_submission(): void {
$field = $this->get_field();
$handler = $field->get_handler();
if (!$handler->can_configure()) {
print_error('nopermissionconfigure', 'core_customfield');
}
}
/**
* Load in existing data as form defaults
*
* Can be overridden to retrieve existing values from db by entity id and also
* to preprocess editor and filemanager elements
*
* Example:
* $this->set_data(get_entity($this->_ajaxformdata['id']));
*/
public function set_data_for_dynamic_submission(): void {
$this->set_data(api::prepare_field_for_config_form($this->get_field()));
}
/**
* Process the form submission
*
* This method can return scalar values or arrays that can be json-encoded, they will be passed to the caller JS.
*
* @return mixed
*/
public function process_dynamic_submission() {
$data = $this->get_data();
$field = $this->get_field();
$handler = $field->get_handler();
$handler->save_field_configuration($field, $data);
return null;
}
/**
* Form context
* @return \context
*/
protected function get_context_for_dynamic_submission(): \context {
return $this->get_field()->get_handler()->get_configuration_context();
}
/**
* Page url
* @return \moodle_url
*/
protected function get_page_url_for_dynamic_submission(): \moodle_url {
$field = $this->get_field();
if ($field->get('id')) {
$params = ['action' => 'editfield', 'id' => $field->get('id')];
} else {
$params = ['action' => 'addfield', 'categoryid' => $field->get('categoryid'), 'type' => $field->get('type')];
}
return new \moodle_url($field->get_handler()->get_configuration_url(), $params);
}
}

View File

@ -178,18 +178,6 @@ abstract class handler {
return true;
}
/**
* The form to create or edit a field
*
* @param field_controller $field
* @return field_config_form
*/
public function get_field_config_form(field_controller $field) : field_config_form {
$form = new field_config_form(null, ['field' => $field]);
$form->set_data(api::prepare_field_for_config_form($field));
return $form;
}
/**
* Generates a name for the new category
*
@ -798,52 +786,4 @@ abstract class handler {
$data->delete();
}
}
/**
* Set up page customfield/edit.php
*
* Handler should override this method and set page context
*
* @param field_controller $field
* @return string page heading
*/
public function setup_edit_page(field_controller $field) : string {
global $PAGE;
// Page context.
$context = $this->get_configuration_context();
if ($context->contextlevel == CONTEXT_MODULE) {
list($course, $cm) = get_course_and_cm_from_cmid($context->instanceid, '', $context->get_course_context()->instanceid);
require_login($course, false, $cm);
} else if ($context->contextlevel == CONTEXT_COURSE) {
require_login($context->instanceid, false);
} else {
$PAGE->set_context(null); // This will set to system context only if the context was not set before.
if ($PAGE->context->id != $context->id) {
// In case of user or block context level this method must be overridden.
debugging('Handler must override setup_edit_page() and set the page context before calling parent method.',
DEBUG_DEVELOPER);
}
}
// Set up url and title.
if ($field->get('id')) {
$field = $this->validate_field($field);
} else {
$this->validate_category($field->get_category());
}
$url = new \moodle_url('/customfield/edit.php',
['id' => $field->get('id'), 'type' => $field->get('type'), 'categoryid' => $field->get('categoryid')]);
$PAGE->set_url($url);
$typestr = get_string('pluginname', 'customfield_' . $field->get('type'));
if ($field->get('id')) {
$title = get_string('editingfield', 'core_customfield',
$field->get_formatted_name());
} else {
$title = get_string('addingnewcustomfield', 'core_customfield', $typestr);
}
$PAGE->set_title($title);
return $title;
}
}

View File

@ -96,10 +96,6 @@ class management implements renderable, templatable {
$fieldarray['shortname'] = $field->get('shortname');
$fieldarray['movetitle'] = get_string('movefield', 'core_customfield', $fieldname);
$fieldarray['editfieldurl'] = (new \moodle_url('/customfield/edit.php', [
'id' => $fieldarray['id'],
]))->out(false);
$categoryarray['fields'][] = $fieldarray;
}
@ -107,11 +103,10 @@ class management implements renderable, templatable {
$menu->set_alignment(\action_menu::BL, \action_menu::BL);
$menu->set_menu_trigger(get_string('createnewcustomfield', 'core_customfield'));
$baseaddfieldurl = new \moodle_url('/customfield/edit.php',
array('action' => 'editfield', 'categoryid' => $category->get('id')));
foreach ($fieldtypes as $type => $fieldname) {
$addfieldurl = new \moodle_url($baseaddfieldurl, array('type' => $type));
$action = new \action_menu_link_secondary($addfieldurl, null, $fieldname);
$action = new \action_menu_link_secondary(new \moodle_url('#'), null, $fieldname,
['data-role' => 'addfield', 'data-categoryid' => $category->get('id'), 'data-type' => $type,
'data-typename' => $fieldname]);
$menu->add($action);
}
$menu->attributes['class'] .= ' float-left mr-1';

View File

@ -1,61 +0,0 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Edit configuration of a custom field
*
* @package core_customfield
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../config.php');
require_once($CFG->libdir . '/adminlib.php');
$id = optional_param('id', 0, PARAM_INT);
$categoryid = optional_param('categoryid', 0, PARAM_INT);
$type = optional_param('type', null, PARAM_COMPONENT);
if ($id) {
$field = \core_customfield\field_controller::create($id);
} else if ($categoryid && $type) {
$category = \core_customfield\category_controller::create($categoryid);
$field = \core_customfield\field_controller::create(0, (object)['type' => $type], $category);
} else {
print_error('fieldnotfound', 'core_customfield');
}
$handler = $field->get_handler();
require_login();
if (!$handler->can_configure()) {
print_error('nopermissionconfigure', 'core_customfield');
}
$title = $handler->setup_edit_page($field);
$mform = $handler->get_field_config_form($field);
if ($mform->is_cancelled()) {
redirect($handler->get_configuration_url());
} else if ($data = $mform->get_data()) {
$handler->save_field_configuration($field, $data);
redirect($handler->get_configuration_url());
}
echo $OUTPUT->header();
echo $OUTPUT->heading($title);
$mform->display();
echo $OUTPUT->footer();

View File

@ -131,7 +131,6 @@ class core_customfield_external extends external_api {
'name' => new external_value(PARAM_NOTAGS, 'name'),
'shortname' => new external_value(PARAM_NOTAGS, 'shortname'),
'type' => new external_value(PARAM_NOTAGS, 'type'),
'editfieldurl' => new external_value(PARAM_URL, 'edit field url'),
'id' => new external_value(PARAM_INT, 'id'),
)
)

View File

@ -1,4 +1,4 @@
@customfield @customfield_checkbox
@customfield @customfield_checkbox @javascript
Feature: Managers can manage course custom fields checkbox
In order to have additional data on the course
As a manager
@ -17,7 +17,7 @@ Feature: Managers can manage course custom fields checkbox
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Checkbox" "dialogue"
Then I should see "Test field"
And I log out
@ -27,29 +27,26 @@ Feature: Managers can manage course custom fields checkbox
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Checkbox" "dialogue"
And I click on "Edit" "link" in the "Test field" "table_row"
And I set the following fields to these values:
| Name | Edited field |
And I press "Save changes"
And I click on "Save changes" "button" in the "Updating Test field" "dialogue"
Then I should see "Edited field"
And I should not see "Test field"
And I log out
@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 "Save changes" "button" in the "Adding a new Checkbox" "dialogue"
And I click on "Delete" "link" in the "Test field" "table_row"
And I click on "Yes" "button" in the "Confirm" "dialogue"
Then I should not see "Test field"
And I log out
@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 |
@ -66,7 +63,7 @@ Feature: Managers can manage course custom fields checkbox
| Name | Test field |
| Short name | testfield |
| Checked by default | Yes |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Checkbox" "dialogue"
And I log out
And I log in as "teacher1"
And I am on site homepage

View File

@ -103,21 +103,24 @@ class customfield_checkbox_plugin_testcase extends advanced_testcase {
* Create a configuration form and submit it with the same values as in the field
*/
public function test_config_form() {
$this->setAdminUser();
$submitdata = (array)$this->cfields[1]->to_record();
$submitdata['configdata'] = $this->cfields[1]->get('configdata');
\core_customfield\field_config_form::mock_submit($submitdata, []);
$handler = $this->cfcat->get_handler();
$form = $handler->get_field_config_form($this->cfields[1]);
$submitdata = \core_customfield\field_config_form::mock_ajax_submit($submitdata);
$form = new \core_customfield\field_config_form(null, null, 'post', '', null, true,
$submitdata, true);
$form->set_data_for_dynamic_submission();
$this->assertTrue($form->is_validated());
$data = $form->get_data();
$handler->save_field_configuration($this->cfields[1], $data);
$form->process_dynamic_submission();
// Try submitting with 'unique values' checked.
$submitdata['configdata']['uniquevalues'] = 1;
\core_customfield\field_config_form::mock_submit($submitdata, []);
$handler = $this->cfcat->get_handler();
$form = $handler->get_field_config_form($this->cfields[1]);
$submitdata = \core_customfield\field_config_form::mock_ajax_submit($submitdata);
$form = new \core_customfield\field_config_form(null, null, 'post', '', null, true,
$submitdata, true);
$form->set_data_for_dynamic_submission();
$this->assertFalse($form->is_validated());
}

View File

@ -1,4 +1,4 @@
@customfield @customfield_date
@customfield @customfield_date @javascript
Feature: Managers can manage course custom fields date
In order to have additional data on the course
As a manager
@ -17,7 +17,7 @@ Feature: Managers can manage course custom fields date
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Date and time" "dialogue"
Then I should see "Test field"
And I log out
@ -27,28 +27,26 @@ Feature: Managers can manage course custom fields date
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Date and time" "dialogue"
And I click on "[data-role='editfield']" "css_element"
And I set the following fields to these values:
| Name | Edited field |
And I press "Save changes"
And I click on "Save changes" "button" in the "Updating Test field" "dialogue"
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 "Save changes" "button" in the "Adding a new Date and time" "dialogue"
And I click on "[data-role='deletefield']" "css_element"
And I click on "Yes" "button" in the "Confirm" "dialogue"
Then I should not see "Test field"
And I log out
@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 |
@ -65,7 +63,7 @@ Feature: Managers can manage course custom fields date
| Name | Test field |
| Short name | testfield |
| Include time | 1 |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Date and time" "dialogue"
And I log out
Then I log in as "teacher1"
When I am on site homepage
@ -76,7 +74,6 @@ Feature: Managers can manage course custom fields date
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 |
@ -93,7 +90,7 @@ Feature: Managers can manage course custom fields date
| Name | Test field |
| Short name | testfield |
| Include time | |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Date and time" "dialogue"
And I log out
Then I log in as "teacher1"
When I am on site homepage

View File

@ -100,15 +100,16 @@ class customfield_date_plugin_testcase extends advanced_testcase {
* Create a configuration form and submit it with the same values as in the field
*/
public function test_config_form() {
$this->setAdminUser();
$submitdata = (array)$this->cfields[1]->to_record();
$submitdata['configdata'] = $this->cfields[1]->get('configdata');
\core_customfield\field_config_form::mock_submit($submitdata, []);
$handler = $this->cfcat->get_handler();
$form = $handler->get_field_config_form($this->cfields[1]);
$submitdata = \core_customfield\field_config_form::mock_ajax_submit($submitdata);
$form = new \core_customfield\field_config_form(null, null, 'post', '', null, true,
$submitdata, true);
$form->set_data_for_dynamic_submission();
$this->assertTrue($form->is_validated());
$data = $form->get_data();
$handler->save_field_configuration($this->cfields[1], $data);
$form->process_dynamic_submission();
}
/**

View File

@ -1,4 +1,4 @@
@customfield @customfield_select
@customfield @customfield_select @javascript
Feature: Managers can manage course custom fields select
In order to have additional data on the course
As a manager
@ -22,7 +22,7 @@ Feature: Managers can manage course custom fields select
a
b
"""
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Dropdown menu" "dialogue"
Then I should see "Test field"
And I log out
@ -37,16 +37,15 @@ Feature: Managers can manage course custom fields select
a
b
"""
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Dropdown menu" "dialogue"
And I click on "Edit" "link" in the "Test field" "table_row"
And I set the following fields to these values:
| Name | Edited field |
And I press "Save changes"
And I click on "Save changes" "button" in the "Updating Test field" "dialogue"
Then I should see "Edited field"
And I should not see "Test field"
And I log out
@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"
@ -58,7 +57,7 @@ Feature: Managers can manage course custom fields select
a
b
"""
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Dropdown menu" "dialogue"
And I click on "Delete" "link" in the "Test field" "table_row"
And I click on "Yes" "button" in the "Confirm" "dialogue"
Then I should not see "Test field"
@ -70,7 +69,7 @@ Feature: Managers can manage course custom fields select
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Dropdown menu" "dialogue"
And I should see "Please provide at least two options, with each on a new line." in the "Menu options (one per line)" "form_row"
And I set the field "Menu options (one per line)" to multiline:
"""
@ -78,9 +77,9 @@ Feature: Managers can manage course custom fields select
b
"""
And I set the field "Default value" to "c"
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Dropdown menu" "dialogue"
And I should see "The default value must be one of the options from the list above" in the "Default value" "form_row"
And I set the field "Default value" to "b"
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Dropdown menu" "dialogue"
And "testfield" "text" should exist in the "Test field" "table_row"
And I log out

View File

@ -104,15 +104,16 @@ class customfield_select_plugin_testcase extends advanced_testcase {
* Create a configuration form and submit it with the same values as in the field
*/
public function test_config_form() {
$this->setAdminUser();
$submitdata = (array)$this->cfields[1]->to_record();
$submitdata['configdata'] = $this->cfields[1]->get('configdata');
\core_customfield\field_config_form::mock_submit($submitdata, []);
$handler = $this->cfcat->get_handler();
$form = $handler->get_field_config_form($this->cfields[1]);
$submitdata = \core_customfield\field_config_form::mock_ajax_submit($submitdata);
$form = new \core_customfield\field_config_form(null, null, 'post', '', null, true,
$submitdata, true);
$form->set_data_for_dynamic_submission();
$this->assertTrue($form->is_validated());
$data = $form->get_data();
$handler->save_field_configuration($this->cfields[1], $data);
$form->process_dynamic_submission();
}
/**

View File

@ -1,4 +1,4 @@
@customfield @customfield_text
@customfield @customfield_text @javascript
Feature: Managers can manage course custom fields text
In order to have additional data on the course
As a manager
@ -17,7 +17,7 @@ Feature: Managers can manage course custom fields text
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
Then I should see "Test field"
And I log out
@ -27,24 +27,23 @@ Feature: Managers can manage course custom fields text
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I click on "Edit" "link" in the "Test field" "table_row"
And I set the following fields to these values:
| Name | Edited field |
And I press "Save changes"
And I click on "Save changes" "button" in the "Updating Test field" "dialogue"
Then I should see "Edited field"
And I navigate to "Reports > Logs" in site administration
And I press "Get these logs"
And I log out
@javascript
Scenario: Delete a custom course text field
When I click on "Add a new custom field" "link"
And I click on "Short text" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I click on "Delete" "link" in the "Test field" "table_row"
And I click on "Yes" "button" in the "Confirm" "dialogue"
And I wait until the page is ready
@ -70,7 +69,7 @@ Feature: Managers can manage course custom fields text
| Short name | testfield |
| Visible to | Everyone |
| Link | https://www.moodle.org/$$ |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I log out
Then I log in as "teacher1"
And I am on "Course 1" course homepage
@ -99,7 +98,7 @@ Feature: Managers can manage course custom fields text
| Name | Test field |
| Short name | testfield |
| Maximum number of characters | 3 |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I log out
Then I log in as "teacher1"
And I am on "Course 1" course homepage
@ -126,7 +125,7 @@ Feature: Managers can manage course custom fields text
| Name | Test field |
| Short name | testfield |
| Default value | testdefault |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I log out
Then I log in as "teacher1"
When I am on site homepage

View File

@ -109,15 +109,16 @@ class customfield_text_plugin_testcase extends advanced_testcase {
* Create a configuration form and submit it with the same values as in the field
*/
public function test_config_form() {
$this->setAdminUser();
$submitdata = (array)$this->cfields[1]->to_record();
$submitdata['configdata'] = $this->cfields[1]->get('configdata');
\core_customfield\field_config_form::mock_submit($submitdata, []);
$handler = $this->cfcat->get_handler();
$form = $handler->get_field_config_form($this->cfields[1]);
$submitdata = \core_customfield\field_config_form::mock_ajax_submit($submitdata);
$form = new \core_customfield\field_config_form(null, null, 'post', '', null, true,
$submitdata, true);
$form->set_data_for_dynamic_submission();
$this->assertTrue($form->is_validated());
$data = $form->get_data();
$handler->save_field_configuration($this->cfields[1], $data);
$form->process_dynamic_submission();
}
/**

View File

@ -35,14 +35,14 @@ Feature: Default value for the textarea custom field can contain images
| 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 "Insert or edit image" "button" in the "Default value" "form_row"
And I click on "Browse repositories..." "button"
And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
And I click on "gd-logo.png" "link"
And I click on "Select this file" "button"
And I set the field "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 click on "Save changes" "button" in the "Adding a new Text area" "dialogue"
And I log out
Scenario: For the courses that existed before the custom field was created the default value is displayed
@ -55,7 +55,7 @@ Feature: Default value for the textarea custom field can contain images
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
Then "//img[contains(@src, 'draftfile.php') and contains(@src, '/gd-logo.png') and @alt='Example']" "xpath_element" should exist in the "Test field" "form_row"
# Save the course without changing the default value.
And I press "Save and display"
And I log out
@ -72,7 +72,7 @@ Feature: Default value for the textarea custom field can contain images
| 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
Then "//img[contains(@src, 'draftfile.php') and contains(@src, '/gd-logo.png') and @alt='Example']" "xpath_element" should exist in the "Test field" "form_row"
And I press "Save and display"
And I log out
# Now the same image is displayed as "value" and not as "defaultvalue".

View File

@ -1,4 +1,4 @@
@customfield @customfield_textarea
@customfield @customfield_textarea @javascript
Feature: Managers can manage course custom fields textarea
In order to have additional data on the course
As a manager
@ -17,7 +17,7 @@ Feature: Managers can manage course custom fields textarea
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Text area" "dialogue"
Then I should see "Test field"
And I log out
@ -27,23 +27,22 @@ Feature: Managers can manage course custom fields textarea
And I set the following fields to these values:
| Name | Test field |
| Short name | testfield |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Text area" "dialogue"
And I click on "Edit" "link" in the "Test field" "table_row"
And I set the following fields to these values:
| Name | Edited field |
And I press "Save changes"
And I click on "Save changes" "button" in the "Updating Test field" "dialogue"
Then I should see "Edited field"
And I should not see "Test field"
And I log out
@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 "Save changes" "button" in the "Adding a new Text area" "dialogue"
And I click on "Delete" "link" in the "Test field" "table_row"
And I click on "Yes" "button" in the "Confirm" "dialogue"
Then I should not see "Test field"

View File

@ -105,15 +105,16 @@ class customfield_textarea_plugin_testcase extends advanced_testcase {
* Create a configuration form and submit it with the same values as in the field
*/
public function test_config_form() {
$this->setAdminUser();
$submitdata = (array)$this->cfields[3]->to_record();
$submitdata['configdata'] = $this->cfields[3]->get('configdata');
\core_customfield\field_config_form::mock_submit($submitdata, []);
$handler = $this->cfcat->get_handler();
$form = $handler->get_field_config_form($this->cfields[3]);
$submitdata = \core_customfield\field_config_form::mock_ajax_submit($submitdata);
$form = new \core_customfield\field_config_form(null, null, 'post', '', null, true,
$submitdata, true);
$form->set_data_for_dynamic_submission();
$this->assertTrue($form->is_validated());
$data = $form->get_data();
$handler->save_field_configuration($this->cfields[3], $data);
$form->process_dynamic_submission();
}
/**

View File

@ -106,7 +106,7 @@
<td class="col-3">{{{shortname}}}</td>
<td class="col-2">{{{type}}}</td>
<td class="col-2 text-right">
<a href="{{editfieldurl}}" data-role="editfield">{{#pix}}
<a href="#" data-role="editfield" data-name="{{name}}" data-id="{{id}}">{{#pix}}
t/edit, core, {{#str}} edit, moodle {{/str}} {{/pix}}</a>
<a href="#" data-id="{{id}}" data-role="deletefield">{{#pix}}
t/delete, core, {{#str}} delete, moodle {{/str}} {{/pix}}</a>

View File

@ -1,4 +1,4 @@
@core @core_course @core_customfield
@core @core_course @core_customfield @javascript
Feature: Teachers can edit course custom fields
In order to have additional data on the course
As a teacher
@ -80,14 +80,14 @@ Feature: Teachers can edit course custom fields
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 "Insert or edit image" "button" in the "Description" "form_row"
And I click on "Browse repositories..." "button"
And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
And I click on "gd-logo.png" "link"
And I click on "Select this file" "button"
And I set the field "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 click on "Save changes" "button" in the "Updating Field 1" "dialogue"
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
@ -104,12 +104,13 @@ Feature: Teachers can edit course custom fields
And I click on "Short text" "link"
And I set the following fields to these values:
| Name | Test field |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
Then I should see "You must supply a value here" in the "Short name" "form_row"
And I set the field "Short name" to "short name"
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I should see "The short name can only contain alphanumeric lowercase characters and underscores (_)." in the "Short name" "form_row"
And I set the field "Short name" to "f1"
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I should see "Short name already exists" in the "Short name" "form_row"
And I click on "Cancel" "button" in the "Adding a new Short text" "dialogue"
And I log out

View File

@ -1,4 +1,4 @@
@core @core_course @core_customfield
@core @core_course @core_customfield @javascript
Feature: Requiredness The course custom fields can be mandatory or not
In order to make users required to fill a custom field
As a manager
@ -27,7 +27,7 @@ Feature: Requiredness The course custom fields can be mandatory or not
| Name | Test field |
| Short name | testfield |
| Required | Yes |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
@ -48,7 +48,7 @@ Feature: Requiredness The course custom fields can be mandatory or not
| Name | Test field |
| Short name | testfield |
| Required | No |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage

View File

@ -1,4 +1,4 @@
@core @core_course @core_customfield
@core @core_course @core_customfield @javascript
Feature: Uniqueness The course custom fields can be mandatory or not
In order to make users required to fill a custom field
As a manager
@ -27,7 +27,7 @@ Feature: Uniqueness The course custom fields can be mandatory or not
| Name | Test field |
| Short name | testfield |
| Unique data | Yes |
And I press "Save changes"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I log out
Scenario: A course custom field with unique data must not allow same data in same field in different courses

8
customfield/upgrade.txt Normal file
View File

@ -0,0 +1,8 @@
This files describes API changes in /customfield/*,
Information provided here is intended especially for developers.
=== 3.11 ===
* Methods \core_customfield\handler::get_field_config_form() and \core_customfield\handler::setup_edit_page() are no
longer used. Components that define custom fields areas do not need to implement them. Field edit form opens in
the modal now.

View File

@ -17,10 +17,10 @@ Feature: View licence links
@javascript @_file_upload
Scenario: Altering a file should display licence list modal
Given I log in as "admin"
And I follow "Manage private files..."
And I follow "Private files"
And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
And I press "Save changes"
And I follow "Manage private files..."
And I follow "Private files"
And I click on "empty.txt" "link"
And I click on "Help with Choose licence" "icon"
Then I should see "Follow these links for further information on the available licence options:"

View File

@ -60,6 +60,7 @@ $string['mustbeoverriden'] = 'Abstract form_definition() method in class {$a} mu
$string['newvaluefor'] = 'New value for {$a}';
$string['nomethodforaddinghelpbutton'] = 'There is no method for adding a help button to form element {$a->name} (class {$a->classname})';
$string['nonexistentformelements'] = 'Trying to add help buttons to non-existent form elements : {$a}';
$string['nopermissionform'] = 'You don\'t have permission to access this form.';
$string['noselection'] = 'No selection';
$string['nosuggestions'] = 'No suggestions';
$string['novalue'] = 'Nothing entered';

View File

@ -1,2 +1,2 @@
define ("core/fragment",["jquery","core/ajax"],function(a,b){var c=function loadFragment(a,c,d,e){var f=[];for(var g in e){f.push({name:g,value:e[g]})}return b.call([{methodname:"core_get_fragment",args:{component:a,callback:c,contextid:d,args:f}}])[0]};return{loadFragment:function loadFragment(b,d,e,f){var g=a.Deferred();c(b,d,e,f).then(function(b){var c=a(b.javascript),d="";c.each(function(b,c){c=a(c);var e=c.prop("tagName");if(e&&"script"==e.toLowerCase()){if(c.attr("src")){var f=!1;a("script").each(function(b,d){if(a(d).attr("src")==c.attr("src")){f=!0}return!f});if(!f){d+=" { ";d+=" node = document.createElement(\"script\"); ";d+=" node.type = \"text/javascript\"; ";d+=" node.src = decodeURI(\""+encodeURI(c.attr("src"))+"\"); ";d+=" document.getElementsByTagName(\"head\")[0].appendChild(node); ";d+=" } "}}else{d+=" "+c.text()}}});g.resolve(b.html,d)}).fail(function(a){g.reject(a)});return g.promise()}}});
define ("core/fragment",["jquery","core/ajax"],function(a,b){var c=function loadFragment(a,c,d,e){var f=[];for(var g in e){f.push({name:g,value:e[g]})}return b.call([{methodname:"core_get_fragment",args:{component:a,callback:c,contextid:d,args:f}}])[0]},d=function processCollectedJavascript(b){var c=a(b),d="";c.each(function(b,c){c=a(c);var e=c.prop("tagName");if(e&&"script"==e.toLowerCase()){if(c.attr("src")){var f=!1;a("script").each(function(b,d){if(a(d).attr("src")==c.attr("src")){f=!0}return!f});if(!f){d+=" { ";d+=" node = document.createElement(\"script\"); ";d+=" node.type = \"text/javascript\"; ";d+=" node.src = decodeURI(\""+encodeURI(c.attr("src"))+"\"); ";d+=" document.getElementsByTagName(\"head\")[0].appendChild(node); ";d+=" } "}}else{d+=" "+c.text()}}});return d};return{loadFragment:function loadFragment(b,e,f,g){var h=a.Deferred();c(b,e,f,g).then(function(a){h.resolve(a.html,d(a.javascript))}).fail(function(a){h.reject(a)});return h.promise()},processCollectedJavascript:function processCollectedJavascript(a){return d(a)}}});
//# sourceMappingURL=fragment.min.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -56,6 +56,44 @@ define(['jquery', 'core/ajax'], function($, ajax) {
}])[0];
};
/**
* Converts the JS that was received from collecting JS requirements on the $PAGE so it can be added to the existing page
*
* @param {string} js
* @return {string}
*/
var processCollectedJavascript = function(js) {
var jsNodes = $(js);
var allScript = '';
jsNodes.each(function(index, scriptNode) {
scriptNode = $(scriptNode);
var tagName = scriptNode.prop('tagName');
if (tagName && (tagName.toLowerCase() == 'script')) {
if (scriptNode.attr('src')) {
// We only reload the script if it was not loaded already.
var exists = false;
$('script').each(function(index, s) {
if ($(s).attr('src') == scriptNode.attr('src')) {
exists = true;
}
return !exists;
});
if (!exists) {
allScript += ' { ';
allScript += ' node = document.createElement("script"); ';
allScript += ' node.type = "text/javascript"; ';
allScript += ' node.src = decodeURI("' + encodeURI(scriptNode.attr('src')) + '"); ';
allScript += ' document.getElementsByTagName("head")[0].appendChild(node); ';
allScript += ' } ';
}
} else {
allScript += ' ' + scriptNode.text();
}
}
});
return allScript;
};
return /** @alias module:core/fragment */{
/**
* Appends HTML and JavaScript fragments to specified nodes.
@ -72,40 +110,21 @@ define(['jquery', 'core/ajax'], function($, ajax) {
loadFragment: function(component, callback, contextid, params) {
var promise = $.Deferred();
loadFragment(component, callback, contextid, params).then(function(data) {
var jsNodes = $(data.javascript);
var allScript = '';
jsNodes.each(function(index, scriptNode) {
scriptNode = $(scriptNode);
var tagName = scriptNode.prop('tagName');
if (tagName && (tagName.toLowerCase() == 'script')) {
if (scriptNode.attr('src')) {
// We only reload the script if it was not loaded already.
var exists = false;
$('script').each(function(index, s) {
if ($(s).attr('src') == scriptNode.attr('src')) {
exists = true;
}
return !exists;
});
if (!exists) {
allScript += ' { ';
allScript += ' node = document.createElement("script"); ';
allScript += ' node.type = "text/javascript"; ';
allScript += ' node.src = decodeURI("' + encodeURI(scriptNode.attr('src')) + '"); ';
allScript += ' document.getElementsByTagName("head")[0].appendChild(node); ';
allScript += ' } ';
}
} else {
allScript += ' ' + scriptNode.text();
}
}
});
promise.resolve(data.html, allScript);
return;
promise.resolve(data.html, processCollectedJavascript(data.javascript));
}).fail(function(ex) {
promise.reject(ex);
});
return promise.promise();
},
/**
* Converts the JS that was received from collecting JS requirements on the $PAGE so it can be added to the existing page
*
* @param {string} js
* @return {string}
*/
processCollectedJavascript: function(js) {
return processCollectedJavascript(js);
}
};
});

View File

@ -445,6 +445,20 @@ define([
}
};
/**
* Alternative to setBody() that can be used from non-Jquery modules
*
* @param {Promise} promise promise that returns {html, js} object
* @return {Promise}
*/
Modal.prototype.setBodyContent = function(promise) {
// Call the leegacy API for now and pass it a jQuery Promise.
// This is a non-spec feature of jQuery and cannot be produced with spec promises.
// We can encourage people to migrate to this approach, and in future we can swap
// it so that setBody() calls setBodyPromise().
return promise.then(({html, js}) => this.setBody($.when(html, js)));
};
/**
* Set the modal footer element. The footer element is made visible, if it
* isn't already.

View File

@ -144,6 +144,7 @@ XPATH
XPATH
, 'dialogue' => <<<XPATH
.//div[contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue ') and
not(contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue-hidden ')) and
normalize-space(descendant::div[
contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue-hd ')
]) = %locator%] |

View File

@ -837,6 +837,13 @@ $functions = array(
'loginrequired' => false,
'ajax' => true,
),
'core_form_dynamic_form' => array(
'classname' => 'core_form\external\dynamic_form',
'methodname' => 'execute',
'description' => 'Process submission of a dynamic (modal) form',
'type' => 'write',
'ajax' => true,
),
'core_get_component_strings' => array(
'classname' => 'core_external',
'methodname' => 'get_component_strings',

2
lib/form/amd/build/dynamicform.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
lib/form/amd/build/modalform.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,389 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Display an embedded form, it is only loaded and reloaded inside its container
*
* Example:
* import DynamicForm from 'core_form/dynamicform';
*
* const dynamicForm = new DynamicForm(document.querySelector('#mycontainer', 'pluginname\\form\\formname');
* dynamicForm.addEventListener(dynamicForm.events.FORM_SUBMITTED, e => {
* e.preventDefault();
* window.console.log(e.detail);
* dynamicForm.container.innerHTML = 'Thank you, your form is submitted!';
* });
* dynamicForm.load();
*
* See also https://docs.moodle.org/dev/Modal_and_AJAX_forms
*
* @module core_form/dynamicform
* @package core_form
* @copyright 2019 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
import Notification from 'core/notification';
import Templates from 'core/templates';
import Event from 'core/event';
import {get_strings as getStrings} from 'core/str';
import Y from 'core/yui';
import Fragment from 'core/fragment';
import Pending from 'core/pending';
export default class DynamicForm {
/**
* Various events that can be observed.
*
* @type {Object}
*/
events = {
// Form was successfully submitted - the response is passed to the event listener.
// Cancellable (in order to prevent default behavior to clear the container).
FORM_SUBMITTED: 'core_form_dynamicform_formsubmitted',
// Cancel button was pressed.
// Cancellable (in order to prevent default behavior to clear the container).
FORM_CANCELLED: 'core_form_dynamicform_formcancelled',
// User attempted to submit the form but there was client-side validation error.
CLIENT_VALIDATION_ERROR: 'core_form_dynamicform_clientvalidationerror',
// User attempted to submit the form but server returned validation error.
SERVER_VALIDATION_ERROR: 'core_form_dynamicform_validationerror',
// Error occurred while performing request to the server.
// Cancellable (by default calls Notification.exception).
ERROR: 'core_form_dynamicform_error',
// Right after user pressed no-submit button,
// listen to this event if you want to add JS validation or processing for no-submit button.
// Cancellable.
NOSUBMIT_BUTTON_PRESSED: 'core_form_dynamicform_nosubmitbutton',
// Right after user pressed submit button,
// listen to this event if you want to add additional JS validation or confirmation dialog.
// Cancellable.
SUBMIT_BUTTON_PRESSED: 'core_form_dynamicform_submitbutton',
// Right after user pressed cancel button,
// listen to this event if you want to add confirmation dialog.
// Cancellable.
CANCEL_BUTTON_PRESSED: 'core_form_dynamicform_cancelbutton',
};
/**
* Constructor
*
* Creates an instance
*
* @param {Element} container - the parent element for the form
* @param {string} formClass full name of the php class that extends \core_form\modal , must be in autoloaded location
*/
constructor(container, formClass) {
this.formClass = formClass;
this.container = container;
// Ensure strings required for shortforms are always available.
getStrings([
{key: 'collapseall', component: 'moodle'},
{key: 'expandall', component: 'moodle'}
]).catch(Notification.exception);
// Register delegated events handlers in vanilla JS.
this.container.addEventListener('click', e => {
if (e.target.matches('form input[type=submit][data-cancel]')) {
e.preventDefault();
const event = this.trigger(this.events.CANCEL_BUTTON_PRESSED, e.target);
if (!event.defaultPrevented) {
this.processCancelButton();
}
} else if (e.target.matches('form input[type=submit][data-no-submit="1"]')) {
e.preventDefault();
const event = this.trigger(this.events.NOSUBMIT_BUTTON_PRESSED, e.target);
if (!event.defaultPrevented) {
this.processNoSubmitButton(e.target);
}
}
});
this.container.addEventListener('submit', e => {
if (e.target.matches('form')) {
e.preventDefault();
const event = this.trigger(this.events.SUBMIT_BUTTON_PRESSED);
if (!event.defaultPrevented) {
this.submitFormAjax();
}
}
});
}
/**
* Loads the form via AJAX and shows it inside a given container
*
* @param {Object} args
* @return {Promise}
* @public
*/
load(args = null) {
const formData = new URLSearchParams(Object.entries(args || {}));
const pendingPromise = new Pending('core_form/dynamicform:load');
return this.getBody(formData.toString())
.then((resp) => this.updateForm(resp))
.then(pendingPromise.resolve);
}
/**
* Triggers a custom event
*
* @private
* @param {String} eventName
* @param {*} detail
* @param {Boolean} cancelable
* @return {CustomEvent<unknown>}
*/
trigger(eventName, detail = null, cancelable = true) {
const e = new CustomEvent(eventName, {detail, cancelable});
this.container.dispatchEvent(e);
return e;
}
/**
* Add listener for an event
*
* Example:
* const dynamicForm = new DynamicForm(...);
* dynamicForm.addEventListener(dynamicForm.events.FORM_SUBMITTED, e => {
* e.preventDefault();
* window.console.log(e.detail);
* dynamicForm.container.innerHTML = 'Thank you, your form is submitted!';
* });
*/
addEventListener(...args) {
this.container.addEventListener(...args);
}
/**
* Get form body
*
* @param {String} formDataString form data in format of a query string
* @private
* @return {Promise}
*/
getBody(formDataString) {
return Ajax.call([{
methodname: 'core_form_dynamic_form',
args: {
formdata: formDataString,
form: this.formClass,
}
}])[0]
.then(response => {
return {html: response.html, js: Fragment.processCollectedJavascript(response.javascript)};
});
}
/**
* On form submit
*
* @param {*} response Response received from the form's "process" method
*/
onSubmitSuccess(response) {
const event = this.trigger(this.events.FORM_SUBMITTED, response);
if (event.defaultPrevented) {
return;
}
// Default implementation is to remove the form. Event listener should either remove or reload the form
// since its contents is no longer correct. For example, if an element was created as a result of
// form submission, the "id" in the form would be still zero. Also the server-side validation
// errors from the previous submission may still be present.
this.container.innerHTML = '';
}
/**
* On exception during form processing
*
* @private
* @param {Object} exception
*/
onSubmitError(exception) {
const event = this.trigger(this.events.ERROR, exception);
if (event.defaultPrevented) {
return;
}
Notification.exception(exception);
}
/**
* Click on a "submit" button that is marked in the form as registerNoSubmitButton()
*
* @method submitButtonPressed
* @param {Element} button that was pressed
*/
processNoSubmitButton(button) {
const pendingPromise = new Pending('core_form/dynamicform:nosubmit');
const form = this.container.querySelector('form');
const formData = new URLSearchParams([...(new FormData(form)).entries()]);
formData.append(button.getAttribute('name'), button.getAttribute('value'));
this.notifyFormSubmitAjax(true)
.then(() => {
// Add the button name to the form data and submit it.
this.disableButtons();
return this.getBody(formData.toString());
})
.then((resp) => this.updateForm(resp))
.finally(pendingPromise.resolve)
.catch(this.onSubmitError);
}
/**
* 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) {
const form = this.container.querySelector('form');
return new Promise(resolve => {
Y.use('event', 'moodle-core-event', 'moodle-core-formchangechecker', function() {
Event.notifyFormSubmitAjax(form, skipValidation);
resolve();
});
});
}
/**
* Notifies listeners that form dirty state should be reset.
*
* @return {Promise<unknown>}
*/
notifyResetFormChanges() {
const form = this.container.querySelector('form');
return new Promise(resolve => {
Y.use('event', 'moodle-core-event', 'moodle-core-formchangechecker', () => {
Event.notifyFormSubmitAjax(form, true);
M.core_formchangechecker.reset_form_dirty_state();
resolve();
});
});
}
/**
* Click on a "cancel" button
*/
processCancelButton() {
// Notify listeners that the form is about to be submitted (this will reset atto autosave).
this.notifyResetFormChanges()
.then(() => {
const event = this.trigger(this.events.FORM_CANCELLED);
if (!event.defaultPrevented) {
// By default removes the form from the DOM.
this.container.innerHTML = '';
}
return null;
})
.catch(null);
}
/**
* Update form contents
*
* @param {string} html
* @param {string} js
*/
updateForm({html, js}) {
Templates.replaceNodeContents(this.container, html, js);
}
/**
* Validate form elements
* @return {Promise} promise that returns true if client-side validation has passed, false if there are errors
*/
validateElements() {
// Notify listeners that the form is about to be submitted (this will reset atto autosave).
return this.notifyFormSubmitAjax()
.then(() => {
// Now the change events have run, see if there are any "invalid" form fields.
const invalid = [...this.container.querySelectorAll('[aria-invalid="true"], .error')];
// If we found invalid fields, focus on the first one and do not submit via ajax.
if (invalid.length) {
invalid[0].focus();
return false;
}
return true;
});
}
/**
* Disable buttons during form submission
*/
disableButtons() {
this.container.querySelectorAll('form input[type="submit"]')
.forEach(el => el.setAttribute('disabled', true));
}
/**
* Enable buttons after form submission (on validation error)
*/
enableButtons() {
this.container.querySelectorAll('form input[type="submit"]')
.forEach(el => el.removeAttribute('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 form = this.container.querySelector('form');
const formData = new URLSearchParams([...(new FormData(form)).entries()]);
// Now we can continue...
Ajax.call([{
methodname: 'core_form_dynamic_form',
args: {
formdata: formData.toString(),
form: this.formClass
}
}])[0]
.then((response) => {
if (!response.submitted) {
// Form was not submitted, it could be either because validation failed or because no-submit button was pressed.
this.updateForm({html: response.html, js: Fragment.processCollectedJavascript(response.javascript)});
this.enableButtons();
this.trigger(this.events.SERVER_VALIDATION_ERROR, null, false);
} else {
// Form was submitted properly.
const data = JSON.parse(response.data);
this.enableButtons();
this.notifyResetFormChanges()
.then(() => this.onSubmitSuccess(data))
.catch();
}
return null;
})
.catch(this.onSubmitError);
}
}

View File

@ -0,0 +1,408 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Display a form in a modal dialogue
*
* Example:
* import ModalForm from 'core_form/modalform';
*
* const modalForm = new ModalForm({
* formClass: 'pluginname\\form\\formname',
* modalConfig: {title: 'Here comes the title'},
* args: {categoryid: 123},
* returnFocus: e.target,
* });
* modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, (c) => window.console.log(c.detail));
* modalForm.show();
*
* See also https://docs.moodle.org/dev/Modal_and_AJAX_forms
*
* @module core_form/modalform
* @package core_form
* @copyright 2018 Mitxel Moriana <mitxel@tresipunt.>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import ModalFactory from 'core/modal_factory';
import ModalEvents from 'core/modal_events';
import Ajax from 'core/ajax';
import Notification from 'core/notification';
import Y from 'core/yui';
import Event from 'core/event';
import Fragment from 'core/fragment';
import Pending from 'core/pending';
export default class ModalForm {
/**
* Various events that can be observed.
*
* @type {Object}
*/
events = {
// Form was successfully submitted - the response is passed to the event listener.
// Cancellable (but it's hardly ever needed to cancel this event).
FORM_SUBMITTED: 'core_form_modalform_formsubmitted',
// Cancel button was pressed.
// Cancellable (but it's hardly ever needed to cancel this event).
FORM_CANCELLED: 'core_form_modalform_formcancelled',
// User attempted to submit the form but there was client-side validation error.
CLIENT_VALIDATION_ERROR: 'core_form_modalform_clientvalidationerror',
// User attempted to submit the form but server returned validation error.
SERVER_VALIDATION_ERROR: 'core_form_modalform_validationerror',
// Error occurred while performing request to the server.
// Cancellable (by default calls Notification.exception).
ERROR: 'core_form_modalform_error',
// Right after user pressed no-submit button,
// listen to this event if you want to add JS validation or processing for no-submit button.
// Cancellable.
NOSUBMIT_BUTTON_PRESSED: 'core_form_modalform_nosubmitbutton',
// Right after user pressed submit button,
// listen to this event if you want to add additional JS validation or confirmation dialog.
// Cancellable.
SUBMIT_BUTTON_PRESSED: 'core_form_modalform_submitbutton',
// Right after user pressed cancel button,
// listen to this event if you want to add confirmation dialog.
// Cancellable.
CANCEL_BUTTON_PRESSED: 'core_form_modalform_cancelbutton',
// Modal was loaded and this.modal is available (but the form content may not be loaded yet).
LOADED: 'core_form_modalform_loaded',
};
/**
* Constructor
*
* Shows the required form inside a modal dialogue
*
* @param {Object} config parameters for the form and modal dialogue:
* @property {String} config.formClass PHP class name that handles the form (should extend \core_form\modal )
* @property {Object} config.modalConfig modal config - title, type, etc.
* Default: {removeOnClose: true, type: ModalFactory.types.SAVE_CANCEL}
* @property {Object} config.args Arguments for the initial form rendering (for example, id of the edited entity)
* @property {String} config.saveButtonText the text to display on the Modal "Save" button (optional)
* @property {String} config.saveButtonClasses additional CSS classes for the Modal "Save" button
* @property {HTMLElement} config.returnFocus element to return focus to after the dialogue is closed
*/
constructor(config) {
this.modal = null;
this.config = config;
this.config.modalConfig = {
removeOnClose: true,
type: ModalFactory.types.SAVE_CANCEL,
large: true,
...(this.config.modalConfig || {}),
};
this.config.args = this.config.args || {};
this.futureListeners = [];
}
/**
* Initialise the modal and shows it
*
* @return {Promise}
*/
show() {
const pendingPromise = new Pending('core_form/modalform:init');
return ModalFactory.create(this.config.modalConfig)
.then((modal) => {
this.modal = modal;
// Retrieve the form and set the modal body. We can not set the body in the modalConfig,
// we need to make sure that the modal already exists when we render the form. Some form elements
// such as date_selector inspect the existing elements on the page to find the highest z-index.
const formParams = new URLSearchParams(Object.entries(this.config.args || {}));
this.modal.setBodyContent(this.getBody(formParams.toString()));
// After successfull submit, when we press "Cancel" or close the dialogue by clicking on X in the top right corner.
this.modal.getRoot().on(ModalEvents.hidden, () => {
this.notifyResetFormChanges()
.then(() => {
this.modal.destroy();
// Focus on the element that actually launched the modal.
if (this.config.returnFocus) {
this.config.returnFocus.focus();
}
return null;
})
.catch(() => null);
});
// Add the class to the modal dialogue.
this.modal.getModal().addClass('modal-form-dialogue');
// We catch the press on submit buttons in the forms.
this.modal.getRoot().on('click', 'form input[type=submit][data-no-submit]',
(e) => {
e.preventDefault();
const event = this.trigger(this.events.NOSUBMIT_BUTTON_PRESSED, e.target);
if (!event.defaultPrevented) {
this.processNoSubmitButton(e.target);
}
});
// We catch the form submit event and use it to submit the form with ajax.
this.modal.getRoot().on('submit', 'form', (e) => {
e.preventDefault();
const event = this.trigger(this.events.SUBMIT_BUTTON_PRESSED);
if (!event.defaultPrevented) {
this.submitFormAjax();
}
});
// Change the text for the save button.
if (typeof this.config.saveButtonText !== 'undefined' &&
typeof this.modal.setSaveButtonText !== 'undefined') {
this.modal.setSaveButtonText(this.config.saveButtonText);
}
// Set classes for the save button.
if (typeof this.config.saveButtonClasses !== 'undefined') {
this.setSaveButtonClasses(this.config.saveButtonClasses);
}
// When Save button is pressed - submit the form.
this.modal.getRoot().on(ModalEvents.save, (e) => {
e.preventDefault();
this.modal.getRoot().find('form').submit();
});
// When Cancel button is pressed - allow to intercept.
this.modal.getRoot().on(ModalEvents.cancel, (e) => {
const event = this.trigger(this.events.CANCEL_BUTTON_PRESSED);
if (event.defaultPrevented) {
e.preventDefault();
}
});
this.futureListeners.forEach(args => this.modal.getRoot()[0].addEventListener(...args));
this.futureListeners = [];
this.trigger(this.events.LOADED, null, false);
return this.modal.show();
})
.then(pendingPromise.resolve);
}
/**
* Triggers a custom event
*
* @private
* @param {String} eventName
* @param {*} detail
* @param {Boolean} cancelable
* @return {CustomEvent<unknown>}
*/
trigger(eventName, detail = null, cancelable = true) {
const e = new CustomEvent(eventName, {detail, cancelable});
this.modal.getRoot()[0].dispatchEvent(e);
return e;
}
/**
* Add listener for an event
*
* Example:
* const modalForm = new ModalForm(...);
* dynamicForm.addEventListener(modalForm.events.FORM_SUBMITTED, e => {
* window.console.log(e.detail);
* });
*/
addEventListener(...args) {
if (!this.modal) {
this.futureListeners.push(args);
} else {
this.modal.getRoot()[0].addEventListener(...args);
}
}
/**
* Get form contents (to be used in ModalForm.setBodyContent())
*
* @param {String} formDataString form data in format of a query string
* @method getBody
* @private
* @return {Promise}
*/
getBody(formDataString) {
const params = {
formdata: formDataString,
form: this.config.formClass
};
const pendingPromise = new Pending('core_form/modalform:form_body');
return Ajax.call([{
methodname: 'core_form_dynamic_form',
args: params
}])[0]
.then(response => {
pendingPromise.resolve();
return {html: response.html, js: Fragment.processCollectedJavascript(response.javascript)};
});
}
/**
* On exception during form processing. Caller may override
*
* @param {Object} exception
*/
onSubmitError(exception) {
const event = this.trigger(this.events.ERROR, exception);
if (event.defaultPrevented) {
return;
}
Notification.exception(exception);
}
/**
* Notifies listeners that form dirty state should be reset.
*
* @return {Promise<unknown>}
*/
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);
}
}

View File

@ -0,0 +1,149 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_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 {@see \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;
}

106
lib/form/classes/external/modal.php vendored Normal file
View File

@ -0,0 +1,106 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_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)
)
);
}
}

View File

@ -0,0 +1,77 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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 "<pre>".json_encode($data->testtext)."</pre>";
} else {
$form->display();
}
echo $OUTPUT->footer();

View File

@ -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\"}"

View File

@ -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.
@ -3052,7 +3143,6 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
if (count($this->_collapsibleElements) > 1) {
$this->_collapseButtons = $this->_collapseButtonsTemplate;
$this->_collapseButtons = str_replace('{strexpandall}', get_string('expandall'), $this->_collapseButtons);
$PAGE->requires->strings_for_js(array('collapseall', 'expandall'), 'moodle');
}
$PAGE->requires->yui_module('moodle-form-shortforms', 'M.form.shortforms', array(array('formid' => $formid)));
}

View File

@ -1684,6 +1684,9 @@ class page_requirements_manager {
'error',
'file',
'url',
// TODO MDL-70830 shortforms should preload the collapseall/expandall strings properly.
'collapseall',
'expandall',
), 'moodle');
$this->strings_for_js(array(
'debuginfo',

View File

@ -30,6 +30,8 @@ information provided here is intended especially for developers.
* Behat now supports date and time selection from the datetime form element. Examples:
- I set the field "<field_string>" to "##15 March 2021 08:15##"
- I set the field "<field_string>" to "##first day of January last year noon##"
* 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:

View File

@ -7,15 +7,13 @@ Feature: Delete files and folders from the file manager
@javascript @_bug_phantomjs
Scenario: Delete a file and a folder
Given I log in as "admin"
And I follow "Manage private files"
And I follow "Private files"
And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
And I create "Delete me" folder in "Files" filemanager
And I press "Save changes"
And I follow "Manage private files"
When I delete "empty.txt" from "Files" filemanager
And I press "Save changes"
Then I should not see "empty.txt"
And I follow "Manage private files"
And I delete "Delete me" from "Files" filemanager
And I press "Save changes"
And I should not see "Delete me"
@ -23,11 +21,10 @@ Feature: Delete files and folders from the file manager
@javascript
Scenario: Delete a file and a folder using bulk functionality (individually)
Given I log in as "admin"
And I follow "Manage private files"
And I follow "Private files"
And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
And I create "Delete me later" folder in "Files" filemanager
And I press "Save changes"
And I follow "Manage private files"
And I click on "Display folder with file details" "link"
And I set the field "Select file 'empty.txt'" to "1"
When I click on "Delete" "link"
@ -36,7 +33,6 @@ Feature: Delete files and folders from the file manager
Then I should not see "empty.txt"
But I should see "Delete me later"
When I press "Save changes"
And I follow "Manage private files"
Then I should not see "empty.txt"
But I should see "Delete me later"
And I set the field "Select file 'Delete me later'" to "1"
@ -49,12 +45,11 @@ Feature: Delete files and folders from the file manager
@javascript
Scenario: Delete a file and a folder using bulk functionality (multiple)
Given I log in as "admin"
And I follow "Manage private files"
And I follow "Private files"
And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
And I create "Delete me" folder in "Files" filemanager
And I create "Do not delete me" folder in "Files" filemanager
And I press "Save changes"
And I follow "Manage private files"
And I click on "Display folder with file details" "link"
And I set the field "Select file 'empty.txt'" to "1"
And I set the field "Select file 'Delete me'" to "1"
@ -65,6 +60,9 @@ Feature: Delete files and folders from the file manager
And I should not see "empty.txt"
But I should see "Do not delete me"
When I press "Save changes"
Then I should not see "Delete me"
And I should not see "empty.txt"
And I am on homepage
Then I should not see "Delete me" in the "Private files" "block"
And I should not see "empty.txt" in the "Private files" "block"
But I should see "Do not delete me" in the "Private files" "block"
@ -72,12 +70,11 @@ Feature: Delete files and folders from the file manager
@javascript
Scenario: Delete files using the select all checkbox
Given I log in as "admin"
And I follow "Manage private files"
And I follow "Private files"
And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
And I create "Delete me" folder in "Files" filemanager
And I create "Delete me too" folder in "Files" filemanager
And I press "Save changes"
And I follow "Manage private files"
And I click on "Display folder with file details" "link"
When I click on "Select all/none" "checkbox"
Then the following fields match these values:
@ -91,6 +88,9 @@ Feature: Delete files and folders from the file manager
And I should not see "empty.txt"
And I should not see "Delete me too"
When I press "Save changes"
Then I should not see "Delete me"
And I should not see "empty.txt"
And I am on homepage
Then I should not see "Delete me" in the "Private files" "block"
And I should not see "empty.txt" in the "Private files" "block"
And I should not see "Delete me too" in the "Private files" "block"

View File

@ -10,7 +10,7 @@ Feature: Upload files
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And I log in as "admin"
When I follow "Manage private files..."
When I follow "Private files"
And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
Then I should see "1" elements in "Files" filemanager
And I should see "empty.txt" in the "div.fp-content" "css_element"
@ -19,4 +19,3 @@ Feature: Upload files
Then I should see "2" elements in "Files" filemanager
And I should see "empty.txt"
And I should see "empty_copy.txt"
And I press "Cancel"

2
user/amd/build/private_files.min.js vendored Normal file
View File

@ -0,0 +1,2 @@
define ("core_user/private_files",["exports","core_form/dynamicform","core_form/modalform","core/str","core/toast"],function(a,b,c,d,e){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.initModal=a.initDynamicForm=void 0;b=f(b);c=f(c);function f(a){return a&&a.__esModule?a:{default:a}}var g=function(a,c){var f=new b.default(document.querySelector(a),c);f.addEventListener(f.events.FORM_SUBMITTED,function(){f.load();(0,d.get_string)("changessaved").then(e.add).catch(null)})};a.initDynamicForm=g;a.initModal=function initModal(a,b){document.querySelector(a).addEventListener("click",function(a){a.preventDefault();var e=new c.default({formClass:b,args:{nosubmit:!0},modalConfig:{title:(0,d.get_string)("privatefilesmanage")},returnFocus:a.target});e.addEventListener(e.events.FORM_SUBMITTED,function(){return window.location.reload()});e.show()})}});
//# sourceMappingURL=private_files.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"sources":["../src/private_files.js"],"names":["initDynamicForm","containerSelector","formClass","form","DynamicForm","document","querySelector","addEventListener","events","FORM_SUBMITTED","load","then","addToast","catch","initModal","elementSelector","e","preventDefault","ModalForm","args","nosubmit","modalConfig","title","returnFocus","target","window","location","reload","show"],"mappings":"2OAsBA,OACA,O,mDAUO,GAAMA,CAAAA,CAAe,CAAG,SAACC,CAAD,CAAoBC,CAApB,CAAkC,CAC7D,GAAMC,CAAAA,CAAI,CAAG,GAAIC,UAAJ,CAAgBC,QAAQ,CAACC,aAAT,CAAuBL,CAAvB,CAAhB,CAA2DC,CAA3D,CAAb,CAEAC,CAAI,CAACI,gBAAL,CAAsBJ,CAAI,CAACK,MAAL,CAAYC,cAAlC,CAAkD,UAAM,CACpDN,CAAI,CAACO,IAAL,GACA,iBAAU,cAAV,EACCC,IADD,CACMC,KADN,EAECC,KAFD,CAEO,IAFP,CAGH,CALD,CAMH,CATM,C,gCAiBkB,QAAZC,CAAAA,SAAY,CAACC,CAAD,CAAkBb,CAAlB,CAAgC,CACrDG,QAAQ,CAACC,aAAT,CAAuBS,CAAvB,EAAwCR,gBAAxC,CAAyD,OAAzD,CAAkE,SAASS,CAAT,CAAY,CAC1EA,CAAC,CAACC,cAAF,GACA,GAAMd,CAAAA,CAAI,CAAG,GAAIe,UAAJ,CAAc,CACvBhB,SAAS,CAATA,CADuB,CAEvBiB,IAAI,CAAE,CAACC,QAAQ,GAAT,CAFiB,CAGvBC,WAAW,CAAE,CAACC,KAAK,CAAE,iBAAU,oBAAV,CAAR,CAHU,CAIvBC,WAAW,CAAEP,CAAC,CAACQ,MAJQ,CAAd,CAAb,CAMArB,CAAI,CAACI,gBAAL,CAAsBJ,CAAI,CAACK,MAAL,CAAYC,cAAlC,CAAkD,iBAAMgB,CAAAA,MAAM,CAACC,QAAP,CAAgBC,MAAhB,EAAN,CAAlD,EACAxB,CAAI,CAACyB,IAAL,EACH,CAVD,CAWH,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Module to handle AJAX interactions with user private files\n *\n * @module core_user/private_files\n * @copyright 2020 Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport DynamicForm from 'core_form/dynamicform';\nimport ModalForm from 'core_form/modalform';\nimport {get_string as getString} from 'core/str';\nimport {add as addToast} from 'core/toast';\n\n/**\n * Initialize private files form as AJAX form\n *\n * @param {String} containerSelector\n * @param {String} formClass\n */\nexport const initDynamicForm = (containerSelector, formClass) => {\n const form = new DynamicForm(document.querySelector(containerSelector), formClass);\n // When form is saved, refresh it to remove validation errors, if any:\n form.addEventListener(form.events.FORM_SUBMITTED, () => {\n form.load();\n getString('changessaved')\n .then(addToast)\n .catch(null);\n });\n};\n\n/**\n * Initialize private files form as Modal form\n *\n * @param {String} elementSelector\n * @param {String} formClass\n */\nexport const initModal = (elementSelector, formClass) => {\n document.querySelector(elementSelector).addEventListener('click', function(e) {\n e.preventDefault();\n const form = new ModalForm({\n formClass,\n args: {nosubmit: true},\n modalConfig: {title: getString('privatefilesmanage')},\n returnFocus: e.target,\n });\n form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload());\n form.show();\n });\n};\n"],"file":"private_files.min.js"}

View File

@ -0,0 +1,63 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Module to handle AJAX interactions with user private files
*
* @module core_user/private_files
* @copyright 2020 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import DynamicForm from 'core_form/dynamicform';
import ModalForm from 'core_form/modalform';
import {get_string as getString} from 'core/str';
import {add as addToast} from 'core/toast';
/**
* Initialize private files form as AJAX form
*
* @param {String} containerSelector
* @param {String} formClass
*/
export const initDynamicForm = (containerSelector, formClass) => {
const form = new DynamicForm(document.querySelector(containerSelector), formClass);
// When form is saved, refresh it to remove validation errors, if any:
form.addEventListener(form.events.FORM_SUBMITTED, () => {
form.load();
getString('changessaved')
.then(addToast)
.catch(null);
});
};
/**
* Initialize private files form as Modal form
*
* @param {String} elementSelector
* @param {String} formClass
*/
export const initModal = (elementSelector, formClass) => {
document.querySelector(elementSelector).addEventListener('click', function(e) {
e.preventDefault();
const form = new ModalForm({
formClass,
args: {nosubmit: true},
modalConfig: {title: getString('privatefilesmanage')},
returnFocus: e.target,
});
form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload());
form.show();
});
};

View File

@ -0,0 +1,190 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_user\form;
use html_writer;
use moodle_url;
/**
* Manage user private area files form
*
* @package core_user
* @copyright 2010 Petr Skoda (http://skodak.org)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class private_files extends \core_form\dynamic_form {
/**
* Add elements to this form.
*/
public function definition() {
global $OUTPUT;
$mform = $this->_form;
$options = $this->get_options();
// Show file area space usage.
$maxareabytes = $options['areamaxbytes'];
if ($maxareabytes != FILE_AREA_MAX_BYTES_UNLIMITED) {
$fileareainfo = file_get_file_area_info($this->get_context_for_dynamic_submission()->id, 'user', 'private');
// Display message only if we have files.
if ($fileareainfo['filecount']) {
$a = (object) [
'used' => display_size($fileareainfo['filesize_without_references']),
'total' => display_size($maxareabytes)
];
$quotamsg = get_string('quotausage', 'moodle', $a);
$notification = new \core\output\notification($quotamsg, \core\output\notification::NOTIFY_INFO);
$mform->addElement('static', 'areabytes', '', $OUTPUT->render($notification));
}
}
$mform->addElement('filemanager', 'files_filemanager', get_string('files'), null, $options);
if ($link = $this->get_emaillink()) {
$emaillink = html_writer::link(new moodle_url('mailto:' . $link), $link);
$mform->addElement('static', 'emailaddress', '',
get_string('emailtoprivatefiles', 'moodle', $emaillink));
}
$mform->setType('returnurl', PARAM_LOCALURL);
if (!$this->optional_param('nosubmit', false, PARAM_BOOL)) {
$this->add_action_buttons(false, get_string('savechanges'));
}
}
/**
* Validate incoming data.
*
* @param array $data
* @param array $files
* @return array
*/
public function validation($data, $files) {
$errors = array();
$draftitemid = $data['files_filemanager'];
$options = $this->get_options();
if (file_is_draft_area_limit_reached($draftitemid, $options['areamaxbytes'])) {
$errors['files_filemanager'] = get_string('userquotalimit', 'error');
}
return $errors;
}
/**
* Link to email private files
*
* @return string|null
* @throws \coding_exception
*/
protected function get_emaillink() {
global $USER;
// Attempt to generate an inbound message address to support e-mail to private files.
$generator = new \core\message\inbound\address_manager();
$generator->set_handler('\core\message\inbound\private_files_handler');
$generator->set_data(-1);
return $generator->generate($USER->id);
}
/**
* Check if current user has access to this form, otherwise throw exception
*
* Sometimes permission check may depend on the action and/or id of the entity.
* If necessary, form data is available in $this->_ajaxformdata or
* by calling $this->optional_param()
*/
protected function check_access_for_dynamic_submission(): void {
require_capability('moodle/user:manageownfiles', $this->get_context_for_dynamic_submission());
}
/**
* Returns form context
*
* If context depends on the form data, it is available in $this->_ajaxformdata or
* by calling $this->optional_param()
*
* @return \context
*/
protected function get_context_for_dynamic_submission(): \context {
global $USER;
return \context_user::instance($USER->id);
}
/**
* File upload options
*
* @return array
* @throws \coding_exception
*/
protected function get_options(): array {
global $CFG;
$maxbytes = $CFG->userquota;
$maxareabytes = $CFG->userquota;
if (has_capability('moodle/user:ignoreuserquota', $this->get_context_for_dynamic_submission())) {
$maxbytes = USER_CAN_IGNORE_FILE_SIZE_LIMITS;
$maxareabytes = FILE_AREA_MAX_BYTES_UNLIMITED;
}
return ['subdirs' => 1, 'maxbytes' => $maxbytes, 'maxfiles' => -1, 'accepted_types' => '*',
'areamaxbytes' => $maxareabytes];
}
/**
* 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()
*
* @return mixed
*/
public function process_dynamic_submission() {
file_postupdate_standard_filemanager($this->get_data(), 'files',
$this->get_options(), $this->get_context_for_dynamic_submission(), 'user', 'private', 0);
return null;
}
/**
* Load in existing data as form defaults
*
* Can be overridden to retrieve existing values from db by entity id and also
* to preprocess editor and filemanager elements
*
* Example:
* $this->set_data(get_entity($this->_ajaxformdata['id']));
*/
public function set_data_for_dynamic_submission(): void {
$data = new \stdClass();
file_prepare_standard_filemanager($data, 'files', $this->get_options(),
$this->get_context_for_dynamic_submission(), 'user', 'private', 0);
$this->set_data($data);
}
/**
* 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.
*
* @return \moodle_url
*/
protected function get_page_url_for_dynamic_submission(): \moodle_url {
return new moodle_url('/user/files.php');
}
}

View File

@ -24,25 +24,16 @@
*/
require('../config.php');
require_once("$CFG->dirroot/user/files_form.php");
require_once("$CFG->dirroot/repository/lib.php");
require_login();
if (isguestuser()) {
die();
}
$returnurl = optional_param('returnurl', '', PARAM_LOCALURL);
if (empty($returnurl)) {
$returnurl = new moodle_url('/user/files.php');
}
$context = context_user::instance($USER->id);
require_capability('moodle/user:manageownfiles', $context);
$title = get_string('privatefiles');
$struser = get_string('user');
$PAGE->set_url('/user/files.php');
$PAGE->set_context($context);
@ -51,52 +42,16 @@ $PAGE->set_heading(fullname($USER));
$PAGE->set_pagelayout('standard');
$PAGE->set_pagetype('user-files');
$maxbytes = $CFG->userquota;
$maxareabytes = $CFG->userquota;
if (has_capability('moodle/user:ignoreuserquota', $context)) {
$maxbytes = USER_CAN_IGNORE_FILE_SIZE_LIMITS;
$maxareabytes = FILE_AREA_MAX_BYTES_UNLIMITED;
}
$data = new stdClass();
$data->returnurl = $returnurl;
$options = array('subdirs' => 1, 'maxbytes' => $maxbytes, 'maxfiles' => -1, 'accepted_types' => '*',
'areamaxbytes' => $maxareabytes);
file_prepare_standard_filemanager($data, 'files', $options, $context, 'user', 'private', 0);
// Attempt to generate an inbound message address to support e-mail to private files.
$generator = new \core\message\inbound\address_manager();
$generator->set_handler('\core\message\inbound\private_files_handler');
$generator->set_data(-1);
$data->emaillink = $generator->generate($USER->id);
$mform = new user_files_form(null, array('data' => $data, 'options' => $options));
if ($mform->is_cancelled()) {
redirect($returnurl);
} else if ($formdata = $mform->get_data()) {
$formdata = file_postupdate_standard_filemanager($formdata, 'files', $options, $context, 'user', 'private', 0);
redirect($returnurl);
}
echo $OUTPUT->header();
echo $OUTPUT->box_start('generalbox');
// Show file area space usage.
if ($maxareabytes != FILE_AREA_MAX_BYTES_UNLIMITED) {
$fileareainfo = file_get_file_area_info($context->id, 'user', 'private');
// Display message only if we have files.
if ($fileareainfo['filecount']) {
$a = (object) [
'used' => display_size($fileareainfo['filesize_without_references']),
'total' => display_size($maxareabytes)
];
$quotamsg = get_string('quotausage', 'moodle', $a);
$notification = new \core\output\notification($quotamsg, \core\output\notification::NOTIFY_INFO);
echo $OUTPUT->render($notification);
}
}
echo html_writer::start_div('', ['id' => 'userfilesform']);
$form = new \core_user\form\private_files();
$form->set_data_for_dynamic_submission();
$form->display();
echo html_writer::end_div();
$PAGE->requires->js_call_amd('core_user/private_files', 'initDynamicForm',
['#userfilesform', \core_user\form\private_files::class]);
$mform->display();
echo $OUTPUT->box_end();
echo $OUTPUT->footer();

View File

@ -1,76 +0,0 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* minimalistic edit form
*
* @package core_user
* @category files
* @copyright 2010 Petr Skoda (http://skodak.org)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once("$CFG->libdir/formslib.php");
/**
* Class user_files_form
* @copyright 2010 Petr Skoda (http://skodak.org)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_files_form extends moodleform {
/**
* Add elements to this form.
*/
public function definition() {
$mform = $this->_form;
$data = $this->_customdata['data'];
$options = $this->_customdata['options'];
$mform->addElement('filemanager', 'files_filemanager', get_string('files'), null, $options);
$mform->addElement('hidden', 'returnurl', $data->returnurl);
if (isset($data->emaillink)) {
$emaillink = html_writer::link(new moodle_url('mailto:' . $data->emaillink), $data->emaillink);
$mform->addElement('static', 'emailaddress', '',
get_string('emailtoprivatefiles', 'moodle', $emaillink));
}
$mform->setType('returnurl', PARAM_LOCALURL);
$this->add_action_buttons(true, get_string('savechanges'));
$this->set_data($data);
}
/**
* Validate incoming data.
*
* @param array $data
* @param array $files
* @return array
*/
public function validation($data, $files) {
$errors = array();
$draftitemid = $data['files_filemanager'];
if (file_is_draft_area_limit_reached($draftitemid, $this->_customdata['options']['areamaxbytes'])) {
$errors['files_filemanager'] = get_string('userquotalimit', 'error');
}
return $errors;
}
}

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2021052500.60; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2021052500.61; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.0dev (Build: 20210211)'; // Human-friendly version name