diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index e7e1ee29f6b..fdad096bbc0 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -4327,6 +4327,14 @@ class restore_create_categories_and_questions extends restore_structure_step { } $data->contextid = $mapping->parentitemid; + // Before 3.1, the 'stamp' field could be erroneously duplicated. + // From 3.1 onwards, there's a unique index of (contextid, stamp). + // If we encounter a duplicate in an old restore file, just generate a new stamp. + // This is the same as what happens during an upgrade to 3.1+ anyway. + if ($DB->record_exists('question_categories', ['stamp' => $data->stamp, 'contextid' => $data->contextid])) { + $data->stamp = make_unique_id_code(); + } + // Let's create the question_category and save mapping $newitemid = $DB->insert_record('question_categories', $data); $this->set_mapping('question_category', $oldid, $newitemid); diff --git a/backup/util/dbops/backup_controller_dbops.class.php b/backup/util/dbops/backup_controller_dbops.class.php index 0b5fb9b6386..d347e7023a5 100644 --- a/backup/util/dbops/backup_controller_dbops.class.php +++ b/backup/util/dbops/backup_controller_dbops.class.php @@ -635,7 +635,9 @@ abstract class backup_controller_dbops extends backup_dbops { $locked = (get_config('backup', $config.'_locked') == true); if ($plan->setting_exists($settingname)) { $setting = $plan->get_setting($settingname); - if ($setting->get_value() != $value || 1==1) { + // We can only update the setting if it isn't already locked by config or permission. + if ($setting->get_status() !== base_setting::LOCKED_BY_CONFIG + && $setting->get_status() !== base_setting::LOCKED_BY_PERMISSION) { $setting->set_value($value); if ($locked) { $setting->set_status(base_setting::LOCKED_BY_CONFIG); diff --git a/course/renderer.php b/course/renderer.php index 787866a4853..f5238ddf4be 100644 --- a/course/renderer.php +++ b/course/renderer.php @@ -1626,14 +1626,11 @@ class core_course_renderer extends plugin_renderer_base { if ($coursecat->get_children_count()) { $classes = array( 'collapseexpand', - 'collapse-all', ); - if ($chelper->get_subcat_depth() == 1) { - $classes[] = 'disabled'; - } + // Only show the collapse/expand if there are children to expand. $content .= html_writer::start_tag('div', array('class' => 'collapsible-actions')); - $content .= html_writer::link('#', get_string('collapseall'), + $content .= html_writer::link('#', get_string('expandall'), array('class' => implode(' ', $classes))); $content .= html_writer::end_tag('div'); $this->page->requires->strings_for_js(array('collapseall', 'expandall'), 'moodle'); diff --git a/course/yui/build/moodle-course-categoryexpander/moodle-course-categoryexpander-debug.js b/course/yui/build/moodle-course-categoryexpander/moodle-course-categoryexpander-debug.js index 92f0c020fd7..e4f866cc737 100644 --- a/course/yui/build/moodle-course-categoryexpander/moodle-course-categoryexpander-debug.js +++ b/course/yui/build/moodle-course-categoryexpander/moodle-course-categoryexpander-debug.js @@ -25,10 +25,12 @@ var CSS = { HASCHILDREN: 'with_children' }, SELECTORS = { + WITHCHILDRENTREES: '.with_children', LOADEDTREES: '.with_children.loaded', CONTENTNODE: '.content', CATEGORYLISTENLINK: '.category .info .categoryname', CATEGORYSPINNERLOCATION: '.categoryname', + CATEGORYWITHCOLLAPSEDCHILDREN: '.category.with_children.collapsed', CATEGORYWITHCOLLAPSEDLOADEDCHILDREN: '.category.with_children.loaded.collapsed', CATEGORYWITHMAXIMISEDLOADEDCHILDREN: '.category.with_children.loaded:not(.collapsed)', COLLAPSEEXPAND: '.collapseexpand', @@ -82,6 +84,58 @@ NS.setup_keyboard_listeners = function() { Y.one(Y.config.doc).delegate('key', this.collapse_expand_all, 'enter', SELECTORS.COLLAPSEEXPAND, this); }; +/** + * Expand all categories. + * + * @method expand_category + * @private + * @param {Node} categorynode The node to expand + */ +NS.expand_category = function(categorynode) { + // Load the actual dependencies now that we've been called. + Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() { + // Overload the expand_category with the _expand_category function to ensure that + // this function isn't called in the future, and call it for the first time. + NS.expand_category = NS._expand_category; + NS.expand_category(categorynode); + }); +}; + +NS._expand_category = function(categorynode) { + var categoryid, + depth; + + if (!categorynode.hasClass(CSS.HASCHILDREN)) { + // Nothing to do here - this category has no children. + return; + } + + if (categorynode.hasClass(CSS.LOADED)) { + // We've already loaded this content so we just need to toggle the view of it. + this.run_expansion(categorynode); + return; + } + + // We use Data attributes to store the category. + categoryid = categorynode.getData('categoryid'); + depth = categorynode.getData('depth'); + if (typeof categoryid === "undefined" || typeof depth === "undefined") { + return; + } + + this._toggle_generic_expansion({ + parentnode: categorynode, + childnode: categorynode.one(SELECTORS.CONTENTNODE), + spinnerhandle: SELECTORS.CATEGORYSPINNERLOCATION, + data: { + categoryid: categoryid, + depth: depth, + showcourses: categorynode.getData('showcourses'), + type: TYPE_CATEGORY + } + }); +}; + /** * Toggle the animation of the clicked category node. * @@ -310,12 +364,12 @@ NS._collapse_expand_all = function(e) { NS.expand_all = function(ancestor) { var finalexpansions = []; - ancestor.all(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN) + ancestor.all(SELECTORS.CATEGORYWITHCOLLAPSEDCHILDREN) .each(function(c) { - if (c.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) { + if (c.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDCHILDREN)) { // Expand the hidden children first without animation. c.removeClass(CSS.SECTIONCOLLAPSED); - c.all(SELECTORS.LOADEDTREES).removeClass(CSS.SECTIONCOLLAPSED); + c.all(SELECTORS.WITHCHILDRENTREES).removeClass(CSS.SECTIONCOLLAPSED); } else { finalexpansions.push(c); } @@ -323,7 +377,7 @@ NS.expand_all = function(ancestor) { // Run the final expansion with animation on the visible items. Y.all(finalexpansions).each(function(c) { - this.run_expansion(c); + this.expand_category(c); }, this); }; diff --git a/course/yui/build/moodle-course-categoryexpander/moodle-course-categoryexpander-min.js b/course/yui/build/moodle-course-categoryexpander/moodle-course-categoryexpander-min.js index 49f5138141a..f655b42ba88 100644 --- a/course/yui/build/moodle-course-categoryexpander/moodle-course-categoryexpander-min.js +++ b/course/yui/build/moodle-course-categoryexpander/moodle-course-categoryexpander-min.js @@ -1 +1,2 @@ -YUI.add("moodle-course-categoryexpander",function(e,t){var n={CONTENTNODE:"content",COLLAPSEALL:"collapse-all",DISABLED:"disabled",LOADED:"loaded",NOTLOADED:"notloaded",SECTIONCOLLAPSED:"collapsed",HASCHILDREN:"with_children"},r={LOADEDTREES:".with_children.loaded",CONTENTNODE:".content",CATEGORYLISTENLINK:".category .info .categoryname",CATEGORYSPINNERLOCATION:".categoryname",CATEGORYWITHCOLLAPSEDLOADEDCHILDREN:".category.with_children.loaded.collapsed",CATEGORYWITHMAXIMISEDLOADEDCHILDREN:".category.with_children.loaded:not(.collapsed)",COLLAPSEEXPAND:".collapseexpand",COURSEBOX:".coursebox",COURSEBOXLISTENLINK:".coursebox .moreinfo",COURSEBOXSPINNERLOCATION:".coursename a",COURSECATEGORYTREE:".course_category_tree",PARENTWITHCHILDREN:".category"},i=e.namespace("Moodle.course.categoryexpander"),s=0,o=1,u=M.cfg.wwwroot+"/course/category.ajax.php";i.init=function(){var t=e.one(e.config.doc);t.delegate("click",this.toggle_category_expansion,r.CATEGORYLISTENLINK,this),t.delegate("click",this.toggle_coursebox_expansion,r.COURSEBOXLISTENLINK,this),t.delegate("click",this.collapse_expand_all,r.COLLAPSEEXPAND,this),t.once("key",this.setup_keyboard_listeners,"tab",this)},i.setup_keyboard_listeners=function(){var t=e.one(e.config.doc);t.all(r.CATEGORYLISTENLINK,r.COURSEBOXLISTENLINK,r.COLLAPSEEXPAND).setAttribute("tabindex","0"),e.one(e.config.doc).delegate("key",this.toggle_category_expansion,"enter",r.CATEGORYLISTENLINK,this),e.one(e.config.doc).delegate("key",this.toggle_coursebox_expansion,"enter",r.COURSEBOXLISTENLINK,this),e.one(e.config.doc).delegate("key",this.collapse_expand_all,"enter",r.COLLAPSEEXPAND,this)},i.toggle_category_expansion=function(t){e.use("io-base","json-parse","moodle-core-notification","anim-node-plugin",function(){i.toggle_category_expansion=i._toggle_category_expansion,i.toggle_category_expansion(t)})},i.toggle_coursebox_expansion=function(t){e.use("io-base","json-parse","moodle-core-notification","anim-node-plugin",function(){i.toggle_coursebox_expansion=i._toggle_coursebox_expansion,i.toggle_coursebox_expansion(t)}),t.preventDefault()},i._toggle_coursebox_expansion=function(e){var t;t=e.target.ancestor(r.COURSEBOX,!0),e.preventDefault();if(t.hasClass(n.LOADED)){this.run_expansion(t);return}this._toggle_generic_expansion({parentnode:t,childnode:t.one(r.CONTENTNODE),spinnerhandle:r.COURSEBOXSPINNERLOCATION,data:{courseid:t.getData("courseid"),type:o}})},i._toggle_category_expansion=function(e){var t,i,o;if(e.target.test("a")||e.target.test("img"))return;t=e.target.ancestor(r.PARENTWITHCHILDREN,!0);if(!t.hasClass(n.HASCHILDREN))return;if(t.hasClass(n.LOADED)){this.run_expansion(t);return}i=t.getData("categoryid"),o=t.getData("depth");if(typeof i=="undefined"||typeof o=="undefined")return;this._toggle_generic_expansion({parentnode:t,childnode:t.one(r.CONTENTNODE),spinnerhandle:r.CATEGORYSPINNERLOCATION,data:{categoryid:i,depth:o,showcourses:t.getData("showcourses"),type:s}})},i._toggle_generic_expansion=function(t){var n;t.spinnerhandle&&(n=M.util.add_spinner(e,t.parentnode.one(t.spinnerhandle)).show()),e.io(u,{method:"POST",context:this,on:{complete:this.process_results},data:t.data,arguments:{parentnode:t.parentnode,childnode:t.childnode,spinner:n}})},i.run_expansion=function(e){var t=e.one(r.CONTENTNODE),i=this,s=e.ancestor(r.COURSECATEGORYTREE);this.add_animation(t),e.hasClass(n.SECTIONCOLLAPSED)?(t.setStyle("height","0"),e.removeClass(n.SECTIONCOLLAPSED),e.setAttribute("aria-expanded","true"),t.fx.set("reverse",!1)):(t.fx.set("reverse",!0),t.fx.once("end",function(e,t){t.addClass(n.SECTIONCOLLAPSED),t.setAttribute("aria-expanded","false")},this,e)),t.fx.once("end",function(e,t){t.setStyles({height:"",opacity:""}),this.destroy(),i.update_collapsible_actions(s)},t.fx,t),t.fx.run()},i.collapse_expand_all=function(t){e.use("io-base","json-parse","moodle-core-notification","anim-node-plugin",function(){i.collapse_expand_all=i._collapse_expand_all,i.collapse_expand_all(t)}),t.preventDefault()},i._collapse_expand_all=function(e){e.preventDefault();if(e.currentTarget.hasClass(n.DISABLED))return;var t=e.currentTarget.ancestor(r.COURSECATEGORYTREE);if(!t)return;var i=t.one(r.COLLAPSEEXPAND);i.hasClass(n.COLLAPSEALL)?this.collapse_all(t):this.expand_all(t),this.update_collapsible_actions(t)},i.expand_all=function(t){var i=[];t.all(r.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN).each(function(e){e.ancestor(r.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)?(e.removeClass(n.SECTIONCOLLAPSED),e.all(r.LOADEDTREES).removeClass(n.SECTIONCOLLAPSED)):i.push(e)},this),e.all(i).each(function(e){this.run_expansion(e)},this)},i.collapse_all=function(t){var i=[];t.all(r.CATEGORYWITHMAXIMISEDLOADEDCHILDREN).each(function(e){e.ancestor(r.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)?i.push(e):this.run_expansion(e)},this),e.all(i).each(function(e){e.addClass(n.SECTIONCOLLAPSED),e.all(r.LOADEDTREES).addClass(n.SECTIONCOLLAPSED)},this)},i.update_collapsible_actions=function(e){var t=!1,i=e.one(r.COLLAPSEEXPAND);if(!i)return;e.all(r.CATEGORYWITHMAXIMISEDLOADEDCHILDREN).each(function(e){return e.ancestor(r.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)?!1:(t=!0,!0)}),t?i.setHTML(M.util.get_string("collapseall","moodle")).addClass(n.COLLAPSEALL).removeClass(n.DISABLED):i.setHTML(M.util.get_string("expandall","moodle")).removeClass(n.COLLAPSEALL).removeClass(n.DISABLED)},i.process_results=function(t,r,i){var s,o;try{o=e.JSON.parse(r.responseText);if(o.error)return new M.core.ajaxException(o)}catch(u){return new M.core.exception(u)}s=e.Node.create(o),i.childnode.appendChild(s),i.parentnode.addClass(n.LOADED).removeClass(n.NOTLOADED),this.run_expansion(i.parentnode),i.spinner&&i.spinner.hide().destroy()},i.add_animation=function(t){return typeof t.fx!="undefined"?t:(t.plug(e.Plugin.NodeFX,{from:{height:0,opacity:0},to:{height:function(e){return e.get("scrollHeight")},opacity:1},duration:.2}),t)}},"@VERSION@",{requires:["node","event-key"]}); +YUI.add("moodle-course-categoryexpander",function(e,t){var n={CONTENTNODE:"content",COLLAPSEALL:"collapse-all",DISABLED:"disabled",LOADED:"loaded",NOTLOADED:"notloaded",SECTIONCOLLAPSED:"collapsed",HASCHILDREN:"with_children"},r={WITHCHILDRENTREES:".with_children",LOADEDTREES:".with_children.loaded",CONTENTNODE:".content",CATEGORYLISTENLINK:".category .info .categoryname",CATEGORYSPINNERLOCATION:".categoryname",CATEGORYWITHCOLLAPSEDCHILDREN:".category.with_children.collapsed",CATEGORYWITHCOLLAPSEDLOADEDCHILDREN:".category.with_children.loaded.collapsed",CATEGORYWITHMAXIMISEDLOADEDCHILDREN:".category.with_children.loaded:not(.collapsed)",COLLAPSEEXPAND:".collapseexpand",COURSEBOX:".coursebox",COURSEBOXLISTENLINK:".coursebox .moreinfo",COURSEBOXSPINNERLOCATION:".coursename a",COURSECATEGORYTREE:".course_category_tree",PARENTWITHCHILDREN:".category"},i=e.namespace("Moodle.course.categoryexpander"),s=0,o=1,u=M.cfg.wwwroot+"/course/category.ajax.php";i.init=function(){var t=e.one(e.config.doc);t.delegate("click",this.toggle_category_expansion,r.CATEGORYLISTENLINK,this),t.delegate("click",this.toggle_coursebox_expansion,r.COURSEBOXLISTENLINK,this),t.delegate("click",this.collapse_expand_all,r.COLLAPSEEXPAND,this),t.once("key",this.setup_keyboard_listeners,"tab",this)},i.setup_keyboard_listeners=function(){var t=e.one(e.config.doc);t.all(r.CATEGORYLISTENLINK,r.COURSEBOXLISTENLINK,r.COLLAPSEEXPAND).setAttribute("tabindex","0"),e.one(e.config.doc).delegate("key",this.toggle_category_expansion,"enter",r.CATEGORYLISTENLINK,this),e.one(e.config.doc).delegate("key",this.toggle_coursebox_expansion,"enter",r.COURSEBOXLISTENLINK,this),e.one(e.config.doc).delegate("key",this.collapse_expand_all,"enter",r.COLLAPSEEXPAND,this)},i.expand_category=function(t){e.use("io-base","json-parse","moodle-core-notification","anim-node-plugin",function(){i.expand_category=i._expand_category,i.expand_category(t)})},i._expand_category=function(e){var t,i;if(!e.hasClass(n.HASCHILDREN))return;if(e.hasClass(n.LOADED)){this.run_expansion(e);return}t=e.getData("categoryid"),i=e.getData("depth");if(typeof t=="undefined"||typeof i=="undefined")return;this._toggle_generic_expansion({parentnode:e,childnode:e.one(r.CONTENTNODE),spinnerhandle:r.CATEGORYSPINNERLOCATION,data:{categoryid:t,depth:i,showcourses:e.getData("showcourses"),type:s}})},i.toggle_category_expansion=function(t){e.use("io-base","json-parse","moodle-core-notification","anim-node-plugin",function(){i.toggle_category_expansion=i._toggle_category_expansion,i.toggle_category_expansion(t)})},i.toggle_coursebox_expansion=function(t){e.use("io-base","json-parse","moodle-core-notification","anim-node-plugin",function(){i.toggle_coursebox_expansion=i._toggle_coursebox_expansion,i.toggle_coursebox_expansion(t)}),t.preventDefault()},i._toggle_coursebox_expansion=function(e){var t;t=e.target.ancestor(r.COURSEBOX,!0),e.preventDefault();if(t.hasClass(n.LOADED)){this.run_expansion(t);return}this._toggle_generic_expansion({parentnode:t,childnode:t.one(r.CONTENTNODE),spinnerhandle:r.COURSEBOXSPINNERLOCATION,data:{courseid:t.getData("courseid"),type:o}})},i._toggle_category_expansion=function(e){var t,i,o;if(e.target.test("a")||e.target.test("img"))return;t=e.target.ancestor(r.PARENTWITHCHILDREN,!0);if(!t.hasClass(n.HASCHILDREN))return;if(t.hasClass(n.LOADED)){this.run_expansion(t);return}i=t.getData("categoryid"),o=t.getData("depth");if(typeof i=="undefined"||typeof o=="undefined")return;this._toggle_generic_expansion({parentnode:t,childnode:t.one(r.CONTENTNODE),spinnerhandle:r.CATEGORYSPINNERLOCATION,data:{categoryid:i,depth:o,showcourses:t.getData("showcourses"),type:s}})},i._toggle_generic_expansion=function(t){var n;t.spinnerhandle&&(n=M.util.add_spinner(e,t.parentnode.one(t.spinnerhandle)).show()),e.io(u,{method:"POST",context:this,on:{complete:this.process_results},data:t.data,arguments:{parentnode:t.parentnode,childnode:t.childnode,spinner:n}})},i.run_expansion=function(e){var t=e.one(r.CONTENTNODE),i=this,s=e.ancestor(r.COURSECATEGORYTREE);this.add_animation(t),e.hasClass(n.SECTIONCOLLAPSED)?(t.setStyle("height","0"),e.removeClass(n.SECTIONCOLLAPSED),e.setAttribute("aria-expanded","true"),t.fx.set("reverse",!1)):(t.fx.set("reverse",!0),t.fx.once("end",function(e,t){t.addClass(n.SECTIONCOLLAPSED),t.setAttribute("aria-expanded","false")},this,e)),t.fx.once("end",function(e,t){t.setStyles({height:"",opacity:""}),this.destroy(),i.update_collapsible_actions(s)},t.fx,t),t.fx.run()},i.collapse_expand_all=function(t){e.use("io-base","json-parse","moodle-core-notification","anim-node-plugin",function(){i.collapse_expand_all=i._collapse_expand_all,i.collapse_expand_all(t)}),t.preventDefault()},i._collapse_expand_all=function(e){e.preventDefault();if(e.currentTarget.hasClass(n.DISABLED))return;var t=e.currentTarget.ancestor(r.COURSECATEGORYTREE);if(!t)return;var i=t.one(r.COLLAPSEEXPAND);i.hasClass(n.COLLAPSEALL)?this.collapse_all(t):this.expand_all(t),this.update_collapsible_actions(t)},i.expand_all=function(t){var i=[];t.all(r.CATEGORYWITHCOLLAPSEDCHILDREN).each(function(e){e.ancestor(r.CATEGORYWITHCOLLAPSEDCHILDREN)?(e.removeClass(n.SECTIONCOLLAPSED),e.all(r.WITHCHILDRENTREES).removeClass(n.SECTIONCOLLAPSED)):i.push(e)},this),e.all(i).each(function(e){this.expand_category(e)},this)},i.collapse_all=function(t){var i=[];t.all(r.CATEGORYWITHMAXIMISEDLOADEDCHILDREN).each(function(e){e.ancestor(r.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)?i.push(e):this.run_expansion(e)},this),e.all(i).each(function(e){e.addClass(n.SECTIONCOLLAPSED),e.all(r.LOADEDTREES).addClass(n.SECTIONCOLLAPSED)},this)},i.update_collapsible_actions=function(e){var t=!1,i=e.one(r.COLLAPSEEXPAND);if(!i)return;e.all(r.CATEGORYWITHMAXIMISEDLOADEDCHILDREN).each(function(e){return e.ancestor(r.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)?!1:(t=!0,!0)}),t?i.setHTML(M.util.get_string("collapseall","moodle")).addClass(n.COLLAPSEALL).removeClass(n.DISABLED):i.setHTML(M.util.get_string("expandall","moodle")).removeClass(n.COLLAPSEALL).removeClass +(n.DISABLED)},i.process_results=function(t,r,i){var s,o;try{o=e.JSON.parse(r.responseText);if(o.error)return new M.core.ajaxException(o)}catch(u){return new M.core.exception(u)}s=e.Node.create(o),i.childnode.appendChild(s),i.parentnode.addClass(n.LOADED).removeClass(n.NOTLOADED),this.run_expansion(i.parentnode),i.spinner&&i.spinner.hide().destroy()},i.add_animation=function(t){return typeof t.fx!="undefined"?t:(t.plug(e.Plugin.NodeFX,{from:{height:0,opacity:0},to:{height:function(e){return e.get("scrollHeight")},opacity:1},duration:.2}),t)}},"@VERSION@",{requires:["node","event-key"]}); diff --git a/course/yui/build/moodle-course-categoryexpander/moodle-course-categoryexpander.js b/course/yui/build/moodle-course-categoryexpander/moodle-course-categoryexpander.js index b7e1e9bdb99..38a5b34bdf3 100644 --- a/course/yui/build/moodle-course-categoryexpander/moodle-course-categoryexpander.js +++ b/course/yui/build/moodle-course-categoryexpander/moodle-course-categoryexpander.js @@ -25,10 +25,12 @@ var CSS = { HASCHILDREN: 'with_children' }, SELECTORS = { + WITHCHILDRENTREES: '.with_children', LOADEDTREES: '.with_children.loaded', CONTENTNODE: '.content', CATEGORYLISTENLINK: '.category .info .categoryname', CATEGORYSPINNERLOCATION: '.categoryname', + CATEGORYWITHCOLLAPSEDCHILDREN: '.category.with_children.collapsed', CATEGORYWITHCOLLAPSEDLOADEDCHILDREN: '.category.with_children.loaded.collapsed', CATEGORYWITHMAXIMISEDLOADEDCHILDREN: '.category.with_children.loaded:not(.collapsed)', COLLAPSEEXPAND: '.collapseexpand', @@ -81,6 +83,58 @@ NS.setup_keyboard_listeners = function() { Y.one(Y.config.doc).delegate('key', this.collapse_expand_all, 'enter', SELECTORS.COLLAPSEEXPAND, this); }; +/** + * Expand all categories. + * + * @method expand_category + * @private + * @param {Node} categorynode The node to expand + */ +NS.expand_category = function(categorynode) { + // Load the actual dependencies now that we've been called. + Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() { + // Overload the expand_category with the _expand_category function to ensure that + // this function isn't called in the future, and call it for the first time. + NS.expand_category = NS._expand_category; + NS.expand_category(categorynode); + }); +}; + +NS._expand_category = function(categorynode) { + var categoryid, + depth; + + if (!categorynode.hasClass(CSS.HASCHILDREN)) { + // Nothing to do here - this category has no children. + return; + } + + if (categorynode.hasClass(CSS.LOADED)) { + // We've already loaded this content so we just need to toggle the view of it. + this.run_expansion(categorynode); + return; + } + + // We use Data attributes to store the category. + categoryid = categorynode.getData('categoryid'); + depth = categorynode.getData('depth'); + if (typeof categoryid === "undefined" || typeof depth === "undefined") { + return; + } + + this._toggle_generic_expansion({ + parentnode: categorynode, + childnode: categorynode.one(SELECTORS.CONTENTNODE), + spinnerhandle: SELECTORS.CATEGORYSPINNERLOCATION, + data: { + categoryid: categoryid, + depth: depth, + showcourses: categorynode.getData('showcourses'), + type: TYPE_CATEGORY + } + }); +}; + /** * Toggle the animation of the clicked category node. * @@ -309,12 +363,12 @@ NS._collapse_expand_all = function(e) { NS.expand_all = function(ancestor) { var finalexpansions = []; - ancestor.all(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN) + ancestor.all(SELECTORS.CATEGORYWITHCOLLAPSEDCHILDREN) .each(function(c) { - if (c.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) { + if (c.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDCHILDREN)) { // Expand the hidden children first without animation. c.removeClass(CSS.SECTIONCOLLAPSED); - c.all(SELECTORS.LOADEDTREES).removeClass(CSS.SECTIONCOLLAPSED); + c.all(SELECTORS.WITHCHILDRENTREES).removeClass(CSS.SECTIONCOLLAPSED); } else { finalexpansions.push(c); } @@ -322,7 +376,7 @@ NS.expand_all = function(ancestor) { // Run the final expansion with animation on the visible items. Y.all(finalexpansions).each(function(c) { - this.run_expansion(c); + this.expand_category(c); }, this); }; diff --git a/course/yui/src/categoryexpander/js/categoryexpander.js b/course/yui/src/categoryexpander/js/categoryexpander.js index a679c796088..2176497558b 100644 --- a/course/yui/src/categoryexpander/js/categoryexpander.js +++ b/course/yui/src/categoryexpander/js/categoryexpander.js @@ -23,10 +23,12 @@ var CSS = { HASCHILDREN: 'with_children' }, SELECTORS = { + WITHCHILDRENTREES: '.with_children', LOADEDTREES: '.with_children.loaded', CONTENTNODE: '.content', CATEGORYLISTENLINK: '.category .info .categoryname', CATEGORYSPINNERLOCATION: '.categoryname', + CATEGORYWITHCOLLAPSEDCHILDREN: '.category.with_children.collapsed', CATEGORYWITHCOLLAPSEDLOADEDCHILDREN: '.category.with_children.loaded.collapsed', CATEGORYWITHMAXIMISEDLOADEDCHILDREN: '.category.with_children.loaded:not(.collapsed)', COLLAPSEEXPAND: '.collapseexpand', @@ -80,6 +82,58 @@ NS.setup_keyboard_listeners = function() { Y.one(Y.config.doc).delegate('key', this.collapse_expand_all, 'enter', SELECTORS.COLLAPSEEXPAND, this); }; +/** + * Expand all categories. + * + * @method expand_category + * @private + * @param {Node} categorynode The node to expand + */ +NS.expand_category = function(categorynode) { + // Load the actual dependencies now that we've been called. + Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() { + // Overload the expand_category with the _expand_category function to ensure that + // this function isn't called in the future, and call it for the first time. + NS.expand_category = NS._expand_category; + NS.expand_category(categorynode); + }); +}; + +NS._expand_category = function(categorynode) { + var categoryid, + depth; + + if (!categorynode.hasClass(CSS.HASCHILDREN)) { + // Nothing to do here - this category has no children. + return; + } + + if (categorynode.hasClass(CSS.LOADED)) { + // We've already loaded this content so we just need to toggle the view of it. + this.run_expansion(categorynode); + return; + } + + // We use Data attributes to store the category. + categoryid = categorynode.getData('categoryid'); + depth = categorynode.getData('depth'); + if (typeof categoryid === "undefined" || typeof depth === "undefined") { + return; + } + + this._toggle_generic_expansion({ + parentnode: categorynode, + childnode: categorynode.one(SELECTORS.CONTENTNODE), + spinnerhandle: SELECTORS.CATEGORYSPINNERLOCATION, + data: { + categoryid: categoryid, + depth: depth, + showcourses: categorynode.getData('showcourses'), + type: TYPE_CATEGORY + } + }); +}; + /** * Toggle the animation of the clicked category node. * @@ -308,12 +362,12 @@ NS._collapse_expand_all = function(e) { NS.expand_all = function(ancestor) { var finalexpansions = []; - ancestor.all(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN) + ancestor.all(SELECTORS.CATEGORYWITHCOLLAPSEDCHILDREN) .each(function(c) { - if (c.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) { + if (c.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDCHILDREN)) { // Expand the hidden children first without animation. c.removeClass(CSS.SECTIONCOLLAPSED); - c.all(SELECTORS.LOADEDTREES).removeClass(CSS.SECTIONCOLLAPSED); + c.all(SELECTORS.WITHCHILDRENTREES).removeClass(CSS.SECTIONCOLLAPSED); } else { finalexpansions.push(c); } @@ -321,7 +375,7 @@ NS.expand_all = function(ancestor) { // Run the final expansion with animation on the visible items. Y.all(finalexpansions).each(function(c) { - this.run_expansion(c); + this.expand_category(c); }, this); }; diff --git a/group/autogroup_form.php b/group/autogroup_form.php index 8e17c173878..cb1380820ba 100644 --- a/group/autogroup_form.php +++ b/group/autogroup_form.php @@ -83,7 +83,7 @@ class autogroup_form extends moodleform { } $coursecontext = context_course::instance($COURSE->id); - if ($cohorts = cohort_get_available_cohorts($coursecontext, COHORT_WITH_ENROLLED_MEMBERS_ONLY)) { + if ($cohorts = cohort_get_available_cohorts($coursecontext, COHORT_WITH_ENROLLED_MEMBERS_ONLY, 0, 0)) { $options = array(0 => get_string('anycohort', 'cohort')); foreach ($cohorts as $c) { $options[$c->id] = format_string($c->name, true, context::instance_by_id($c->contextid)); diff --git a/lib/behat/classes/util.php b/lib/behat/classes/util.php index 93ffb91fadc..95b3faa53e9 100644 --- a/lib/behat/classes/util.php +++ b/lib/behat/classes/util.php @@ -114,6 +114,9 @@ class behat_util extends testing_util { // Enable web cron. set_config('cronclionly', 0); + // Set editor autosave to high value, so as to avoid unwanted ajax. + set_config('autosavefrequency', '604800', 'editor_atto'); + // Keeps the current version of database and dataroot. self::store_versions_hash(); diff --git a/lib/behat/lib.php b/lib/behat/lib.php index b121e5e6346..6dd4784999d 100644 --- a/lib/behat/lib.php +++ b/lib/behat/lib.php @@ -134,6 +134,13 @@ function behat_error_handler($errno, $errstr, $errfile, $errline, $errcontext) { return true; } + // No need to report the before_scenario warning generated to clear last error. + // As error_clear_last is only available in php 7.0+, we trigger E_USER_WARNING + // to clear any last error which was generated during reset in before_scenario. + if (($errno === E_USER_WARNING) && $errstr == 'before_scenario') { + return; + } + // This error handler receives E_ALL | E_STRICT, running the behat test site the debug level is // set to DEVELOPER and will always include E_NOTICE,E_USER_NOTICE... as part of E_ALL, if the current // error_reporting() value does not include one of those levels is because it has been forced through @@ -175,6 +182,14 @@ function behat_shutdown_function() { if ($error = error_get_last()) { // Ignore E_WARNING, as they might come via ( @ )suppression and might lead to false failure. if (isset($error['type']) && !($error['type'] & E_WARNING)) { + + // No need to report the before_scenario warning generated to clear last error. + // As error_clear_last is only available in php 7.0+, we trigger E_USER_WARNING + // to clear any last error which was generated during reset in before_scenario. + if (($error['type'] & E_USER_WARNING) && $error['message'] == 'before_scenario') { + return; + } + $errors = behat_get_shutdown_process_errors(); $errors[] = $error; diff --git a/lib/tests/behat/behat_hooks.php b/lib/tests/behat/behat_hooks.php index db90140d178..975d16f7187 100644 --- a/lib/tests/behat/behat_hooks.php +++ b/lib/tests/behat/behat_hooks.php @@ -96,6 +96,13 @@ class behat_hooks extends behat_base { */ protected static $timings = array(); + /** + * Keeps track of php error generated during reset. + * + * @var int keep track of how many php errors were generated. + */ + public static $phperrorduringresetcounter = 0; + /** * Hook to capture BeforeSuite event so as to give access to moodle codebase. * This will try and catch any exception and exists if anything fails. @@ -302,7 +309,30 @@ class behat_hooks extends behat_base { // Reset $SESSION. \core\session\manager::init_empty_session(); + // Set custom handler to try reset all data, if failed because of previous ajax. + set_error_handler( + function($errno, $errstr, $errfile, $errline) { + behat_hooks::$phperrorduringresetcounter++; + if (behat_hooks::$phperrorduringresetcounter < self::TIMEOUT) { + sleep(1); + behat_util::reset_all_data(); + } + return true; + }, -1 & ~E_NOTICE & ~E_WARNING); behat_util::reset_all_data(); + restore_error_handler(); + + // Trigger an error which will be ignored by behat_shutdown_function, this is hacky way to clear last error in php < 7.0. + if (self::$phperrorduringresetcounter > 0) { + if (function_exists('error_clear_last')) { + error_clear_last(); + } else { + trigger_error('before_scenario', E_USER_WARNING); + } + } + + // Reset the counter here, as this won't be required. + self::$phperrorduringresetcounter = 0; // Assign valid data to admin user (some generator-related code needs a valid user). $user = $DB->get_record('user', array('username' => 'admin')); @@ -338,6 +368,29 @@ class behat_hooks extends behat_base { $this->resize_window('medium'); } + /** + * Executed after scenario to go to a page where no JS is executed. + * This will ensure there are no unwanted ajax calls from browser and + * site can be reset safely. + * + * @param AfterScenarioScope $scope scope passed by event fired after scenario. + * @AfterScenario + */ + public function after_scenario(AfterScenarioScope $scope) { + try { + $this->wait_for_pending_js(); + $this->getSession()->visit($this->locate_path('/README.txt')); + $this->getSession()->reset(); + } catch (DriverException $e) { + // Try restart session, if DriverException caught. + try { + $this->getSession()->restart(); + } catch (DriverException $e) { + // Do nothing, as this will be caught while starting session in before_scenario. + } + } + } + /** * Wait for JS to complete before beginning interacting with the DOM. * diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index c2ab71857f4..33e30bb54ae 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -4592,7 +4592,7 @@ class assign { if ($this->can_view_submission($user->id)) { - if (has_capability('mod/assign:submit', $this->get_context(), $user)) { + if (has_capability('mod/assign:submit', $this->get_context(), $user, false)) { $submissionstatus = $this->get_assign_submission_status_renderable($user, $showlinks); $o .= $this->get_renderer()->render($submissionstatus); } diff --git a/repository/skydrive/microsoftliveapi.php b/repository/skydrive/microsoftliveapi.php index 710777bdc67..9fe541a6cd6 100644 --- a/repository/skydrive/microsoftliveapi.php +++ b/repository/skydrive/microsoftliveapi.php @@ -60,18 +60,6 @@ class microsoft_skydrive extends oauth2_client { $this->foldernamecache = cache::make('repository_skydrive', 'foldername'); } - /** - * Should HTTP GET be used instead of POST? - * - * The Microsoft API does not support POST, so we should use - * GET instead (with the auth_token passed as a GET param). - * - * @return bool true if GET should be used - */ - protected function use_http_get() { - return true; - } - /** * Returns the auth url for OAuth 2.0 request * @return string the auth url @@ -88,6 +76,20 @@ class microsoft_skydrive extends oauth2_client { return 'https://login.live.com/oauth20_token.srf'; } + /** + * Post request. + * + * Overridden to convert the data to a string, else curl will set the wrong headers. + * + * @param string $url The URL. + * @param array|string $params The parameters. + * @param array $options The options. + * @return bool + */ + public function post($url, $params = '', $options = array()) { + return parent::post($url, format_postdata_for_curlcall($params), $options); + } + /** * Downloads a file to a file from skydrive using authenticated request * diff --git a/repository/webdav/lib.php b/repository/webdav/lib.php index 72c27c23d11..11cb7db45d2 100644 --- a/repository/webdav/lib.php +++ b/repository/webdav/lib.php @@ -72,7 +72,7 @@ class repository_webdav extends repository { } public function get_file($url, $title = '') { $url = urldecode($url); - $path = $this->prepare_file($title); + $path = $this->prepare_file(); if (!$this->dav->open()) { return false; } diff --git a/version.php b/version.php index 223f7a47134..c129c6b5d56 100644 --- a/version.php +++ b/version.php @@ -29,11 +29,11 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2016052303.02; // 20160523 = branching date YYYYMMDD - do not modify! +$version = 2016052303.03; // 20160523 = branching date YYYYMMDD - do not modify! // RR = release increments - 00 in DEV branches. // .XX = incremental changes. -$release = '3.1.3+ (Build: 20161118)'; // Human-friendly version name +$release = '3.1.3+ (Build: 20161122)'; // Human-friendly version name $branch = '31'; // This version's branch. $maturity = MATURITY_STABLE; // This version's maturity level.