mirror of
https://github.com/moodle/moodle.git
synced 2025-04-13 20:42:22 +02:00
Merge branch 'MDL-64554-master' of git://github.com/andrewnicols/moodle
This commit is contained in:
commit
d6e50d14d2
@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
2
customfield/amd/build/form.min.js
vendored
2
customfield/amd/build/form.min.js
vendored
@ -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
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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();
|
@ -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'),
|
||||
)
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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".
|
||||
|
@ -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"
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
8
customfield/upgrade.txt
Normal 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.
|
||||
|
@ -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:"
|
||||
|
@ -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';
|
||||
|
2
lib/amd/build/fragment.min.js
vendored
2
lib/amd/build/fragment.min.js
vendored
@ -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
2
lib/amd/build/modal.min.js
vendored
2
lib/amd/build/modal.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
@ -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.
|
||||
|
@ -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%] |
|
||||
|
@ -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
2
lib/form/amd/build/dynamicform.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
lib/form/amd/build/dynamicform.min.js.map
Normal file
1
lib/form/amd/build/dynamicform.min.js.map
Normal file
File diff suppressed because one or more lines are too long
2
lib/form/amd/build/modalform.min.js
vendored
Normal file
2
lib/form/amd/build/modalform.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
lib/form/amd/build/modalform.min.js.map
Normal file
1
lib/form/amd/build/modalform.min.js.map
Normal file
File diff suppressed because one or more lines are too long
389
lib/form/amd/src/dynamicform.js
Normal file
389
lib/form/amd/src/dynamicform.js
Normal 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);
|
||||
}
|
||||
}
|
408
lib/form/amd/src/modalform.js
Normal file
408
lib/form/amd/src/modalform.js
Normal 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);
|
||||
}
|
||||
}
|
149
lib/form/classes/dynamic_form.php
Normal file
149
lib/form/classes/dynamic_form.php
Normal 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
106
lib/form/classes/external/modal.php
vendored
Normal 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)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
77
lib/form/tests/behat/fixtures/repeat_with_delete_form.php
Normal file
77
lib/form/tests/behat/fixtures/repeat_with_delete_form.php
Normal 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();
|
@ -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\"}"
|
||||
|
134
lib/formslib.php
134
lib/formslib.php
@ -519,20 +519,58 @@ abstract class moodleform {
|
||||
return $nosubmit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an element of multi-dimensional array given the list of keys
|
||||
*
|
||||
* Example:
|
||||
* $array['a']['b']['c'] = 13;
|
||||
* $v = $this->get_array_value_by_keys($array, ['a', 'b', 'c']);
|
||||
*
|
||||
* Will result it $v==13
|
||||
*
|
||||
* @param array $array
|
||||
* @param array $keys
|
||||
* @return mixed returns null if keys not present
|
||||
*/
|
||||
protected function get_array_value_by_keys(array $array, array $keys) {
|
||||
$value = $array;
|
||||
foreach ($keys as $key) {
|
||||
if (array_key_exists($key, $value)) {
|
||||
$value = $value[$key];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a parameter was passed in the previous form submission
|
||||
*
|
||||
* @param string $name the name of the page parameter we want
|
||||
* @param string $name the name of the page parameter we want, for example 'id' or 'element[sub][13]'
|
||||
* @param mixed $default the default value to return if nothing is found
|
||||
* @param string $type expected type of parameter
|
||||
* @return mixed
|
||||
*/
|
||||
public function optional_param($name, $default, $type) {
|
||||
if (isset($this->_ajaxformdata[$name])) {
|
||||
return clean_param($this->_ajaxformdata[$name], $type);
|
||||
} else {
|
||||
return optional_param($name, $default, $type);
|
||||
$nameparsed = [];
|
||||
// Convert element name into a sequence of keys, for example 'element[sub][13]' -> ['element', 'sub', '13'].
|
||||
parse_str($name . '=1', $nameparsed);
|
||||
$keys = [];
|
||||
while (is_array($nameparsed)) {
|
||||
$key = key($nameparsed);
|
||||
$keys[] = $key;
|
||||
$nameparsed = $nameparsed[$key];
|
||||
}
|
||||
|
||||
// Search for the element first in $this->_ajaxformdata, then in $_POST and then in $_GET.
|
||||
if (($value = $this->get_array_value_by_keys($this->_ajaxformdata ?? [], $keys)) !== null ||
|
||||
($value = $this->get_array_value_by_keys($_POST, $keys)) !== null ||
|
||||
($value = $this->get_array_value_by_keys($_GET, $keys)) !== null) {
|
||||
return $type == PARAM_RAW ? $value : clean_param($value, $type);
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1099,11 +1137,14 @@ abstract class moodleform {
|
||||
* @param int $addfieldsno how many fields to add at a time
|
||||
* @param string $addstring name of button, {no} is replaced by no of blanks that will be added.
|
||||
* @param bool $addbuttoninside if true, don't call closeHeaderBefore($addfieldsname). Default false.
|
||||
* @param string $deletebuttonname if specified, treats the no-submit button with this name as a "delete element" button
|
||||
* in each of the elements
|
||||
* @return int no of repeats of element in this page
|
||||
*/
|
||||
function repeat_elements($elementobjs, $repeats, $options, $repeathiddenname,
|
||||
$addfieldsname, $addfieldsno=5, $addstring=null, $addbuttoninside=false){
|
||||
if ($addstring===null){
|
||||
public function repeat_elements($elementobjs, $repeats, $options, $repeathiddenname,
|
||||
$addfieldsname, $addfieldsno = 5, $addstring = null, $addbuttoninside = false,
|
||||
$deletebuttonname = '') {
|
||||
if ($addstring === null) {
|
||||
$addstring = get_string('addfields', 'form', $addfieldsno);
|
||||
} else {
|
||||
$addstring = str_ireplace('{no}', $addfieldsno, $addstring);
|
||||
@ -1121,7 +1162,18 @@ abstract class moodleform {
|
||||
//value not to be overridden by submitted value
|
||||
$mform->setConstants(array($repeathiddenname=>$repeats));
|
||||
$namecloned = array();
|
||||
$no = 1;
|
||||
for ($i = 0; $i < $repeats; $i++) {
|
||||
if ($deletebuttonname) {
|
||||
$mform->registerNoSubmitButton($deletebuttonname . "[$i]");
|
||||
$isdeleted = $this->optional_param($deletebuttonname . "[$i]", false, PARAM_RAW) ||
|
||||
$this->optional_param($deletebuttonname . "-hidden[$i]", false, PARAM_RAW);
|
||||
if ($isdeleted) {
|
||||
$mform->addElement('hidden', $deletebuttonname . "-hidden[$i]", 1);
|
||||
$mform->setType($deletebuttonname . "-hidden[$i]", PARAM_INT);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
foreach ($elementobjs as $elementobj){
|
||||
$elementclone = fullclone($elementobj);
|
||||
$this->repeat_elements_fix_clone($i, $elementclone, $namecloned);
|
||||
@ -1130,7 +1182,13 @@ abstract class moodleform {
|
||||
foreach ($elementclone->getElements() as $el) {
|
||||
$this->repeat_elements_fix_clone($i, $el, $namecloned);
|
||||
}
|
||||
$elementclone->setLabel(str_replace('{no}', $i + 1, $elementclone->getLabel()));
|
||||
$elementclone->setLabel(str_replace('{no}', $no, $elementclone->getLabel()));
|
||||
} else if ($elementobj instanceof \HTML_QuickForm_submit && $elementobj->getName() == $deletebuttonname) {
|
||||
// Mark the "Delete" button as no-submit.
|
||||
$onclick = $elementclone->getAttribute('onclick');
|
||||
$skip = 'skipClientValidation = true;';
|
||||
$onclick = ($onclick !== null) ? $skip . ' ' . $onclick : $skip;
|
||||
$elementclone->updateAttributes(['data-skip-validation' => 1, 'data-no-submit' => 1, 'onclick' => $onclick]);
|
||||
}
|
||||
|
||||
// Mark newly created elements, so they know not to look for any submitted data.
|
||||
@ -1139,6 +1197,7 @@ abstract class moodleform {
|
||||
}
|
||||
|
||||
$mform->addElement($elementclone);
|
||||
$no++;
|
||||
}
|
||||
}
|
||||
for ($i=0; $i<$repeats; $i++) {
|
||||
@ -1161,24 +1220,22 @@ abstract class moodleform {
|
||||
call_user_func_array(array(&$mform, 'addHelpButton'), $params);
|
||||
break;
|
||||
case 'disabledif' :
|
||||
foreach ($namecloned as $num => $name){
|
||||
if ($params[0] == $name){
|
||||
$params[0] = $params[0]."[$i]";
|
||||
break;
|
||||
}
|
||||
}
|
||||
$params = array_merge(array($realelementname), $params);
|
||||
call_user_func_array(array(&$mform, 'disabledIf'), $params);
|
||||
break;
|
||||
case 'hideif' :
|
||||
$pos = strpos($params[0], '[');
|
||||
$ending = '';
|
||||
if ($pos !== false) {
|
||||
$ending = substr($params[0], $pos);
|
||||
$params[0] = substr($params[0], 0, $pos);
|
||||
}
|
||||
foreach ($namecloned as $num => $name){
|
||||
if ($params[0] == $name){
|
||||
$params[0] = $params[0]."[$i]";
|
||||
$params[0] = $params[0] . "[$i]" . $ending;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$params = array_merge(array($realelementname), $params);
|
||||
call_user_func_array(array(&$mform, 'hideIf'), $params);
|
||||
$function = ($option === 'disabledif') ? 'disabledIf' : 'hideIf';
|
||||
call_user_func_array(array(&$mform, $function), $params);
|
||||
break;
|
||||
case 'rule' :
|
||||
if (is_string($params)){
|
||||
@ -1203,7 +1260,7 @@ abstract class moodleform {
|
||||
}
|
||||
}
|
||||
}
|
||||
$mform->addElement('submit', $addfieldsname, $addstring);
|
||||
$mform->addElement('submit', $addfieldsname, $addstring, [], false);
|
||||
|
||||
if (!$addbuttoninside) {
|
||||
$mform->closeHeaderBefore($addfieldsname);
|
||||
@ -1432,6 +1489,40 @@ abstract class moodleform {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by tests to simulate submitted form data submission via AJAX.
|
||||
*
|
||||
* For form fields where no data is submitted the default for that field as set by set_data or setDefault will be passed to
|
||||
* get_data.
|
||||
*
|
||||
* This method sets $_POST or $_GET and $_FILES with the data supplied. Our unit test code empties all these
|
||||
* global arrays after each test.
|
||||
*
|
||||
* @param array $simulatedsubmitteddata An associative array of form values (same format as $_POST).
|
||||
* @param array $simulatedsubmittedfiles An associative array of files uploaded (same format as $_FILES). Can be omitted.
|
||||
* @param string $method 'post' or 'get', defaults to 'post'.
|
||||
* @param null $formidentifier the default is to use the class name for this class but you may need to provide
|
||||
* a different value here for some forms that are used more than once on the
|
||||
* same page.
|
||||
* @return array array to pass to form constructor as $ajaxdata
|
||||
*/
|
||||
public static function mock_ajax_submit($simulatedsubmitteddata, $simulatedsubmittedfiles = array(), $method = 'post',
|
||||
$formidentifier = null) {
|
||||
$_FILES = $simulatedsubmittedfiles;
|
||||
if ($formidentifier === null) {
|
||||
$formidentifier = get_called_class();
|
||||
$formidentifier = str_replace('\\', '_', $formidentifier); // See MDL-56233 for more information.
|
||||
}
|
||||
$simulatedsubmitteddata['_qf__'.$formidentifier] = 1;
|
||||
$simulatedsubmitteddata['sesskey'] = sesskey();
|
||||
if (strtolower($method) === 'get') {
|
||||
$_GET = ['sesskey' => sesskey()];
|
||||
} else {
|
||||
$_POST = ['sesskey' => sesskey()];
|
||||
}
|
||||
return $simulatedsubmitteddata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by tests to generate valid submit keys for moodle forms that are
|
||||
* submitted with ajax data.
|
||||
@ -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)));
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
@ -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
2
user/amd/build/private_files.min.js
vendored
Normal 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
|
1
user/amd/build/private_files.min.js.map
Normal file
1
user/amd/build/private_files.min.js.map
Normal 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"}
|
63
user/amd/src/private_files.js
Normal file
63
user/amd/src/private_files.js
Normal 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();
|
||||
});
|
||||
};
|
190
user/classes/form/private_files.php
Normal file
190
user/classes/form/private_files.php
Normal 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');
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user