diff --git a/admin/tool/recyclebin/classes/category_bin.php b/admin/tool/recyclebin/classes/category_bin.php
index b08b6a5f5bb..6477c4d25c2 100644
--- a/admin/tool/recyclebin/classes/category_bin.php
+++ b/admin/tool/recyclebin/classes/category_bin.php
@@ -108,6 +108,16 @@ class category_bin extends base_bin {
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+ // As far as recycle bin is using MODE_AUTOMATED, it observes the backup_auto_storage
+ // setting (storing backups @ real location. For recycle bin we want to ensure that
+ // backup files are always stored in Moodle file area. In order to achieve that, we
+ // hack the setting here via $CFG->forced_plugin_settings, so it won't interfere other
+ // operations. See MDL-65218 for more information.
+ // This hack will be removed once recycle bin switches to use its own backup mode, with
+ // own preferences and 100% appart from MODLE_AUTOMATED.
+ // TODO: Remove this as part of MDL-65228.
+ $CFG->forced_plugin_settings['backup'] = ['backup_auto_storage' => 0];
+
// Backup the course.
$user = get_admin();
$controller = new \backup_controller(
@@ -115,11 +125,15 @@ class category_bin extends base_bin {
$course->id,
\backup::FORMAT_MOODLE,
\backup::INTERACTIVE_NO,
- \backup::MODE_GENERAL,
+ \backup::MODE_AUTOMATED,
$user->id
);
$controller->execute_plan();
+ // We don't need the forced setting anymore, hence unsetting it.
+ // TODO: Remove this as part of MDL-65228.
+ unset($CFG->forced_plugin_settings['backup']);
+
// Grab the result.
$result = $controller->get_results();
if (!isset($result['backup_destination'])) {
@@ -233,7 +247,7 @@ class category_bin extends base_bin {
$tempdir,
$course->id,
\backup::INTERACTIVE_NO,
- \backup::MODE_GENERAL,
+ \backup::MODE_AUTOMATED,
$user->id,
\backup::TARGET_NEW_COURSE
);
diff --git a/admin/tool/recyclebin/classes/course_bin.php b/admin/tool/recyclebin/classes/course_bin.php
index d58d1a77f7c..deec65c1cb2 100644
--- a/admin/tool/recyclebin/classes/course_bin.php
+++ b/admin/tool/recyclebin/classes/course_bin.php
@@ -112,6 +112,16 @@ class course_bin extends base_bin {
return;
}
+ // As far as recycle bin is using MODE_AUTOMATED, it observes the backup_auto_storage
+ // setting (storing backups @ real location. For recycle bin we want to ensure that
+ // backup files are always stored in Moodle file area. In order to achieve that, we
+ // hack the setting here via $CFG->forced_plugin_settings, so it won't interfere other
+ // operations. See MDL-65218 for more information.
+ // This hack will be removed once recycle bin switches to use its own backup mode, with
+ // own preferences and 100% appart from MODLE_AUTOMATED.
+ // TODO: Remove this as part of MDL-65228.
+ $CFG->forced_plugin_settings['backup'] = ['backup_auto_storage' => 0];
+
// Backup the activity.
$user = get_admin();
$controller = new \backup_controller(
@@ -124,6 +134,10 @@ class course_bin extends base_bin {
);
$controller->execute_plan();
+ // We don't need the forced setting anymore, hence unsetting it.
+ // TODO: Remove this as part of MDL-65228.
+ unset($CFG->forced_plugin_settings['backup']);
+
// Grab the result.
$result = $controller->get_results();
if (!isset($result['backup_destination'])) {
@@ -344,4 +358,4 @@ class course_bin extends base_bin {
$context = \context_course::instance($this->_courseid);
return has_capability('tool/recyclebin:deleteitems', $context);
}
-}
\ No newline at end of file
+}
diff --git a/admin/tool/recyclebin/tests/category_bin_test.php b/admin/tool/recyclebin/tests/category_bin_test.php
index bada3d63853..0e24c311ca1 100644
--- a/admin/tool/recyclebin/tests/category_bin_test.php
+++ b/admin/tool/recyclebin/tests/category_bin_test.php
@@ -176,4 +176,116 @@ class tool_recyclebin_category_bin_tests extends advanced_testcase {
$course = reset($courses);
$this->assertEquals('Test course 2', $course->fullname);
}
+
+ /**
+ * Provider for test_course_restore_with_userdata() and test_course_restore_without_userdata()
+ *
+ * Used to verify that recycle bin is immune to various settings. Provides plugin, name, value for
+ * direct usage with set_config()
+ */
+ public function recycle_bin_settings_provider() {
+ return [
+ 'backup/backup_auto_storage moodle' => [[
+ (object)['plugin' => 'backup', 'name' => 'backup_auto_storage', 'value' => 0],
+ ]],
+
+ 'backup/backup_auto_storage external' => [[
+ (object)['plugin' => 'backup', 'name' => 'backup_auto_storage', 'value' => 1],
+ (object)['plugin' => 'backup', 'name' => 'backup_auto_destination', 'value' => true],
+ ]],
+
+ 'backup/backup_auto_storage mixed' => [[
+ (object)['plugin' => 'backup', 'name' => 'backup_auto_storage', 'value' => 2],
+ (object)['plugin' => 'backup', 'name' => 'backup_auto_destination', 'value' => true],
+ ]],
+ ];
+ }
+
+ /**
+ * Tests that user data is restored when course is restored.
+ *
+ * @dataProvider recycle_bin_settings_provider
+ * @param array $settings array of plugin, name, value stdClass().
+ */
+ public function test_course_restore_with_userdata($settings) {
+ global $DB;
+
+ // Force configuration changes from provider.
+ foreach ($settings as $setting) {
+ // Need to create a directory for backup_auto_destination.
+ if ($setting->plugin === 'backup' && $setting->name === 'backup_auto_destination' && $setting->value === true) {
+ $setting->value = make_request_directory();
+ }
+ set_config($setting->name, $setting->value, $setting->plugin);
+ }
+
+ // We want user data to be included for this test.
+ set_config('backup_auto_users', true, 'backup');
+
+ $student = $this->getDataGenerator()->create_and_enrol($this->course, 'student');
+
+ // Delete course.
+ delete_course($this->course, false);
+ $this->assertFalse($DB->record_exists('course', ['id' => $this->course->id]));
+
+ // Verify there is now a backup @ cat recycle bin file area.
+ $recyclebin = new \tool_recyclebin\category_bin($this->course->category);
+ $this->assertEquals(1, count($recyclebin->get_items()));
+
+ // Restore the recycle bin item.
+ $recyclebin->restore_item(current($recyclebin->get_items()));
+
+ // Get the new course.
+ $newcourse = $DB->get_record('course', ['shortname' => $this->course->shortname], '*', MUST_EXIST);
+
+ // Check that it was removed from the recycle bin.
+ $this->assertEquals(0, count($recyclebin->get_items()));
+
+ // Verify that student DOES continue enrolled.
+ $this->assertTrue(is_enrolled(context_course::instance($newcourse->id), $student->id));
+ }
+
+ /**
+ * Tests that user data is not restored when course is restored.
+ *
+ * @dataProvider recycle_bin_settings_provider
+ * @param array $settings array of plugin, name, value stdClass().
+ */
+ public function test_course_restore_without_userdata($settings) {
+ global $DB;
+
+ // Force configuration changes from provider.
+ foreach ($settings as $setting) {
+ // Need to create a directory for backup_auto_destination.
+ if ($setting->plugin === 'backup' && $setting->name === 'backup_auto_destination' && $setting->value === true) {
+ $setting->value = make_request_directory();
+ }
+ set_config($setting->name, $setting->value, $setting->plugin);
+ }
+
+ // We want user data to be included for this test.
+ set_config('backup_auto_users', false, 'backup');
+
+ $student = $this->getDataGenerator()->create_and_enrol($this->course, 'student');
+
+ // Delete course.
+ delete_course($this->course, false);
+ $this->assertFalse($DB->record_exists('course', ['id' => $this->course->id]));
+
+ // Verify there is now a backup @ cat recycle bin file area.
+ $recyclebin = new \tool_recyclebin\category_bin($this->course->category);
+ $this->assertEquals(1, count($recyclebin->get_items()));
+
+ // Restore the recycle bin item.
+ $recyclebin->restore_item(current($recyclebin->get_items()));
+
+ // Get the new course.
+ $newcourse = $DB->get_record('course', ['shortname' => $this->course->shortname], '*', MUST_EXIST);
+
+ // Check that it was removed from the recycle bin.
+ $this->assertEquals(0, count($recyclebin->get_items()));
+
+ // Verify that student DOES NOT continue enrolled.
+ $this->assertFalse(is_enrolled(context_course::instance($newcourse->id), $student->id));
+ }
}
diff --git a/admin/tool/recyclebin/tests/course_bin_test.php b/admin/tool/recyclebin/tests/course_bin_test.php
index 9d993260c36..44f9db6cc06 100644
--- a/admin/tool/recyclebin/tests/course_bin_test.php
+++ b/admin/tool/recyclebin/tests/course_bin_test.php
@@ -175,9 +175,45 @@ class tool_recyclebin_course_bin_tests extends advanced_testcase {
}
/**
- * Tests that user data is restored when module is restored.
+ * Provider for test_coursemodule_restore_with_userdata() and test_coursemodule_restore_without_userdata()
+ *
+ * Used to verify that recycle bin is immune to various settings. Provides plugin, name, value for
+ * direct usage with set_config()
*/
- public function test_coursemodule_restore_with_userdata() {
+ public function recycle_bin_settings_provider() {
+ return [
+ 'backup/backup_auto_storage moodle' => [[
+ (object)['plugin' => 'backup', 'name' => 'backup_auto_storage', 'value' => 0],
+ ]],
+
+ 'backup/backup_auto_storage external' => [[
+ (object)['plugin' => 'backup', 'name' => 'backup_auto_storage', 'value' => 1],
+ (object)['plugin' => 'backup', 'name' => 'backup_auto_destination', 'value' => true],
+ ]],
+
+ 'backup/backup_auto_storage mixed' => [[
+ (object)['plugin' => 'backup', 'name' => 'backup_auto_storage', 'value' => 2],
+ (object)['plugin' => 'backup', 'name' => 'backup_auto_destination', 'value' => true],
+ ]],
+ ];
+ }
+
+ /**
+ * Tests that user data is restored when module is restored.
+ *
+ * @dataProvider recycle_bin_settings_provider
+ * @param array $settings array of plugin, name, value stdClass().
+ */
+ public function test_coursemodule_restore_with_userdata($settings) {
+ // Force configuration changes from provider.
+ foreach ($settings as $setting) {
+ // Need to create a directory for backup_auto_destination.
+ if ($setting->plugin === 'backup' && $setting->name === 'backup_auto_destination' && $setting->value === true) {
+ $setting->value = make_request_directory();
+ }
+ set_config($setting->name, $setting->value, $setting->plugin);
+ }
+
$student = $this->getDataGenerator()->create_and_enrol($this->course, 'student');
$this->setUser($student);
@@ -211,8 +247,20 @@ class tool_recyclebin_course_bin_tests extends advanced_testcase {
/**
* Tests that user data is not restored when module is restored.
+ *
+ * @dataProvider recycle_bin_settings_provider
+ * @param array $settings array of plugin, name, value stdClass().
*/
- public function test_coursemodule_restore_without_userdata() {
+ public function test_coursemodule_restore_without_userdata($settings) {
+ // Force configuration changes from provider.
+ foreach ($settings as $setting) {
+ // Need to create a directory for backup_auto_destination.
+ if ($setting->plugin === 'backup' && $setting->name === 'backup_auto_destination' && $setting->value === true) {
+ $setting->value = make_request_directory();
+ }
+ set_config($setting->name, $setting->value, $setting->plugin);
+ }
+
$student = $this->getDataGenerator()->create_and_enrol($this->course, 'student');
$this->setUser($student);
diff --git a/course/amd/build/actions.min.js b/course/amd/build/actions.min.js
index 2875e390ae2..2227311b6c0 100644
--- a/course/amd/build/actions.min.js
+++ b/course/amd/build/actions.min.js
@@ -1 +1 @@
-define(["jquery","core/ajax","core/templates","core/notification","core/str","core/url","core/yui","core/modal_factory","core/modal_events","core/key_codes"],function(a,b,c,d,e,f,g,h,i,j){var k={EDITINPROGRESS:"editinprogress",SECTIONDRAGGABLE:"sectiondraggable",EDITINGMOVE:"editing_move"},l={ACTIVITYLI:"li.activity",ACTIONAREA:".actions",ACTIVITYACTION:"a.cm-edit-action",MENU:".moodle-actionmenu[data-enhance=moodle-core-actionmenu]",TOGGLE:".toggle-display,.dropdown-toggle",SECTIONLI:"li.section",SECTIONACTIONMENU:".section_action_menu",ADDSECTIONS:"#changenumsections [data-add-sections]"};g.use("moodle-course-coursebase",function(){var a=M.course.format.get_section_selector();a&&(l.SECTIONLI=a)});var m=function(a){var b;return g.use("moodle-course-util",function(c){b=c.Moodle.core_course.util.cm.getId(c.Node(a.get(0)))}),b},n=function(a){var b;return g.use("moodle-course-util",function(c){b=c.Moodle.core_course.util.cm.getName(c.Node(a.get(0)))}),b},o=function(a){a.addClass(k.EDITINPROGRESS);var b=a.find(l.ACTIONAREA).get(0);if(b){var c=M.util.add_spinner(g,g.Node(b));return c.show(),c}return null},p=function(a){a.addClass(k.EDITINPROGRESS);var b=a.find(l.SECTIONACTIONMENU).get(0);if(b){var c=M.util.add_spinner(g,g.Node(b));return c.show(),c}return null},q=function(a){var b=M.util.add_lightbox(g,g.Node(a.get(0)));return b.show(),b},r=function(a,b,c){window.setTimeout(function(){a.removeClass(k.EDITINPROGRESS),b&&b.hide()},c)},s=function(a,b){a&&window.setTimeout(function(){a.hide()},b)},t=function(a,b){if(g.use("moodle-course-coursebase",function(){M.course.coursebase.invoke_function("setup_for_resource","#"+a)}),M.core.actionmenu&&M.core.actionmenu.newDOMNode&&M.core.actionmenu.newDOMNode(g.one("#"+a)),b){var c=g.one("#"+a+" "+l.MENU).one(l.TOGGLE);c&&c.simulate&&c.simulate("click")}},u=function(b,c){var d=a("#"+b),e="[data-action="+c+"]";"groupsseparate"!==c&&"groupsvisible"!==c&&"groupsnone"!==c||(e="[data-action=groupsseparate],[data-action=groupsvisible],[data-action=groupsnone]"),d.find(e).is(":visible")?d.find(e).focus():d.find(l.MENU).find(l.TOGGLE).focus()},v=function(b){var c=a("a:visible"),d=!1,e=null;return c.each(function(){if(a.contains(b[0],this))d=!0;else if(d)return e=this,!1}),e},w=function(c,e,f){var g,h=f.attr("data-keepopen"),i=f.attr("data-action"),j=o(c),k=b.call([{methodname:"core_course_edit_module",args:{id:e,action:i,sectionreturn:f.attr("data-sectionreturn")?f.attr("data-sectionreturn"):0}}],!0);"duplicate"===i&&(g=q(f.closest(l.SECTIONLI))),a.when.apply(a,k).done(function(b){var d=v(c);c.replaceWith(b),a("
"+b+"
").find(l.ACTIVITYLI).each(function(b){t(a(this).attr("id"),h),0===b&&(u(a(this).attr("id"),i),d=null)}),d&&d.focus(),r(c,j,400),s(g,400),c.trigger(a.Event("coursemoduleedited",{ajaxreturn:b,action:i}))}).fail(function(b){r(c,j),s(g);var e=a.Event("coursemoduleeditfailed",{exception:b,action:i});c.trigger(e),e.isDefaultPrevented()||d.exception(b)})},x=function(c,d,e){var f=o(c),g=b.call([{methodname:"core_course_get_module",args:{id:d,sectionreturn:e}}],!0);a.when.apply(a,g).done(function(a){r(c,f,400),C(a)}).fail(function(){r(c,f)})},y=function(a,b){var c=a.attr("class").match(/modtype_([^\s]*)/)[1],f=n(a);e.get_string("pluginname",c).done(function(a){var c={type:a,name:f};e.get_strings([{key:"confirm"},{key:null===f?"deletechecktype":"deletechecktypename",param:c},{key:"yes"},{key:"no"}]).done(function(a){d.confirm(a[0],a[1],a[2],a[3],b)})})},z=function(a,b){e.get_strings([{key:"confirm"},{key:"yes"},{key:"no"}]).done(function(c){d.confirm(c[0],a,c[1],c[2],b)})},A=function(a,b,f,g,h,i,j){var k=[{key:f,component:g}];return h&&k.push({key:h,component:i}),e.get_strings(k).then(function(d){a.find("span.menu-action-text").html(d[0]),a.attr("title",d[0]);var e="";return h&&(e=d[1],a.attr("title",e)),c.renderPix(b,"core",e)}).then(function(b){a.find(".icon").replaceWith(b),a.attr("data-action",j)})["catch"](d.exception)},B=function(b,c,d,e){var f=c.attr("data-action");if("hide"===f||"show"===f){if("hide"===f?(b.addClass("hidden"),A(c,"i/show","showfromothers","format_"+e,null,null,"show")):(b.removeClass("hidden"),A(c,"i/hide","hidefromothers","format_"+e,null,null,"hide")),void 0!==d.modules)for(var g in d.modules)C(d.modules[g]);void 0!==d.section_availability&&b.find(".section_availability").first().replaceWith(d.section_availability)}else if("setmarker"===f){var h=a(l.SECTIONLI+".current"),i=h.find(l.SECTIONACTIONMENU+" a[data-action=removemarker]");h.removeClass("current"),A(i,"i/marker","highlight","core","markthistopic","core","setmarker"),b.addClass("current"),A(c,"i/marked","highlightoff","core","markedthistopic","core","removemarker")}else"removemarker"===f&&(b.removeClass("current"),A(c,"i/marker","highlight","core","markthistopic","core","setmarker"))},C=function(b){a("
');f.find("label").html(b),h.create({title:d,type:h.types.SAVE_CANCEL,body:f.html()},c).done(function(b){var e=a(b.getBody()).find("#add_section_numsections"),f=function(){""+parseInt(e.val())===e.val()&&parseInt(e.val())>=1&&(document.location=c.attr("href")+"&numsections="+parseInt(e.val()))};b.setSaveButtonText(d),b.getRoot().on(i.shown,function(){e.focus().select().on("keydown",function(a){a.keyCode===j.enter&&f()})}),b.getRoot().on(i.save,function(a){a.preventDefault(),f()})})})},replaceSectionActionItem:function(a,b,c,d,e,f,g,h){var i=a.find(l.SECTIONACTIONMENU+" "+b);A(i,c,d,e,f,g,h)}}});
\ No newline at end of file
diff --git a/course/amd/src/actions.js b/course/amd/src/actions.js
index 76d42320d6d..b9910bba41c 100644
--- a/course/amd/src/actions.js
+++ b/course/amd/src/actions.js
@@ -352,30 +352,21 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
* @param {String} image new image name ("i/show", "i/hide", etc.)
* @param {String} stringname new string for the action menu item
* @param {String} stringcomponent
- * @param {String} titlestr string for "title" attribute (if different from stringname)
- * @param {String} titlecomponent
+ * @param {String} titlestr not used
+ * @param {String} titlecomponent not used
* @param {String} newaction new value for data-action attribute of the link
* @return {Promise} promise which is resolved when the replacement has completed
*/
var replaceActionItem = function(actionitem, image, stringname,
stringcomponent, titlestr, titlecomponent, newaction) {
-
var stringRequests = [{key: stringname, component: stringcomponent}];
- if (titlestr) {
- stringRequests.push({key: titlestr, component: titlecomponent});
- }
+ // Do not provide an icon with duplicate, different text to the menu item.
return str.get_strings(stringRequests).then(function(strings) {
actionitem.find('span.menu-action-text').html(strings[0]);
- actionitem.attr('title', strings[0]);
- var title = '';
- if (titlestr) {
- title = strings[1];
- actionitem.attr('title', title);
- }
- return templates.renderPix(image, 'core', title);
+ return templates.renderPix(image, 'core');
}).then(function(pixhtml) {
actionitem.find('.icon').replaceWith(pixhtml);
actionitem.attr('data-action', newaction);
diff --git a/course/format/renderer.php b/course/format/renderer.php
index 45ff162e6b4..2f94041f734 100644
--- a/course/format/renderer.php
+++ b/course/format/renderer.php
@@ -117,10 +117,9 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
$name = empty($value['name']) ? '' : $value['name'];
$attr = empty($value['attr']) ? array() : $value['attr'];
$class = empty($value['pixattr']['class']) ? '' : $value['pixattr']['class'];
- $alt = empty($value['pixattr']['alt']) ? '' : $value['pixattr']['alt'];
$al = new action_menu_link_secondary(
new moodle_url($url),
- new pix_icon($icon, $alt, null, array('class' => "smallicon " . $class)),
+ new pix_icon($icon, '', null, array('class' => "smallicon " . $class)),
$name,
$attr
);
@@ -328,8 +327,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
'url' => new moodle_url('/course/editsection.php', array('id' => $section->id, 'sr' => $sectionreturn)),
'icon' => 'i/settings',
'name' => $streditsection,
- 'pixattr' => array('class' => '', 'alt' => $streditsection),
- 'attr' => array('class' => 'icon edit', 'title' => $streditsection));
+ 'pixattr' => array('class' => ''),
+ 'attr' => array('class' => 'icon edit'));
}
if ($section->section) {
@@ -343,8 +342,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
'url' => $url,
'icon' => 'i/hide',
'name' => $strhidefromothers,
- 'pixattr' => array('class' => '', 'alt' => $strhidefromothers),
- 'attr' => array('class' => 'icon editing_showhide', 'title' => $strhidefromothers,
+ 'pixattr' => array('class' => ''),
+ 'attr' => array('class' => 'icon editing_showhide',
'data-sectionreturn' => $sectionreturn, 'data-action' => 'hide'));
} else {
$strshowfromothers = get_string('showfromothers', 'format_'.$course->format);
@@ -353,8 +352,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
'url' => $url,
'icon' => 'i/show',
'name' => $strshowfromothers,
- 'pixattr' => array('class' => '', 'alt' => $strshowfromothers),
- 'attr' => array('class' => 'icon editing_showhide', 'title' => $strshowfromothers,
+ 'pixattr' => array('class' => ''),
+ 'attr' => array('class' => 'icon editing_showhide',
'data-sectionreturn' => $sectionreturn, 'data-action' => 'show'));
}
}
@@ -370,8 +369,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
'url' => $url,
'icon' => 'i/up',
'name' => $strmoveup,
- 'pixattr' => array('class' => '', 'alt' => $strmoveup),
- 'attr' => array('class' => 'icon moveup', 'title' => $strmoveup));
+ 'pixattr' => array('class' => ''),
+ 'attr' => array('class' => 'icon moveup'));
}
$url = clone($baseurl);
@@ -383,8 +382,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
'url' => $url,
'icon' => 'i/down',
'name' => $strmovedown,
- 'pixattr' => array('class' => '', 'alt' => $strmovedown),
- 'attr' => array('class' => 'icon movedown', 'title' => $strmovedown));
+ 'pixattr' => array('class' => ''),
+ 'attr' => array('class' => 'icon movedown'));
}
}
}
@@ -405,8 +404,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
'url' => $url,
'icon' => 'i/delete',
'name' => $strdelete,
- 'pixattr' => array('class' => '', 'alt' => $strdelete),
- 'attr' => array('class' => 'icon editing_delete', 'title' => $strdelete));
+ 'pixattr' => array('class' => ''),
+ 'attr' => array('class' => 'icon editing_delete'));
}
}
@@ -1009,7 +1008,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
if ($sectionreturn !== null) {
$url->param('sectionreturn', $sectionreturn);
}
- $icon = $this->output->pix_icon('t/add', $straddsections);
+ $icon = $this->output->pix_icon('t/add', '');
$newsections = $maxsections - $lastsection;
echo html_writer::link($url, $icon . $straddsections,
array('class' => 'add-sections', 'data-add-sections' => $straddsections, 'new-sections' => $newsections));
diff --git a/course/format/topics/renderer.php b/course/format/topics/renderer.php
index 248c73a5cdb..1efad308bd5 100644
--- a/course/format/topics/renderer.php
+++ b/course/format/topics/renderer.php
@@ -123,21 +123,19 @@ class format_topics_renderer extends format_section_renderer_base {
if ($section->section && has_capability('moodle/course:setcurrentsection', $coursecontext)) {
if ($course->marker == $section->section) { // Show the "light globe" on/off.
$url->param('marker', 0);
- $markedthistopic = get_string('markedthistopic');
$highlightoff = get_string('highlightoff');
$controls['highlight'] = array('url' => $url, "icon" => 'i/marked',
'name' => $highlightoff,
- 'pixattr' => array('class' => '', 'alt' => $markedthistopic),
- 'attr' => array('class' => 'editing_highlight', 'title' => $markedthistopic,
+ 'pixattr' => array('class' => ''),
+ 'attr' => array('class' => 'editing_highlight',
'data-action' => 'removemarker'));
} else {
$url->param('marker', $section->section);
- $markthistopic = get_string('markthistopic');
$highlight = get_string('highlight');
$controls['highlight'] = array('url' => $url, "icon" => 'i/marker',
'name' => $highlight,
- 'pixattr' => array('class' => '', 'alt' => $markthistopic),
- 'attr' => array('class' => 'editing_highlight', 'title' => $markthistopic,
+ 'pixattr' => array('class' => ''),
+ 'attr' => array('class' => 'editing_highlight',
'data-action' => 'setmarker'));
}
}
diff --git a/course/lib.php b/course/lib.php
index d0cb0b39a93..2294e71c862 100644
--- a/course/lib.php
+++ b/course/lib.php
@@ -1891,7 +1891,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
if ($hasmanageactivities) {
$actions['update'] = new action_menu_link_secondary(
new moodle_url($baseurl, array('update' => $mod->id)),
- new pix_icon('t/edit', $str->editsettings, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+ new pix_icon('t/edit', '', 'moodle', array('class' => 'iconsmall')),
$str->editsettings,
array('class' => 'editing_update', 'data-action' => 'update')
);
@@ -1917,7 +1917,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
}
$actions['moveright'] = new action_menu_link_secondary(
new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '1')),
- new pix_icon($rightarrow, $str->moveright, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+ new pix_icon($rightarrow, '', 'moodle', array('class' => 'iconsmall')),
$str->moveright,
array('class' => 'editing_moveright ' . $enabledclass, 'data-action' => 'moveright',
'data-keepopen' => true, 'data-sectionreturn' => $sr)
@@ -1930,7 +1930,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
}
$actions['moveleft'] = new action_menu_link_secondary(
new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '-1')),
- new pix_icon($leftarrow, $str->moveleft, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+ new pix_icon($leftarrow, '', 'moodle', array('class' => 'iconsmall')),
$str->moveleft,
array('class' => 'editing_moveleft ' . $enabledclass, 'data-action' => 'moveleft',
'data-keepopen' => true, 'data-sectionreturn' => $sr)
@@ -1953,7 +1953,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
if ($displayedoncoursepage) {
$actions['hide'] = new action_menu_link_secondary(
new moodle_url($baseurl, array('hide' => $mod->id)),
- new pix_icon('t/hide', $str->modhide, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+ new pix_icon('t/hide', '', 'moodle', array('class' => 'iconsmall')),
$str->modhide,
array('class' => 'editing_hide', 'data-action' => 'hide')
);
@@ -1961,7 +1961,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
// Offer to "show" only if the section is visible.
$actions['show'] = new action_menu_link_secondary(
new moodle_url($baseurl, array('show' => $mod->id)),
- new pix_icon('t/show', $str->modshow, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+ new pix_icon('t/show', '', 'moodle', array('class' => 'iconsmall')),
$str->modshow,
array('class' => 'editing_show', 'data-action' => 'show')
);
@@ -1971,7 +1971,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
// When making the "stealth" module unavailable we perform the same action as hiding the visible module.
$actions['hide'] = new action_menu_link_secondary(
new moodle_url($baseurl, array('hide' => $mod->id)),
- new pix_icon('t/unblock', $str->makeunavailable, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+ new pix_icon('t/unblock', '', 'moodle', array('class' => 'iconsmall')),
$str->makeunavailable,
array('class' => 'editing_makeunavailable', 'data-action' => 'hide', 'data-sectionreturn' => $sr)
);
@@ -1982,7 +1982,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
$action = $sectionvisible ? 'stealth' : 'show';
$actions[$action] = new action_menu_link_secondary(
new moodle_url($baseurl, array($action => $mod->id)),
- new pix_icon('t/block', $str->makeavailable, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+ new pix_icon('t/block', '', 'moodle', array('class' => 'iconsmall')),
$str->makeavailable,
array('class' => 'editing_makeavailable', 'data-action' => $action, 'data-sectionreturn' => $sr)
);
@@ -1995,7 +1995,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
course_allowed_module($mod->get_course(), $mod->modname)) {
$actions['duplicate'] = new action_menu_link_secondary(
new moodle_url($baseurl, array('duplicate' => $mod->id)),
- new pix_icon('t/copy', $str->duplicate, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+ new pix_icon('t/copy', '', 'moodle', array('class' => 'iconsmall')),
$str->duplicate,
array('class' => 'editing_duplicate', 'data-action' => 'duplicate', 'data-sectionreturn' => $sr)
);
@@ -2026,7 +2026,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
$actions[$actionname] = new action_menu_link_primary(
new moodle_url($baseurl, array('id' => $mod->id, 'groupmode' => $nextgroupmode)),
- new pix_icon($groupimage, $grouptitle, 'moodle', array('class' => 'iconsmall')),
+ new pix_icon($groupimage, '', 'moodle', array('class' => 'iconsmall')),
$grouptitle,
array('class' => 'editing_'. $actionname, 'data-action' => $nextactionname,
'aria-live' => 'assertive', 'data-sectionreturn' => $sr)
@@ -2040,7 +2040,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
if (has_capability('moodle/role:assign', $modcontext)){
$actions['assign'] = new action_menu_link_secondary(
new moodle_url('/admin/roles/assign.php', array('contextid' => $modcontext->id)),
- new pix_icon('t/assignroles', $str->assign, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+ new pix_icon('t/assignroles', '', 'moodle', array('class' => 'iconsmall')),
$str->assign,
array('class' => 'editing_assign', 'data-action' => 'assignroles', 'data-sectionreturn' => $sr)
);
@@ -2050,7 +2050,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
if ($hasmanageactivities) {
$actions['delete'] = new action_menu_link_secondary(
new moodle_url($baseurl, array('delete' => $mod->id)),
- new pix_icon('t/delete', $str->delete, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+ new pix_icon('t/delete', '', 'moodle', array('class' => 'iconsmall')),
$str->delete,
array('class' => 'editing_delete', 'data-action' => 'delete', 'data-sectionreturn' => $sr)
);
diff --git a/course/tests/behat/behat_course.php b/course/tests/behat/behat_course.php
index 732ae95bbb8..7239e1a5fd7 100644
--- a/course/tests/behat/behat_course.php
+++ b/course/tests/behat/behat_course.php
@@ -304,7 +304,7 @@ class behat_course extends behat_base {
// Click on highlight topic link.
$this->execute('behat_general::i_click_on_in_the',
- array(get_string('markthistopic'), "link", $this->escape($xpath), "xpath_element")
+ array(get_string('highlight'), "link", $this->escape($xpath), "xpath_element")
);
}
@@ -326,7 +326,7 @@ class behat_course extends behat_base {
// Click on un-highlight topic link.
$this->execute('behat_general::i_click_on_in_the',
- array(get_string('markedthistopic'), "link", $this->escape($xpath), "xpath_element")
+ array(get_string('highlightoff'), "link", $this->escape($xpath), "xpath_element")
);
}
@@ -444,7 +444,7 @@ class behat_course extends behat_base {
$xpath = $this->section_exists($sectionnumber);
// The important checking, we can not check the img.
- $this->execute('behat_general::should_exist_in_the', ['This topic is highlighted as the current topic', 'icon', $xpath, 'xpath_element']);
+ $this->execute('behat_general::should_exist_in_the', ['Remove highlight', 'link', $xpath, 'xpath_element']);
}
/**
@@ -775,11 +775,10 @@ class behat_course extends behat_base {
// Ensure the destination is valid.
$sectionxpath = $this->section_exists($sectionnumber);
- $activitynode = $this->get_activity_element('Move', 'icon', $activityname);
-
// JS enabled.
if ($this->running_javascript()) {
+ $activitynode = $this->get_activity_element('Move', 'icon', $activityname);
$destinationxpath = $sectionxpath . "/descendant::ul[contains(concat(' ', normalize-space(@class), ' '), ' yui3-dd-drop ')]";
$this->execute("behat_general::i_drag_and_i_drop_it_in",
@@ -1165,9 +1164,9 @@ class behat_course extends behat_base {
// Checking the show button alt text and show icon.
$showtext = get_string('showfromothers', $courseformat);
- $linkxpath = $xpath . "/descendant::a[@title=" . behat_context_helper::escape($showtext) . "]";
+ $linkxpath = $xpath . "//a[*[contains(text(), " . behat_context_helper::escape($showtext) . ")]]";
- $exception = new ElementNotFoundException($this->getSession(), 'Show section link ');
+ $exception = new ElementNotFoundException($this->getSession(), 'Show section link');
// Returing the link so both Non-JS and JS browsers can interact with it.
return $this->find('xpath', $linkxpath, $exception);
diff --git a/lib/amd/build/icon_system_fontawesome.min.js b/lib/amd/build/icon_system_fontawesome.min.js
index 2a6fd4019da..9b9bcf3f158 100644
--- a/lib/amd/build/icon_system_fontawesome.min.js
+++ b/lib/amd/build/icon_system_fontawesome.min.js
@@ -1 +1 @@
-define(["core/icon_system","jquery","core/ajax","core/mustache","core/localstorage","core/url"],function(a,b,c,d,e,f){var g=null,h=null,i=function(){a.apply(this,arguments)};return i.prototype=Object.create(a.prototype),i.prototype.init=function(){if(g)return b.when(this);var a=e.get("core/iconmap-fontawesome");return a&&(a=JSON.parse(a)),a?(g=a,b.when(this)):(null===h&&(h=c.call([{methodname:"core_output_load_fontawesome_icon_map",args:[]}],!0,!1)[0]),h.then(function(a){return g={},b.each(a,function(a,b){g[b.component+"/"+b.pix]=b.to}),e.set("core/iconmap-fontawesome",JSON.stringify(g)),this}.bind(this)))},i.prototype.renderIcon=function(a,b,c,e){var h=g[b+"/"+a],i=!1;if("undefined"==typeof h){var j=f.imageUrl(a,b);i={attributes:[{name:"src",value:j},{name:"alt",value:c},{name:"title",value:c}]}}var k={key:h,title:c,alt:c,unmappedIcon:i},l=d.render(e,k);return l.trim()},i.prototype.getTemplateName=function(){return"core/pix_icon_fontawesome"},i});
\ No newline at end of file
+define(["core/icon_system","jquery","core/ajax","core/mustache","core/localstorage","core/url"],function(a,b,c,d,e,f){var g=null,h=null,i=function(){a.apply(this,arguments)};return i.prototype=Object.create(a.prototype),i.prototype.init=function(){if(g)return b.when(this);var a=e.get("core/iconmap-fontawesome");return a&&(a=JSON.parse(a)),a?(g=a,b.when(this)):(null===h&&(h=c.call([{methodname:"core_output_load_fontawesome_icon_map",args:[]}],!0,!1)[0]),h.then(function(a){return g={},b.each(a,function(a,b){g[b.component+"/"+b.pix]=b.to}),e.set("core/iconmap-fontawesome",JSON.stringify(g)),this}.bind(this)))},i.prototype.renderIcon=function(a,b,c,e){var h=g[b+"/"+a],i=!1;if("undefined"==typeof h){var j=f.imageUrl(a,b);i={attributes:[{name:"src",value:j},{name:"alt",value:c},{name:"title",value:c}]}}var k={key:h,title:c,alt:c,unmappedIcon:i};"undefined"!=typeof c&&""!==c||(k["aria-hidden"]=!0);var l=d.render(e,k);return l.trim()},i.prototype.getTemplateName=function(){return"core/pix_icon_fontawesome"},i});
\ No newline at end of file
diff --git a/lib/amd/build/icon_system_standard.min.js b/lib/amd/build/icon_system_standard.min.js
index d165a79845c..4591141c2a9 100644
--- a/lib/amd/build/icon_system_standard.min.js
+++ b/lib/amd/build/icon_system_standard.min.js
@@ -1 +1 @@
-define(["core/icon_system","core/url","core/mustache"],function(a,b,c){var d=function(){a.apply(this,arguments)};return d.prototype=Object.create(a.prototype),d.prototype.renderIcon=function(a,d,e,f){var g=b.imageUrl(a,d),h={attributes:[{name:"src",value:g},{name:"alt",value:e},{name:"title",value:e}]},i=c.render(f,h);return i.trim()},d.prototype.getTemplateName=function(){return"core/pix_icon"},d});
\ No newline at end of file
+define(["core/icon_system","core/url","core/mustache"],function(a,b,c){var d=function(){a.apply(this,arguments)};return d.prototype=Object.create(a.prototype),d.prototype.renderIcon=function(a,d,e,f){var g=b.imageUrl(a,d),h={attributes:[{name:"src",value:g},{name:"alt",value:e},{name:"title",value:e}]};"undefined"!=typeof e&&""!=e||h.attributes.push({name:"aria-hidden",value:"true"});var i=c.render(f,h);return i.trim()},d.prototype.getTemplateName=function(){return"core/pix_icon"},d});
\ No newline at end of file
diff --git a/lib/amd/src/icon_system_fontawesome.js b/lib/amd/src/icon_system_fontawesome.js
index 970b0b95911..5ef205ac2fc 100644
--- a/lib/amd/src/icon_system_fontawesome.js
+++ b/lib/amd/src/icon_system_fontawesome.js
@@ -105,6 +105,10 @@ define(['core/icon_system', 'jquery', 'core/ajax', 'core/mustache', 'core/locals
unmappedIcon: unmappedIcon
};
+ if (typeof title === "undefined" || title === '') {
+ context['aria-hidden'] = true;
+ }
+
var result = Mustache.render(template, context);
return result.trim();
};
diff --git a/lib/amd/src/icon_system_standard.js b/lib/amd/src/icon_system_standard.js
index 516245474d9..a1bce77a6ab 100644
--- a/lib/amd/src/icon_system_standard.js
+++ b/lib/amd/src/icon_system_standard.js
@@ -52,6 +52,10 @@ define(['core/icon_system', 'core/url', 'core/mustache'],
{name: 'title', value: title}
]
};
+ if (typeof title === "undefined" || title == "") {
+ templatecontext.attributes.push({name: 'aria-hidden', value: 'true'});
+ }
+
var result = Mustache.render(template, templatecontext);
return result.trim();
};
diff --git a/lib/behat/classes/partial_named_selector.php b/lib/behat/classes/partial_named_selector.php
index 91757840ec5..26fa673fe89 100644
--- a/lib/behat/classes/partial_named_selector.php
+++ b/lib/behat/classes/partial_named_selector.php
@@ -89,6 +89,7 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
'file' => 'file',
'filemanager' => 'filemanager',
'group_message' => 'group_message',
+ 'group_message_conversation' => 'group_message_conversation',
'group_message_header' => 'group_message_header',
'group_message_member' => 'group_message_member',
'group_message_tab' => 'group_message_tab',
@@ -154,7 +155,10 @@ XPATH
, 'group_message' => << << << << <<execute("behat_general::i_click_on", [get_string('togglemessagemenu', 'core_message'), 'link']);
}
+ /**
+ * Open the messaging conversation list.
+ *
+ * @Given /^I open the "(?P(?:[^"]|\\")*)" conversations list/
+ * @param string $tab
+ */
+ public function i_open_the_conversations_list(string $tab) {
+ $this->execute('behat_general::i_click_on', [
+ $this->escape($tab),
+ 'group_message_tab'
+ ]);
+ }
+
/**
* Open the messaging UI.
*
@@ -153,7 +166,7 @@ class behat_message extends behat_base {
array("//textarea[@data-region='send-message-txt']", $this->escape($messagecontent))
);
- $this->execute("behat_forms::press_button", get_string('send', 'message'));
+ $this->execute("behat_forms::press_button", get_string('sendmessage', 'message'));
}
/**
@@ -204,4 +217,19 @@ class behat_message extends behat_base {
)
);
}
+
+ /**
+ * Select a user in a specific messaging UI conversations list.
+ *
+ * @Given /^I select "(?P(?:[^"]|\\")*)" conversation in the "(?P(?:[^"]|\\")*)" conversations list$/
+ * @param string $convname
+ * @param string $listname
+ */
+ public function i_select_conversation_in_the_conversations_list(string $convname, string $listname) {
+ $xpath = '//*[@data-region="message-drawer"]//div[@data-region="view-overview-'.
+ $this->escape($listname).
+ '"]//*[@data-conversation-id]//img[contains(@alt,"'.
+ $this->escape($convname).'")]';
+ $this->execute('behat_general::i_click_on', array($xpath, 'xpath_element'));
+ }
}
diff --git a/message/tests/behat/message_drawer_manage_contacts.feature b/message/tests/behat/message_drawer_manage_contacts.feature
index 07190be7228..b129e24dbf3 100644
--- a/message/tests/behat/message_drawer_manage_contacts.feature
+++ b/message/tests/behat/message_drawer_manage_contacts.feature
@@ -31,6 +31,7 @@ Feature: Manage contacts
And I open contact menu
And I click on "Add to contacts" "link"
And I click on "Add" "button"
+ And I should see "Contact request sent"
And I log out
And I log in as "student4"
Then I should see "2" in the "//*[@data-region='count-container']" "xpath_element"
@@ -41,6 +42,7 @@ Feature: Manage contacts
And I click on "Student 1 Would like to contact you" "link"
Then I should see "Accept and add to contacts"
And I click on "Accept and add to contacts" "link_or_button"
+ And I should not see "Accept and add to contacts"
And I log out
And I log in as "student1"
And I open messaging
@@ -54,6 +56,7 @@ Feature: Manage contacts
And I open contact menu
And I click on "Add to contacts" "link"
And I click on "Add" "button"
+ And I should see "Contact request sent"
And I log out
And I log in as "student3"
Then I should see "1" in the "//*[@data-region='count-container']" "xpath_element"
@@ -64,6 +67,7 @@ Feature: Manage contacts
And I click on "Student 1 Would like to contact you" "link"
Then I should see "Accept and add to contacts"
And I click on "Decline" "link_or_button"
+ And I should not see "Accept and add to contacts"
And I open contact menu
Then I should see "Add to contacts" in the "//div[@data-region='header-container']" "xpath_element"
diff --git a/message/tests/behat/message_send_messages.feature b/message/tests/behat/message_send_messages.feature
new file mode 100644
index 00000000000..4a566b38981
--- /dev/null
+++ b/message/tests/behat/message_send_messages.feature
@@ -0,0 +1,84 @@
+@core @core_message @javascript
+Feature: Message send messages
+ In order to communicate with fellow users
+ As a user
+ I need to be able to send a message
+
+ Background:
+ Given the following "courses" exist:
+ | fullname | shortname | category | groupmode |
+ | Course 1 | C1 | 0 | 1 |
+ And the following "users" exist:
+ | username | firstname | lastname | email |
+ | student1 | Student | 1 | student1@example.com |
+ | student2 | Student | 2 | student2@example.com |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | student1 | C1 | student |
+ | student2 | C1 | student |
+ And the following "groups" exist:
+ | name | course | idnumber | enablemessaging |
+ | Group 1 | C1 | G1 | 1 |
+ And the following "group members" exist:
+ | user | group |
+ | student1 | G1 |
+ | student2 | G1 |
+ And the following config values are set as admin:
+ | messaging | 1 |
+
+ Scenario: Send a message to a group conversation
+ Given I log in as "student1"
+ And I open messaging
+ And "Group 1" "group_message" should exist
+ And I select "Group 1" conversation in messaging
+ When I send "Hi!" message in the message area
+ Then I should see "Hi!" in the "Group 1" "group_message_conversation"
+ And I should see "##today##j F##" in the "Group 1" "group_message_conversation"
+ And I log out
+ And I log in as "student2"
+ And I open messaging
+ And "Group 1" "group_message" should exist
+ And I select "Group 1" conversation in messaging
+ And I should see "Hi!" in the "Group 1" "group_message_conversation"
+
+ Scenario: Send a message to a starred conversation
+ Given I log in as "student1"
+ When I open messaging
+ Then "Group 1" "group_message" should exist
+ And I select "Group 1" conversation in the "group-messages" conversations list
+ And I open contact menu
+ And I click on "Star" "link" in the "//div[@data-region='header-container']" "xpath_element"
+ And I go back in "view-conversation" message drawer
+ And I open the "Starred" conversations list
+ And I should see "Group 1"
+ And I select "Group 1" conversation in the "favourites" conversations list
+ And I send "Hi!" message in the message area
+ And I should see "Hi!" in the "Group 1" "group_message_conversation"
+ And I should see "##today##j F##" in the "Group 1" "group_message_conversation"
+ And I go back in "view-conversation" message drawer
+ And I open the "Group" conversations list
+ And I should not see "Group 1" in the "Group" "group_message_tab"
+
+ Scenario: Send a message to a private conversation via contact tab
+ Given the following "message contacts" exist:
+ | user | contact |
+ | student1 | student2 |
+ And I log in as "student1"
+ And I open messaging
+ And I click on "Contacts" "link"
+ And I click on "Student 2" "link" in the "//*[@data-section='contacts']" "xpath_element"
+ When I send "Hi!" message in the message area
+ Then I should see "Hi!" in the "Student 2" "group_message_conversation"
+ And I should see "##today##j F##" in the "Student 2" "group_message_conversation"
+
+ Scenario: Try to send a message to a private conversation is not contact but you are allowed to send a message
+ Given I log in as "student1"
+ And I open messaging
+ When I send "Hi!" message to "Student 2" user
+ Then I should see "Hi!" in the "Student 2" "group_message_conversation"
+ And I should see "##today##j F##" in the "Student 2" "group_message_conversation"
+ And I log out
+ And I log in as "student2"
+ And I open messaging
+ And I select "Student 1" conversation in messaging
+ And I should see "Hi!" in the "Student 1" "group_message_conversation"
\ No newline at end of file
diff --git a/version.php b/version.php
index 9bf2ce21af7..1a926fc2cb8 100644
--- a/version.php
+++ b/version.php
@@ -29,11 +29,11 @@
defined('MOODLE_INTERNAL') || die();
-$version = 2018120303.08; // 20181203 = branching date YYYYMMDD - do not modify!
+$version = 2018120303.09; // 20181203 = branching date YYYYMMDD - do not modify!
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
-$release = '3.6.3+ (Build: 20190406)'; // Human-friendly version name
+$release = '3.6.3+ (Build: 20190410)'; // Human-friendly version name
$branch = '36'; // This version's branch.
$maturity = MATURITY_STABLE; // This version's maturity level.