From 084c2ef1ef496d819563d28ed98eacab96c4f3ba Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Fri, 30 Sep 2016 11:37:06 +0800 Subject: [PATCH 1/6] MDL-4782 course: swap availability info and description --- course/format/renderer.php | 9 +++++---- course/renderer.php | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/course/format/renderer.php b/course/format/renderer.php index c88d14d947b..283ca760fb0 100644 --- a/course/format/renderer.php +++ b/course/format/renderer.php @@ -226,14 +226,15 @@ abstract class format_section_renderer_base extends plugin_renderer_base { $sectionname = html_writer::tag('span', $this->section_title($section, $course)); $o.= $this->output->heading($sectionname, 3, 'sectionname' . $classes); - $o.= html_writer::start_tag('div', array('class' => 'summary')); - $o.= $this->format_summary_text($section); - $o.= html_writer::end_tag('div'); - $context = context_course::instance($course->id); + $o .= $this->section_availability_message($section, has_capability('moodle/course:viewhiddensections', $context)); + $o .= html_writer::start_tag('div', array('class' => 'summary')); + $o .= $this->format_summary_text($section); + $o .= html_writer::end_tag('div'); + return $o; } diff --git a/course/renderer.php b/course/renderer.php index 90ad00f4694..e23f5812c7e 100644 --- a/course/renderer.php +++ b/course/renderer.php @@ -890,15 +890,15 @@ class core_course_renderer extends plugin_renderer_base { $output .= html_writer::span($modicons, 'actions'); } + // Show availability info (if module is not available). + $output .= $this->course_section_cm_availability($mod, $displayoptions); + // If there is content AND a link, then display the content here // (AFTER any icons). Otherwise it was displayed before if (!empty($url)) { $output .= $contentpart; } - // show availability info (if module is not available) - $output .= $this->course_section_cm_availability($mod, $displayoptions); - $output .= html_writer::end_tag('div'); // $indentclasses // End of indentation div. From 28ac3a54b9009cc2cc8eac94ebc53b900fd8492e Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Fri, 3 Feb 2017 09:34:38 +0800 Subject: [PATCH 2/6] MDL-4782 availability: new template for course display --- lib/templates/availability_info.mustache | 33 +++++++++++++++++++ .../templates/core/availability_info.mustache | 27 +++++++++++++++ .../templates/core/availability_info.mustache | 27 +++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 lib/templates/availability_info.mustache create mode 100644 theme/boost/templates/core/availability_info.mustache create mode 100644 theme/bootstrapbase/templates/core/availability_info.mustache diff --git a/lib/templates/availability_info.mustache b/lib/templates/availability_info.mustache new file mode 100644 index 00000000000..954123ab382 --- /dev/null +++ b/lib/templates/availability_info.mustache @@ -0,0 +1,33 @@ +{{! + 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 . +}} +{{! + @template core/availability_info + + Moodle template for the course or section availability information. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Example context (json): + { "classes": "", "text": "This activity is not available" } +}} +{{#text}} +
{{{text}}}
+{{/text}} diff --git a/theme/boost/templates/core/availability_info.mustache b/theme/boost/templates/core/availability_info.mustache new file mode 100644 index 00000000000..3cc64343b67 --- /dev/null +++ b/theme/boost/templates/core/availability_info.mustache @@ -0,0 +1,27 @@ +{{! + 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 . +}} +{{! + Availability info. + + Example context (json): + { "classes": "", "text": "This activity is not available" } +}} +{{#text}} +
+ {{{text}}} +
+{{/text}} diff --git a/theme/bootstrapbase/templates/core/availability_info.mustache b/theme/bootstrapbase/templates/core/availability_info.mustache new file mode 100644 index 00000000000..fd81eb50ea1 --- /dev/null +++ b/theme/bootstrapbase/templates/core/availability_info.mustache @@ -0,0 +1,27 @@ +{{! + 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 . +}} +{{! + Availability info. + + Example context (json): + { "classes": "", "text": "This activity is not available" } +}} +{{#text}} +
+ {{{text}}} +
+{{/text}} From 4529327a22baad84b9f137956cb42b5006858ff1 Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Fri, 3 Feb 2017 09:38:17 +0800 Subject: [PATCH 3/6] MDL-4782 course: new WS for section and module editing --- course/externallib.php | 226 +++++++++++++++++++++++++++++++++++++++++ lib/db/services.php | 24 +++++ 2 files changed, 250 insertions(+) diff --git a/course/externallib.php b/course/externallib.php index d509033e675..c97800e3d54 100644 --- a/course/externallib.php +++ b/course/externallib.php @@ -270,6 +270,7 @@ class core_course_external extends external_api { context_module::instance($cm->id)); //user that can view hidden module should know about the visibility $module['visible'] = $cm->visible; + $module['visibleoncoursepage'] = $cm->visibleoncoursepage; // Availability date (also send to user who can see hidden module). if ($CFG->enableavailability && ($canviewhidden || $canupdatecourse)) { @@ -342,6 +343,8 @@ class core_course_external extends external_api { 'instance' => new external_value(PARAM_INT, 'instance id', VALUE_OPTIONAL), 'description' => new external_value(PARAM_RAW, 'activity description', VALUE_OPTIONAL), 'visible' => new external_value(PARAM_INT, 'is the module visible', VALUE_OPTIONAL), + 'visibleoncoursepage' => new external_value(PARAM_INT, 'is the module visible on course page', + VALUE_OPTIONAL), 'modicon' => new external_value(PARAM_URL, 'activity icon url'), 'modname' => new external_value(PARAM_PLUGIN, 'activity module type'), 'modplural' => new external_value(PARAM_TEXT, 'activity module plural name'), @@ -2558,6 +2561,7 @@ class core_course_external extends external_api { 'score' => new external_value(PARAM_INT, 'Score', VALUE_OPTIONAL), 'indent' => new external_value(PARAM_INT, 'Indentation', VALUE_OPTIONAL), 'visible' => new external_value(PARAM_INT, 'If visible', VALUE_OPTIONAL), + 'visibleoncoursepage' => new external_value(PARAM_INT, 'If visible on course page', VALUE_OPTIONAL), 'visibleold' => new external_value(PARAM_INT, 'Visible old', VALUE_OPTIONAL), 'completiongradeitemnumber' => new external_value(PARAM_INT, 'Completion grade item', VALUE_OPTIONAL), 'completionview' => new external_value(PARAM_INT, 'Completion view setting', VALUE_OPTIONAL), @@ -3235,4 +3239,226 @@ class core_course_external extends external_api { public static function get_updates_since_returns() { return self::check_updates_returns(); } + + /** + * Parameters for function edit_module() + * + * @since Moodle 3.3 + * @return external_function_parameters + */ + public static function edit_module_parameters() { + return new external_function_parameters( + array( + 'action' => new external_value(PARAM_ALPHA, + 'action: hide, show, stealth, duplicate, delete, moveleft, moveright, group...', VALUE_REQUIRED), + 'id' => new external_value(PARAM_INT, 'course module id', VALUE_REQUIRED), + 'sectionreturn' => new external_value(PARAM_INT, 'section to return to', VALUE_DEFAULT, null), + )); + } + + /** + * Performs one of the edit module actions and return new html for AJAX + * + * Returns html to replace the current module html with, for example: + * - empty string for "delete" action, + * - two modules html for "duplicate" action + * - updated module html for everything else + * + * Throws exception if operation is not permitted/possible + * + * @since Moodle 3.3 + * @param string $action + * @param int $id + * @param null|int $sectionreturn + * @return string + */ + public static function edit_module($action, $id, $sectionreturn = null) { + global $PAGE, $DB; + // Validate and normalize parameters. + $params = self::validate_parameters(self::edit_module_parameters(), + array('action' => $action, 'id' => $id, 'sectionreturn' => $sectionreturn)); + $action = $params['action']; + $id = $params['id']; + $sectionreturn = $params['sectionreturn']; + + list($course, $cm) = get_course_and_cm_from_cmid($id); + $modcontext = context_module::instance($cm->id); + $coursecontext = context_course::instance($course->id); + self::validate_context($modcontext); + $courserenderer = $PAGE->get_renderer('core', 'course'); + $completioninfo = new completion_info($course); + + switch($action) { + case 'hide': + case 'show': + case 'stealth': + require_capability('moodle/course:activityvisibility', $modcontext); + $visible = ($action === 'hide') ? 0 : 1; + $visibleoncoursepage = ($action === 'stealth') ? 0 : 1; + set_coursemodule_visible($id, $visible, $visibleoncoursepage); + \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger(); + break; + case 'duplicate': + require_capability('moodle/course:manageactivities', $coursecontext); + require_capability('moodle/backup:backuptargetimport', $coursecontext); + require_capability('moodle/restore:restoretargetimport', $coursecontext); + if (!course_allowed_module($course, $cm->modname)) { + throw new moodle_exception('No permission to create that activity'); + } + if ($newcm = duplicate_module($course, $cm)) { + $cm = get_fast_modinfo($course)->get_cm($id); + $newcm = get_fast_modinfo($course)->get_cm($newcm->id); + return $courserenderer->course_section_cm_list_item($course, $completioninfo, $cm, $sectionreturn) . + $courserenderer->course_section_cm_list_item($course, $completioninfo, $newcm, $sectionreturn); + } + break; + case 'groupsseparate': + case 'groupsvisible': + case 'groupsnone': + require_capability('moodle/course:manageactivities', $modcontext); + if ($action === 'groupsseparate') { + $newgroupmode = SEPARATEGROUPS; + } else if ($action === 'groupsvisible') { + $newgroupmode = VISIBLEGROUPS; + } else { + $newgroupmode = NOGROUPS; + } + if (set_coursemodule_groupmode($cm->id, $newgroupmode)) { + \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger(); + } + break; + case 'moveleft': + case 'moveright': + require_capability('moodle/course:manageactivities', $modcontext); + $indent = $cm->indent + (($action === 'moveright') ? 1 : -1); + if ($cm->indent >= 0) { + $DB->update_record('course_modules', array('id' => $cm->id, 'indent' => $indent)); + rebuild_course_cache($cm->course); + } + break; + case 'delete': + require_capability('moodle/course:manageactivities', $modcontext); + course_delete_module($cm->id, true); + return ''; + default: + throw new coding_exception('Unrecognised action'); + } + + $cm = get_fast_modinfo($course)->get_cm($id); + return $courserenderer->course_section_cm_list_item($course, $completioninfo, $cm, $sectionreturn); + } + + /** + * Return structure for edit_module() + * + * @since Moodle 3.3 + * @return external_description + */ + public static function edit_module_returns() { + return new external_value(PARAM_RAW, 'html to replace the current module with'); + } + + /** + * Parameters for function get_module() + * + * @since Moodle 3.3 + * @return external_function_parameters + */ + public static function get_module_parameters() { + return new external_function_parameters( + array( + 'id' => new external_value(PARAM_INT, 'course module id', VALUE_REQUIRED), + 'sectionreturn' => new external_value(PARAM_INT, 'section to return to', VALUE_DEFAULT, null), + )); + } + + /** + * Returns html for displaying one activity module on course page + * + * @since Moodle 3.3 + * @param int $id + * @param null|int $sectionreturn + * @return string + */ + public static function get_module($id, $sectionreturn = null) { + global $PAGE; + // Validate and normalize parameters. + $params = self::validate_parameters(self::get_module_parameters(), + array('id' => $id, 'sectionreturn' => $sectionreturn)); + $id = $params['id']; + $sectionreturn = $params['sectionreturn']; + + // Validate access to the course (note, this is html for the course view page, we don't validate access to the module). + list($course, $cm) = get_course_and_cm_from_cmid($id); + self::validate_context(context_course::instance($course->id)); + + $courserenderer = $PAGE->get_renderer('core', 'course'); + $completioninfo = new completion_info($course); + return $courserenderer->course_section_cm_list_item($course, $completioninfo, $cm, $sectionreturn); + } + + /** + * Return structure for edit_module() + * + * @since Moodle 3.3 + * @return external_description + */ + public static function get_module_returns() { + return new external_value(PARAM_RAW, 'html to replace the current module with'); + } + + /** + * Parameters for function edit_section() + * + * @since Moodle 3.3 + * @return external_function_parameters + */ + public static function edit_section_parameters() { + return new external_function_parameters( + array( + 'action' => new external_value(PARAM_ALPHA, 'action: hide, show, stealth, setmarker, removemarker', VALUE_REQUIRED), + 'id' => new external_value(PARAM_INT, 'course section id', VALUE_REQUIRED), + 'sectionreturn' => new external_value(PARAM_INT, 'section to return to', VALUE_DEFAULT, null), + )); + } + + /** + * Performs one of the edit section actions + * + * @since Moodle 3.3 + * @param string $action + * @param int $id section id + * @param int $sectionreturn section to return to + * @return string + */ + public static function edit_section($action, $id, $sectionreturn) { + global $DB; + // Validate and normalize parameters. + $params = self::validate_parameters(self::edit_section_parameters(), + array('action' => $action, 'id' => $id, 'sectionreturn' => $sectionreturn)); + $action = $params['action']; + $id = $params['id']; + $sr = $params['sectionreturn']; + + $section = $DB->get_record('course_sections', array('id' => $id), '*', MUST_EXIST); + $coursecontext = context_course::instance($section->course); + self::validate_context($coursecontext); + + $rv = course_get_format($section->course)->section_action($section, $action, $sectionreturn); + if ($rv) { + return json_encode($rv); + } else { + return null; + } + } + + /** + * Return structure for edit_section() + * + * @since Moodle 3.3 + * @return external_description + */ + public static function edit_section_returns() { + return new external_value(PARAM_RAW, 'Additional data for javascript (JSON-encoded string)'); + } } diff --git a/lib/db/services.php b/lib/db/services.php index afabd035e87..453d5f7b3b1 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -250,6 +250,30 @@ $functions = array( 'type' => 'read', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), + 'core_course_get_module' => array( + 'classname' => 'core_course_external', + 'methodname' => 'get_module', + 'classpath' => 'course/externallib.php', + 'description' => 'Returns html with one activity module on course page', + 'type' => 'read', + 'ajax' => true, + ), + 'core_course_edit_module' => array( + 'classname' => 'core_course_external', + 'methodname' => 'edit_module', + 'classpath' => 'course/externallib.php', + 'description' => 'Performs an action on course module (change visibility, duplicate, delete, etc.)', + 'type' => 'write', + 'ajax' => true, + ), + 'core_course_edit_section' => array( + 'classname' => 'core_course_external', + 'methodname' => 'edit_section', + 'classpath' => 'course/externallib.php', + 'description' => 'Performs an action on course section (change visibility, set marker, delete)', + 'type' => 'write', + 'ajax' => true, + ), 'core_course_get_courses' => array( 'classname' => 'core_course_external', 'methodname' => 'get_courses', From 4b6728e431d4384b2498e3fd6249110d6bc30232 Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Fri, 3 Feb 2017 09:40:02 +0800 Subject: [PATCH 4/6] MDL-4782 course: substitute YUI with AMD and rest.php with calling WS --- course/amd/build/actions.min.js | 1 + course/amd/src/actions.js | 594 +++++++++++ course/rest.php | 145 +-- .../moodle-course-dragdrop-debug.js | 3 +- .../moodle-course-dragdrop-min.js | 4 +- .../moodle-course-dragdrop.js | 3 +- .../moodle-course-toolboxes-debug.js | 947 ------------------ .../moodle-course-toolboxes-min.js | 2 - .../moodle-course-toolboxes.js | 947 ------------------ course/yui/src/dragdrop/js/resource.js | 3 +- course/yui/src/toolboxes/build.json | 12 - course/yui/src/toolboxes/js/resource.js | 552 ---------- course/yui/src/toolboxes/js/section.js | 189 ---- course/yui/src/toolboxes/js/shared.js | 0 course/yui/src/toolboxes/js/toolbox.js | 201 ---- course/yui/src/toolboxes/meta/toolboxes.json | 13 - 16 files changed, 632 insertions(+), 2984 deletions(-) create mode 100644 course/amd/build/actions.min.js create mode 100644 course/amd/src/actions.js delete mode 100644 course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js delete mode 100644 course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js delete mode 100644 course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js delete mode 100644 course/yui/src/toolboxes/build.json delete mode 100644 course/yui/src/toolboxes/js/resource.js delete mode 100644 course/yui/src/toolboxes/js/section.js delete mode 100644 course/yui/src/toolboxes/js/shared.js delete mode 100644 course/yui/src/toolboxes/js/toolbox.js delete mode 100644 course/yui/src/toolboxes/meta/toolboxes.json diff --git a/course/amd/build/actions.min.js b/course/amd/build/actions.min.js new file mode 100644 index 00000000000..797a71ec81c --- /dev/null +++ b/course/amd/build/actions.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax","core/templates","core/notification","core/str","core/url","core/yui"],function(a,b,c,d,e,f,g){var h={EDITINPROGRESS:"editinprogress",SECTIONDRAGGABLE:"sectiondraggable",EDITINGMOVE:"editing_move"},i={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"};g.use("moodle-course-coursebase",function(){var a=M.course.format.get_section_selector();a&&(i.SECTIONLI=a)});var j=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},k=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},l=function(a){a.addClass(h.EDITINPROGRESS);var b=a.find(i.ACTIONAREA).get(0);if(b){var c=M.util.add_spinner(g,g.Node(b));return c.show(),c}return null},m=function(a){a.addClass(h.EDITINPROGRESS);var b=a.find(i.SECTIONACTIONMENU).get(0);if(b){var c=M.util.add_spinner(g,g.Node(b));return c.show(),c}return null},n=function(a){var b=M.util.add_lightbox(g,g.Node(a.get(0)));return b.show(),b},o=function(a,b,c){window.setTimeout(function(){a.removeClass(h.EDITINPROGRESS),b&&b.hide()},c)},p=function(a,b){a&&window.setTimeout(function(){a.hide()},b)},q=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+" "+i.MENU).one(i.TOGGLE);c&&c.simulate&&c.simulate("click")}},r=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(i.MENU).find(i.TOGGLE).focus()},s=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},t=function(c,e,f){var g,h=f.attr("data-keepopen"),j=f.attr("data-action"),k=l(c),m=b.call([{methodname:"core_course_edit_module",args:{id:e,action:j,sectionreturn:f.attr("data-sectionreturn")?f.attr("data-sectionreturn"):0}}],!0);"duplicate"===j&&(g=n(f.closest(i.SECTIONLI))),a.when.apply(a,m).done(function(b){var d=s(c);c.replaceWith(b),a("
"+b+"
").find(i.ACTIVITYLI).each(function(b){q(a(this).attr("id"),h),0===b&&(r(a(this).attr("id"),j),d=null)}),d&&d.focus(),o(c,k,400),p(g,400),c.trigger(a.Event("coursemoduleedited",{ajaxreturn:b,action:j}))}).fail(function(b){o(c,k),p(g);var e=a.Event("coursemoduleeditfailed",{exception:b,action:j});c.trigger(e),e.isDefaultPrevented()||d.exception(b)})},u=function(c,d,e){var f=l(c),g=b.call([{methodname:"core_course_get_module",args:{id:d,sectionreturn:e}}],!0);a.when.apply(a,g).done(function(a){o(c,f,400),z(a)}).fail(function(){o(c,f)})},v=function(a,b){var c=a.attr("class").match(/modtype_([^\s]*)/)[1],f=k(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)})})},w=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)})},x=function(a,b,c,d,g,h,i){a.find("img").attr("src",f.imageUrl(b,"core")),e.get_string(c,d).done(function(b){a.find("span.menu-action-text").html(b),a.attr("title",b)}),g&&e.get_string(g,h).done(function(b){a.attr("title",b)}),a.attr("data-action",i)},y=function(b,c,d,e){var f=c.attr("data-action");if("hide"===f||"show"===f){if("hide"===f?(b.addClass("hidden"),x(c,"i/show","showfromothers","format_"+e,null,null,"show")):(b.removeClass("hidden"),x(c,"i/hide","hidefromothers","format_"+e,null,null,"hide")),void 0!==d.modules)for(var g in d.modules)z(d.modules[g]);void 0!==d.section_availability&&b.find(".section_availability").first().replaceWith(d.section_availability)}else if("setmarker"===f){var h=a(i.SECTIONLI+".current"),j=h.find(i.SECTIONACTIONMENU+" a[data-action=removemarker]");h.removeClass("current"),x(j,"i/marker","highlight","core","markthistopic","core","setmarker"),b.addClass("current"),x(c,"i/marked","highlightoff","core","markedthistopic","core","removemarker")}else"removemarker"===f&&(b.removeClass("current"),x(c,"i/marker","highlight","core","markthistopic","core","setmarker"))},z=function(b){a("
"+b+"
").find(i.ACTIVITYLI).each(function(){var c=a(this).attr("id");a(i.ACTIVITYLI+"#"+c).replaceWith(b),q(c,!1)})},A=function(c,e,f,g){var h=f.attr("data-action"),j=f.attr("data-sectionreturn")?f.attr("data-sectionreturn"):0,k=m(c),l=b.call([{methodname:"core_course_edit_section",args:{id:e,action:h,sectionreturn:j}}],!0),q=n(c);a.when.apply(a,l).done(function(b){var d=a.parseJSON(b);o(c,k),p(q),c.find(i.SECTIONACTIONMENU).find(i.TOGGLE).focus();var e=a.Event("coursesectionedited",{ajaxreturn:d,action:h});c.trigger(e),e.isDefaultPrevented()||y(c,f,d,g)}).fail(function(b){o(c,k),p(q);var e=a.Event("coursesectioneditfailed",{exception:b,action:h});c.trigger(e),e.isDefaultPrevented()||d.exception(b)})};return g.use("moodle-course-coursebase",function(){M.course.coursebase.register_module({set_visibility_resource_ui:function(b){var c=a(b.element.getDOMNode()),d=j(c);if(d){var e=c.find("."+h.EDITINGMOVE).attr("data-sectionreturn");u(c,d,e)}}})}),{initCoursePage:function(b){a("body").on("click keypress",i.ACTIVITYLI+" "+i.ACTIVITYACTION+"[data-action]",function(b){if("keypress"!==b.type||13===b.keyCode){var c=a(this),d=c.closest(i.ACTIVITYLI),e=c.attr("data-action"),f=j(d);switch(e){case"moveleft":case"moveright":case"delete":case"duplicate":case"hide":case"stealth":case"show":case"groupsseparate":case"groupsvisible":case"groupsnone":break;default:return}f&&(b.preventDefault(),"delete"===e?v(d,function(){t(d,f,c)}):t(d,f,c))}}),a("body").on("click keypress",i.SECTIONLI+" "+i.SECTIONACTIONMENU+"[data-sectionid] a[data-action]",function(c){if("keypress"!==c.type||13===c.keyCode){var d=a(this),e=d.closest(i.SECTIONLI),f=d.closest(i.SECTIONACTIONMENU).attr("data-sectionid");c.preventDefault(),d.attr("data-confirm")?w(d.attr("data-confirm"),function(){A(e,f,d,b)}):A(e,f,d,b)}})},replaceSectionActionItem:function(a,b,c,d,e,f,g,h){var j=a.find(i.SECTIONACTIONMENU+" "+b);x(j,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 new file mode 100644 index 00000000000..5c4a72cdadb --- /dev/null +++ b/course/amd/src/actions.js @@ -0,0 +1,594 @@ +// 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 . + +/** + * Various actions on modules and sections in the editing mode - hiding, duplicating, deleting, etc. + * + * @module core_course/actions + * @package core + * @copyright 2016 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 3.3 + */ +define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/url', 'core/yui'], + function($, ajax, templates, notification, str, url, Y) { + var CSS = { + EDITINPROGRESS: 'editinprogress', + SECTIONDRAGGABLE: 'sectiondraggable', + EDITINGMOVE: 'editing_move' + }; + var SELECTOR = { + 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' + }; + + Y.use('moodle-course-coursebase', function() { + var courseformatselector = M.course.format.get_section_selector(); + if (courseformatselector) { + SELECTOR.SECTIONLI = courseformatselector; + } + }); + + /** + * Wrapper for Y.Moodle.core_course.util.cm.getId + * + * @param {JQuery} element + * @returns {Integer} + */ + var getModuleId = function(element) { + var id; + Y.use('moodle-course-util', function(Y) { + id = Y.Moodle.core_course.util.cm.getId(Y.Node(element.get(0))); + }); + return id; + }; + + /** + * Wrapper for Y.Moodle.core_course.util.cm.getName + * + * @param {JQuery} element + * @returns {String} + */ + var getModuleName = function(element) { + var name; + Y.use('moodle-course-util', function(Y) { + name = Y.Moodle.core_course.util.cm.getName(Y.Node(element.get(0))); + }); + return name; + }; + + /** + * Wrapper for M.util.add_spinner for an activity + * + * @param {JQuery} activity + * @returns {Node} + */ + var addActivitySpinner = function(activity) { + activity.addClass(CSS.EDITINPROGRESS); + var actionarea = activity.find(SELECTOR.ACTIONAREA).get(0); + if (actionarea) { + var spinner = M.util.add_spinner(Y, Y.Node(actionarea)); + spinner.show(); + return spinner; + } + return null; + }; + + /** + * Wrapper for M.util.add_spinner for a section + * + * @param {JQuery} sectionelement + * @returns {Node} + */ + var addSectionSpinner = function(sectionelement) { + sectionelement.addClass(CSS.EDITINPROGRESS); + var actionarea = sectionelement.find(SELECTOR.SECTIONACTIONMENU).get(0); + if (actionarea) { + var spinner = M.util.add_spinner(Y, Y.Node(actionarea)); + spinner.show(); + return spinner; + } + return null; + }; + + /** + * Wrapper for M.util.add_lightbox + * + * @param {JQuery} sectionelement + * @returns {Node} + */ + var addSectionLightbox = function(sectionelement) { + var lightbox = M.util.add_lightbox(Y, Y.Node(sectionelement.get(0))); + lightbox.show(); + return lightbox; + }; + + /** + * Removes the spinner element + * + * @param {JQuery} element + * @param {Node} spinner + * @param {Number} delay + */ + var removeSpinner = function(element, spinner, delay) { + window.setTimeout(function() { + element.removeClass(CSS.EDITINPROGRESS); + if (spinner) { + spinner.hide(); + } + }, delay); + }; + + /** + * Removes the lightbox element + * + * @param {Node} lightbox lighbox YUI element returned by addSectionLightbox + * @param {Number} delay + */ + var removeLightbox = function(lightbox, delay) { + if (lightbox) { + window.setTimeout(function() { + lightbox.hide(); + }, delay); + } + }; + + /** + * Initialise action menu for the element (section or module) + * + * @param {String} elementid CSS id attribute of the element + * @param {Boolean} openmenu whether to open menu - this can be used when re-initiating menu after indent action was pressed + */ + var initActionMenu = function(elementid, openmenu) { + // Initialise action menu in the new activity. + Y.use('moodle-course-coursebase', function() { + M.course.coursebase.invoke_function('setup_for_resource', '#' + elementid); + }); + if (M.core.actionmenu && M.core.actionmenu.newDOMNode) { + M.core.actionmenu.newDOMNode(Y.one('#' + elementid)); + } + // Open action menu if the original element had data-keepopen. + if (openmenu) { + // We must use YUI click simulate here so the toggle works in Clean theme. This toggle is not + // needed in Boost because we use standard bootstrapbase action menu. + var toggle = Y.one('#' + elementid + ' ' + SELECTOR.MENU).one(SELECTOR.TOGGLE); + if (toggle && toggle.simulate) { + toggle.simulate('click'); + } + } + }; + + /** + * Returns focus to the element that was clicked or "Edit" link if element is no longer visible. + * + * @param {String} elementId CSS id attribute of the element + * @param {String} action data-action property of the element that was clicked + */ + var focusActionItem = function(elementId, action) { + var mainelement = $('#' + elementId); + var selector = '[data-action=' + action + ']'; + if (action === 'groupsseparate' || action === 'groupsvisible' || action === 'groupsnone') { + // New element will have different data-action. + selector = '[data-action=groupsseparate],[data-action=groupsvisible],[data-action=groupsnone]'; + } + if (mainelement.find(selector).is(':visible')) { + mainelement.find(selector).focus(); + } else { + // Element not visible, focus the "Edit" link. + mainelement.find(SELECTOR.MENU).find(SELECTOR.TOGGLE).focus(); + } + }; + + /** + * Find next after the element + * + * @param {JQuery} mainElement element that is about to be deleted + * @returns {JQuery} + */ + var findNextFocusable = function(mainElement) { + var tabables = $("a:visible"); + var isInside = false, foundElement = null; + tabables.each(function() { + if ($.contains(mainElement[0], this)) { + isInside = true; + } else if (isInside) { + foundElement = this; + return false; // Returning false in .each() is equivalent to "break;" inside the loop in php. + } + }); + return foundElement; + }; + + /** + * Performs an action on a module (moving, deleting, duplicating, hiding, etc.) + * + * @param {JQuery} moduleElement activity element we perform action on + * @param {Number} cmid + * @param {JQuery} target the element (menu item) that was clicked + */ + var editModule = function(moduleElement, cmid, target) { + var keepopen = target.attr('data-keepopen'), + action = target.attr('data-action'); + var spinner = addActivitySpinner(moduleElement); + var promises = ajax.call([{ + methodname: 'core_course_edit_module', + args: {id: cmid, + action: action, + sectionreturn: target.attr('data-sectionreturn') ? target.attr('data-sectionreturn') : 0 + } + }], true); + + var lightbox; + if (action === 'duplicate') { + lightbox = addSectionLightbox(target.closest(SELECTOR.SECTIONLI)); + } + $.when.apply($, promises) + .done(function(data) { + var elementToFocus = findNextFocusable(moduleElement); + moduleElement.replaceWith(data); + // Initialise action menu for activity(ies) added as a result of this. + $('
' + data + '
').find(SELECTOR.ACTIVITYLI).each(function(index) { + initActionMenu($(this).attr('id'), keepopen); + if (index === 0) { + focusActionItem($(this).attr('id'), action); + elementToFocus = null; + } + }); + // In case of activity deletion focus the next focusable element. + if (elementToFocus) { + elementToFocus.focus(); + } + // Remove spinner and lightbox with a delay. + removeSpinner(moduleElement, spinner, 400); + removeLightbox(lightbox, 400); + // Trigger event that can be observed by course formats. + moduleElement.trigger($.Event('coursemoduleedited', {ajaxreturn: data, action: action})); + }).fail(function(ex) { + // Remove spinner and lightbox. + removeSpinner(moduleElement, spinner); + removeLightbox(lightbox); + // Trigger event that can be observed by course formats. + var e = $.Event('coursemoduleeditfailed', {exception: ex, action: action}); + moduleElement.trigger(e); + if (!e.isDefaultPrevented()) { + notification.exception(ex); + } + }); + }; + + /** + * Requests html for the module via WS core_course_get_module and updates the module on the course page + * + * Used after d&d of the module to another section + * + * @param {JQuery} activityElement + * @param {Number} cmid + * @param {Number} sectionreturn + */ + var refreshModule = function(activityElement, cmid, sectionreturn) { + var spinner = addActivitySpinner(activityElement); + var promises = ajax.call([{ + methodname: 'core_course_get_module', + args: {id: cmid, sectionreturn: sectionreturn} + }], true); + + $.when.apply($, promises) + .done(function(data) { + removeSpinner(activityElement, spinner, 400); + replaceActivityHtmlWith(data); + }).fail(function() { + removeSpinner(activityElement, spinner); + }); + }; + + /** + * Displays the delete confirmation to delete a module + * + * @param {JQuery} mainelement activity element we perform action on + * @param {function} onconfirm function to execute on confirm + */ + var confirmDeleteModule = function(mainelement, onconfirm) { + var modtypename = mainelement.attr('class').match(/modtype_([^\s]*)/)[1]; + var modulename = getModuleName(mainelement); + + str.get_string('pluginname', modtypename).done(function(pluginname) { + var plugindata = { + type: pluginname, + name: modulename + }; + str.get_strings([ + {key: 'confirm'}, + {key: modulename === null ? 'deletechecktype' : 'deletechecktypename', param: plugindata}, + {key: 'yes'}, + {key: 'no'} + ]).done(function(s) { + notification.confirm(s[0], s[1], s[2], s[3], onconfirm); + } + ); + }); + }; + + /** + * Displays the delete confirmation to delete a section + * + * @param {String} message confirmation message + * @param {function} onconfirm function to execute on confirm + */ + var confirmEditSection = function(message, onconfirm) { + str.get_strings([ + {key: 'confirm'}, // TODO link text + {key: 'yes'}, + {key: 'no'} + ]).done(function(s) { + notification.confirm(s[0], message, s[1], s[2], onconfirm); + } + ); + }; + + /** + * Replaces an action menu item with another one (for example Show->Hide, Set marker->Remove marker) + * + * @param {JQuery} actionitem + * @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} newaction new value for data-action attribute of the link + */ + var replaceActionItem = function(actionitem, image, stringname, + stringcomponent, titlestr, titlecomponent, newaction) { + actionitem.find('img').attr('src', url.imageUrl(image, 'core')); + str.get_string(stringname, stringcomponent).done(function(newstring) { + actionitem.find('span.menu-action-text').html(newstring); + actionitem.attr('title', newstring); + }); + if (titlestr) { + str.get_string(titlestr, titlecomponent).done(function(newtitle) { + actionitem.attr('title', newtitle); + }); + } + actionitem.attr('data-action', newaction); + }; + + /** + * Default post-processing for section AJAX edit actions. + * + * This can be overridden in course formats by listening to event coursesectionedited: + * + * $('body').on('coursesectionedited', 'li.section', function(e) { + * var action = e.action, + * sectionElement = $(e.target), + * data = e.ajaxreturn; + * // ... Do some processing here. + * e.preventDefault(); // Prevent default handler. + * }); + * + * @param {JQuery} sectionElement + * @param {JQuery} actionItem + * @param {Object} data + * @param {String} courseformat + */ + var defaultEditSectionHandler = function(sectionElement, actionItem, data, courseformat) { + var action = actionItem.attr('data-action'); + if (action === 'hide' || action === 'show') { + if (action === 'hide') { + sectionElement.addClass('hidden'); + replaceActionItem(actionItem, 'i/show', + 'showfromothers', 'format_' + courseformat, null, null, 'show'); + } else { + sectionElement.removeClass('hidden'); + replaceActionItem(actionItem, 'i/hide', + 'hidefromothers', 'format_' + courseformat, null, null, 'hide'); + } + // Replace the modules with new html (that indicates that they are now hidden or not hidden). + if (data.modules !== undefined) { + for (var i in data.modules) { + replaceActivityHtmlWith(data.modules[i]); + } + } + // Replace the section availability information. + if (data.section_availability !== undefined) { + sectionElement.find('.section_availability').first().replaceWith(data.section_availability); + } + } else if (action === 'setmarker') { + var oldmarker = $(SELECTOR.SECTIONLI + '.current'), + oldActionItem = oldmarker.find(SELECTOR.SECTIONACTIONMENU + ' ' + 'a[data-action=removemarker]'); + oldmarker.removeClass('current'); + replaceActionItem(oldActionItem, 'i/marker', + 'highlight', 'core', 'markthistopic', 'core', 'setmarker'); + sectionElement.addClass('current'); + replaceActionItem(actionItem, 'i/marked', + 'highlightoff', 'core', 'markedthistopic', 'core', 'removemarker'); + } else if (action === 'removemarker') { + sectionElement.removeClass('current'); + replaceActionItem(actionItem, 'i/marker', + 'highlight', 'core', 'markthistopic', 'core', 'setmarker'); + } + }; + + /** + * Replaces the course module with the new html (used to update module after it was edited or its visibility was changed). + * + * @param {String} activityHTML + */ + var replaceActivityHtmlWith = function(activityHTML) { + $('
' + activityHTML + '
').find(SELECTOR.ACTIVITYLI).each(function() { + // Extract id from the new activity html. + var id = $(this).attr('id'); + // Find the existing element with the same id and replace its contents with new html. + $(SELECTOR.ACTIVITYLI + '#' + id).replaceWith(activityHTML); + // Initialise action menu. + initActionMenu(id, false); + }); + }; + + /** + * Performs an action on a module (moving, deleting, duplicating, hiding, etc.) + * + * @param {JQuery} sectionElement section element we perform action on + * @param {Nunmber} sectionid + * @param {JQuery} target the element (menu item) that was clicked + * @param {String} courseformat + */ + var editSection = function(sectionElement, sectionid, target, courseformat) { + var action = target.attr('data-action'), + sectionreturn = target.attr('data-sectionreturn') ? target.attr('data-sectionreturn') : 0; + var spinner = addSectionSpinner(sectionElement); + var promises = ajax.call([{ + methodname: 'core_course_edit_section', + args: {id: sectionid, action: action, sectionreturn: sectionreturn} + }], true); + + var lightbox = addSectionLightbox(sectionElement); + $.when.apply($, promises) + .done(function(dataencoded) { + var data = $.parseJSON(dataencoded); + removeSpinner(sectionElement, spinner); + removeLightbox(lightbox); + sectionElement.find(SELECTOR.SECTIONACTIONMENU).find(SELECTOR.TOGGLE).focus(); + // Trigger event that can be observed by course formats. + var e = $.Event('coursesectionedited', {ajaxreturn: data, action: action}); + sectionElement.trigger(e); + if (!e.isDefaultPrevented()) { + defaultEditSectionHandler(sectionElement, target, data, courseformat); + } + }).fail(function(ex) { + // Remove spinner and lightbox. + removeSpinner(sectionElement, spinner); + removeLightbox(lightbox); + // Trigger event that can be observed by course formats. + var e = $.Event('coursesectioneditfailed', {exception: ex, action: action}); + sectionElement.trigger(e); + if (!e.isDefaultPrevented()) { + notification.exception(ex); + } + }); + }; + + // Register a function to be executed after D&D of an activity. + Y.use('moodle-course-coursebase', function() { + M.course.coursebase.register_module({ + // Ignore camelcase eslint rule for the next line because it is an expected name of the callback. + // eslint-disable-next-line camelcase + set_visibility_resource_ui: function(args) { + var mainelement = $(args.element.getDOMNode()); + var cmid = getModuleId(mainelement); + if (cmid) { + var sectionreturn = mainelement.find('.' + CSS.EDITINGMOVE).attr('data-sectionreturn'); + refreshModule(mainelement, cmid, sectionreturn); + } + } + }); + }); + + return /** @alias module:core_course/actions */ { + + /** + * Initialises course page + * + * @method init + * @param {String} courseformat name of the current course format (for fetching strings) + */ + initCoursePage: function(courseformat) { + + // Add a handler for course module actions. + $('body').on('click keypress', SELECTOR.ACTIVITYLI + ' ' + + SELECTOR.ACTIVITYACTION + '[data-action]', function(e) { + if (e.type === 'keypress' && e.keyCode !== 13) { + return; + } + var actionItem = $(this), + moduleElement = actionItem.closest(SELECTOR.ACTIVITYLI), + action = actionItem.attr('data-action'), + moduleId = getModuleId(moduleElement); + switch (action) { + case 'moveleft': + case 'moveright': + case 'delete': + case 'duplicate': + case 'hide': + case 'stealth': + case 'show': + case 'groupsseparate': + case 'groupsvisible': + case 'groupsnone': + break; + default: + // Nothing to do here! + return; + } + if (!moduleId) { + return; + } + e.preventDefault(); + if (action === 'delete') { + // Deleting requires confirmation. + confirmDeleteModule(moduleElement, function() { + editModule(moduleElement, moduleId, actionItem); + }); + } else { + editModule(moduleElement, moduleId, actionItem); + } + }); + + // Add a handler for section show/hide actions. + $('body').on('click keypress', SELECTOR.SECTIONLI + ' ' + + SELECTOR.SECTIONACTIONMENU + '[data-sectionid] ' + + 'a[data-action]', function(e) { + if (e.type === 'keypress' && e.keyCode !== 13) { + return; + } + var actionItem = $(this), + sectionElement = actionItem.closest(SELECTOR.SECTIONLI), + sectionId = actionItem.closest(SELECTOR.SECTIONACTIONMENU).attr('data-sectionid'); + e.preventDefault(); + if (actionItem.attr('data-confirm')) { + // Action requires confirmation. + confirmEditSection(actionItem.attr('data-confirm'), function() { + editSection(sectionElement, sectionId, actionItem, courseformat); + }); + } else { + editSection(sectionElement, sectionId, actionItem, courseformat); + } + }); + }, + + /** + * Replaces a section action menu item with another one (for example Show->Hide, Set marker->Remove marker) + * + * This method can be used by course formats in their listener to the coursesectionedited event + * + * @param {JQuery} sectionelement + * @param {String} selector CSS selector inside the section element, for example "a[data-action=show]" + * @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} newaction new value for data-action attribute of the link + */ + replaceSectionActionItem: function(sectionelement, selector, image, stringname, + stringcomponent, titlestr, titlecomponent, newaction) { + var actionitem = sectionelement.find(SELECTOR.SECTIONACTIONMENU + ' ' + selector); + replaceActionItem(actionitem, image, stringname, stringcomponent, titlestr, titlecomponent, newaction); + } + }; + }); \ No newline at end of file diff --git a/course/rest.php b/course/rest.php index a63f6cef128..e7d5b2e83b3 100644 --- a/course/rest.php +++ b/course/rest.php @@ -33,17 +33,10 @@ require_once($CFG->dirroot.'/course/lib.php'); $courseid = required_param('courseId', PARAM_INT); $class = required_param('class', PARAM_ALPHA); $field = optional_param('field', '', PARAM_ALPHA); -$instanceid = optional_param('instanceId', 0, PARAM_INT); $sectionid = optional_param('sectionId', 0, PARAM_INT); $beforeid = optional_param('beforeId', 0, PARAM_INT); $value = optional_param('value', 0, PARAM_INT); -$column = optional_param('column', 0, PARAM_ALPHA); $id = optional_param('id', 0, PARAM_INT); -$summary = optional_param('summary', '', PARAM_RAW); -$sequence = optional_param('sequence', '', PARAM_SEQUENCE); -$visible = optional_param('visible', 0, PARAM_INT); -$pageaction = optional_param('action', '', PARAM_ALPHA); // Used to simulate a DELETE command -$title = optional_param('title', '', PARAM_TEXT); $PAGE->set_url('/course/rest.php', array('courseId'=>$courseid,'class'=>$class)); @@ -63,113 +56,33 @@ require_sesskey(); echo $OUTPUT->header(); // send headers -// OK, now let's process the parameters and do stuff -// MDL-10221 the DELETE method is not allowed on some web servers, so we simulate it with the action URL param -$requestmethod = $_SERVER['REQUEST_METHOD']; -if ($pageaction == 'DELETE') { - $requestmethod = 'DELETE'; -} - -switch($requestmethod) { - case 'POST': - - switch ($class) { - case 'section': - - if (!$DB->record_exists('course_sections', array('course'=>$course->id, 'section'=>$id))) { - throw new moodle_exception('AJAX commands.php: Bad Section ID '.$id); - } - - switch ($field) { - case 'visible': - require_capability('moodle/course:sectionvisibility', $coursecontext); - $resourcestotoggle = set_section_visible($course->id, $id, $value); - echo json_encode(array('resourcestotoggle' => $resourcestotoggle)); - break; - - case 'move': - require_capability('moodle/course:movesections', $coursecontext); - move_section_to($course, $id, $value); - // See if format wants to do something about it - $response = course_get_format($course)->ajax_section_move(); - if ($response !== null) { - echo json_encode($response); - } - break; - } - break; - - case 'resource': - switch ($field) { - case 'visible': - require_capability('moodle/course:activityvisibility', $modcontext); - set_coursemodule_visible($cm->id, $value); - \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger(); - break; - - case 'duplicate': - require_capability('moodle/course:manageactivities', $coursecontext); - require_capability('moodle/backup:backuptargetimport', $coursecontext); - require_capability('moodle/restore:restoretargetimport', $coursecontext); - if (!course_allowed_module($course, $cm->modname)) { - throw new moodle_exception('No permission to create that activity'); - } - $sr = optional_param('sr', null, PARAM_INT); - $result = mod_duplicate_activity($course, $cm, $sr); - echo json_encode($result); - break; - - case 'groupmode': - require_capability('moodle/course:manageactivities', $modcontext); - set_coursemodule_groupmode($cm->id, $value); - \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger(); - break; - - case 'indent': - require_capability('moodle/course:manageactivities', $modcontext); - $cm->indent = $value; - if ($cm->indent >= 0) { - $DB->update_record('course_modules', $cm); - rebuild_course_cache($cm->course); - } - break; - - case 'move': - require_capability('moodle/course:manageactivities', $modcontext); - if (!$section = $DB->get_record('course_sections', array('course'=>$course->id, 'section'=>$sectionid))) { - throw new moodle_exception('AJAX commands.php: Bad section ID '.$sectionid); - } - - if ($beforeid > 0){ - $beforemod = get_coursemodule_from_id('', $beforeid, $course->id); - $beforemod = $DB->get_record('course_modules', array('id'=>$beforeid)); - } else { - $beforemod = NULL; - } - - $isvisible = moveto_module($cm, $section, $beforemod); - echo json_encode(array('visible' => (bool) $isvisible)); - break; - } - break; - - case 'course': - switch($field) { - case 'marker': - require_capability('moodle/course:setcurrentsection', $coursecontext); - course_set_marker($course->id, $value); - break; - } - break; - } - break; - - case 'DELETE': - switch ($class) { - case 'resource': - require_capability('moodle/course:manageactivities', $modcontext); - course_delete_module($cm->id, true); - break; - } - break; +if ($class === 'section' && $field === 'move') { + if (!$DB->record_exists('course_sections', array('course' => $course->id, 'section' => $id))) { + throw new moodle_exception('AJAX commands.php: Bad Section ID ' . $id); + } + + require_capability('moodle/course:movesections', $coursecontext); + move_section_to($course, $id, $value); + // See if format wants to do something about it. + $response = course_get_format($course)->ajax_section_move(); + if ($response !== null) { + echo json_encode($response); + } + +} else if ($class === 'resource' && $field === 'move') { + + require_capability('moodle/course:manageactivities', $modcontext); + if (!$section = $DB->get_record('course_sections', array('course' => $course->id, 'section' => $sectionid))) { + throw new moodle_exception('AJAX commands.php: Bad section ID '.$sectionid); + } + + if ($beforeid > 0) { + $beforemod = get_coursemodule_from_id('', $beforeid, $course->id); + $beforemod = $DB->get_record('course_modules', array('id' => $beforeid)); + } else { + $beforemod = null; + } + + $isvisible = moveto_module($cm, $section, $beforemod); + echo json_encode(array('visible' => (bool) $isvisible)); } diff --git a/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-debug.js b/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-debug.js index b47c80f465f..8eed14d12cb 100644 --- a/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-debug.js +++ b/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-debug.js @@ -423,7 +423,8 @@ Y.extend(DRAGRESOURCE, M.core.dragdrop, { // Replace move icons var move = resourcesnode.one('a.' + CSS.EDITINGMOVE); if (move) { - move.replace(this.resourcedraghandle.cloneNode(true)); + var sr = move.getData('sr'); + move.replace(this.resourcedraghandle.cloneNode(true).setAttribute('data-sectionreturn', sr)); } }, this); }, diff --git a/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-min.js b/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-min.js index 65266a54c03..1dc79224e08 100644 --- a/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-min.js +++ b/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-min.js @@ -1,2 +1,2 @@ -YUI.add("moodle-course-dragdrop",function(e,t){var n={ACTIONAREA:".actions",ACTIVITY:"activity",ACTIVITYINSTANCE:"activityinstance",CONTENT:"content",COURSECONTENT:"course-content",EDITINGMOVE:"editing_move",ICONCLASS:"iconsmall",JUMPMENU:"jumpmenu",LEFT:"left",LIGHTBOX:"lightbox",MOVEDOWN:"movedown",MOVEUP:"moveup",PAGECONTENT:"page-content",RIGHT:"right",SECTION:"section",SECTIONADDMENUS:"section_add_menus",SECTIONHANDLE:"section-handle",SUMMARY:"summary",SECTIONDRAGGABLE:"sectiondraggable"};M.course=M.course||{};var r=function(){r.superclass.constructor.apply(this,arguments)};e.extend(r,M.core.dragdrop,{sectionlistselector:null,initializer:function(){this.groups=[n.SECTIONDRAGGABLE],this.samenodeclass=M.course.format.get_sectionwrapperclass(),this.parentnodeclass=M.course.format.get_containerclass();if(e.Node.one("."+n.JUMPMENU))return!1;this.sectionlistselector=M.course.format.get_section_wrapper(e);if(this.sectionlistselector){this.sectionlistselector="."+n.COURSECONTENT+" "+this.sectionlistselector,this.setup_for_section(this.sectionlistselector);var t=new e.DD.Delegate({container:"."+n.COURSECONTENT,nodes:"."+n.SECTIONDRAGGABLE,target:!0,handles:["."+n.LEFT],dragConfig:{groups:this.groups}});t.dd.plug(e.Plugin.DDProxy,{moveOnEnd:!1}),t.dd.plug(e.Plugin.DDConstrained,{constrain:"#"+n.PAGECONTENT,stickY:!0}),t.dd.plug(e.Plugin.DDWinScroll)}},setup_for_section:function(t){e.Node.all(t).each(function(t){var r=e.Moodle.core_course.util.section.getId(t);if(r>0){var i=t.one("."+n.RIGHT+" a."+n.MOVEDOWN),s=t.one("."+n.RIGHT+" a."+n.MOVEUP),o=M.util.get_string("movesection","moodle",r),u=t.one("."+n.LEFT);if((i||s)&&u){u.setStyle("cursor","move"),u.appendChild(this.get_drag_handle(o,n.SECTIONHANDLE,"icon",!0)),s&&(s.previous("br")?s.previous("br").remove():s.next("br")&&s.next("br").remove(),s.ancestor(".section_action_menu")&&s.ancestor().get("nodeName").toLowerCase()=="li"?s.ancestor().remove():s.remove());if(i){i.previous("br")?i.previous("br").remove():i.next("br")&&i.next("br").remove();var a=i.ancestor().get("nodeName").toLowerCase();i.ancestor(".section_action_menu")&&a=="li"?i.ancestor().remove():i.remove()}t.addClass(n.SECTIONDRAGGABLE)}}},this)},drag_start:function(t){var r=t.target,i=e.Node.create("<"+M.course.format.get_containernode()+">");i.addClass(M.course.format.get_containerclass());var s=e.Node.create("<"+M.course.format.get_sectionwrappernode()+">");s.addClass(M.course.format.get_sectionwrapperclass()),s.setStyle("margin",0),s.setContent(r.get("node").get("innerHTML")),i.appendChild(s),r.get("dragNode").setContent(i),r.get("dragNode").addClass(n.COURSECONTENT)},drag_dropmiss:function(e){this.drop_hit(e)},get_section_index:function(t){var r="."+n.COURSECONTENT+" "+M.course.format.get_section_selector(e),i=e.all(r),s=i.indexOf(t),o=i.indexOf(e.one("#section-0"));return s-o},drop_hit:function(t){var r=t.drag,i=r.get("node"),s=e.Moodle.core_course.util.section.getId(i),o=s,u=this.get_section_index(i),a=u;if(s===u)return;o>a&&(o=u,a=s),r.get("dragNode").removeClass(n.COURSECONTENT);var f=e.Node.all(this.sectionlistselector),l=M.util.add_lightbox(e,i),c={},h=this.get("config").pageparams,p;for(p in h){if(!h.hasOwnProperty(p))continue;c[p]=h[p]}c.sesskey=M.cfg.sesskey,c.courseId=this.get("courseid"),c["class"]="section",c.field="move",c.id=s,c.value=u;var d=M.cfg.wwwroot+this.get("ajaxurl");e.io(d,{method:"POST",data:c,on:{start:function(){l.show()},success:function(t,n){try{var r=e.JSON.parse(n.responseText);r.error&&new M.core.ajaxException(r),M.course.format.process_sections(e,f,r,o,a)}catch(i){}var s,u=!1;do{u=!1;for(s=o;s<=a;s++)if(e.Moodle.core_course.util.section.getId(f.item(s-1))>e.Moodle.core_course.util.section.getId(f.item(s))){var c=f.item(s-1).get("id");f.item(s-1).set("id",f.item(s).get("id")),f.item(s).set("id",c),M.course.format.swap_sections(e,s-1,s),u=!0}a-=1}while(u);window.setTimeout(function(){l.hide()},250)},failure:function(e,t){this.ajax_failure(t),l.hide()}},context:this})}},{NAME:"course-dragdrop-section",ATTRS:{courseid:{value:null},ajaxurl:{value:0},config:{value:0}}}),M.course=M.course||{},M.course.init_section_dragdrop=function(e){new r(e)};var i=function(){i.superclass.constructor.apply(this,arguments)};e.extend(i,M.core.dragdrop,{initializer:function(){this.groups=["resource"],this.samenodeclass=n.ACTIVITY,this.parentnodeclass=n.SECTION,this.resourcedraghandle=this.get_drag_handle(M.util.get_string("movecoursemodule","moodle"),n.EDITINGMOVE,n.ICONCLASS,!0),this.samenodelabel={identifier:"afterresource",component:"moodle"},this.parentnodelabel={identifier:"totopofsection",component:"moodle"};var t=M.course.format.get_section_selector(e);if(t){t="."+n.COURSECONTENT+" "+t,this.setup_for_section(t);var r=t.slice(n.COURSECONTENT.length+2)+" li."+n.ACTIVITY,i=new e.DD.Delegate({container:"."+n.COURSECONTENT,nodes:r,target:!0,handles:["."+n.EDITINGMOVE],dragConfig:{groups:this.groups}});i.dd.plug(e.Plugin.DDProxy,{moveOnEnd:!1,cloneNode:!0}),i.dd.plug(e.Plugin.DDConstrained,{constrain:"#"+n.PAGECONTENT}),i.dd.plug(e.Plugin.DDWinScroll),M.course.coursebase.register_module(this),M.course.dragres=this}},setup_for_section:function(t){e.Node.all(t).each(function(t){var r=t.one("."+n.CONTENT+" ul."+n.SECTION);r||(r=e.Node.create("
    "),r.addClass(n.SECTION),t.one("."+n.CONTENT+" div."+n.SUMMARY).insert(r,"after")),r.setAttribute("data-draggroups",this.groups.join(" ")),new e.DD.Drop({node:r,groups:this.groups,padding:"20 0 20 0"}),this.setup_for_resource("#"+t.get("id")+" li."+n.ACTIVITY)},this)},setup_for_resource:function(t){e.Node.all(t).each(function(t){var r=t.getData("draggroups");r||(t.setAttribute("data-draggroups",this.groups.join(" ")),new e.DD.Drop({node:t,groups:this.groups,padding:"20 0 20 0"}));var i=t.one("a."+n.EDITINGMOVE);i&&i.replace(this.resourcedraghandle.cloneNode(!0))},this)},drag_start:function(e){var t=e.target;t.get("dragNode").setContent( -t.get("node").get("innerHTML")),t.get("dragNode").all("img.iconsmall").setStyle("vertical-align","baseline")},drag_dropmiss:function(e){this.drop_hit(e)},drop_hit:function(t){var r=t.drag,i=r.get("node"),s=t.drop.get("node"),o=i.one(n.ACTIONAREA),u=M.util.add_spinner(e,o),a={},f=this.get("config").pageparams,l;for(l in f)a[l]=f[l];a.sesskey=M.cfg.sesskey,a.courseId=this.get("courseid"),a["class"]="resource",a.field="move",a.id=Number(e.Moodle.core_course.util.cm.getId(i)),a.sectionId=e.Moodle.core_course.util.section.getId(s.ancestor(M.course.format.get_section_wrapper(e),!0)),i.next()&&(a.beforeId=Number(e.Moodle.core_course.util.cm.getId(i.next())));var c=M.cfg.wwwroot+this.get("ajaxurl");e.io(c,{method:"POST",data:a,on:{start:function(){this.lock_drag_handle(r,n.EDITINGMOVE),u.show()},success:function(t,s){var o=e.JSON.parse(s.responseText),a={element:i,visible:o.visible};M.course.coursebase.invoke_function("set_visibility_resource_ui",a),this.unlock_drag_handle(r,n.EDITINGMOVE),window.setTimeout(function(){u.hide()},250)},failure:function(e,t){this.ajax_failure(t),this.unlock_drag_handle(r,n.SECTIONHANDLE),u.hide()}},context:this})}},{NAME:"course-dragdrop-resource",ATTRS:{courseid:{value:null},ajaxurl:{value:0},config:{value:0}}}),M.course=M.course||{},M.course.init_resource_dragdrop=function(e){new i(e)}},"@VERSION@",{requires:["base","node","io","dom","dd","dd-scroll","moodle-core-dragdrop","moodle-core-notification","moodle-course-coursebase","moodle-course-util"]}); +YUI.add("moodle-course-dragdrop",function(e,t){var n={ACTIONAREA:".actions",ACTIVITY:"activity",ACTIVITYINSTANCE:"activityinstance",CONTENT:"content",COURSECONTENT:"course-content",EDITINGMOVE:"editing_move",ICONCLASS:"iconsmall",JUMPMENU:"jumpmenu",LEFT:"left",LIGHTBOX:"lightbox",MOVEDOWN:"movedown",MOVEUP:"moveup",PAGECONTENT:"page-content",RIGHT:"right",SECTION:"section",SECTIONADDMENUS:"section_add_menus",SECTIONHANDLE:"section-handle",SUMMARY:"summary",SECTIONDRAGGABLE:"sectiondraggable"};M.course=M.course||{};var r=function(){r.superclass.constructor.apply(this,arguments)};e.extend(r,M.core.dragdrop,{sectionlistselector:null,initializer:function(){this.groups=[n.SECTIONDRAGGABLE],this.samenodeclass=M.course.format.get_sectionwrapperclass(),this.parentnodeclass=M.course.format.get_containerclass();if(e.Node.one("."+n.JUMPMENU))return!1;this.sectionlistselector=M.course.format.get_section_wrapper(e);if(this.sectionlistselector){this.sectionlistselector="."+n.COURSECONTENT+" "+this.sectionlistselector,this.setup_for_section(this.sectionlistselector);var t=new e.DD.Delegate({container:"."+n.COURSECONTENT,nodes:"."+n.SECTIONDRAGGABLE,target:!0,handles:["."+n.LEFT],dragConfig:{groups:this.groups}});t.dd.plug(e.Plugin.DDProxy,{moveOnEnd:!1}),t.dd.plug(e.Plugin.DDConstrained,{constrain:"#"+n.PAGECONTENT,stickY:!0}),t.dd.plug(e.Plugin.DDWinScroll)}},setup_for_section:function(t){e.Node.all(t).each(function(t){var r=e.Moodle.core_course.util.section.getId(t);if(r>0){var i=t.one("."+n.RIGHT+" a."+n.MOVEDOWN),s=t.one("."+n.RIGHT+" a."+n.MOVEUP),o=M.util.get_string("movesection","moodle",r),u=t.one("."+n.LEFT);if((i||s)&&u){u.setStyle("cursor","move"),u.appendChild(this.get_drag_handle(o,n.SECTIONHANDLE,"icon",!0)),s&&(s.previous("br")?s.previous("br").remove():s.next("br")&&s.next("br").remove(),s.ancestor(".section_action_menu")&&s.ancestor().get("nodeName").toLowerCase()=="li"?s.ancestor().remove():s.remove());if(i){i.previous("br")?i.previous("br").remove():i.next("br")&&i.next("br").remove();var a=i.ancestor().get("nodeName").toLowerCase();i.ancestor(".section_action_menu")&&a=="li"?i.ancestor().remove():i.remove()}t.addClass(n.SECTIONDRAGGABLE)}}},this)},drag_start:function(t){var r=t.target,i=e.Node.create("<"+M.course.format.get_containernode()+">");i.addClass(M.course.format.get_containerclass());var s=e.Node.create("<"+M.course.format.get_sectionwrappernode()+">");s.addClass(M.course.format.get_sectionwrapperclass()),s.setStyle("margin",0),s.setContent(r.get("node").get("innerHTML")),i.appendChild(s),r.get("dragNode").setContent(i),r.get("dragNode").addClass(n.COURSECONTENT)},drag_dropmiss:function(e){this.drop_hit(e)},get_section_index:function(t){var r="."+n.COURSECONTENT+" "+M.course.format.get_section_selector(e),i=e.all(r),s=i.indexOf(t),o=i.indexOf(e.one("#section-0"));return s-o},drop_hit:function(t){var r=t.drag,i=r.get("node"),s=e.Moodle.core_course.util.section.getId(i),o=s,u=this.get_section_index(i),a=u;if(s===u)return;o>a&&(o=u,a=s),r.get("dragNode").removeClass(n.COURSECONTENT);var f=e.Node.all(this.sectionlistselector),l=M.util.add_lightbox(e,i),c={},h=this.get("config").pageparams,p;for(p in h){if(!h.hasOwnProperty(p))continue;c[p]=h[p]}c.sesskey=M.cfg.sesskey,c.courseId=this.get("courseid"),c["class"]="section",c.field="move",c.id=s,c.value=u;var d=M.cfg.wwwroot+this.get("ajaxurl");e.io(d,{method:"POST",data:c,on:{start:function(){l.show()},success:function(t,n){try{var r=e.JSON.parse(n.responseText);r.error&&new M.core.ajaxException(r),M.course.format.process_sections(e,f,r,o,a)}catch(i){}var s,u=!1;do{u=!1;for(s=o;s<=a;s++)if(e.Moodle.core_course.util.section.getId(f.item(s-1))>e.Moodle.core_course.util.section.getId(f.item(s))){var c=f.item(s-1).get("id");f.item(s-1).set("id",f.item(s).get("id")),f.item(s).set("id",c),M.course.format.swap_sections(e,s-1,s),u=!0}a-=1}while(u);window.setTimeout(function(){l.hide()},250)},failure:function(e,t){this.ajax_failure(t),l.hide()}},context:this})}},{NAME:"course-dragdrop-section",ATTRS:{courseid:{value:null},ajaxurl:{value:0},config:{value:0}}}),M.course=M.course||{},M.course.init_section_dragdrop=function(e){new r(e)};var i=function(){i.superclass.constructor.apply(this,arguments)};e.extend(i,M.core.dragdrop,{initializer:function(){this.groups=["resource"],this.samenodeclass=n.ACTIVITY,this.parentnodeclass=n.SECTION,this.resourcedraghandle=this.get_drag_handle(M.util.get_string("movecoursemodule","moodle"),n.EDITINGMOVE,n.ICONCLASS,!0),this.samenodelabel={identifier:"afterresource",component:"moodle"},this.parentnodelabel={identifier:"totopofsection",component:"moodle"};var t=M.course.format.get_section_selector(e);if(t){t="."+n.COURSECONTENT+" "+t,this.setup_for_section(t);var r=t.slice(n.COURSECONTENT.length+2)+" li."+n.ACTIVITY,i=new e.DD.Delegate({container:"."+n.COURSECONTENT,nodes:r,target:!0,handles:["."+n.EDITINGMOVE],dragConfig:{groups:this.groups}});i.dd.plug(e.Plugin.DDProxy,{moveOnEnd:!1,cloneNode:!0}),i.dd.plug(e.Plugin.DDConstrained,{constrain:"#"+n.PAGECONTENT}),i.dd.plug(e.Plugin.DDWinScroll),M.course.coursebase.register_module(this),M.course.dragres=this}},setup_for_section:function(t){e.Node.all(t).each(function(t){var r=t.one("."+n.CONTENT+" ul."+n.SECTION);r||(r=e.Node.create("
      "),r.addClass(n.SECTION),t.one("."+n.CONTENT+" div."+n.SUMMARY).insert(r,"after")),r.setAttribute("data-draggroups",this.groups.join(" ")),new e.DD.Drop({node:r,groups:this.groups,padding:"20 0 20 0"}),this.setup_for_resource("#"+t.get("id")+" li."+n.ACTIVITY)},this)},setup_for_resource:function(t){e.Node.all(t).each(function(t){var r=t.getData("draggroups");r||(t.setAttribute("data-draggroups",this.groups.join(" ")),new e.DD.Drop({node:t,groups:this.groups,padding:"20 0 20 0"}));var i=t.one("a."+n.EDITINGMOVE);if(i){var s=i.getData("sr");i.replace(this.resourcedraghandle.cloneNode(!0).setAttribute("data-sectionreturn",s))}},this)},drag_start +:function(e){var t=e.target;t.get("dragNode").setContent(t.get("node").get("innerHTML")),t.get("dragNode").all("img.iconsmall").setStyle("vertical-align","baseline")},drag_dropmiss:function(e){this.drop_hit(e)},drop_hit:function(t){var r=t.drag,i=r.get("node"),s=t.drop.get("node"),o=i.one(n.ACTIONAREA),u=M.util.add_spinner(e,o),a={},f=this.get("config").pageparams,l;for(l in f)a[l]=f[l];a.sesskey=M.cfg.sesskey,a.courseId=this.get("courseid"),a["class"]="resource",a.field="move",a.id=Number(e.Moodle.core_course.util.cm.getId(i)),a.sectionId=e.Moodle.core_course.util.section.getId(s.ancestor(M.course.format.get_section_wrapper(e),!0)),i.next()&&(a.beforeId=Number(e.Moodle.core_course.util.cm.getId(i.next())));var c=M.cfg.wwwroot+this.get("ajaxurl");e.io(c,{method:"POST",data:a,on:{start:function(){this.lock_drag_handle(r,n.EDITINGMOVE),u.show()},success:function(t,s){var o=e.JSON.parse(s.responseText),a={element:i,visible:o.visible};M.course.coursebase.invoke_function("set_visibility_resource_ui",a),this.unlock_drag_handle(r,n.EDITINGMOVE),window.setTimeout(function(){u.hide()},250)},failure:function(e,t){this.ajax_failure(t),this.unlock_drag_handle(r,n.SECTIONHANDLE),u.hide()}},context:this})}},{NAME:"course-dragdrop-resource",ATTRS:{courseid:{value:null},ajaxurl:{value:0},config:{value:0}}}),M.course=M.course||{},M.course.init_resource_dragdrop=function(e){new i(e)}},"@VERSION@",{requires:["base","node","io","dom","dd","dd-scroll","moodle-core-dragdrop","moodle-core-notification","moodle-course-coursebase","moodle-course-util"]}); diff --git a/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop.js b/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop.js index bd057c87b34..0796caebdf7 100644 --- a/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop.js +++ b/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop.js @@ -419,7 +419,8 @@ Y.extend(DRAGRESOURCE, M.core.dragdrop, { // Replace move icons var move = resourcesnode.one('a.' + CSS.EDITINGMOVE); if (move) { - move.replace(this.resourcedraghandle.cloneNode(true)); + var sr = move.getData('sr'); + move.replace(this.resourcedraghandle.cloneNode(true).setAttribute('data-sectionreturn', sr)); } }, this); }, diff --git a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js b/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js deleted file mode 100644 index 2465ecc0f96..00000000000 --- a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js +++ /dev/null @@ -1,947 +0,0 @@ -YUI.add('moodle-course-toolboxes', function (Y, NAME) { - -/* eslint-disable no-unused-vars */ -/** - * Resource and activity toolbox class. - * - * This class is responsible for managing AJAX interactions with activities and resources - * when viewing a course in editing mode. - * - * @module moodle-course-toolboxes - * @namespace M.course.toolboxes - */ - -// The CSS classes we use. -var CSS = { - ACTIVITYINSTANCE: 'activityinstance', - AVAILABILITYINFODIV: 'div.availabilityinfo', - CONTENTWITHOUTLINK: 'contentwithoutlink', - CONDITIONALHIDDEN: 'conditionalhidden', - DIMCLASS: 'dimmed', - DIMMEDTEXT: 'dimmed_text', - EDITINSTRUCTIONS: 'editinstructions', - HIDE: 'hide', - MODINDENTCOUNT: 'mod-indent-', - MODINDENTHUGE: 'mod-indent-huge', - MODULEIDPREFIX: 'module-', - SECTIONHIDDENCLASS: 'hidden', - SECTIONIDPREFIX: 'section-', - SHOW: 'editing_show' - }, - // The CSS selectors we use. - SELECTOR = { - ACTIONAREA: '.actions', - ACTIONLINKTEXT: '.actionlinktext', - ACTIVITYACTION: 'a.cm-edit-action[data-action]', - ACTIVITYICON: 'img.activityicon', - ACTIVITYINSTANCE: '.' + CSS.ACTIVITYINSTANCE, - ACTIVITYLINK: '.' + CSS.ACTIVITYINSTANCE + ' > a, .' + CSS.ACTIVITYINSTANCE + - ' > span[data-inplaceeditable] > a:not([data-inplaceeditablelink])', - ACTIVITYLI: 'li.activity', - COMMANDSPAN: '.commands', - CONTENTAFTERLINK: 'div.contentafterlink', - CONTENTWITHOUTLINK: 'div.contentwithoutlink', - GROUPINGLABEL: '.' + CSS.ACTIVITYINSTANCE + ' .groupinglabel', - HIDE: 'a.editing_hide', - HIGHLIGHT: 'a.editing_highlight', - INSTANCENAME: 'span.instancename', - MODINDENTDIV: '.mod-indent', - MODINDENTOUTER: '.mod-indent-outer', - PAGECONTENT: 'body', - SECTIONLI: 'li.section', - SHOW: 'a.' + CSS.SHOW, - SHOWHIDE: 'a.editing_showhide' - }, - INDENTLIMITS = { - MIN: 0, - MAX: 16 - }, - BODY = Y.one(document.body); - -// Setup the basic namespace. -M.course = M.course || {}; - -/** - * The toolbox class is a generic class which should never be directly - * instantiated. Please extend it instead. - * - * @class toolbox - * @constructor - * @protected - * @extends Base - */ -var TOOLBOX = function() { - TOOLBOX.superclass.constructor.apply(this, arguments); -}; - -Y.extend(TOOLBOX, Y.Base, { - /** - * Send a request using the REST API - * - * @method send_request - * @param {Object} data The data to submit with the AJAX request - * @param {Node} [statusspinner] A statusspinner which may contain a section loader - * @param {Function} success_callback The callback to use on success - * @param {Object} [optionalconfig] Any additional configuration to submit - * @chainable - */ - send_request: function(data, statusspinner, success_callback, optionalconfig) { - // Default data structure - if (!data) { - data = {}; - } - // Handle any variables which we must pass back through to - var pageparams = this.get('config').pageparams, - varname; - for (varname in pageparams) { - data[varname] = pageparams[varname]; - } - - data.sesskey = M.cfg.sesskey; - data.courseId = this.get('courseid'); - - var uri = M.cfg.wwwroot + this.get('ajaxurl'); - - // Define the configuration to send with the request - var responsetext = []; - var config = { - method: 'POST', - data: data, - on: { - success: function(tid, response) { - try { - responsetext = Y.JSON.parse(response.responseText); - if (responsetext.error) { - new M.core.ajaxException(responsetext); - } - } catch (e) { - // Ignore. - } - - // Run the callback if we have one. - if (success_callback) { - Y.bind(success_callback, this, responsetext)(); - } - - if (statusspinner) { - window.setTimeout(function() { - statusspinner.hide(); - }, 400); - } - }, - failure: function(tid, response) { - if (statusspinner) { - statusspinner.hide(); - } - new M.core.ajaxException(response); - } - }, - context: this - }; - - // Apply optional config - if (optionalconfig) { - for (varname in optionalconfig) { - config[varname] = optionalconfig[varname]; - } - } - - if (statusspinner) { - statusspinner.show(); - } - - // Send the request - Y.io(uri, config); - return this; - } -}, -{ - NAME: 'course-toolbox', - ATTRS: { - /** - * The ID of the Moodle Course being edited. - * - * @attribute courseid - * @default 0 - * @type Number - */ - courseid: { - 'value': 0 - }, - - /** - * The Moodle course format. - * - * @attribute format - * @default 'topics' - * @type String - */ - format: { - 'value': 'topics' - }, - /** - * The URL to use when submitting requests. - * @attribute ajaxurl - * @default null - * @type String - */ - ajaxurl: { - 'value': null - }, - /** - * Any additional configuration passed when creating the instance. - * - * @attribute config - * @default {} - * @type Object - */ - config: { - 'value': {} - } - } -} -); -/* global TOOLBOX, BODY, SELECTOR, INDENTLIMITS */ - -/** - * Resource and activity toolbox class. - * - * This class is responsible for managing AJAX interactions with activities and resources - * when viewing a course in editing mode. - * - * @module moodle-course-toolboxes - * @namespace M.course.toolboxes - */ - -/** - * Resource and activity toolbox class. - * - * This is a class extending TOOLBOX containing code specific to resources - * - * This class is responsible for managing AJAX interactions with activities and resources - * when viewing a course in editing mode. - * - * @class resources - * @constructor - * @extends M.course.toolboxes.toolbox - */ -var RESOURCETOOLBOX = function() { - RESOURCETOOLBOX.superclass.constructor.apply(this, arguments); -}; - -Y.extend(RESOURCETOOLBOX, TOOLBOX, { - /** - * No groups are being used. - * - * @property GROUPS_NONE - * @protected - * @type Number - */ - GROUPS_NONE: 0, - - /** - * Separate groups are being used. - * - * @property GROUPS_SEPARATE - * @protected - * @type Number - */ - GROUPS_SEPARATE: 1, - - /** - * Visible groups are being used. - * - * @property GROUPS_VISIBLE - * @protected - * @type Number - */ - GROUPS_VISIBLE: 2, - - /** - * Initialize the resource toolbox - * - * For each activity the commands are updated and a reference to the activity is attached. - * This way it doesn't matter where the commands are going to called from they have a reference to the - * activity that they relate to. - * This is essential as some of the actions are displayed in an actionmenu which removes them from the - * page flow. - * - * This function also creates a single event delegate to manage all AJAX actions for all activities on - * the page. - * - * @method initializer - * @protected - */ - initializer: function() { - M.course.coursebase.register_module(this); - BODY.delegate('key', this.handle_data_action, 'down:enter', SELECTOR.ACTIVITYACTION, this); - Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this); - }, - - /** - * Handles the delegation event. When this is fired someone has triggered an action. - * - * Note not all actions will result in an AJAX enhancement. - * - * @protected - * @method handle_data_action - * @param {EventFacade} ev The event that was triggered. - * @return {boolean} - */ - handle_data_action: function(ev) { - // We need to get the anchor element that triggered this event. - var node = ev.target; - if (!node.test('a')) { - node = node.ancestor(SELECTOR.ACTIVITYACTION); - } - - // From the anchor we can get both the activity (added during initialisation) and the action being - // performed (added by the UI as a data attribute). - var action = node.getData('action'), - activity = node.ancestor(SELECTOR.ACTIVITYLI); - - if (!node.test('a') || !action || !activity) { - // It wasn't a valid action node. - return; - } - - // Switch based upon the action and do the desired thing. - switch (action) { - case 'moveleft': - case 'moveright': - // The user changing the indent of the activity. - this.change_indent(ev, node, activity, action); - break; - case 'delete': - // The user is deleting the activity. - this.delete_with_confirmation(ev, node, activity, action); - break; - case 'duplicate': - // The user is duplicating the activity. - this.duplicate(ev, node, activity, action); - break; - case 'hide': - case 'show': - // The user is changing the visibility of the activity. - this.change_visibility(ev, node, activity, action); - break; - case 'groupsseparate': - case 'groupsvisible': - case 'groupsnone': - // The user is changing the group mode. - this.change_groupmode(ev, node, activity, action); - break; - case 'move': - case 'update': - case 'assignroles': - break; - default: - // Nothing to do here! - break; - } - }, - - /** - * Add a loading icon to the specified activity. - * - * The icon is added within the action area. - * - * @method add_spinner - * @param {Node} activity The activity to add a loading icon to - * @return {Node|null} The newly created icon, or null if the action area was not found. - */ - add_spinner: function(activity) { - var actionarea = activity.one(SELECTOR.ACTIONAREA); - if (actionarea) { - return M.util.add_spinner(Y, actionarea); - } - return null; - }, - - /** - * Change the indent of the activity or resource. - * - * @method change_indent - * @protected - * @param {EventFacade} ev The event that was fired. - * @param {Node} button The button that triggered this action. - * @param {Node} activity The activity node that this action will be performed on. - * @param {String} action The action that has been requested. Will be 'moveleft' or 'moveright'. - */ - change_indent: function(ev, button, activity, action) { - // Prevent the default button action - ev.preventDefault(); - - var direction = (action === 'moveleft') ? -1 : 1; - - // And we need to determine the current and new indent level - var indentdiv = activity.one(SELECTOR.MODINDENTDIV), - indent = indentdiv.getAttribute('class').match(/mod-indent-(\d{1,})/), - oldindent = 0, - newindent; - - if (indent) { - oldindent = parseInt(indent[1], 10); - } - newindent = oldindent + parseInt(direction, 10); - - if (newindent < INDENTLIMITS.MIN || newindent > INDENTLIMITS.MAX) { - return; - } - - if (indent) { - indentdiv.removeClass(indent[0]); - } - - // Perform the move - indentdiv.addClass(CSS.MODINDENTCOUNT + newindent); - var data = { - 'class': 'resource', - 'field': 'indent', - 'value': newindent, - 'id': Y.Moodle.core_course.util.cm.getId(activity) - }; - var spinner = this.add_spinner(activity); - this.send_request(data, spinner); - - var remainingmove; - - // Handle removal/addition of the moveleft button. - if (newindent === INDENTLIMITS.MIN) { - button.addClass('hidden'); - remainingmove = activity.one('.editing_moveright'); - } else if (newindent > INDENTLIMITS.MIN && oldindent === INDENTLIMITS.MIN) { - button.ancestor('.menu').one('[data-action=moveleft]').removeClass('hidden'); - } - - if (newindent === INDENTLIMITS.MAX) { - button.addClass('hidden'); - remainingmove = activity.one('.editing_moveleft'); - } else if (newindent < INDENTLIMITS.MAX && oldindent === INDENTLIMITS.MAX) { - button.ancestor('.menu').one('[data-action=moveright]').removeClass('hidden'); - } - - // Handle massive indentation to match non-ajax display - var hashugeclass = indentdiv.hasClass(CSS.MODINDENTHUGE); - if (newindent > 15 && !hashugeclass) { - indentdiv.addClass(CSS.MODINDENTHUGE); - } else if (newindent <= 15 && hashugeclass) { - indentdiv.removeClass(CSS.MODINDENTHUGE); - } - - if (ev.type && ev.type === "key" && remainingmove) { - remainingmove.focus(); - } - }, - - /** - * Deletes the given activity or resource after confirmation. - * - * @protected - * @method delete_with_confirmation - * @param {EventFacade} ev The event that was fired. - * @param {Node} button The button that triggered this action. - * @param {Node} activity The activity node that this action will be performed on. - * @chainable - */ - delete_with_confirmation: function(ev, button, activity) { - // Prevent the default button action - ev.preventDefault(); - - // Get the element we're working on - var element = activity, - // Create confirm string (different if element has or does not have name) - confirmstring = '', - plugindata = { - type: M.util.get_string('pluginname', element.getAttribute('class').match(/modtype_([^\s]*)/)[1]) - }; - if (Y.Moodle.core_course.util.cm.getName(element) !== null) { - plugindata.name = Y.Moodle.core_course.util.cm.getName(element); - confirmstring = M.util.get_string('deletechecktypename', 'moodle', plugindata); - } else { - confirmstring = M.util.get_string('deletechecktype', 'moodle', plugindata); - } - - // Create the confirmation dialogue. - var confirm = new M.core.confirm({ - question: confirmstring, - modal: true, - visible: false - }); - confirm.show(); - - // If it is confirmed. - confirm.on('complete-yes', function() { - - // Actually remove the element. - element.remove(); - var data = { - 'class': 'resource', - 'action': 'DELETE', - 'id': Y.Moodle.core_course.util.cm.getId(element) - }; - this.send_request(data); - if (M.core.actionmenu && M.core.actionmenu.instance) { - M.core.actionmenu.instance.hideMenu(ev); - } - - }, this); - - return this; - }, - - /** - * Duplicates the activity. - * - * @method duplicate - * @protected - * @param {EventFacade} ev The event that was fired. - * @param {Node} button The button that triggered this action. - * @param {Node} activity The activity node that this action will be performed on. - * @chainable - */ - duplicate: function(ev, button, activity) { - // Prevent the default button action - ev.preventDefault(); - - // Get the element we're working on - var element = activity; - - // Add the lightbox. - var section = activity.ancestor(M.course.format.get_section_selector(Y)), - lightbox = M.util.add_lightbox(Y, section).show(); - - // Build and send the request. - var data = { - 'class': 'resource', - 'field': 'duplicate', - 'id': Y.Moodle.core_course.util.cm.getId(element), - 'sr': button.getData('sr') - }; - this.send_request(data, lightbox, function(response) { - var newcm = Y.Node.create(response.fullcontent); - - // Append to the section? - activity.insert(newcm, 'after'); - Y.use('moodle-course-coursebase', function() { - M.course.coursebase.invoke_function('setup_for_resource', newcm); - }); - if (M.core.actionmenu && M.core.actionmenu.newDOMNode) { - M.core.actionmenu.newDOMNode(newcm); - } - }); - return this; - }, - - /** - * Changes the visibility of this activity or resource. - * - * @method change_visibility - * @protected - * @param {EventFacade} ev The event that was fired. - * @param {Node} button The button that triggered this action. - * @param {Node} activity The activity node that this action will be performed on. - * @param {String} action The action that has been requested. - * @chainable - */ - change_visibility: function(ev, button, activity, action) { - // Prevent the default button action - ev.preventDefault(); - - // Get the element we're working on - var element = activity; - var value = this.handle_resource_dim(button, activity, action); - - // Send the request - var data = { - 'class': 'resource', - 'field': 'visible', - 'value': value, - 'id': Y.Moodle.core_course.util.cm.getId(element) - }; - var spinner = this.add_spinner(element); - this.send_request(data, spinner); - - return this; - }, - - /** - * Handles the UI aspect of dimming the activity or resource. - * - * @method handle_resource_dim - * @protected - * @param {Node} button The button that triggered the action. - * @param {Node} activity The activity node that this action will be performed on. - * @param {String} action 'show' or 'hide'. - * @return {Number} 1 if we changed to visible, 0 if we were hiding. - */ - handle_resource_dim: function(button, activity, action) { - var toggleclass = CSS.DIMCLASS, - dimarea = activity.one([ - SELECTOR.ACTIVITYLINK, - SELECTOR.CONTENTWITHOUTLINK - ].join(', ')), - availabilityinfo = activity.one(CSS.AVAILABILITYINFODIV), - nextaction = (action === 'hide') ? 'show' : 'hide', - buttontext = button.one('span'), - newstring = M.util.get_string(nextaction, 'moodle'), - buttonimg = button.one('img'); - - // Update button info. - buttonimg.setAttrs({ - 'src': M.util.image_url('t/' + nextaction) - }); - - if (Y.Lang.trim(button.getAttribute('title'))) { - button.setAttribute('title', newstring); - } - - if (Y.Lang.trim(buttonimg.getAttribute('alt'))) { - buttonimg.setAttribute('alt', newstring); - } - - button.replaceClass('editing_' + action, 'editing_' + nextaction); - button.setData('action', nextaction); - if (buttontext) { - buttontext.set('text', newstring); - } - - if (activity.one(SELECTOR.CONTENTWITHOUTLINK)) { - dimarea = activity.one(SELECTOR.CONTENTWITHOUTLINK); - toggleclass = CSS.DIMMEDTEXT; - } - - // If activity is conditionally hidden, then don't toggle. - if (!dimarea.hasClass(CSS.CONDITIONALHIDDEN)) { - if (action === 'hide') { - // Change the UI. - dimarea.addClass(toggleclass); - // We need to toggle dimming on the description too. - activity.all(SELECTOR.CONTENTAFTERLINK).addClass(CSS.DIMMEDTEXT); - activity.all(SELECTOR.GROUPINGLABEL).addClass(CSS.DIMMEDTEXT); - } else { - // Change the UI. - dimarea.removeClass(toggleclass); - // We need to toggle dimming on the description too. - activity.all(SELECTOR.CONTENTAFTERLINK).removeClass(CSS.DIMMEDTEXT); - activity.all(SELECTOR.GROUPINGLABEL).removeClass(CSS.DIMMEDTEXT); - } - } - // Toggle availablity info for conditional activities. - if (availabilityinfo) { - availabilityinfo.toggleClass(CSS.HIDE); - } - return (action === 'hide') ? 0 : 1; - }, - - /** - * Changes the groupmode of the activity to the next groupmode in the sequence. - * - * @method change_groupmode - * @protected - * @param {EventFacade} ev The event that was fired. - * @param {Node} button The button that triggered this action. - * @param {Node} activity The activity node that this action will be performed on. - * @chainable - */ - change_groupmode: function(ev, button, activity) { - // Prevent the default button action. - ev.preventDefault(); - - // Current Mode - var groupmode = parseInt(button.getData('nextgroupmode'), 10), - newtitle = '', - iconsrc = '', - newtitlestr, - data, - spinner, - nextgroupmode = groupmode + 1, - buttonimg = button.one('img'); - - if (nextgroupmode > 2) { - nextgroupmode = 0; - } - - if (groupmode === this.GROUPS_NONE) { - newtitle = 'groupsnone'; - iconsrc = M.util.image_url('i/groupn', 'moodle'); - } else if (groupmode === this.GROUPS_SEPARATE) { - newtitle = 'groupsseparate'; - iconsrc = M.util.image_url('i/groups', 'moodle'); - } else if (groupmode === this.GROUPS_VISIBLE) { - newtitle = 'groupsvisible'; - iconsrc = M.util.image_url('i/groupv', 'moodle'); - } - newtitlestr = M.util.get_string('clicktochangeinbrackets', 'moodle', M.util.get_string(newtitle, 'moodle')); - - // Change the UI - var oldAction = button.getData('action'); - button.replaceClass('editing_' + oldAction, 'editing_' + newtitle); - buttonimg.setAttrs({ - 'src': iconsrc - }); - if (Y.Lang.trim(button.getAttribute('title'))) { - button.setAttribute('title', newtitlestr).setData('action', newtitle).setData('nextgroupmode', nextgroupmode); - } - - if (Y.Lang.trim(buttonimg.getAttribute('alt'))) { - buttonimg.setAttribute('alt', newtitlestr); - } - - // And send the request - data = { - 'class': 'resource', - 'field': 'groupmode', - 'value': groupmode, - 'id': Y.Moodle.core_course.util.cm.getId(activity) - }; - - spinner = this.add_spinner(activity); - this.send_request(data, spinner); - return this; - }, - - /** - * Set the visibility of the specified resource to match the visible parameter. - * - * Note: This is not a toggle function and only changes the visibility - * in the browser (no ajax update is performed). - * - * @method set_visibility_resource_ui - * @param {object} args An object containing the required information to trigger a change. - * @param {Node} args.element The resource to toggle - * @param {Boolean} args.visible The target visibility - */ - set_visibility_resource_ui: function(args) { - var element = args.element, - buttonnode = element.one(SELECTOR.HIDE), - // By default we assume that the item is visible and we're going to hide it. - currentVisibility = true, - targetVisibility = false; - - if (!buttonnode) { - // If the buttonnode was not found, try to find the HIDE button - // and change the target visibility setting to false. - buttonnode = element.one(SELECTOR.SHOW); - currentVisibility = false; - targetVisibility = true; - } - - if (typeof args.visible !== 'undefined') { - // If we were provided with a visibility argument, use that instead. - targetVisibility = args.visible; - } - - // Only trigger a change if necessary. - if (currentVisibility !== targetVisibility) { - var action = 'hide'; - if (targetVisibility) { - action = 'show'; - } - - this.handle_resource_dim(buttonnode, element, action); - } - } -}, { - NAME: 'course-resource-toolbox', - ATTRS: { - } -}); - -M.course.resource_toolbox = null; -M.course.init_resource_toolbox = function(config) { - M.course.resource_toolbox = new RESOURCETOOLBOX(config); - return M.course.resource_toolbox; -}; -/* global SELECTOR, TOOLBOX */ - -/** - * Resource and activity toolbox class. - * - * This class is responsible for managing AJAX interactions with activities and resources - * when viewing a course in editing mode. - * - * @module moodle-course-toolboxes - * @namespace M.course.toolboxes - */ - -/** - * Section toolbox class. - * - * This class is responsible for managing AJAX interactions with sections - * when viewing a course in editing mode. - * - * @class section - * @constructor - * @extends M.course.toolboxes.toolbox - */ -var SECTIONTOOLBOX = function() { - SECTIONTOOLBOX.superclass.constructor.apply(this, arguments); -}; - -Y.extend(SECTIONTOOLBOX, TOOLBOX, { - /** - * Initialize the section toolboxes module. - * - * Updates all span.commands with relevant handlers and other required changes. - * - * @method initializer - * @protected - */ - initializer: function() { - M.course.coursebase.register_module(this); - - // Section Highlighting. - Y.delegate('click', this.toggle_highlight, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.HIGHLIGHT, this); - - // Section Visibility. - Y.delegate('click', this.toggle_hide_section, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.SHOWHIDE, this); - }, - - toggle_hide_section: function(e) { - // Prevent the default button action. - e.preventDefault(); - - // Get the section we're working on. - var section = e.target.ancestor(M.course.format.get_section_selector(Y)), - button = e.target.ancestor('a', true), - hideicon = button.one('img'), - buttontext = button.one('span'), - - // The value to submit - value, - - // The text for strings and images. Also determines the icon to display. - action, - nextaction; - - if (!section.hasClass(CSS.SECTIONHIDDENCLASS)) { - section.addClass(CSS.SECTIONHIDDENCLASS); - value = 0; - action = 'hide'; - nextaction = 'show'; - } else { - section.removeClass(CSS.SECTIONHIDDENCLASS); - value = 1; - action = 'show'; - nextaction = 'hide'; - } - - var newstring = M.util.get_string(nextaction + 'fromothers', 'format_' + this.get('format')); - hideicon.setAttrs({ - 'alt': newstring, - 'src': M.util.image_url('i/' + nextaction) - }); - button.set('title', newstring); - if (buttontext) { - buttontext.set('text', newstring); - } - - // Change the show/hide status - var data = { - 'class': 'section', - 'field': 'visible', - 'id': Y.Moodle.core_course.util.section.getId(section.ancestor(M.course.format.get_section_wrapper(Y), true)), - 'value': value - }; - - var lightbox = M.util.add_lightbox(Y, section); - lightbox.show(); - - this.send_request(data, lightbox, function(response) { - var activities = section.all(SELECTOR.ACTIVITYLI); - activities.each(function(node) { - var button; - if (node.one(SELECTOR.SHOW)) { - button = node.one(SELECTOR.SHOW); - } else { - button = node.one(SELECTOR.HIDE); - } - var activityid = Y.Moodle.core_course.util.cm.getId(node); - - // NOTE: resourcestotoggle is returned as a string instead - // of a Number so we must cast our activityid to a String. - if (Y.Array.indexOf(response.resourcestotoggle, "" + activityid) !== -1) { - M.course.resource_toolbox.handle_resource_dim(button, node, action); - } - }, this); - }); - }, - - /** - * Toggle highlighting the current section. - * - * @method toggle_highlight - * @param {EventFacade} e - */ - toggle_highlight: function(e) { - // Prevent the default button action. - e.preventDefault(); - - // Get the section we're working on. - var section = e.target.ancestor(M.course.format.get_section_selector(Y)); - var button = e.target.ancestor('a', true); - var buttonicon = button.one('img'); - var buttontext = button.one('span'); - - // Determine whether the marker is currently set. - var togglestatus = section.hasClass('current'); - var value = 0; - - // Set the current highlighted item text. - var old_string = M.util.get_string('markthistopic', 'moodle'); - - var selectedpage = Y.one(SELECTOR.PAGECONTENT); - selectedpage - .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT) - .set('title', old_string); - selectedpage - .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT + ' span') - .set('text', M.util.get_string('highlight', 'moodle')); - selectedpage - .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT + ' img') - .set('alt', old_string) - .set('src', M.util.image_url('i/marker')); - - // Remove the highlighting from all sections. - selectedpage.all(M.course.format.get_section_selector(Y)) - .removeClass('current'); - - // Then add it if required to the selected section. - if (!togglestatus) { - section.addClass('current'); - value = Y.Moodle.core_course.util.section.getId(section.ancestor(M.course.format.get_section_wrapper(Y), true)); - var new_string = M.util.get_string('markedthistopic', 'moodle'); - button - .set('title', new_string); - buttonicon - .set('alt', new_string) - .set('src', M.util.image_url('i/marked')); - if (buttontext) { - buttontext - .set('text', M.util.get_string('highlightoff', 'moodle')); - } - } - - // Change the highlight status. - var data = { - 'class': 'course', - 'field': 'marker', - 'value': value - }; - var lightbox = M.util.add_lightbox(Y, section); - lightbox.show(); - this.send_request(data, lightbox); - } -}, { - NAME: 'course-section-toolbox', - ATTRS: { - } -}); - -M.course.init_section_toolbox = function(config) { - return new SECTIONTOOLBOX(config); -}; - - -}, '@VERSION@', {"requires": ["node", "base", "event-key", "node", "io", "moodle-course-coursebase", "moodle-course-util"]}); diff --git a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js b/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js deleted file mode 100644 index 9a0d3302afc..00000000000 --- a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js +++ /dev/null @@ -1,2 +0,0 @@ -YUI.add("moodle-course-toolboxes",function(e,t){var n={ACTIVITYINSTANCE:"activityinstance",AVAILABILITYINFODIV:"div.availabilityinfo",CONTENTWITHOUTLINK:"contentwithoutlink",CONDITIONALHIDDEN:"conditionalhidden",DIMCLASS:"dimmed",DIMMEDTEXT:"dimmed_text",EDITINSTRUCTIONS:"editinstructions",HIDE:"hide",MODINDENTCOUNT:"mod-indent-",MODINDENTHUGE:"mod-indent-huge",MODULEIDPREFIX:"module-",SECTIONHIDDENCLASS:"hidden",SECTIONIDPREFIX:"section-",SHOW:"editing_show"},r={ACTIONAREA:".actions",ACTIONLINKTEXT:".actionlinktext",ACTIVITYACTION:"a.cm-edit-action[data-action]",ACTIVITYICON:"img.activityicon",ACTIVITYINSTANCE:"."+n.ACTIVITYINSTANCE,ACTIVITYLINK:"."+n.ACTIVITYINSTANCE+" > a, ."+n.ACTIVITYINSTANCE+" > span[data-inplaceeditable] > a:not([data-inplaceeditablelink])",ACTIVITYLI:"li.activity",COMMANDSPAN:".commands",CONTENTAFTERLINK:"div.contentafterlink",CONTENTWITHOUTLINK:"div.contentwithoutlink",GROUPINGLABEL:"."+n.ACTIVITYINSTANCE+" .groupinglabel",HIDE:"a.editing_hide",HIGHLIGHT:"a.editing_highlight",INSTANCENAME:"span.instancename",MODINDENTDIV:".mod-indent",MODINDENTOUTER:".mod-indent-outer",PAGECONTENT:"body",SECTIONLI:"li.section",SHOW:"a."+n.SHOW,SHOWHIDE:"a.editing_showhide"},i={MIN:0,MAX:16},s=e.one(document.body);M.course=M.course||{};var o=function(){o.superclass.constructor.apply(this,arguments)};e.extend(o,e.Base,{send_request:function(t,n,r,i){t||(t={});var s=this.get("config").pageparams,o;for(o in s)t[o]=s[o];t.sesskey=M.cfg.sesskey,t.courseId=this.get("courseid");var u=M.cfg.wwwroot+this.get("ajaxurl"),a=[],f={method:"POST",data:t,on:{success:function(t,i){try{a=e.JSON.parse(i.responseText),a.error&&new M.core.ajaxException(a)}catch(s){}r&&e.bind(r,this,a)(),n&&window.setTimeout(function(){n.hide()},400)},failure:function(e,t){n&&n.hide(),new M.core.ajaxException(t)}},context:this};if(i)for(o in i)f[o]=i[o];return n&&n.show(),e.io(u,f),this}},{NAME:"course-toolbox",ATTRS:{courseid:{value:0},format:{value:"topics"},ajaxurl:{value:null},config:{value:{}}}});var u=function(){u.superclass.constructor.apply(this,arguments)};e.extend(u,o,{GROUPS_NONE:0,GROUPS_SEPARATE:1,GROUPS_VISIBLE:2,initializer:function(){M.course.coursebase.register_module(this),s.delegate("key",this.handle_data_action,"down:enter",r.ACTIVITYACTION,this),e.delegate("click",this.handle_data_action,s,r.ACTIVITYACTION,this)},handle_data_action:function(e){var t=e.target;t.test("a")||(t=t.ancestor(r.ACTIVITYACTION));var n=t.getData("action"),i=t.ancestor(r.ACTIVITYLI);if(!t.test("a")||!n||!i)return;switch(n){case"moveleft":case"moveright":this.change_indent(e,t,i,n);break;case"delete":this.delete_with_confirmation(e,t,i,n);break;case"duplicate":this.duplicate(e,t,i,n);break;case"hide":case"show":this.change_visibility(e,t,i,n);break;case"groupsseparate":case"groupsvisible":case"groupsnone":this.change_groupmode(e,t,i,n);break;case"move":case"update":case"assignroles":break;default:}},add_spinner:function(t){var n=t.one(r.ACTIONAREA);return n?M.util.add_spinner(e,n):null},change_indent:function(t,s,o,u){t.preventDefault();var a=u==="moveleft"?-1:1,f=o.one(r.MODINDENTDIV),l=f.getAttribute("class").match(/mod-indent-(\d{1,})/),c=0,h;l&&(c=parseInt(l[1],10)),h=c+parseInt(a,10);if(hi.MAX)return;l&&f.removeClass(l[0]),f.addClass(n.MODINDENTCOUNT+h);var p={"class":"resource",field:"indent",value:h,id:e.Moodle.core_course.util.cm.getId(o)},d=this.add_spinner(o);this.send_request(p,d);var v;h===i.MIN?(s.addClass("hidden"),v=o.one(".editing_moveright")):h>i.MIN&&c===i.MIN&&s.ancestor(".menu").one("[data-action=moveleft]").removeClass("hidden"),h===i.MAX?(s.addClass("hidden"),v=o.one(".editing_moveleft")):h15&&!m?f.addClass(n.MODINDENTHUGE):h<=15&&m&&f.removeClass(n.MODINDENTHUGE),t.type&&t.type==="key"&&v&&v.focus()},delete_with_confirmation:function(t,n,r){t.preventDefault();var i=r,s="",o={type:M.util.get_string("pluginname",i.getAttribute("class").match(/modtype_([^\s]*)/)[1])};e.Moodle.core_course.util.cm.getName(i)!==null?(o.name=e.Moodle.core_course.util.cm.getName(i),s=M.util.get_string("deletechecktypename","moodle",o)):s=M.util.get_string("deletechecktype","moodle",o);var u=new M.core.confirm({question:s,modal:!0,visible:!1});return u.show(),u.on("complete-yes",function(){i.remove();var n={"class":"resource",action:"DELETE",id:e.Moodle.core_course.util.cm.getId(i)};this.send_request(n),M.core.actionmenu&&M.core.actionmenu.instance&&M.core.actionmenu.instance.hideMenu(t)},this),this},duplicate:function(t,n,r){t.preventDefault();var i=r,s=r.ancestor(M.course.format.get_section_selector(e)),o=M.util.add_lightbox(e,s).show(),u={"class":"resource",field:"duplicate",id:e.Moodle.core_course.util.cm.getId(i),sr:n.getData("sr")};return this.send_request(u,o,function(t){var n=e.Node.create(t.fullcontent);r.insert(n,"after"),e.use("moodle-course-coursebase",function(){M.course.coursebase.invoke_function("setup_for_resource",n)}),M.core.actionmenu&&M.core.actionmenu.newDOMNode&&M.core.actionmenu.newDOMNode(n)}),this},change_visibility:function(t,n,r,i){t.preventDefault();var s=r,o=this.handle_resource_dim(n,r,i),u={"class":"resource",field:"visible",value:o,id:e.Moodle.core_course.util.cm.getId(s)},a=this.add_spinner(s);return this.send_request(u,a),this},handle_resource_dim:function(t,i,s){var o=n.DIMCLASS,u=i.one([r.ACTIVITYLINK,r.CONTENTWITHOUTLINK].join(", ")),a=i.one(n.AVAILABILITYINFODIV),f=s==="hide"?"show":"hide",l=t.one("span"),c=M.util.get_string(f,"moodle"),h=t.one("img");return h.setAttrs({src:M.util.image_url("t/"+f)}),e.Lang.trim(t.getAttribute("title"))&&t.setAttribute("title",c),e.Lang.trim(h.getAttribute("alt"))&&h.setAttribute("alt",c),t.replaceClass("editing_"+s,"editing_"+f),t.setData("action",f),l&&l.set("text",c),i.one(r.CONTENTWITHOUTLINK)&&(u=i.one(r.CONTENTWITHOUTLINK),o=n.DIMMEDTEXT),u.hasClass(n.CONDITIONALHIDDEN)||(s==="hide"?(u.addClass(o),i.all(r.CONTENTAFTERLINK -).addClass(n.DIMMEDTEXT),i.all(r.GROUPINGLABEL).addClass(n.DIMMEDTEXT)):(u.removeClass(o),i.all(r.CONTENTAFTERLINK).removeClass(n.DIMMEDTEXT),i.all(r.GROUPINGLABEL).removeClass(n.DIMMEDTEXT))),a&&a.toggleClass(n.HIDE),s==="hide"?0:1},change_groupmode:function(t,n,r){t.preventDefault();var i=parseInt(n.getData("nextgroupmode"),10),s="",o="",u,a,f,l=i+1,c=n.one("img");l>2&&(l=0),i===this.GROUPS_NONE?(s="groupsnone",o=M.util.image_url("i/groupn","moodle")):i===this.GROUPS_SEPARATE?(s="groupsseparate",o=M.util.image_url("i/groups","moodle")):i===this.GROUPS_VISIBLE&&(s="groupsvisible",o=M.util.image_url("i/groupv","moodle")),u=M.util.get_string("clicktochangeinbrackets","moodle",M.util.get_string(s,"moodle"));var h=n.getData("action");return n.replaceClass("editing_"+h,"editing_"+s),c.setAttrs({src:o}),e.Lang.trim(n.getAttribute("title"))&&n.setAttribute("title",u).setData("action",s).setData("nextgroupmode",l),e.Lang.trim(c.getAttribute("alt"))&&c.setAttribute("alt",u),a={"class":"resource",field:"groupmode",value:i,id:e.Moodle.core_course.util.cm.getId(r)},f=this.add_spinner(r),this.send_request(a,f),this},set_visibility_resource_ui:function(e){var t=e.element,n=t.one(r.HIDE),i=!0,s=!1;n||(n=t.one(r.SHOW),i=!1,s=!0),typeof e.visible!="undefined"&&(s=e.visible);if(i!==s){var o="hide";s&&(o="show"),this.handle_resource_dim(n,t,o)}}},{NAME:"course-resource-toolbox",ATTRS:{}}),M.course.resource_toolbox=null,M.course.init_resource_toolbox=function(e){return M.course.resource_toolbox=new u(e),M.course.resource_toolbox};var a=function(){a.superclass.constructor.apply(this,arguments)};e.extend(a,o,{initializer:function(){M.course.coursebase.register_module(this),e.delegate("click",this.toggle_highlight,r.PAGECONTENT,r.SECTIONLI+" "+r.HIGHLIGHT,this),e.delegate("click",this.toggle_hide_section,r.PAGECONTENT,r.SECTIONLI+" "+r.SHOWHIDE,this)},toggle_hide_section:function(t){t.preventDefault();var i=t.target.ancestor(M.course.format.get_section_selector(e)),s=t.target.ancestor("a",!0),o=s.one("img"),u=s.one("span"),a,f,l;i.hasClass(n.SECTIONHIDDENCLASS)?(i.removeClass(n.SECTIONHIDDENCLASS),a=1,f="show",l="hide"):(i.addClass(n.SECTIONHIDDENCLASS),a=0,f="hide",l="show");var c=M.util.get_string(l+"fromothers","format_"+this.get("format"));o.setAttrs({alt:c,src:M.util.image_url("i/"+l)}),s.set("title",c),u&&u.set("text",c);var h={"class":"section",field:"visible",id:e.Moodle.core_course.util.section.getId(i.ancestor(M.course.format.get_section_wrapper(e),!0)),value:a},p=M.util.add_lightbox(e,i);p.show(),this.send_request(h,p,function(t){var n=i.all(r.ACTIVITYLI);n.each(function(n){var i;n.one(r.SHOW)?i=n.one(r.SHOW):i=n.one(r.HIDE);var s=e.Moodle.core_course.util.cm.getId(n);e.Array.indexOf(t.resourcestotoggle,""+s)!==-1&&M.course.resource_toolbox.handle_resource_dim(i,n,f)},this)})},toggle_highlight:function(t){t.preventDefault();var n=t.target.ancestor(M.course.format.get_section_selector(e)),i=t.target.ancestor("a",!0),s=i.one("img"),o=i.one("span"),u=n.hasClass("current"),a=0,f=M.util.get_string("markthistopic","moodle"),l=e.one(r.PAGECONTENT);l.all(M.course.format.get_section_selector(e)+".current "+r.HIGHLIGHT).set("title",f),l.all(M.course.format.get_section_selector(e)+".current "+r.HIGHLIGHT+" span").set("text",M.util.get_string("highlight","moodle")),l.all(M.course.format.get_section_selector(e)+".current "+r.HIGHLIGHT+" img").set("alt",f).set("src",M.util.image_url("i/marker")),l.all(M.course.format.get_section_selector(e)).removeClass("current");if(!u){n.addClass("current"),a=e.Moodle.core_course.util.section.getId(n.ancestor(M.course.format.get_section_wrapper(e),!0));var c=M.util.get_string("markedthistopic","moodle");i.set("title",c),s.set("alt",c).set("src",M.util.image_url("i/marked")),o&&o.set("text",M.util.get_string("highlightoff","moodle"))}var h={"class":"course",field:"marker",value:a},p=M.util.add_lightbox(e,n);p.show(),this.send_request(h,p)}},{NAME:"course-section-toolbox",ATTRS:{}}),M.course.init_section_toolbox=function(e){return new a(e)}},"@VERSION@",{requires:["node","base","event-key","node","io","moodle-course-coursebase","moodle-course-util"]}); diff --git a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js b/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js deleted file mode 100644 index 2465ecc0f96..00000000000 --- a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js +++ /dev/null @@ -1,947 +0,0 @@ -YUI.add('moodle-course-toolboxes', function (Y, NAME) { - -/* eslint-disable no-unused-vars */ -/** - * Resource and activity toolbox class. - * - * This class is responsible for managing AJAX interactions with activities and resources - * when viewing a course in editing mode. - * - * @module moodle-course-toolboxes - * @namespace M.course.toolboxes - */ - -// The CSS classes we use. -var CSS = { - ACTIVITYINSTANCE: 'activityinstance', - AVAILABILITYINFODIV: 'div.availabilityinfo', - CONTENTWITHOUTLINK: 'contentwithoutlink', - CONDITIONALHIDDEN: 'conditionalhidden', - DIMCLASS: 'dimmed', - DIMMEDTEXT: 'dimmed_text', - EDITINSTRUCTIONS: 'editinstructions', - HIDE: 'hide', - MODINDENTCOUNT: 'mod-indent-', - MODINDENTHUGE: 'mod-indent-huge', - MODULEIDPREFIX: 'module-', - SECTIONHIDDENCLASS: 'hidden', - SECTIONIDPREFIX: 'section-', - SHOW: 'editing_show' - }, - // The CSS selectors we use. - SELECTOR = { - ACTIONAREA: '.actions', - ACTIONLINKTEXT: '.actionlinktext', - ACTIVITYACTION: 'a.cm-edit-action[data-action]', - ACTIVITYICON: 'img.activityicon', - ACTIVITYINSTANCE: '.' + CSS.ACTIVITYINSTANCE, - ACTIVITYLINK: '.' + CSS.ACTIVITYINSTANCE + ' > a, .' + CSS.ACTIVITYINSTANCE + - ' > span[data-inplaceeditable] > a:not([data-inplaceeditablelink])', - ACTIVITYLI: 'li.activity', - COMMANDSPAN: '.commands', - CONTENTAFTERLINK: 'div.contentafterlink', - CONTENTWITHOUTLINK: 'div.contentwithoutlink', - GROUPINGLABEL: '.' + CSS.ACTIVITYINSTANCE + ' .groupinglabel', - HIDE: 'a.editing_hide', - HIGHLIGHT: 'a.editing_highlight', - INSTANCENAME: 'span.instancename', - MODINDENTDIV: '.mod-indent', - MODINDENTOUTER: '.mod-indent-outer', - PAGECONTENT: 'body', - SECTIONLI: 'li.section', - SHOW: 'a.' + CSS.SHOW, - SHOWHIDE: 'a.editing_showhide' - }, - INDENTLIMITS = { - MIN: 0, - MAX: 16 - }, - BODY = Y.one(document.body); - -// Setup the basic namespace. -M.course = M.course || {}; - -/** - * The toolbox class is a generic class which should never be directly - * instantiated. Please extend it instead. - * - * @class toolbox - * @constructor - * @protected - * @extends Base - */ -var TOOLBOX = function() { - TOOLBOX.superclass.constructor.apply(this, arguments); -}; - -Y.extend(TOOLBOX, Y.Base, { - /** - * Send a request using the REST API - * - * @method send_request - * @param {Object} data The data to submit with the AJAX request - * @param {Node} [statusspinner] A statusspinner which may contain a section loader - * @param {Function} success_callback The callback to use on success - * @param {Object} [optionalconfig] Any additional configuration to submit - * @chainable - */ - send_request: function(data, statusspinner, success_callback, optionalconfig) { - // Default data structure - if (!data) { - data = {}; - } - // Handle any variables which we must pass back through to - var pageparams = this.get('config').pageparams, - varname; - for (varname in pageparams) { - data[varname] = pageparams[varname]; - } - - data.sesskey = M.cfg.sesskey; - data.courseId = this.get('courseid'); - - var uri = M.cfg.wwwroot + this.get('ajaxurl'); - - // Define the configuration to send with the request - var responsetext = []; - var config = { - method: 'POST', - data: data, - on: { - success: function(tid, response) { - try { - responsetext = Y.JSON.parse(response.responseText); - if (responsetext.error) { - new M.core.ajaxException(responsetext); - } - } catch (e) { - // Ignore. - } - - // Run the callback if we have one. - if (success_callback) { - Y.bind(success_callback, this, responsetext)(); - } - - if (statusspinner) { - window.setTimeout(function() { - statusspinner.hide(); - }, 400); - } - }, - failure: function(tid, response) { - if (statusspinner) { - statusspinner.hide(); - } - new M.core.ajaxException(response); - } - }, - context: this - }; - - // Apply optional config - if (optionalconfig) { - for (varname in optionalconfig) { - config[varname] = optionalconfig[varname]; - } - } - - if (statusspinner) { - statusspinner.show(); - } - - // Send the request - Y.io(uri, config); - return this; - } -}, -{ - NAME: 'course-toolbox', - ATTRS: { - /** - * The ID of the Moodle Course being edited. - * - * @attribute courseid - * @default 0 - * @type Number - */ - courseid: { - 'value': 0 - }, - - /** - * The Moodle course format. - * - * @attribute format - * @default 'topics' - * @type String - */ - format: { - 'value': 'topics' - }, - /** - * The URL to use when submitting requests. - * @attribute ajaxurl - * @default null - * @type String - */ - ajaxurl: { - 'value': null - }, - /** - * Any additional configuration passed when creating the instance. - * - * @attribute config - * @default {} - * @type Object - */ - config: { - 'value': {} - } - } -} -); -/* global TOOLBOX, BODY, SELECTOR, INDENTLIMITS */ - -/** - * Resource and activity toolbox class. - * - * This class is responsible for managing AJAX interactions with activities and resources - * when viewing a course in editing mode. - * - * @module moodle-course-toolboxes - * @namespace M.course.toolboxes - */ - -/** - * Resource and activity toolbox class. - * - * This is a class extending TOOLBOX containing code specific to resources - * - * This class is responsible for managing AJAX interactions with activities and resources - * when viewing a course in editing mode. - * - * @class resources - * @constructor - * @extends M.course.toolboxes.toolbox - */ -var RESOURCETOOLBOX = function() { - RESOURCETOOLBOX.superclass.constructor.apply(this, arguments); -}; - -Y.extend(RESOURCETOOLBOX, TOOLBOX, { - /** - * No groups are being used. - * - * @property GROUPS_NONE - * @protected - * @type Number - */ - GROUPS_NONE: 0, - - /** - * Separate groups are being used. - * - * @property GROUPS_SEPARATE - * @protected - * @type Number - */ - GROUPS_SEPARATE: 1, - - /** - * Visible groups are being used. - * - * @property GROUPS_VISIBLE - * @protected - * @type Number - */ - GROUPS_VISIBLE: 2, - - /** - * Initialize the resource toolbox - * - * For each activity the commands are updated and a reference to the activity is attached. - * This way it doesn't matter where the commands are going to called from they have a reference to the - * activity that they relate to. - * This is essential as some of the actions are displayed in an actionmenu which removes them from the - * page flow. - * - * This function also creates a single event delegate to manage all AJAX actions for all activities on - * the page. - * - * @method initializer - * @protected - */ - initializer: function() { - M.course.coursebase.register_module(this); - BODY.delegate('key', this.handle_data_action, 'down:enter', SELECTOR.ACTIVITYACTION, this); - Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this); - }, - - /** - * Handles the delegation event. When this is fired someone has triggered an action. - * - * Note not all actions will result in an AJAX enhancement. - * - * @protected - * @method handle_data_action - * @param {EventFacade} ev The event that was triggered. - * @return {boolean} - */ - handle_data_action: function(ev) { - // We need to get the anchor element that triggered this event. - var node = ev.target; - if (!node.test('a')) { - node = node.ancestor(SELECTOR.ACTIVITYACTION); - } - - // From the anchor we can get both the activity (added during initialisation) and the action being - // performed (added by the UI as a data attribute). - var action = node.getData('action'), - activity = node.ancestor(SELECTOR.ACTIVITYLI); - - if (!node.test('a') || !action || !activity) { - // It wasn't a valid action node. - return; - } - - // Switch based upon the action and do the desired thing. - switch (action) { - case 'moveleft': - case 'moveright': - // The user changing the indent of the activity. - this.change_indent(ev, node, activity, action); - break; - case 'delete': - // The user is deleting the activity. - this.delete_with_confirmation(ev, node, activity, action); - break; - case 'duplicate': - // The user is duplicating the activity. - this.duplicate(ev, node, activity, action); - break; - case 'hide': - case 'show': - // The user is changing the visibility of the activity. - this.change_visibility(ev, node, activity, action); - break; - case 'groupsseparate': - case 'groupsvisible': - case 'groupsnone': - // The user is changing the group mode. - this.change_groupmode(ev, node, activity, action); - break; - case 'move': - case 'update': - case 'assignroles': - break; - default: - // Nothing to do here! - break; - } - }, - - /** - * Add a loading icon to the specified activity. - * - * The icon is added within the action area. - * - * @method add_spinner - * @param {Node} activity The activity to add a loading icon to - * @return {Node|null} The newly created icon, or null if the action area was not found. - */ - add_spinner: function(activity) { - var actionarea = activity.one(SELECTOR.ACTIONAREA); - if (actionarea) { - return M.util.add_spinner(Y, actionarea); - } - return null; - }, - - /** - * Change the indent of the activity or resource. - * - * @method change_indent - * @protected - * @param {EventFacade} ev The event that was fired. - * @param {Node} button The button that triggered this action. - * @param {Node} activity The activity node that this action will be performed on. - * @param {String} action The action that has been requested. Will be 'moveleft' or 'moveright'. - */ - change_indent: function(ev, button, activity, action) { - // Prevent the default button action - ev.preventDefault(); - - var direction = (action === 'moveleft') ? -1 : 1; - - // And we need to determine the current and new indent level - var indentdiv = activity.one(SELECTOR.MODINDENTDIV), - indent = indentdiv.getAttribute('class').match(/mod-indent-(\d{1,})/), - oldindent = 0, - newindent; - - if (indent) { - oldindent = parseInt(indent[1], 10); - } - newindent = oldindent + parseInt(direction, 10); - - if (newindent < INDENTLIMITS.MIN || newindent > INDENTLIMITS.MAX) { - return; - } - - if (indent) { - indentdiv.removeClass(indent[0]); - } - - // Perform the move - indentdiv.addClass(CSS.MODINDENTCOUNT + newindent); - var data = { - 'class': 'resource', - 'field': 'indent', - 'value': newindent, - 'id': Y.Moodle.core_course.util.cm.getId(activity) - }; - var spinner = this.add_spinner(activity); - this.send_request(data, spinner); - - var remainingmove; - - // Handle removal/addition of the moveleft button. - if (newindent === INDENTLIMITS.MIN) { - button.addClass('hidden'); - remainingmove = activity.one('.editing_moveright'); - } else if (newindent > INDENTLIMITS.MIN && oldindent === INDENTLIMITS.MIN) { - button.ancestor('.menu').one('[data-action=moveleft]').removeClass('hidden'); - } - - if (newindent === INDENTLIMITS.MAX) { - button.addClass('hidden'); - remainingmove = activity.one('.editing_moveleft'); - } else if (newindent < INDENTLIMITS.MAX && oldindent === INDENTLIMITS.MAX) { - button.ancestor('.menu').one('[data-action=moveright]').removeClass('hidden'); - } - - // Handle massive indentation to match non-ajax display - var hashugeclass = indentdiv.hasClass(CSS.MODINDENTHUGE); - if (newindent > 15 && !hashugeclass) { - indentdiv.addClass(CSS.MODINDENTHUGE); - } else if (newindent <= 15 && hashugeclass) { - indentdiv.removeClass(CSS.MODINDENTHUGE); - } - - if (ev.type && ev.type === "key" && remainingmove) { - remainingmove.focus(); - } - }, - - /** - * Deletes the given activity or resource after confirmation. - * - * @protected - * @method delete_with_confirmation - * @param {EventFacade} ev The event that was fired. - * @param {Node} button The button that triggered this action. - * @param {Node} activity The activity node that this action will be performed on. - * @chainable - */ - delete_with_confirmation: function(ev, button, activity) { - // Prevent the default button action - ev.preventDefault(); - - // Get the element we're working on - var element = activity, - // Create confirm string (different if element has or does not have name) - confirmstring = '', - plugindata = { - type: M.util.get_string('pluginname', element.getAttribute('class').match(/modtype_([^\s]*)/)[1]) - }; - if (Y.Moodle.core_course.util.cm.getName(element) !== null) { - plugindata.name = Y.Moodle.core_course.util.cm.getName(element); - confirmstring = M.util.get_string('deletechecktypename', 'moodle', plugindata); - } else { - confirmstring = M.util.get_string('deletechecktype', 'moodle', plugindata); - } - - // Create the confirmation dialogue. - var confirm = new M.core.confirm({ - question: confirmstring, - modal: true, - visible: false - }); - confirm.show(); - - // If it is confirmed. - confirm.on('complete-yes', function() { - - // Actually remove the element. - element.remove(); - var data = { - 'class': 'resource', - 'action': 'DELETE', - 'id': Y.Moodle.core_course.util.cm.getId(element) - }; - this.send_request(data); - if (M.core.actionmenu && M.core.actionmenu.instance) { - M.core.actionmenu.instance.hideMenu(ev); - } - - }, this); - - return this; - }, - - /** - * Duplicates the activity. - * - * @method duplicate - * @protected - * @param {EventFacade} ev The event that was fired. - * @param {Node} button The button that triggered this action. - * @param {Node} activity The activity node that this action will be performed on. - * @chainable - */ - duplicate: function(ev, button, activity) { - // Prevent the default button action - ev.preventDefault(); - - // Get the element we're working on - var element = activity; - - // Add the lightbox. - var section = activity.ancestor(M.course.format.get_section_selector(Y)), - lightbox = M.util.add_lightbox(Y, section).show(); - - // Build and send the request. - var data = { - 'class': 'resource', - 'field': 'duplicate', - 'id': Y.Moodle.core_course.util.cm.getId(element), - 'sr': button.getData('sr') - }; - this.send_request(data, lightbox, function(response) { - var newcm = Y.Node.create(response.fullcontent); - - // Append to the section? - activity.insert(newcm, 'after'); - Y.use('moodle-course-coursebase', function() { - M.course.coursebase.invoke_function('setup_for_resource', newcm); - }); - if (M.core.actionmenu && M.core.actionmenu.newDOMNode) { - M.core.actionmenu.newDOMNode(newcm); - } - }); - return this; - }, - - /** - * Changes the visibility of this activity or resource. - * - * @method change_visibility - * @protected - * @param {EventFacade} ev The event that was fired. - * @param {Node} button The button that triggered this action. - * @param {Node} activity The activity node that this action will be performed on. - * @param {String} action The action that has been requested. - * @chainable - */ - change_visibility: function(ev, button, activity, action) { - // Prevent the default button action - ev.preventDefault(); - - // Get the element we're working on - var element = activity; - var value = this.handle_resource_dim(button, activity, action); - - // Send the request - var data = { - 'class': 'resource', - 'field': 'visible', - 'value': value, - 'id': Y.Moodle.core_course.util.cm.getId(element) - }; - var spinner = this.add_spinner(element); - this.send_request(data, spinner); - - return this; - }, - - /** - * Handles the UI aspect of dimming the activity or resource. - * - * @method handle_resource_dim - * @protected - * @param {Node} button The button that triggered the action. - * @param {Node} activity The activity node that this action will be performed on. - * @param {String} action 'show' or 'hide'. - * @return {Number} 1 if we changed to visible, 0 if we were hiding. - */ - handle_resource_dim: function(button, activity, action) { - var toggleclass = CSS.DIMCLASS, - dimarea = activity.one([ - SELECTOR.ACTIVITYLINK, - SELECTOR.CONTENTWITHOUTLINK - ].join(', ')), - availabilityinfo = activity.one(CSS.AVAILABILITYINFODIV), - nextaction = (action === 'hide') ? 'show' : 'hide', - buttontext = button.one('span'), - newstring = M.util.get_string(nextaction, 'moodle'), - buttonimg = button.one('img'); - - // Update button info. - buttonimg.setAttrs({ - 'src': M.util.image_url('t/' + nextaction) - }); - - if (Y.Lang.trim(button.getAttribute('title'))) { - button.setAttribute('title', newstring); - } - - if (Y.Lang.trim(buttonimg.getAttribute('alt'))) { - buttonimg.setAttribute('alt', newstring); - } - - button.replaceClass('editing_' + action, 'editing_' + nextaction); - button.setData('action', nextaction); - if (buttontext) { - buttontext.set('text', newstring); - } - - if (activity.one(SELECTOR.CONTENTWITHOUTLINK)) { - dimarea = activity.one(SELECTOR.CONTENTWITHOUTLINK); - toggleclass = CSS.DIMMEDTEXT; - } - - // If activity is conditionally hidden, then don't toggle. - if (!dimarea.hasClass(CSS.CONDITIONALHIDDEN)) { - if (action === 'hide') { - // Change the UI. - dimarea.addClass(toggleclass); - // We need to toggle dimming on the description too. - activity.all(SELECTOR.CONTENTAFTERLINK).addClass(CSS.DIMMEDTEXT); - activity.all(SELECTOR.GROUPINGLABEL).addClass(CSS.DIMMEDTEXT); - } else { - // Change the UI. - dimarea.removeClass(toggleclass); - // We need to toggle dimming on the description too. - activity.all(SELECTOR.CONTENTAFTERLINK).removeClass(CSS.DIMMEDTEXT); - activity.all(SELECTOR.GROUPINGLABEL).removeClass(CSS.DIMMEDTEXT); - } - } - // Toggle availablity info for conditional activities. - if (availabilityinfo) { - availabilityinfo.toggleClass(CSS.HIDE); - } - return (action === 'hide') ? 0 : 1; - }, - - /** - * Changes the groupmode of the activity to the next groupmode in the sequence. - * - * @method change_groupmode - * @protected - * @param {EventFacade} ev The event that was fired. - * @param {Node} button The button that triggered this action. - * @param {Node} activity The activity node that this action will be performed on. - * @chainable - */ - change_groupmode: function(ev, button, activity) { - // Prevent the default button action. - ev.preventDefault(); - - // Current Mode - var groupmode = parseInt(button.getData('nextgroupmode'), 10), - newtitle = '', - iconsrc = '', - newtitlestr, - data, - spinner, - nextgroupmode = groupmode + 1, - buttonimg = button.one('img'); - - if (nextgroupmode > 2) { - nextgroupmode = 0; - } - - if (groupmode === this.GROUPS_NONE) { - newtitle = 'groupsnone'; - iconsrc = M.util.image_url('i/groupn', 'moodle'); - } else if (groupmode === this.GROUPS_SEPARATE) { - newtitle = 'groupsseparate'; - iconsrc = M.util.image_url('i/groups', 'moodle'); - } else if (groupmode === this.GROUPS_VISIBLE) { - newtitle = 'groupsvisible'; - iconsrc = M.util.image_url('i/groupv', 'moodle'); - } - newtitlestr = M.util.get_string('clicktochangeinbrackets', 'moodle', M.util.get_string(newtitle, 'moodle')); - - // Change the UI - var oldAction = button.getData('action'); - button.replaceClass('editing_' + oldAction, 'editing_' + newtitle); - buttonimg.setAttrs({ - 'src': iconsrc - }); - if (Y.Lang.trim(button.getAttribute('title'))) { - button.setAttribute('title', newtitlestr).setData('action', newtitle).setData('nextgroupmode', nextgroupmode); - } - - if (Y.Lang.trim(buttonimg.getAttribute('alt'))) { - buttonimg.setAttribute('alt', newtitlestr); - } - - // And send the request - data = { - 'class': 'resource', - 'field': 'groupmode', - 'value': groupmode, - 'id': Y.Moodle.core_course.util.cm.getId(activity) - }; - - spinner = this.add_spinner(activity); - this.send_request(data, spinner); - return this; - }, - - /** - * Set the visibility of the specified resource to match the visible parameter. - * - * Note: This is not a toggle function and only changes the visibility - * in the browser (no ajax update is performed). - * - * @method set_visibility_resource_ui - * @param {object} args An object containing the required information to trigger a change. - * @param {Node} args.element The resource to toggle - * @param {Boolean} args.visible The target visibility - */ - set_visibility_resource_ui: function(args) { - var element = args.element, - buttonnode = element.one(SELECTOR.HIDE), - // By default we assume that the item is visible and we're going to hide it. - currentVisibility = true, - targetVisibility = false; - - if (!buttonnode) { - // If the buttonnode was not found, try to find the HIDE button - // and change the target visibility setting to false. - buttonnode = element.one(SELECTOR.SHOW); - currentVisibility = false; - targetVisibility = true; - } - - if (typeof args.visible !== 'undefined') { - // If we were provided with a visibility argument, use that instead. - targetVisibility = args.visible; - } - - // Only trigger a change if necessary. - if (currentVisibility !== targetVisibility) { - var action = 'hide'; - if (targetVisibility) { - action = 'show'; - } - - this.handle_resource_dim(buttonnode, element, action); - } - } -}, { - NAME: 'course-resource-toolbox', - ATTRS: { - } -}); - -M.course.resource_toolbox = null; -M.course.init_resource_toolbox = function(config) { - M.course.resource_toolbox = new RESOURCETOOLBOX(config); - return M.course.resource_toolbox; -}; -/* global SELECTOR, TOOLBOX */ - -/** - * Resource and activity toolbox class. - * - * This class is responsible for managing AJAX interactions with activities and resources - * when viewing a course in editing mode. - * - * @module moodle-course-toolboxes - * @namespace M.course.toolboxes - */ - -/** - * Section toolbox class. - * - * This class is responsible for managing AJAX interactions with sections - * when viewing a course in editing mode. - * - * @class section - * @constructor - * @extends M.course.toolboxes.toolbox - */ -var SECTIONTOOLBOX = function() { - SECTIONTOOLBOX.superclass.constructor.apply(this, arguments); -}; - -Y.extend(SECTIONTOOLBOX, TOOLBOX, { - /** - * Initialize the section toolboxes module. - * - * Updates all span.commands with relevant handlers and other required changes. - * - * @method initializer - * @protected - */ - initializer: function() { - M.course.coursebase.register_module(this); - - // Section Highlighting. - Y.delegate('click', this.toggle_highlight, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.HIGHLIGHT, this); - - // Section Visibility. - Y.delegate('click', this.toggle_hide_section, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.SHOWHIDE, this); - }, - - toggle_hide_section: function(e) { - // Prevent the default button action. - e.preventDefault(); - - // Get the section we're working on. - var section = e.target.ancestor(M.course.format.get_section_selector(Y)), - button = e.target.ancestor('a', true), - hideicon = button.one('img'), - buttontext = button.one('span'), - - // The value to submit - value, - - // The text for strings and images. Also determines the icon to display. - action, - nextaction; - - if (!section.hasClass(CSS.SECTIONHIDDENCLASS)) { - section.addClass(CSS.SECTIONHIDDENCLASS); - value = 0; - action = 'hide'; - nextaction = 'show'; - } else { - section.removeClass(CSS.SECTIONHIDDENCLASS); - value = 1; - action = 'show'; - nextaction = 'hide'; - } - - var newstring = M.util.get_string(nextaction + 'fromothers', 'format_' + this.get('format')); - hideicon.setAttrs({ - 'alt': newstring, - 'src': M.util.image_url('i/' + nextaction) - }); - button.set('title', newstring); - if (buttontext) { - buttontext.set('text', newstring); - } - - // Change the show/hide status - var data = { - 'class': 'section', - 'field': 'visible', - 'id': Y.Moodle.core_course.util.section.getId(section.ancestor(M.course.format.get_section_wrapper(Y), true)), - 'value': value - }; - - var lightbox = M.util.add_lightbox(Y, section); - lightbox.show(); - - this.send_request(data, lightbox, function(response) { - var activities = section.all(SELECTOR.ACTIVITYLI); - activities.each(function(node) { - var button; - if (node.one(SELECTOR.SHOW)) { - button = node.one(SELECTOR.SHOW); - } else { - button = node.one(SELECTOR.HIDE); - } - var activityid = Y.Moodle.core_course.util.cm.getId(node); - - // NOTE: resourcestotoggle is returned as a string instead - // of a Number so we must cast our activityid to a String. - if (Y.Array.indexOf(response.resourcestotoggle, "" + activityid) !== -1) { - M.course.resource_toolbox.handle_resource_dim(button, node, action); - } - }, this); - }); - }, - - /** - * Toggle highlighting the current section. - * - * @method toggle_highlight - * @param {EventFacade} e - */ - toggle_highlight: function(e) { - // Prevent the default button action. - e.preventDefault(); - - // Get the section we're working on. - var section = e.target.ancestor(M.course.format.get_section_selector(Y)); - var button = e.target.ancestor('a', true); - var buttonicon = button.one('img'); - var buttontext = button.one('span'); - - // Determine whether the marker is currently set. - var togglestatus = section.hasClass('current'); - var value = 0; - - // Set the current highlighted item text. - var old_string = M.util.get_string('markthistopic', 'moodle'); - - var selectedpage = Y.one(SELECTOR.PAGECONTENT); - selectedpage - .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT) - .set('title', old_string); - selectedpage - .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT + ' span') - .set('text', M.util.get_string('highlight', 'moodle')); - selectedpage - .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT + ' img') - .set('alt', old_string) - .set('src', M.util.image_url('i/marker')); - - // Remove the highlighting from all sections. - selectedpage.all(M.course.format.get_section_selector(Y)) - .removeClass('current'); - - // Then add it if required to the selected section. - if (!togglestatus) { - section.addClass('current'); - value = Y.Moodle.core_course.util.section.getId(section.ancestor(M.course.format.get_section_wrapper(Y), true)); - var new_string = M.util.get_string('markedthistopic', 'moodle'); - button - .set('title', new_string); - buttonicon - .set('alt', new_string) - .set('src', M.util.image_url('i/marked')); - if (buttontext) { - buttontext - .set('text', M.util.get_string('highlightoff', 'moodle')); - } - } - - // Change the highlight status. - var data = { - 'class': 'course', - 'field': 'marker', - 'value': value - }; - var lightbox = M.util.add_lightbox(Y, section); - lightbox.show(); - this.send_request(data, lightbox); - } -}, { - NAME: 'course-section-toolbox', - ATTRS: { - } -}); - -M.course.init_section_toolbox = function(config) { - return new SECTIONTOOLBOX(config); -}; - - -}, '@VERSION@', {"requires": ["node", "base", "event-key", "node", "io", "moodle-course-coursebase", "moodle-course-util"]}); diff --git a/course/yui/src/dragdrop/js/resource.js b/course/yui/src/dragdrop/js/resource.js index 1b0be26364a..f8c4e51a23f 100644 --- a/course/yui/src/dragdrop/js/resource.js +++ b/course/yui/src/dragdrop/js/resource.js @@ -108,7 +108,8 @@ Y.extend(DRAGRESOURCE, M.core.dragdrop, { // Replace move icons var move = resourcesnode.one('a.' + CSS.EDITINGMOVE); if (move) { - move.replace(this.resourcedraghandle.cloneNode(true)); + var sr = move.getData('sr'); + move.replace(this.resourcedraghandle.cloneNode(true).setAttribute('data-sectionreturn', sr)); } }, this); }, diff --git a/course/yui/src/toolboxes/build.json b/course/yui/src/toolboxes/build.json deleted file mode 100644 index ffd593957bc..00000000000 --- a/course/yui/src/toolboxes/build.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "moodle-course-toolboxes", - "builds": { - "moodle-course-toolboxes": { - "jsfiles": [ - "toolbox.js", - "resource.js", - "section.js" - ] - } - } -} diff --git a/course/yui/src/toolboxes/js/resource.js b/course/yui/src/toolboxes/js/resource.js deleted file mode 100644 index 677836fb2db..00000000000 --- a/course/yui/src/toolboxes/js/resource.js +++ /dev/null @@ -1,552 +0,0 @@ -/* global TOOLBOX, BODY, SELECTOR, INDENTLIMITS */ - -/** - * Resource and activity toolbox class. - * - * This class is responsible for managing AJAX interactions with activities and resources - * when viewing a course in editing mode. - * - * @module moodle-course-toolboxes - * @namespace M.course.toolboxes - */ - -/** - * Resource and activity toolbox class. - * - * This is a class extending TOOLBOX containing code specific to resources - * - * This class is responsible for managing AJAX interactions with activities and resources - * when viewing a course in editing mode. - * - * @class resources - * @constructor - * @extends M.course.toolboxes.toolbox - */ -var RESOURCETOOLBOX = function() { - RESOURCETOOLBOX.superclass.constructor.apply(this, arguments); -}; - -Y.extend(RESOURCETOOLBOX, TOOLBOX, { - /** - * No groups are being used. - * - * @property GROUPS_NONE - * @protected - * @type Number - */ - GROUPS_NONE: 0, - - /** - * Separate groups are being used. - * - * @property GROUPS_SEPARATE - * @protected - * @type Number - */ - GROUPS_SEPARATE: 1, - - /** - * Visible groups are being used. - * - * @property GROUPS_VISIBLE - * @protected - * @type Number - */ - GROUPS_VISIBLE: 2, - - /** - * Initialize the resource toolbox - * - * For each activity the commands are updated and a reference to the activity is attached. - * This way it doesn't matter where the commands are going to called from they have a reference to the - * activity that they relate to. - * This is essential as some of the actions are displayed in an actionmenu which removes them from the - * page flow. - * - * This function also creates a single event delegate to manage all AJAX actions for all activities on - * the page. - * - * @method initializer - * @protected - */ - initializer: function() { - M.course.coursebase.register_module(this); - BODY.delegate('key', this.handle_data_action, 'down:enter', SELECTOR.ACTIVITYACTION, this); - Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this); - }, - - /** - * Handles the delegation event. When this is fired someone has triggered an action. - * - * Note not all actions will result in an AJAX enhancement. - * - * @protected - * @method handle_data_action - * @param {EventFacade} ev The event that was triggered. - * @return {boolean} - */ - handle_data_action: function(ev) { - // We need to get the anchor element that triggered this event. - var node = ev.target; - if (!node.test('a')) { - node = node.ancestor(SELECTOR.ACTIVITYACTION); - } - - // From the anchor we can get both the activity (added during initialisation) and the action being - // performed (added by the UI as a data attribute). - var action = node.getData('action'), - activity = node.ancestor(SELECTOR.ACTIVITYLI); - - if (!node.test('a') || !action || !activity) { - // It wasn't a valid action node. - return; - } - - // Switch based upon the action and do the desired thing. - switch (action) { - case 'moveleft': - case 'moveright': - // The user changing the indent of the activity. - this.change_indent(ev, node, activity, action); - break; - case 'delete': - // The user is deleting the activity. - this.delete_with_confirmation(ev, node, activity, action); - break; - case 'duplicate': - // The user is duplicating the activity. - this.duplicate(ev, node, activity, action); - break; - case 'hide': - case 'show': - // The user is changing the visibility of the activity. - this.change_visibility(ev, node, activity, action); - break; - case 'groupsseparate': - case 'groupsvisible': - case 'groupsnone': - // The user is changing the group mode. - this.change_groupmode(ev, node, activity, action); - break; - case 'move': - case 'update': - case 'assignroles': - break; - default: - // Nothing to do here! - break; - } - }, - - /** - * Add a loading icon to the specified activity. - * - * The icon is added within the action area. - * - * @method add_spinner - * @param {Node} activity The activity to add a loading icon to - * @return {Node|null} The newly created icon, or null if the action area was not found. - */ - add_spinner: function(activity) { - var actionarea = activity.one(SELECTOR.ACTIONAREA); - if (actionarea) { - return M.util.add_spinner(Y, actionarea); - } - return null; - }, - - /** - * Change the indent of the activity or resource. - * - * @method change_indent - * @protected - * @param {EventFacade} ev The event that was fired. - * @param {Node} button The button that triggered this action. - * @param {Node} activity The activity node that this action will be performed on. - * @param {String} action The action that has been requested. Will be 'moveleft' or 'moveright'. - */ - change_indent: function(ev, button, activity, action) { - // Prevent the default button action - ev.preventDefault(); - - var direction = (action === 'moveleft') ? -1 : 1; - - // And we need to determine the current and new indent level - var indentdiv = activity.one(SELECTOR.MODINDENTDIV), - indent = indentdiv.getAttribute('class').match(/mod-indent-(\d{1,})/), - oldindent = 0, - newindent; - - if (indent) { - oldindent = parseInt(indent[1], 10); - } - newindent = oldindent + parseInt(direction, 10); - - if (newindent < INDENTLIMITS.MIN || newindent > INDENTLIMITS.MAX) { - return; - } - - if (indent) { - indentdiv.removeClass(indent[0]); - } - - // Perform the move - indentdiv.addClass(CSS.MODINDENTCOUNT + newindent); - var data = { - 'class': 'resource', - 'field': 'indent', - 'value': newindent, - 'id': Y.Moodle.core_course.util.cm.getId(activity) - }; - var spinner = this.add_spinner(activity); - this.send_request(data, spinner); - - var remainingmove; - - // Handle removal/addition of the moveleft button. - if (newindent === INDENTLIMITS.MIN) { - button.addClass('hidden'); - remainingmove = activity.one('.editing_moveright'); - } else if (newindent > INDENTLIMITS.MIN && oldindent === INDENTLIMITS.MIN) { - button.ancestor('.menu').one('[data-action=moveleft]').removeClass('hidden'); - } - - if (newindent === INDENTLIMITS.MAX) { - button.addClass('hidden'); - remainingmove = activity.one('.editing_moveleft'); - } else if (newindent < INDENTLIMITS.MAX && oldindent === INDENTLIMITS.MAX) { - button.ancestor('.menu').one('[data-action=moveright]').removeClass('hidden'); - } - - // Handle massive indentation to match non-ajax display - var hashugeclass = indentdiv.hasClass(CSS.MODINDENTHUGE); - if (newindent > 15 && !hashugeclass) { - indentdiv.addClass(CSS.MODINDENTHUGE); - } else if (newindent <= 15 && hashugeclass) { - indentdiv.removeClass(CSS.MODINDENTHUGE); - } - - if (ev.type && ev.type === "key" && remainingmove) { - remainingmove.focus(); - } - }, - - /** - * Deletes the given activity or resource after confirmation. - * - * @protected - * @method delete_with_confirmation - * @param {EventFacade} ev The event that was fired. - * @param {Node} button The button that triggered this action. - * @param {Node} activity The activity node that this action will be performed on. - * @chainable - */ - delete_with_confirmation: function(ev, button, activity) { - // Prevent the default button action - ev.preventDefault(); - - // Get the element we're working on - var element = activity, - // Create confirm string (different if element has or does not have name) - confirmstring = '', - plugindata = { - type: M.util.get_string('pluginname', element.getAttribute('class').match(/modtype_([^\s]*)/)[1]) - }; - if (Y.Moodle.core_course.util.cm.getName(element) !== null) { - plugindata.name = Y.Moodle.core_course.util.cm.getName(element); - confirmstring = M.util.get_string('deletechecktypename', 'moodle', plugindata); - } else { - confirmstring = M.util.get_string('deletechecktype', 'moodle', plugindata); - } - - // Create the confirmation dialogue. - var confirm = new M.core.confirm({ - question: confirmstring, - modal: true, - visible: false - }); - confirm.show(); - - // If it is confirmed. - confirm.on('complete-yes', function() { - - // Actually remove the element. - element.remove(); - var data = { - 'class': 'resource', - 'action': 'DELETE', - 'id': Y.Moodle.core_course.util.cm.getId(element) - }; - this.send_request(data); - if (M.core.actionmenu && M.core.actionmenu.instance) { - M.core.actionmenu.instance.hideMenu(ev); - } - - }, this); - - return this; - }, - - /** - * Duplicates the activity. - * - * @method duplicate - * @protected - * @param {EventFacade} ev The event that was fired. - * @param {Node} button The button that triggered this action. - * @param {Node} activity The activity node that this action will be performed on. - * @chainable - */ - duplicate: function(ev, button, activity) { - // Prevent the default button action - ev.preventDefault(); - - // Get the element we're working on - var element = activity; - - // Add the lightbox. - var section = activity.ancestor(M.course.format.get_section_selector(Y)), - lightbox = M.util.add_lightbox(Y, section).show(); - - // Build and send the request. - var data = { - 'class': 'resource', - 'field': 'duplicate', - 'id': Y.Moodle.core_course.util.cm.getId(element), - 'sr': button.getData('sr') - }; - this.send_request(data, lightbox, function(response) { - var newcm = Y.Node.create(response.fullcontent); - - // Append to the section? - activity.insert(newcm, 'after'); - Y.use('moodle-course-coursebase', function() { - M.course.coursebase.invoke_function('setup_for_resource', newcm); - }); - if (M.core.actionmenu && M.core.actionmenu.newDOMNode) { - M.core.actionmenu.newDOMNode(newcm); - } - }); - return this; - }, - - /** - * Changes the visibility of this activity or resource. - * - * @method change_visibility - * @protected - * @param {EventFacade} ev The event that was fired. - * @param {Node} button The button that triggered this action. - * @param {Node} activity The activity node that this action will be performed on. - * @param {String} action The action that has been requested. - * @chainable - */ - change_visibility: function(ev, button, activity, action) { - // Prevent the default button action - ev.preventDefault(); - - // Get the element we're working on - var element = activity; - var value = this.handle_resource_dim(button, activity, action); - - // Send the request - var data = { - 'class': 'resource', - 'field': 'visible', - 'value': value, - 'id': Y.Moodle.core_course.util.cm.getId(element) - }; - var spinner = this.add_spinner(element); - this.send_request(data, spinner); - - return this; - }, - - /** - * Handles the UI aspect of dimming the activity or resource. - * - * @method handle_resource_dim - * @protected - * @param {Node} button The button that triggered the action. - * @param {Node} activity The activity node that this action will be performed on. - * @param {String} action 'show' or 'hide'. - * @return {Number} 1 if we changed to visible, 0 if we were hiding. - */ - handle_resource_dim: function(button, activity, action) { - var toggleclass = CSS.DIMCLASS, - dimarea = activity.one([ - SELECTOR.ACTIVITYLINK, - SELECTOR.CONTENTWITHOUTLINK - ].join(', ')), - availabilityinfo = activity.one(CSS.AVAILABILITYINFODIV), - nextaction = (action === 'hide') ? 'show' : 'hide', - buttontext = button.one('span'), - newstring = M.util.get_string(nextaction, 'moodle'), - buttonimg = button.one('img'); - - // Update button info. - buttonimg.setAttrs({ - 'src': M.util.image_url('t/' + nextaction) - }); - - if (Y.Lang.trim(button.getAttribute('title'))) { - button.setAttribute('title', newstring); - } - - if (Y.Lang.trim(buttonimg.getAttribute('alt'))) { - buttonimg.setAttribute('alt', newstring); - } - - button.replaceClass('editing_' + action, 'editing_' + nextaction); - button.setData('action', nextaction); - if (buttontext) { - buttontext.set('text', newstring); - } - - if (activity.one(SELECTOR.CONTENTWITHOUTLINK)) { - dimarea = activity.one(SELECTOR.CONTENTWITHOUTLINK); - toggleclass = CSS.DIMMEDTEXT; - } - - // If activity is conditionally hidden, then don't toggle. - if (!dimarea.hasClass(CSS.CONDITIONALHIDDEN)) { - if (action === 'hide') { - // Change the UI. - dimarea.addClass(toggleclass); - // We need to toggle dimming on the description too. - activity.all(SELECTOR.CONTENTAFTERLINK).addClass(CSS.DIMMEDTEXT); - activity.all(SELECTOR.GROUPINGLABEL).addClass(CSS.DIMMEDTEXT); - } else { - // Change the UI. - dimarea.removeClass(toggleclass); - // We need to toggle dimming on the description too. - activity.all(SELECTOR.CONTENTAFTERLINK).removeClass(CSS.DIMMEDTEXT); - activity.all(SELECTOR.GROUPINGLABEL).removeClass(CSS.DIMMEDTEXT); - } - } - // Toggle availablity info for conditional activities. - if (availabilityinfo) { - availabilityinfo.toggleClass(CSS.HIDE); - } - return (action === 'hide') ? 0 : 1; - }, - - /** - * Changes the groupmode of the activity to the next groupmode in the sequence. - * - * @method change_groupmode - * @protected - * @param {EventFacade} ev The event that was fired. - * @param {Node} button The button that triggered this action. - * @param {Node} activity The activity node that this action will be performed on. - * @chainable - */ - change_groupmode: function(ev, button, activity) { - // Prevent the default button action. - ev.preventDefault(); - - // Current Mode - var groupmode = parseInt(button.getData('nextgroupmode'), 10), - newtitle = '', - iconsrc = '', - newtitlestr, - data, - spinner, - nextgroupmode = groupmode + 1, - buttonimg = button.one('img'); - - if (nextgroupmode > 2) { - nextgroupmode = 0; - } - - if (groupmode === this.GROUPS_NONE) { - newtitle = 'groupsnone'; - iconsrc = M.util.image_url('i/groupn', 'moodle'); - } else if (groupmode === this.GROUPS_SEPARATE) { - newtitle = 'groupsseparate'; - iconsrc = M.util.image_url('i/groups', 'moodle'); - } else if (groupmode === this.GROUPS_VISIBLE) { - newtitle = 'groupsvisible'; - iconsrc = M.util.image_url('i/groupv', 'moodle'); - } - newtitlestr = M.util.get_string('clicktochangeinbrackets', 'moodle', M.util.get_string(newtitle, 'moodle')); - - // Change the UI - var oldAction = button.getData('action'); - button.replaceClass('editing_' + oldAction, 'editing_' + newtitle); - buttonimg.setAttrs({ - 'src': iconsrc - }); - if (Y.Lang.trim(button.getAttribute('title'))) { - button.setAttribute('title', newtitlestr).setData('action', newtitle).setData('nextgroupmode', nextgroupmode); - } - - if (Y.Lang.trim(buttonimg.getAttribute('alt'))) { - buttonimg.setAttribute('alt', newtitlestr); - } - - // And send the request - data = { - 'class': 'resource', - 'field': 'groupmode', - 'value': groupmode, - 'id': Y.Moodle.core_course.util.cm.getId(activity) - }; - - spinner = this.add_spinner(activity); - this.send_request(data, spinner); - return this; - }, - - /** - * Set the visibility of the specified resource to match the visible parameter. - * - * Note: This is not a toggle function and only changes the visibility - * in the browser (no ajax update is performed). - * - * @method set_visibility_resource_ui - * @param {object} args An object containing the required information to trigger a change. - * @param {Node} args.element The resource to toggle - * @param {Boolean} args.visible The target visibility - */ - set_visibility_resource_ui: function(args) { - var element = args.element, - buttonnode = element.one(SELECTOR.HIDE), - // By default we assume that the item is visible and we're going to hide it. - currentVisibility = true, - targetVisibility = false; - - if (!buttonnode) { - // If the buttonnode was not found, try to find the HIDE button - // and change the target visibility setting to false. - buttonnode = element.one(SELECTOR.SHOW); - currentVisibility = false; - targetVisibility = true; - } - - if (typeof args.visible !== 'undefined') { - // If we were provided with a visibility argument, use that instead. - targetVisibility = args.visible; - } - - // Only trigger a change if necessary. - if (currentVisibility !== targetVisibility) { - var action = 'hide'; - if (targetVisibility) { - action = 'show'; - } - - this.handle_resource_dim(buttonnode, element, action); - } - } -}, { - NAME: 'course-resource-toolbox', - ATTRS: { - } -}); - -M.course.resource_toolbox = null; -M.course.init_resource_toolbox = function(config) { - M.course.resource_toolbox = new RESOURCETOOLBOX(config); - return M.course.resource_toolbox; -}; diff --git a/course/yui/src/toolboxes/js/section.js b/course/yui/src/toolboxes/js/section.js deleted file mode 100644 index dab7a4455fb..00000000000 --- a/course/yui/src/toolboxes/js/section.js +++ /dev/null @@ -1,189 +0,0 @@ -/* global SELECTOR, TOOLBOX */ - -/** - * Resource and activity toolbox class. - * - * This class is responsible for managing AJAX interactions with activities and resources - * when viewing a course in editing mode. - * - * @module moodle-course-toolboxes - * @namespace M.course.toolboxes - */ - -/** - * Section toolbox class. - * - * This class is responsible for managing AJAX interactions with sections - * when viewing a course in editing mode. - * - * @class section - * @constructor - * @extends M.course.toolboxes.toolbox - */ -var SECTIONTOOLBOX = function() { - SECTIONTOOLBOX.superclass.constructor.apply(this, arguments); -}; - -Y.extend(SECTIONTOOLBOX, TOOLBOX, { - /** - * Initialize the section toolboxes module. - * - * Updates all span.commands with relevant handlers and other required changes. - * - * @method initializer - * @protected - */ - initializer: function() { - M.course.coursebase.register_module(this); - - // Section Highlighting. - Y.delegate('click', this.toggle_highlight, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.HIGHLIGHT, this); - - // Section Visibility. - Y.delegate('click', this.toggle_hide_section, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.SHOWHIDE, this); - }, - - toggle_hide_section: function(e) { - // Prevent the default button action. - e.preventDefault(); - - // Get the section we're working on. - var section = e.target.ancestor(M.course.format.get_section_selector(Y)), - button = e.target.ancestor('a', true), - hideicon = button.one('img'), - buttontext = button.one('span'), - - // The value to submit - value, - - // The text for strings and images. Also determines the icon to display. - action, - nextaction; - - if (!section.hasClass(CSS.SECTIONHIDDENCLASS)) { - section.addClass(CSS.SECTIONHIDDENCLASS); - value = 0; - action = 'hide'; - nextaction = 'show'; - } else { - section.removeClass(CSS.SECTIONHIDDENCLASS); - value = 1; - action = 'show'; - nextaction = 'hide'; - } - - var newstring = M.util.get_string(nextaction + 'fromothers', 'format_' + this.get('format')); - hideicon.setAttrs({ - 'alt': newstring, - 'src': M.util.image_url('i/' + nextaction) - }); - button.set('title', newstring); - if (buttontext) { - buttontext.set('text', newstring); - } - - // Change the show/hide status - var data = { - 'class': 'section', - 'field': 'visible', - 'id': Y.Moodle.core_course.util.section.getId(section.ancestor(M.course.format.get_section_wrapper(Y), true)), - 'value': value - }; - - var lightbox = M.util.add_lightbox(Y, section); - lightbox.show(); - - this.send_request(data, lightbox, function(response) { - var activities = section.all(SELECTOR.ACTIVITYLI); - activities.each(function(node) { - var button; - if (node.one(SELECTOR.SHOW)) { - button = node.one(SELECTOR.SHOW); - } else { - button = node.one(SELECTOR.HIDE); - } - var activityid = Y.Moodle.core_course.util.cm.getId(node); - - // NOTE: resourcestotoggle is returned as a string instead - // of a Number so we must cast our activityid to a String. - if (Y.Array.indexOf(response.resourcestotoggle, "" + activityid) !== -1) { - M.course.resource_toolbox.handle_resource_dim(button, node, action); - } - }, this); - }); - }, - - /** - * Toggle highlighting the current section. - * - * @method toggle_highlight - * @param {EventFacade} e - */ - toggle_highlight: function(e) { - // Prevent the default button action. - e.preventDefault(); - - // Get the section we're working on. - var section = e.target.ancestor(M.course.format.get_section_selector(Y)); - var button = e.target.ancestor('a', true); - var buttonicon = button.one('img'); - var buttontext = button.one('span'); - - // Determine whether the marker is currently set. - var togglestatus = section.hasClass('current'); - var value = 0; - - // Set the current highlighted item text. - var old_string = M.util.get_string('markthistopic', 'moodle'); - - var selectedpage = Y.one(SELECTOR.PAGECONTENT); - selectedpage - .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT) - .set('title', old_string); - selectedpage - .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT + ' span') - .set('text', M.util.get_string('highlight', 'moodle')); - selectedpage - .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT + ' img') - .set('alt', old_string) - .set('src', M.util.image_url('i/marker')); - - // Remove the highlighting from all sections. - selectedpage.all(M.course.format.get_section_selector(Y)) - .removeClass('current'); - - // Then add it if required to the selected section. - if (!togglestatus) { - section.addClass('current'); - value = Y.Moodle.core_course.util.section.getId(section.ancestor(M.course.format.get_section_wrapper(Y), true)); - var new_string = M.util.get_string('markedthistopic', 'moodle'); - button - .set('title', new_string); - buttonicon - .set('alt', new_string) - .set('src', M.util.image_url('i/marked')); - if (buttontext) { - buttontext - .set('text', M.util.get_string('highlightoff', 'moodle')); - } - } - - // Change the highlight status. - var data = { - 'class': 'course', - 'field': 'marker', - 'value': value - }; - var lightbox = M.util.add_lightbox(Y, section); - lightbox.show(); - this.send_request(data, lightbox); - } -}, { - NAME: 'course-section-toolbox', - ATTRS: { - } -}); - -M.course.init_section_toolbox = function(config) { - return new SECTIONTOOLBOX(config); -}; diff --git a/course/yui/src/toolboxes/js/shared.js b/course/yui/src/toolboxes/js/shared.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/course/yui/src/toolboxes/js/toolbox.js b/course/yui/src/toolboxes/js/toolbox.js deleted file mode 100644 index 6f78e0970ef..00000000000 --- a/course/yui/src/toolboxes/js/toolbox.js +++ /dev/null @@ -1,201 +0,0 @@ -/* eslint-disable no-unused-vars */ -/** - * Resource and activity toolbox class. - * - * This class is responsible for managing AJAX interactions with activities and resources - * when viewing a course in editing mode. - * - * @module moodle-course-toolboxes - * @namespace M.course.toolboxes - */ - -// The CSS classes we use. -var CSS = { - ACTIVITYINSTANCE: 'activityinstance', - AVAILABILITYINFODIV: 'div.availabilityinfo', - CONTENTWITHOUTLINK: 'contentwithoutlink', - CONDITIONALHIDDEN: 'conditionalhidden', - DIMCLASS: 'dimmed', - DIMMEDTEXT: 'dimmed_text', - EDITINSTRUCTIONS: 'editinstructions', - HIDE: 'hide', - MODINDENTCOUNT: 'mod-indent-', - MODINDENTHUGE: 'mod-indent-huge', - MODULEIDPREFIX: 'module-', - SECTIONHIDDENCLASS: 'hidden', - SECTIONIDPREFIX: 'section-', - SHOW: 'editing_show' - }, - // The CSS selectors we use. - SELECTOR = { - ACTIONAREA: '.actions', - ACTIONLINKTEXT: '.actionlinktext', - ACTIVITYACTION: 'a.cm-edit-action[data-action]', - ACTIVITYICON: 'img.activityicon', - ACTIVITYINSTANCE: '.' + CSS.ACTIVITYINSTANCE, - ACTIVITYLINK: '.' + CSS.ACTIVITYINSTANCE + ' > a, .' + CSS.ACTIVITYINSTANCE + - ' > span[data-inplaceeditable] > a:not([data-inplaceeditablelink])', - ACTIVITYLI: 'li.activity', - COMMANDSPAN: '.commands', - CONTENTAFTERLINK: 'div.contentafterlink', - CONTENTWITHOUTLINK: 'div.contentwithoutlink', - GROUPINGLABEL: '.' + CSS.ACTIVITYINSTANCE + ' .groupinglabel', - HIDE: 'a.editing_hide', - HIGHLIGHT: 'a.editing_highlight', - INSTANCENAME: 'span.instancename', - MODINDENTDIV: '.mod-indent', - MODINDENTOUTER: '.mod-indent-outer', - PAGECONTENT: 'body', - SECTIONLI: 'li.section', - SHOW: 'a.' + CSS.SHOW, - SHOWHIDE: 'a.editing_showhide' - }, - INDENTLIMITS = { - MIN: 0, - MAX: 16 - }, - BODY = Y.one(document.body); - -// Setup the basic namespace. -M.course = M.course || {}; - -/** - * The toolbox class is a generic class which should never be directly - * instantiated. Please extend it instead. - * - * @class toolbox - * @constructor - * @protected - * @extends Base - */ -var TOOLBOX = function() { - TOOLBOX.superclass.constructor.apply(this, arguments); -}; - -Y.extend(TOOLBOX, Y.Base, { - /** - * Send a request using the REST API - * - * @method send_request - * @param {Object} data The data to submit with the AJAX request - * @param {Node} [statusspinner] A statusspinner which may contain a section loader - * @param {Function} success_callback The callback to use on success - * @param {Object} [optionalconfig] Any additional configuration to submit - * @chainable - */ - send_request: function(data, statusspinner, success_callback, optionalconfig) { - // Default data structure - if (!data) { - data = {}; - } - // Handle any variables which we must pass back through to - var pageparams = this.get('config').pageparams, - varname; - for (varname in pageparams) { - data[varname] = pageparams[varname]; - } - - data.sesskey = M.cfg.sesskey; - data.courseId = this.get('courseid'); - - var uri = M.cfg.wwwroot + this.get('ajaxurl'); - - // Define the configuration to send with the request - var responsetext = []; - var config = { - method: 'POST', - data: data, - on: { - success: function(tid, response) { - try { - responsetext = Y.JSON.parse(response.responseText); - if (responsetext.error) { - new M.core.ajaxException(responsetext); - } - } catch (e) { - // Ignore. - } - - // Run the callback if we have one. - if (success_callback) { - Y.bind(success_callback, this, responsetext)(); - } - - if (statusspinner) { - window.setTimeout(function() { - statusspinner.hide(); - }, 400); - } - }, - failure: function(tid, response) { - if (statusspinner) { - statusspinner.hide(); - } - new M.core.ajaxException(response); - } - }, - context: this - }; - - // Apply optional config - if (optionalconfig) { - for (varname in optionalconfig) { - config[varname] = optionalconfig[varname]; - } - } - - if (statusspinner) { - statusspinner.show(); - } - - // Send the request - Y.io(uri, config); - return this; - } -}, -{ - NAME: 'course-toolbox', - ATTRS: { - /** - * The ID of the Moodle Course being edited. - * - * @attribute courseid - * @default 0 - * @type Number - */ - courseid: { - 'value': 0 - }, - - /** - * The Moodle course format. - * - * @attribute format - * @default 'topics' - * @type String - */ - format: { - 'value': 'topics' - }, - /** - * The URL to use when submitting requests. - * @attribute ajaxurl - * @default null - * @type String - */ - ajaxurl: { - 'value': null - }, - /** - * Any additional configuration passed when creating the instance. - * - * @attribute config - * @default {} - * @type Object - */ - config: { - 'value': {} - } - } -} -); diff --git a/course/yui/src/toolboxes/meta/toolboxes.json b/course/yui/src/toolboxes/meta/toolboxes.json deleted file mode 100644 index ce2e74df145..00000000000 --- a/course/yui/src/toolboxes/meta/toolboxes.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "moodle-course-toolboxes": { - "requires": [ - "node", - "base", - "event-key", - "node", - "io", - "moodle-course-coursebase", - "moodle-course-util" - ] - } -} From 8341055eb47147a4cf3946c24f1a6f17be10a1cc Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Fri, 3 Feb 2017 09:40:46 +0800 Subject: [PATCH 5/6] MDL-4782 course: Allow activities in the "stealth" mode Add field 'visibleoncoursepage' to the course_modules table Add site-wide setting for turning on stealth mode availability Add callback for "stealth" mode support in the course formats Change display of modules/sections availability on the course page --- admin/settings/subsystems.php | 6 + backup/moodle2/backup_stepslib.php | 2 +- .../site_main_menu/block_site_main_menu.php | 8 +- .../block_social_activities.php | 8 +- course/format/lib.php | 74 +++++++++ course/format/renderer.php | 73 +++++---- course/format/social/lib.php | 12 ++ course/format/topics/lib.php | 30 ++++ course/format/topics/renderer.php | 6 +- course/format/upgrade.txt | 9 + course/format/weeks/lib.php | 23 +++ course/lib.php | 136 +++++++++------ course/mod.php | 35 ++-- course/modlib.php | 4 +- course/moodleform_mod.php | 7 +- course/renderer.php | 155 ++++++++++-------- course/tests/courselib_test.php | 2 + course/tests/externallib_test.php | 4 +- course/tests/modlib_test.php | 1 + lang/en/moodle.php | 7 + lib/db/install.xml | 3 +- lib/db/upgrade.php | 14 ++ lib/form/modvisible.php | 84 +++++++++- lib/modinfolib.php | 68 +++++++- lib/moodlelib.php | 3 +- lib/navigationlib.php | 4 +- lib/testing/generator/module_generator.php | 1 + mod/glossary/import.php | 1 + theme/boost/scss/moodle/core.scss | 4 - theme/boost/scss/moodle/course.scss | 27 +-- theme/bootstrapbase/less/moodle/core.less | 3 - theme/bootstrapbase/less/moodle/course.less | 66 ++++---- theme/bootstrapbase/style/moodle.css | 38 ++--- version.php | 2 +- 34 files changed, 659 insertions(+), 261 deletions(-) diff --git a/admin/settings/subsystems.php b/admin/settings/subsystems.php index cc65569f43e..6f5ded55cef 100644 --- a/admin/settings/subsystems.php +++ b/admin/settings/subsystems.php @@ -54,4 +54,10 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page $optionalsubsystems->add(new admin_setting_configcheckbox('enableglobalsearch', new lang_string('enableglobalsearch', 'admin'), new lang_string('enableglobalsearch_desc', 'admin'), 0, 1, 0)); + + $choices = array(); + $choices[0] = new lang_string('no'); + $choices[1] = new lang_string('yes'); + $optionalsubsystems->add(new admin_setting_configselect('allowstealth', new lang_string('allowstealthmodules'), + new lang_string('allowstealthmodules_help'), 0, $choices)); } diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index a51d34d74ef..f81e3bea8bd 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -264,7 +264,7 @@ class backup_module_structure_step extends backup_structure_step { $module = new backup_nested_element('module', array('id', 'version'), array( 'modulename', 'sectionid', 'sectionnumber', 'idnumber', - 'added', 'score', 'indent', 'visible', + 'added', 'score', 'indent', 'visible', 'visibleoncoursepage', 'visibleold', 'groupmode', 'groupingid', 'completion', 'completiongradeitemnumber', 'completionview', 'completionexpected', 'availability', 'showdescription')); diff --git a/blocks/site_main_menu/block_site_main_menu.php b/blocks/site_main_menu/block_site_main_menu.php index 26bbf00929e..60b20a3f0bf 100644 --- a/blocks/site_main_menu/block_site_main_menu.php +++ b/blocks/site_main_menu/block_site_main_menu.php @@ -59,7 +59,7 @@ class block_site_main_menu extends block_list { if (!empty($modinfo->sections[0])) { foreach($modinfo->sections[0] as $cmid) { $cm = $modinfo->cms[$cmid]; - if (!$cm->uservisible) { + if (!$cm->uservisible || !$cm->is_visible_on_course_page()) { continue; } @@ -72,7 +72,7 @@ class block_site_main_menu extends block_list { if (!empty($cm->url)) { $content = html_writer::div($courserenderer->course_section_cm_name($cm), 'activity'); } else { - $content = $cm->get_formatted_content(array('overflowdiv' => true, 'noclean' => true)); + $content = $courserenderer->course_section_cm_text($cm); } $this->content->items[] = $indent . html_writer::div($content, 'main-menu-content'); @@ -103,7 +103,7 @@ class block_site_main_menu extends block_list { if (!empty($modinfo->sections[0])) { foreach ($modinfo->sections[0] as $modnumber) { $mod = $modinfo->cms[$modnumber]; - if (!$mod->uservisible) { + if (!$mod->uservisible || !$mod->is_visible_on_course_page()) { continue; } if (!$ismoving) { @@ -138,7 +138,7 @@ class block_site_main_menu extends block_list { $indent = ''; } if (!$mod->url) { - $content = $mod->get_formatted_content(array('overflowdiv' => true, 'noclean' => true)); + $content = $courserenderer->course_section_cm_text($mod); } else { $content = html_writer::div($courserenderer->course_section_cm_name($mod), ' activity'); } diff --git a/blocks/social_activities/block_social_activities.php b/blocks/social_activities/block_social_activities.php index 2553413169b..63e9cbbc374 100644 --- a/blocks/social_activities/block_social_activities.php +++ b/blocks/social_activities/block_social_activities.php @@ -61,12 +61,12 @@ class block_social_activities extends block_list { if (!empty($modinfo->sections[0])) { foreach($modinfo->sections[0] as $cmid) { $cm = $modinfo->cms[$cmid]; - if (!$cm->uservisible) { + if (!$cm->uservisible || !$cm->is_visible_on_course_page()) { continue; } if (!$cm->url) { - $content = $cm->get_formatted_content(array('overflowdiv' => true, 'noclean' => true)); + $content = $courserenderer->course_section_cm_text($cm); $this->content->items[] = $content; $this->content->icons[] = ''; } else { @@ -98,7 +98,7 @@ class block_social_activities extends block_list { if (!empty($modinfo->sections[0])) { foreach ($modinfo->sections[0] as $modnumber) { $mod = $modinfo->cms[$modnumber]; - if (!$mod->uservisible) { + if (!$mod->uservisible || !$mod->is_visible_on_course_page()) { continue; } if (!$ismoving) { @@ -128,7 +128,7 @@ class block_social_activities extends block_list { $this->content->icons[] = ''; } if (!$mod->url) { - $content = $mod->get_formatted_content(array('overflowdiv' => true, 'noclean' => true)); + $content = $courserenderer->course_section_cm_text($mod); $this->content->items[] = $content . $editbuttons; $this->content->icons[] = ''; } else { diff --git a/course/format/lib.php b/course/format/lib.php index 3bb98305c96..0c7a4f265af 100644 --- a/course/format/lib.php +++ b/course/format/lib.php @@ -1170,6 +1170,68 @@ abstract class format_base { $startdate = $mform->getElementValue($fieldnames['startdate']); return $mform->getElement($fieldnames['startdate'])->exportValue($startdate); } + + /** + * Returns whether this course format allows the activity to + * have "triple visibility state" - visible always, hidden on course page but available, hidden. + * + * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module) + * @param stdClass|section_info $section section where this module is located or will be added to + * @return bool + */ + public function allow_stealth_module_visibility($cm, $section) { + return false; + } + + /** + * Callback used in WS core_course_edit_section when teacher performs an AJAX action on a section (show/hide) + * + * Access to the course is already validated in the WS but the callback has to make sure + * that particular action is allowed by checking capabilities + * + * Course formats should register + * + * @param stdClass|section_info $section + * @param string $action + * @param int $sr + * @return null|array|stdClass any data for the Javascript post-processor (must be json-encodeable) + */ + public function section_action($section, $action, $sr) { + global $PAGE; + if (!$this->uses_sections() || !$section->section) { + // No section actions are allowed if course format does not support sections. + // No actions are allowed on the 0-section by default (overwrite in course format if needed). + throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action)); + } + + $course = $this->get_course(); + $coursecontext = context_course::instance($course->id); + switch($action) { + case 'hide': + case 'show': + require_capability('moodle/course:sectionvisibility', $coursecontext); + $visible = ($action === 'hide') ? 0 : 1; + course_update_section($course, $section, array('visible' => $visible)); + break; + default: + throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action)); + } + + $modules = []; + + $modinfo = get_fast_modinfo($course); + $coursesections = $modinfo->sections; + if (array_key_exists($section->section, $coursesections)) { + $courserenderer = $PAGE->get_renderer('core', 'course'); + $completioninfo = new completion_info($course); + foreach ($coursesections[$section->section] as $cmid) { + $cm = $modinfo->get_cm($cmid); + $modules[] = $courserenderer->course_section_cm_list_item($course, $completioninfo, $cm, $sr); + } + } + + return ['modules' => $modules]; + } } /** @@ -1231,4 +1293,16 @@ class format_site extends format_base { } return $courseformatoptions; } + + /** + * Returns whether this course format allows the activity to + * have "triple visibility state" - visible always, hidden on course page but available, hidden. + * + * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module) + * @param stdClass|section_info $section section where this module is located or will be added to + * @return bool + */ + public function allow_stealth_module_visibility($cm, $section) { + return true; + } } diff --git a/course/format/renderer.php b/course/format/renderer.php index 283ca760fb0..c9237eb6df6 100644 --- a/course/format/renderer.php +++ b/course/format/renderer.php @@ -38,7 +38,7 @@ defined('MOODLE_INTERNAL') || die(); */ abstract class format_section_renderer_base extends plugin_renderer_base { - /** @var contains instance of core course renderer */ + /** @var core_course_renderer contains instance of core course renderer */ protected $courserenderer; /** @@ -127,7 +127,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base { $menu->add($al); } - $o .= html_writer::div($this->render($menu), 'section_action_menu'); + $o .= html_writer::div($this->render($menu), 'section_action_menu', + array('data-sectionid' => $section->id)); } return $o; @@ -194,7 +195,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base { // Only in the non-general sections. if (!$section->visible) { $sectionstyle = ' hidden'; - } else if (course_get_format($course)->is_section_current($section)) { + } + if (course_get_format($course)->is_section_current($section)) { $sectionstyle = ' current'; } } @@ -226,10 +228,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base { $sectionname = html_writer::tag('span', $this->section_title($section, $course)); $o.= $this->output->heading($sectionname, 3, 'sectionname' . $classes); - $context = context_course::instance($course->id); - - $o .= $this->section_availability_message($section, - has_capability('moodle/course:viewhiddensections', $context)); + $o .= $this->section_availability($section); $o .= html_writer::start_tag('div', array('class' => 'summary')); $o .= $this->format_summary_text($section); @@ -306,14 +305,12 @@ abstract class format_section_renderer_base extends plugin_renderer_base { return array(); } + $sectionreturn = $onsectionpage ? $section->section : null; + $coursecontext = context_course::instance($course->id); $isstealth = isset($course->numsections) && ($section->section > $course->numsections); - if ($onsectionpage) { - $baseurl = course_get_url($course, $section->section); - } else { - $baseurl = course_get_url($course); - } + $baseurl = course_get_url($course, $sectionreturn); $baseurl->param('sesskey', sesskey()); $controls = array(); @@ -326,7 +323,6 @@ abstract class format_section_renderer_base extends plugin_renderer_base { $streditsection = get_string('editsection'); } - $sectionreturn = $onsectionpage ? $section->section : 0; $controls['edit'] = array( 'url' => new moodle_url('/course/editsection.php', array('id' => $section->id, 'sr' => $sectionreturn)), 'icon' => 'i/settings', @@ -347,7 +343,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base { 'icon' => 'i/hide', 'name' => $strhidefromothers, 'pixattr' => array('class' => '', 'alt' => $strhidefromothers), - 'attr' => array('class' => 'icon editing_showhide', 'title' => $strhidefromothers)); + 'attr' => array('class' => 'icon editing_showhide', 'title' => $strhidefromothers, + 'data-sectionreturn' => $sectionreturn, 'data-action' => 'hide')); } else { $strshowfromothers = get_string('showfromothers', 'format_'.$course->format); $url->param('show', $section->section); @@ -356,7 +353,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base { 'icon' => 'i/show', 'name' => $strshowfromothers, 'pixattr' => array('class' => '', 'alt' => $strshowfromothers), - 'attr' => array('class' => 'icon editing_showhide', 'title' => $strshowfromothers)); + 'attr' => array('class' => 'icon editing_showhide', 'title' => $strshowfromothers, + 'data-sectionreturn' => $sectionreturn, 'data-action' => 'show')); } } @@ -399,14 +397,14 @@ abstract class format_section_renderer_base extends plugin_renderer_base { } $url = new moodle_url('/course/editsection.php', array( 'id' => $section->id, - 'sr' => $onsectionpage ? $section->section : 0, + 'sr' => $sectionreturn, 'delete' => 1)); $controls['delete'] = array( 'url' => $url, 'icon' => 'i/delete', 'name' => $strdelete, 'pixattr' => array('class' => '', 'alt' => $strdelete), - 'attr' => array('class' => 'icon delete', 'title' => $strdelete)); + 'attr' => array('class' => 'icon editing_delete', 'title' => $strdelete)); } } @@ -453,9 +451,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base { $o.= html_writer::end_tag('div'); $o.= $this->section_activity_summary($section, $course, null); - $context = context_course::instance($course->id); - $o .= $this->section_availability_message($section, - has_capability('moodle/course:viewhiddensections', $context)); + $o .= $this->section_availability($section); $o .= html_writer::end_tag('div'); $o .= html_writer::end_tag('li'); @@ -549,31 +545,50 @@ abstract class format_section_renderer_base extends plugin_renderer_base { * are going to be unavailable etc). This logic is the same as for * activities. * - * @param stdClass $section The course_section entry from DB + * @param section_info $section The course_section entry from DB * @param bool $canviewhidden True if user can view hidden sections * @return string HTML to output */ protected function section_availability_message($section, $canviewhidden) { global $CFG; $o = ''; - if (!$section->uservisible) { - // Note: We only get to this function if availableinfo is non-empty, - // so there is definitely something to print. - $formattedinfo = \core_availability\info::format_info( - $section->availableinfo, $section->course); - $o .= html_writer::div($formattedinfo, 'availabilityinfo'); - } else if ($canviewhidden && !empty($CFG->enableavailability) && $section->visible) { + if (!$section->visible) { + if ($canviewhidden) { + $o .= $this->courserenderer->availability_info(get_string('hiddenfromstudents'), 'ishidden'); + } + } else if (!$section->uservisible) { + if ($section->availableinfo) { + // Note: We only get to this function if availableinfo is non-empty, + // so there is definitely something to print. + $formattedinfo = \core_availability\info::format_info( + $section->availableinfo, $section->course); + $o .= $this->courserenderer->availability_info($formattedinfo); + } + } else if ($canviewhidden && !empty($CFG->enableavailability)) { + // Check if there is an availability restriction. $ci = new \core_availability\info_section($section); $fullinfo = $ci->get_full_information(); if ($fullinfo) { $formattedinfo = \core_availability\info::format_info( $fullinfo, $section->course); - $o .= html_writer::div($formattedinfo, 'availabilityinfo'); + $o .= $this->courserenderer->availability_info($formattedinfo); } } return $o; } + /** + * Displays availability information for the section (hidden, not available unles, etc.) + * + * @param section_info $section + * @return string + */ + public function section_availability($section) { + $context = context_course::instance($section->course); + $canviewhidden = has_capability('moodle/course:viewhiddensections', $context); + return html_writer::div($this->section_availability_message($section, $canviewhidden), 'section_availability'); + } + /** * Show if something is on on the course clipboard (moving around) * diff --git a/course/format/social/lib.php b/course/format/social/lib.php index d405e7c444b..c44e0f6ff44 100644 --- a/course/format/social/lib.php +++ b/course/format/social/lib.php @@ -108,4 +108,16 @@ class format_social extends format_base { } return $courseformatoptions; } + + /** + * Returns whether this course format allows the activity to + * have "triple visibility state" - visible always, hidden on course page but available, hidden. + * + * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module) + * @param stdClass|section_info $section section where this module is located or will be added to + * @return bool + */ + public function allow_stealth_module_visibility($cm, $section) { + return true; + } } diff --git a/course/format/topics/lib.php b/course/format/topics/lib.php index f5e0488084c..1bc58ab3e8e 100644 --- a/course/format/topics/lib.php +++ b/course/format/topics/lib.php @@ -410,6 +410,36 @@ class format_topics extends format_base { public function supports_news() { return true; } + + /** + * Returns whether this course format allows the activity to + * have "triple visibility state" - visible always, hidden on course page but available, hidden. + * + * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module) + * @param stdClass|section_info $section section where this module is located or will be added to + * @return bool + */ + public function allow_stealth_module_visibility($cm, $section) { + // Allow the third visibility state inside visible sections or in section 0, not allow in orphaned sections. + return !$section->section || ($section->visible && $section->section <= $this->get_course()->numsections); + } + + public function section_action($section, $action, $sr) { + global $PAGE; + + if ($section->section && ($action === 'setmarker' || $action === 'removemarker')) { + // Format 'topics' allows to set and remove markers in addition to common section actions. + require_capability('moodle/course:setcurrentsection', context_course::instance($this->courseid)); + course_set_marker($this->courseid, ($action === 'setmarker') ? $section->section : 0); + return null; + } + + // For show/hide actions call the parent method and return the new content for .section_availability element. + $rv = parent::section_action($section, $action, $sr); + $renderer = $PAGE->get_renderer('format_topics'); + $rv['section_availability'] = $renderer->section_availability($this->get_section($section)); + return $rv; + } } /** diff --git a/course/format/topics/renderer.php b/course/format/topics/renderer.php index 9ed95bdd358..963e0666a94 100644 --- a/course/format/topics/renderer.php +++ b/course/format/topics/renderer.php @@ -129,7 +129,8 @@ class format_topics_renderer extends format_section_renderer_base { $controls['highlight'] = array('url' => $url, "icon" => 'i/marked', 'name' => $highlightoff, 'pixattr' => array('class' => '', 'alt' => $markedthistopic), - 'attr' => array('class' => 'editing_highlight', 'title' => $markedthistopic)); + 'attr' => array('class' => 'editing_highlight', 'title' => $markedthistopic, + 'data-action' => 'removemarker')); } else { $url->param('marker', $section->section); $markthistopic = get_string('markthistopic'); @@ -137,7 +138,8 @@ class format_topics_renderer extends format_section_renderer_base { $controls['highlight'] = array('url' => $url, "icon" => 'i/marker', 'name' => $highlight, 'pixattr' => array('class' => '', 'alt' => $markthistopic), - 'attr' => array('class' => 'editing_highlight', 'title' => $markthistopic)); + 'attr' => array('class' => 'editing_highlight', 'title' => $markthistopic, + 'data-action' => 'setmarker')); } } diff --git a/course/format/upgrade.txt b/course/format/upgrade.txt index ccc38b6b440..f5cf7caf072 100644 --- a/course/format/upgrade.txt +++ b/course/format/upgrade.txt @@ -2,6 +2,15 @@ This files describes API changes for course formats Overview of this plugin type at http://docs.moodle.org/dev/Course_formats +=== 3.3 === +* Javascript code for editing activities and sections was moved to an AMD module, course/rest.php is no longer + responsible for editing actions, instead it is done in web services. Carefully test all editing actions during upgrade. +* The new method format_base::allow_stealth_module_visibility() can indicate whether course format supports "stealth" + activities mode when they are available but not visible on course page. Course format that supports stealth mode + must check $cm->is_visible_on_course_page() when displaying activities list on the course page instead of $cm->uservisible. + For all other plugins except course formats the same property $cm->uservisible indicates if the activity contents + is actually available to student. + === 3.2 === * Callback delete_course is deprecated and should be replaced with observer for event \core\event\course_content_deleted * Course formats can overwrite get_default_course_enddate function to set the default course end date for new courses. diff --git a/course/format/weeks/lib.php b/course/format/weeks/lib.php index af0fba40424..408b5900ed7 100644 --- a/course/format/weeks/lib.php +++ b/course/format/weeks/lib.php @@ -497,6 +497,29 @@ class format_weeks extends format_base { public function supports_news() { return true; } + + /** + * Returns whether this course format allows the activity to + * have "triple visibility state" - visible always, hidden on course page but available, hidden. + * + * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module) + * @param stdClass|section_info $section section where this module is located or will be added to + * @return bool + */ + public function allow_stealth_module_visibility($cm, $section) { + // Allow the third visibility state inside visible sections or in section 0, not allow in orphaned sections. + return !$section->section || ($section->visible && $section->section <= $this->get_course()->numsections); + } + + public function section_action($section, $action, $sr) { + global $PAGE; + + // Call the parent method and return the new content for .section_availability element. + $rv = parent::section_action($section, $action, $sr); + $renderer = $PAGE->get_renderer('format_weeks'); + $rv['section_availability'] = $renderer->section_availability($this->get_section($section)); + return $rv; + } } /** diff --git a/course/lib.php b/course/lib.php index 22627111e2e..4fdcc4d9536 100644 --- a/course/lib.php +++ b/course/lib.php @@ -388,13 +388,16 @@ function get_array_of_activities($courseid) { if (empty($rawmods)) { return $mod; // always return array } + $courseformat = course_get_format($course); - if ($sections = $DB->get_records('course_sections', array('course' => $courseid), 'section ASC', 'id,section,sequence')) { + if ($sections = $DB->get_records('course_sections', array('course' => $courseid), + 'section ASC', 'id,section,sequence,visible')) { // First check and correct obvious mismatches between course_sections.sequence and course_modules.section. if ($errormessages = course_integrity_check($courseid, $rawmods, $sections)) { debugging(join('
      ', $errormessages)); $rawmods = get_course_mods($courseid); - $sections = $DB->get_records('course_sections', array('course' => $courseid), 'section ASC', 'id,section,sequence'); + $sections = $DB->get_records('course_sections', array('course' => $courseid), + 'section ASC', 'id,section,sequence,visible'); } // Build array of activities. foreach ($sections as $section) { @@ -404,6 +407,13 @@ function get_array_of_activities($courseid) { if (empty($rawmods[$seq])) { continue; } + // Adjust visibleoncoursepage, value in DB may not respect format availability. + $rawmods[$seq]->visibleoncoursepage = (!$rawmods[$seq]->visible + || $rawmods[$seq]->visibleoncoursepage + || empty($CFG->allowstealth) + || !$courseformat->allow_stealth_module_visibility($rawmods[$seq], $section)) ? 1 : 0; + + // Create an object that will be cached. $mod[$seq] = new stdClass(); $mod[$seq]->id = $rawmods[$seq]->instance; $mod[$seq]->cm = $rawmods[$seq]->id; @@ -418,6 +428,7 @@ function get_array_of_activities($courseid) { $mod[$seq]->score = $rawmods[$seq]->score; $mod[$seq]->idnumber = $rawmods[$seq]->idnumber; $mod[$seq]->visible = $rawmods[$seq]->visible; + $mod[$seq]->visibleoncoursepage = $rawmods[$seq]->visibleoncoursepage; $mod[$seq]->visibleold = $rawmods[$seq]->visibleold; $mod[$seq]->groupmode = $rawmods[$seq]->groupmode; $mod[$seq]->groupingid = $rawmods[$seq]->groupingid; @@ -557,9 +568,15 @@ function get_module_types_names($plural = false) { * @return void */ function course_set_marker($courseid, $marker) { - global $DB; + global $DB, $COURSE; $DB->set_field("course", "marker", $marker, array('id' => $courseid)); - format_base::reset_course_cache($courseid); + if ($COURSE && $COURSE->id == $courseid) { + $COURSE->marker = $marker; + } + if (class_exists('format_base')) { + format_base::reset_course_cache($courseid); + } + course_modinfo::clear_instance_cache($courseid); } /** @@ -950,33 +967,29 @@ function set_coursemodule_idnumber($id, $idnumber) { * * @param int $id of the module * @param int $visible state of the module + * @param int $visibleoncoursepage state of the module on the course page * @return bool false when the module was not found, true otherwise */ -function set_coursemodule_visible($id, $visible) { +function set_coursemodule_visible($id, $visible, $visibleoncoursepage = 1) { global $DB, $CFG; require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/calendar/lib.php'); - // Trigger developer's attention when using the previously removed argument. - if (func_num_args() > 2) { - debugging('Wrong number of arguments passed to set_coursemodule_visible(), $prevstateoverrides - has been removed.', DEBUG_DEVELOPER); - } - if (!$cm = $DB->get_record('course_modules', array('id'=>$id))) { return false; } // Create events and propagate visibility to associated grade items if the value has changed. // Only do this if it's changed to avoid accidently overwriting manual showing/hiding of student grades. - if ($cm->visible == $visible) { + if ($cm->visible == $visible && $cm->visibleoncoursepage == $visibleoncoursepage) { return true; } if (!$modulename = $DB->get_field('modules', 'name', array('id'=>$cm->module))) { return false; } - if ($events = $DB->get_records('event', array('instance'=>$cm->instance, 'modulename'=>$modulename))) { + if (($cm->visible != $visible) && + ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename)))) { foreach($events as $event) { if ($visible) { $event = new calendar_event($event); @@ -993,17 +1006,19 @@ function set_coursemodule_visible($id, $visible) { $cminfo = new stdClass(); $cminfo->id = $id; $cminfo->visible = $visible; + $cminfo->visibleoncoursepage = $visibleoncoursepage; $cminfo->visibleold = $visible; $DB->update_record('course_modules', $cminfo); // Hide the associated grade items so the teacher doesn't also have to go to the gradebook and hide them there. // Note that this must be done after updating the row in course_modules, in case // the modules grade_item_update function needs to access $cm->visible. - if (plugin_supports('mod', $modulename, FEATURE_CONTROLS_GRADE_VISIBILITY) && + if ($cm->visible != $visible && + plugin_supports('mod', $modulename, FEATURE_CONTROLS_GRADE_VISIBILITY) && component_callback_exists('mod_' . $modulename, 'grade_item_update')) { $instance = $DB->get_record($modulename, array('id' => $cm->instance), '*', MUST_EXIST); component_callback('mod_' . $modulename, 'grade_item_update', array($instance)); - } else { + } else if ($cm->visible != $visible) { $grade_items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$modulename, 'iteminstance'=>$cm->instance, 'courseid'=>$cm->course)); if ($grade_items) { foreach ($grade_items as $grade_item) { @@ -1593,10 +1608,10 @@ function course_update_section($course, $section, $data) { if ($cm = get_coursemodule_from_id(null, $moduleid, $courseid)) { if ($data['visible']) { // As we unhide the section, we use the previously saved visibility stored in visibleold. - set_coursemodule_visible($moduleid, $cm->visibleold); + set_coursemodule_visible($moduleid, $cm->visibleold, $cm->visibleoncoursepage); } else { // We hide the section, so we hide the module but we store the original state in visibleold. - set_coursemodule_visible($moduleid, 0); + set_coursemodule_visible($moduleid, 0, $cm->visibleoncoursepage); $DB->set_field('course_modules', 'visibleold', $cm->visible, array('id' => $moduleid)); } \core\event\course_module_updated::create_from_cm($cm)->trigger(); @@ -1765,12 +1780,13 @@ function moveto_module($mod, $section, $beforemod=NULL) { * @return array array of action_link or pix_icon objects */ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) { - global $COURSE, $SITE; + global $COURSE, $SITE, $CFG; static $str; $coursecontext = context_course::instance($mod->course); $modcontext = context_module::instance($mod->id); + $courseformat = course_get_format($mod->get_course()); $editcaps = array('moodle/course:manageactivities', 'moodle/course:activityvisibility', 'moodle/role:assign'); $dupecaps = array('moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport'); @@ -1784,7 +1800,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) { if (!isset($str)) { $str = get_strings(array('delete', 'move', 'moveright', 'moveleft', - 'editsettings', 'duplicate', 'hide', 'show'), 'moodle'); + 'editsettings', 'duplicate', 'hide', 'makeavailable', 'makeunavailable', 'show'), 'moodle'); $str->assign = get_string('assignroles', 'role'); $str->groupsnone = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsnone")); $str->groupsseparate = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsseparate")); @@ -1830,7 +1846,8 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) { new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '1')), new pix_icon($rightarrow, $str->moveright, 'moodle', array('class' => 'iconsmall', 'title' => '')), $str->moveright, - array('class' => 'editing_moveright ' . $enabledclass, 'data-action' => 'moveright', 'data-keepopen' => true) + array('class' => 'editing_moveright ' . $enabledclass, 'data-action' => 'moveright', + 'data-keepopen' => true, 'data-sectionreturn' => $sr) ); if ($indent <= $indentlimits->min) { @@ -1842,21 +1859,33 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) { new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '-1')), new pix_icon($leftarrow, $str->moveleft, 'moodle', array('class' => 'iconsmall', 'title' => '')), $str->moveleft, - array('class' => 'editing_moveleft ' . $enabledclass, 'data-action' => 'moveleft', 'data-keepopen' => true) + array('class' => 'editing_moveleft ' . $enabledclass, 'data-action' => 'moveleft', + 'data-keepopen' => true, 'data-sectionreturn' => $sr) ); } - // Hide/Show. + // Hide/Show/Available/Unavailable. if (has_capability('moodle/course:activityvisibility', $modcontext)) { - if ($mod->visible) { + $allowstealth = !empty($CFG->allowstealth) && $courseformat->allow_stealth_module_visibility($mod, $mod->get_section_info()); + + $sectionvisible = $mod->get_section_info()->visible; + // The module on the course page may be in one of the following states: + // - Available and displayed on the course page ($displayedoncoursepage); + // - Not available and not displayed on the course page ($unavailable); + // - Available but not displayed on the course page ($stealth) - this can also be a visible activity in a hidden section. + $displayedoncoursepage = $mod->visible && $mod->visibleoncoursepage && $sectionvisible; + $unavailable = !$mod->visible; + $stealth = $mod->visible && (!$mod->visibleoncoursepage || !$sectionvisible); + if ($displayedoncoursepage) { $actions['hide'] = new action_menu_link_secondary( new moodle_url($baseurl, array('hide' => $mod->id)), new pix_icon('t/hide', $str->hide, 'moodle', array('class' => 'iconsmall', 'title' => '')), $str->hide, array('class' => 'editing_hide', 'data-action' => 'hide') ); - } else { + } else if (!$displayedoncoursepage && $sectionvisible) { + // 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->show, 'moodle', array('class' => 'iconsmall', 'title' => '')), @@ -1864,16 +1893,38 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) { array('class' => 'editing_show', 'data-action' => 'show') ); } + + if ($stealth) { + // 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' => '')), + $str->makeunavailable, + array('class' => 'editing_makeunavailable', 'data-action' => 'hide', 'data-sectionreturn' => $sr) + ); + } else if ($unavailable && (!$sectionvisible || $allowstealth) && $mod->has_view()) { + // Allow to make visually hidden module available in gradebook and other reports by making it a "stealth" module. + // When the section is hidden it is an equivalent of "showing" the module. + // Activities without the link (i.e. labels) can not be made available but hidden on course page. + $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' => '')), + $str->makeavailable, + array('class' => 'editing_makeavailable', 'data-action' => $action, 'data-sectionreturn' => $sr) + ); + } } // Duplicate (require both target import caps to be able to duplicate and backup2 support, see modduplicate.php) if (has_all_capabilities($dupecaps, $coursecontext) && - plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2)) { + plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2) && + 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' => '')), $str->duplicate, - array('class' => 'editing_duplicate', 'data-action' => 'duplicate', 'data-sr' => $sr) + array('class' => 'editing_duplicate', 'data-action' => 'duplicate', 'data-sectionreturn' => $sr) ); } @@ -1884,16 +1935,19 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) { $nextgroupmode = VISIBLEGROUPS; $grouptitle = $str->groupsseparate; $actionname = 'groupsseparate'; + $nextactionname = 'groupsvisible'; $groupimage = 'i/groups'; } else if ($mod->effectivegroupmode == VISIBLEGROUPS) { $nextgroupmode = NOGROUPS; $grouptitle = $str->groupsvisible; $actionname = 'groupsvisible'; + $nextactionname = 'groupsnone'; $groupimage = 'i/groupv'; } else { $nextgroupmode = SEPARATEGROUPS; $grouptitle = $str->groupsnone; $actionname = 'groupsnone'; + $nextactionname = 'groupsseparate'; $groupimage = 'i/groupn'; } @@ -1901,7 +1955,8 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) { new moodle_url($baseurl, array('id' => $mod->id, 'groupmode' => $nextgroupmode)), new pix_icon($groupimage, null, 'moodle', array('class' => 'iconsmall')), $grouptitle, - array('class' => 'editing_'. $actionname, 'data-action' => $actionname, 'data-nextgroupmode' => $nextgroupmode, 'aria-live' => 'assertive') + array('class' => 'editing_'. $actionname, 'data-action' => $nextactionname, + 'aria-live' => 'assertive', 'data-sectionreturn' => $sr) ); } else { $actions['nogroupsupport'] = new action_menu_filler(); @@ -1914,7 +1969,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) { new moodle_url('/admin/roles/assign.php', array('contextid' => $modcontext->id)), new pix_icon('t/assignroles', $str->assign, 'moodle', array('class' => 'iconsmall', 'title' => '')), $str->assign, - array('class' => 'editing_assign', 'data-action' => 'assignroles') + array('class' => 'editing_assign', 'data-action' => 'assignroles', 'data-sectionreturn' => $sr) ); } @@ -1924,7 +1979,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) { new moodle_url($baseurl, array('delete' => $mod->id)), new pix_icon('t/delete', $str->delete, 'moodle', array('class' => 'iconsmall', 'title' => '')), $str->delete, - array('class' => 'editing_delete', 'data-action' => 'delete') + array('class' => 'editing_delete', 'data-action' => 'delete', 'data-sectionreturn' => $sr) ); } @@ -1970,7 +2025,7 @@ function course_get_cm_move(cm_info $mod, $sr = null) { return html_writer::link( new moodle_url($baseurl, array('copy' => $mod->id)), $OUTPUT->pix_icon($pixicon, $str->move, 'moodle', array('class' => 'iconsmall', 'title' => '')), - array('class' => 'editing_move', 'data-action' => 'move') + array('class' => 'editing_move', 'data-action' => 'move', 'data-sectionreturn' => $sr) ); } return ''; @@ -3007,25 +3062,6 @@ function include_course_ajax($course, $usedmodules = array(), $enabledmodules = $config->pageparams = array(); } - // Include toolboxes - $PAGE->requires->yui_module('moodle-course-toolboxes', - 'M.course.init_resource_toolbox', - array(array( - 'courseid' => $course->id, - 'ajaxurl' => $config->resourceurl, - 'config' => $config, - )) - ); - $PAGE->requires->yui_module('moodle-course-toolboxes', - 'M.course.init_section_toolbox', - array(array( - 'courseid' => $course->id, - 'format' => $course->format, - 'ajaxurl' => $config->sectionurl, - 'config' => $config, - )) - ); - // Include course dragdrop if (course_format_uses_sections($course->format)) { $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_section_dragdrop', @@ -3088,6 +3124,8 @@ function include_course_ajax($course, $usedmodules = array(), $enabledmodules = require_once($CFG->dirroot.'/course/dnduploadlib.php'); dndupload_add_to_course($course, $enabledmodules); + $PAGE->requires->js_call_amd('core_course/actions', 'initCoursePage', array($course->format)); + return true; } diff --git a/course/mod.php b/course/mod.php index 9c23493ae2a..54f6f9cb5a2 100644 --- a/course/mod.php +++ b/course/mod.php @@ -33,6 +33,7 @@ $indent = optional_param('indent', 0, PARAM_INT); $update = optional_param('update', 0, PARAM_INT); $duplicate = optional_param('duplicate', 0, PARAM_INT); $hide = optional_param('hide', 0, PARAM_INT); +$stealth = optional_param('stealth', 0, PARAM_INT); $show = optional_param('show', 0, PARAM_INT); $copy = optional_param('copy', 0, PARAM_INT); $moveto = optional_param('moveto', 0, PARAM_INT); @@ -202,28 +203,30 @@ if ((!empty($movetosection) or !empty($moveto)) and confirm_sesskey()) { $modcontext = context_module::instance($cm->id); require_capability('moodle/course:activityvisibility', $modcontext); - set_coursemodule_visible($cm->id, 0); - \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger(); + if (set_coursemodule_visible($cm->id, 0)) { + \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger(); + } + redirect(course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn))); + +} else if (!empty($stealth) and confirm_sesskey()) { + list($course, $cm) = get_course_and_cm_from_cmid($stealth); + require_login($course, false, $cm); + require_capability('moodle/course:activityvisibility', $cm->context); + + if (set_coursemodule_visible($cm->id, 1, 0)) { + \core\event\course_module_updated::create_from_cm($cm)->trigger(); + } redirect(course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn))); } else if (!empty($show) and confirm_sesskey()) { - $cm = get_coursemodule_from_id('', $show, 0, true, MUST_EXIST); - $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); - + list($course, $cm) = get_course_and_cm_from_cmid($show); require_login($course, false, $cm); - $coursecontext = context_course::instance($course->id); - $modcontext = context_module::instance($cm->id); - require_capability('moodle/course:activityvisibility', $modcontext); + require_capability('moodle/course:activityvisibility', $cm->context); + $section = $cm->get_section_info(); - $section = $DB->get_record('course_sections', array('id'=>$cm->section), '*', MUST_EXIST); - - $module = $DB->get_record('modules', array('id'=>$cm->module), '*', MUST_EXIST); - - if ($module->visible and ($section->visible or (SITEID == $cm->course))) { - set_coursemodule_visible($cm->id, 1); - \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger(); + if (set_coursemodule_visible($cm->id, 1)) { + \core\event\course_module_updated::create_from_cm($cm)->trigger(); } - redirect(course_get_url($course, $section->section, array('sr' => $sectionreturn))); } else if ($groupmode > -1 and confirm_sesskey()) { diff --git a/course/modlib.php b/course/modlib.php index d45966d3a63..68ed661ad6a 100644 --- a/course/modlib.php +++ b/course/modlib.php @@ -60,6 +60,7 @@ function add_moduleinfo($moduleinfo, $course, $mform = null) { $newcm->module = $moduleinfo->module; $newcm->instance = 0; // Not known yet, will be updated later (this is similar to restore code). $newcm->visible = $moduleinfo->visible; + $newcm->visibleoncoursepage = $moduleinfo->visibleoncoursepage; $newcm->visibleold = $moduleinfo->visible; if (isset($moduleinfo->cmidnumber)) { $newcm->idnumber = $moduleinfo->cmidnumber; @@ -597,7 +598,7 @@ function update_moduleinfo($cm, $moduleinfo, $course, $mform = null) { // Make sure visibility is set correctly (in particular in calendar). if (has_capability('moodle/course:activityvisibility', $modcontext)) { - set_coursemodule_visible($moduleinfo->coursemodule, $moduleinfo->visible); + set_coursemodule_visible($moduleinfo->coursemodule, $moduleinfo->visible, $moduleinfo->visibleoncoursepage); } if (isset($moduleinfo->cmidnumber)) { // Label. @@ -655,6 +656,7 @@ function get_moduleinfo_data($cm, $course) { $data->coursemodule = $cm->id; $data->section = $cw->section; // The section number itself - relative!!! (section column in course_sections) $data->visible = $cm->visible; //?? $cw->visible ? $cm->visible : 0; // section hiding overrides + $data->visibleoncoursepage = $cm->visibleoncoursepage; $data->cmidnumber = $cm->idnumber; // The cm IDnumber $data->groupmode = groups_get_activity_groupmode($cm); // locked later if forced $data->groupingid = $cm->groupingid; diff --git a/course/moodleform_mod.php b/course/moodleform_mod.php index f5c76e06830..f98c1fddbe3 100644 --- a/course/moodleform_mod.php +++ b/course/moodleform_mod.php @@ -21,7 +21,7 @@ abstract class moodleform_mod extends moodleform { * Section of course that module instance will be put in or is in. * This is always the section number itself (column 'section' from 'course_sections' table). * - * @var mixed + * @var int */ protected $_section; /** @@ -555,7 +555,10 @@ abstract class moodleform_mod extends moodleform { $mform->addElement('header', 'modstandardelshdr', get_string('modstandardels', 'form')); - $mform->addElement('modvisible', 'visible', get_string('visible')); + $section = get_fast_modinfo($COURSE)->get_section_info($this->_section); + $allowstealth = !empty($CFG->allowstealth) && $this->courseformat->allow_stealth_module_visibility($this->_cm, $section); + $mform->addElement('modvisible', 'visible', get_string('visible'), null, + array('allowstealth' => $allowstealth, 'sectionvisible' => $section->visible, 'cm' => $this->_cm)); if (!empty($this->_cm)) { $context = context_module::instance($this->_cm->id); if (!has_capability('moodle/course:activityvisibility', $context)) { diff --git a/course/renderer.php b/course/renderer.php index e23f5812c7e..6a6c10047e2 100644 --- a/course/renderer.php +++ b/course/renderer.php @@ -450,7 +450,7 @@ class core_course_renderer extends plugin_renderer_base { public function course_section_cm_completion($course, &$completioninfo, cm_info $mod, $displayoptions = array()) { global $CFG; $output = ''; - if (!empty($displayoptions['hidecompletion']) || !isloggedin() || isguestuser() || !$mod->uservisible) { + if (!$mod->is_visible_on_course_page()) { return $output; } if ($completioninfo === null) { @@ -582,15 +582,58 @@ class core_course_renderer extends plugin_renderer_base { * @return string */ public function course_section_cm_name(cm_info $mod, $displayoptions = array()) { - if ((!$mod->uservisible && empty($mod->availableinfo)) || !$mod->url) { + if (!$mod->is_visible_on_course_page() || !$mod->url) { // Nothing to be displayed to the user. return ''; } + list($linkclasses, $textclasses) = $this->course_section_cm_classes($mod); + $groupinglabel = $mod->get_grouping_label($textclasses); + // Render element that allows to edit activity name inline. It calls {@link course_section_cm_name_title()} // to get the display title of the activity. $tmpl = new \core_course\output\course_module_name($mod, $this->page->user_is_editing(), $displayoptions); - return $this->output->render_from_template('core/inplace_editable', $tmpl->export_for_template($this->output)); + return $this->output->render_from_template('core/inplace_editable', $tmpl->export_for_template($this->output)) . + $groupinglabel; + } + + /** + * Returns the CSS classes for the activity name/content + * + * For items which are hidden, unavailable or stealth but should be displayed + * to current user ($mod->is_visible_on_course_page()), we show those as dimmed. + * Students will also see as dimmed activities names that are not yet available + * but should still be displayed (without link) with availability info. + * + * @param cm_info $mod + * @return array array of two elements ($linkclasses, $textclasses) + */ + protected function course_section_cm_classes(cm_info $mod) { + $linkclasses = ''; + $textclasses = ''; + if ($mod->uservisible) { + $conditionalhidden = $this->is_cm_conditionally_hidden($mod); + $accessiblebutdim = (!$mod->visible || $conditionalhidden) && + has_capability('moodle/course:viewhiddenactivities', $mod->context); + if ($accessiblebutdim) { + $linkclasses .= ' dimmed'; + $textclasses .= ' dimmed_text'; + if ($conditionalhidden) { + $linkclasses .= ' conditionalhidden'; + $textclasses .= ' conditionalhidden'; + } + } + if ($mod->is_stealth()) { + // Stealth activity is the one that is not visible on course page. + // It still may be displayed to the users who can manage it. + $linkclasses .= ' stealth'; + $textclasses .= ' stealth'; + } + } else { + $linkclasses .= ' dimmed'; + $textclasses .= ' dimmed_text'; + } + return array($linkclasses, $textclasses); } /** @@ -608,12 +651,9 @@ class core_course_renderer extends plugin_renderer_base { */ public function course_section_cm_name_title(cm_info $mod, $displayoptions = array()) { $output = ''; - if (!$mod->uservisible && empty($mod->availableinfo)) { - // Nothing to be displayed to the user. - return $output; - } $url = $mod->url; - if (!$url) { + if (!$mod->is_visible_on_course_page() || !$url) { + // Nothing to be displayed to the user. return $output; } @@ -632,51 +672,22 @@ class core_course_renderer extends plugin_renderer_base { $altname = get_accesshide(' '.$altname); } - // For items which are hidden but available to current user - // ($mod->uservisible), we show those as dimmed only if the user has - // viewhiddenactivities, so that teachers see 'items which might not - // be available to some students' dimmed but students do not see 'item - // which is actually available to current student' dimmed. - $linkclasses = ''; - $accesstext = ''; - $textclasses = ''; - if ($mod->uservisible) { - $conditionalhidden = $this->is_cm_conditionally_hidden($mod); - $accessiblebutdim = (!$mod->visible || $conditionalhidden) && - has_capability('moodle/course:viewhiddenactivities', $mod->context); - if ($accessiblebutdim) { - $linkclasses .= ' dimmed'; - $textclasses .= ' dimmed_text'; - if ($conditionalhidden) { - $linkclasses .= ' conditionalhidden'; - $textclasses .= ' conditionalhidden'; - } - // Show accessibility note only if user can access the module himself. - $accesstext = get_accesshide(get_string('hiddenfromstudents').':'. $mod->modfullname); - } - } else { - $linkclasses .= ' dimmed'; - $textclasses .= ' dimmed_text'; - } + list($linkclasses, $textclasses) = $this->course_section_cm_classes($mod); // Get on-click attribute value if specified and decode the onclick - it // has already been encoded for display (puke). $onclick = htmlspecialchars_decode($mod->onclick, ENT_QUOTES); - $groupinglabel = $mod->get_grouping_label($textclasses); - // Display link itself. $activitylink = html_writer::empty_tag('img', array('src' => $mod->get_icon_url(), - 'class' => 'iconlarge activityicon', 'alt' => ' ', 'role' => 'presentation')) . $accesstext . + 'class' => 'iconlarge activityicon', 'alt' => ' ', 'role' => 'presentation')) . html_writer::tag('span', $instancename . $altname, array('class' => 'instancename')); if ($mod->uservisible) { - $output .= html_writer::link($url, $activitylink, array('class' => $linkclasses, 'onclick' => $onclick)) . - $groupinglabel; + $output .= html_writer::link($url, $activitylink, array('class' => $linkclasses, 'onclick' => $onclick)); } else { // We may be displaying this just in order to show information - // about visibility, without the actual link ($mod->uservisible) - $output .= html_writer::tag('div', $activitylink, array('class' => $textclasses)) . - $groupinglabel; + // about visibility, without the actual link ($mod->is_visible_on_course_page()). + $output .= html_writer::tag('div', $activitylink, array('class' => $textclasses)); } return $output; } @@ -690,29 +701,13 @@ class core_course_renderer extends plugin_renderer_base { */ public function course_section_cm_text(cm_info $mod, $displayoptions = array()) { $output = ''; - if (!$mod->uservisible && empty($mod->availableinfo)) { + if (!$mod->is_visible_on_course_page()) { // nothing to be displayed to the user return $output; } $content = $mod->get_formatted_content(array('overflowdiv' => true, 'noclean' => true)); - $accesstext = ''; - $textclasses = ''; - if ($mod->uservisible) { - $conditionalhidden = $this->is_cm_conditionally_hidden($mod); - $accessiblebutdim = (!$mod->visible || $conditionalhidden) && - has_capability('moodle/course:viewhiddenactivities', $mod->context); - if ($accessiblebutdim) { - $textclasses .= ' dimmed_text'; - if ($conditionalhidden) { - $textclasses .= ' conditionalhidden'; - } - // Show accessibility note only if user can access the module himself. - $accesstext = get_accesshide(get_string('hiddenfromstudents').':'. $mod->modfullname); - } - } else { - $textclasses .= ' dimmed_text'; - } - if ($mod->url) { + list($linkclasses, $textclasses) = $this->course_section_cm_classes($mod); + if ($mod->url && $mod->uservisible) { if ($content) { // If specified, display extra content after link. $output = html_writer::tag('div', $content, array('class' => @@ -722,12 +717,24 @@ class core_course_renderer extends plugin_renderer_base { $groupinglabel = $mod->get_grouping_label($textclasses); // No link, so display only content. - $output = html_writer::tag('div', $accesstext . $content . $groupinglabel, + $output = html_writer::tag('div', $content . $groupinglabel, array('class' => 'contentwithoutlink ' . $textclasses)); } return $output; } + /** + * Displays availability info for a course section or course module + * + * @param string $text + * @param string $additionalclasses + * @return string + */ + public function availability_info($text, $additionalclasses = '') { + $data = ['text' => $text, 'classes' => $additionalclasses]; + return $this->render_from_template('core/availability_info', $data); + } + /** * Renders HTML to show course module availability information (for someone who isn't allowed * to see the activity itself, or for staff) @@ -738,13 +745,17 @@ class core_course_renderer extends plugin_renderer_base { */ public function course_section_cm_availability(cm_info $mod, $displayoptions = array()) { global $CFG; + $output = ''; + if (!$mod->is_visible_on_course_page()) { + return $output; + } if (!$mod->uservisible) { // this is a student who is not allowed to see the module but might be allowed // to see availability info (i.e. "Available from ...") if (!empty($mod->availableinfo)) { $formattedinfo = \core_availability\info::format_info( $mod->availableinfo, $mod->get_course()); - $output = html_writer::tag('div', $formattedinfo, array('class' => 'availabilityinfo')); + $output = $this->availability_info($formattedinfo); } return $output; } @@ -752,7 +763,19 @@ class core_course_renderer extends plugin_renderer_base { // information that module is not available to all/some students $modcontext = context_module::instance($mod->id); $canviewhidden = has_capability('moodle/course:viewhiddenactivities', $modcontext); + if ($canviewhidden && !$mod->visible) { + // This module is hidden but current user has capability to see it. + // Do not display the availability info if the whole section is hidden. + if ($mod->get_section_info()->visible) { + $output .= $this->availability_info(get_string('hiddenfromstudents'), 'ishidden'); + } + } else if ($mod->is_stealth()) { + // This module is available but is normally not displayed on the course page + // (this user can see it because they can manage it). + $output .= $this->availability_info(get_string('hiddenoncoursepage'), 'isstealth'); + } if ($canviewhidden && !empty($CFG->enableavailability)) { + // Display information about conditional availability. // Don't add availability information if user is not editing and activity is hidden. if ($mod->visible || $this->page->user_is_editing()) { $hidinfoclass = ''; @@ -764,11 +787,11 @@ class core_course_renderer extends plugin_renderer_base { if ($fullinfo) { $formattedinfo = \core_availability\info::format_info( $fullinfo, $mod->get_course()); - return html_writer::div($formattedinfo, 'availabilityinfo ' . $hidinfoclass); + $output .= $this->availability_info($formattedinfo, $hidinfoclass); } } } - return ''; + return $output; } /** @@ -823,7 +846,7 @@ class core_course_renderer extends plugin_renderer_base { // 2) The 'availableinfo' is empty, i.e. the activity was // hidden in a way that leaves no info, such as using the // eye icon. - if (!$mod->uservisible && empty($mod->availableinfo)) { + if (!$mod->is_visible_on_course_page()) { return $output; } diff --git a/course/tests/courselib_test.php b/course/tests/courselib_test.php index e67ec73a61c..c726278b396 100644 --- a/course/tests/courselib_test.php +++ b/course/tests/courselib_test.php @@ -201,6 +201,7 @@ class core_course_courselib_testcase extends advanced_testcase { $moduleinfo->course = $course->id; $moduleinfo->groupingid = $grouping->id; $moduleinfo->visible = true; + $moduleinfo->visibleoncoursepage = true; // Sometimes optional generic values for some modules. $moduleinfo->name = 'My test module'; @@ -436,6 +437,7 @@ class core_course_courselib_testcase extends advanced_testcase { $moduleinfo->course = $course->id; $moduleinfo->groupingid = $grouping->id; $moduleinfo->visible = true; + $moduleinfo->visibleoncoursepage = true; // Sometimes optional generic values for some modules. $moduleinfo->name = 'My test module'; diff --git a/course/tests/externallib_test.php b/course/tests/externallib_test.php index 7d732b06660..cc232a00d8f 100644 --- a/course/tests/externallib_test.php +++ b/course/tests/externallib_test.php @@ -1680,7 +1680,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase { $this->assertCount(0, $result['warnings']); // Test we retrieve all the fields. - $this->assertCount(27, $result['cm']); + $this->assertCount(28, $result['cm']); $this->assertEquals($record['name'], $result['cm']['name']); $this->assertEquals($options['idnumber'], $result['cm']['idnumber']); $this->assertEquals(100, $result['cm']['grade']); @@ -1747,7 +1747,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase { $this->assertCount(0, $result['warnings']); // Test we retrieve all the fields. - $this->assertCount(22, $result['cm']); + $this->assertCount(23, $result['cm']); $this->assertEquals($record['name'], $result['cm']['name']); $this->assertEquals($options['idnumber'], $result['cm']['idnumber']); diff --git a/course/tests/modlib_test.php b/course/tests/modlib_test.php index 3d5c64c647a..f4b607c0488 100644 --- a/course/tests/modlib_test.php +++ b/course/tests/modlib_test.php @@ -99,6 +99,7 @@ class core_course_modlib_testcase extends advanced_testcase { $expecteddata->coursemodule = $assigncm->id; $expecteddata->section = $cw->section; $expecteddata->visible = $assigncm->visible; + $expecteddata->visibleoncoursepage = $assigncm->visibleoncoursepage; $expecteddata->cmidnumber = $assigncm->idnumber; $expecteddata->groupmode = groups_get_activity_groupmode($cm); $expecteddata->groupingid = $assigncm->groupingid; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index d89a33c36b1..13d8bd0bfb8 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -133,6 +133,8 @@ $string['alllogs'] = 'All logs'; $string['allmods'] = 'All {$a}'; $string['allow'] = 'Allow'; $string['allowinternal'] = 'Allow internal methods as well'; +$string['allowstealthmodules'] = 'Allow hidden but available activities'; +$string['allowstealthmodules_help'] = 'If enabled, activities can be marked as hidden on the course page but available by following a link from elsewhere. These activities are listed in the gradebook and other reports'; $string['allownone'] = 'Allow none'; $string['allownot'] = 'Do not allow'; $string['allparticipants'] = 'All participants'; @@ -918,6 +920,7 @@ $string['helpprefix2'] = 'Help with {$a}'; $string['helpwiththis'] = 'Help with this'; $string['hiddenassign'] = 'Hidden assignment'; $string['hiddenfromstudents'] = 'Hidden from students'; +$string['hiddenoncoursepage'] = 'Available but not displayed on course page'; $string['hiddensections'] = 'Hidden sections'; $string['hiddensections_help'] = 'This setting determines whether hidden sections are displayed to students in collapsed form (perhaps for a course in weekly format to indicate holidays) or are completely hidden.'; $string['hiddensectionscollapsed'] = 'Hidden sections are shown in collapsed form'; @@ -925,6 +928,7 @@ $string['hiddensectionsinvisible'] = 'Hidden sections are completely invisible'; $string['hide'] = 'Hide'; $string['hideadvancedsettings'] = 'Hide advanced settings'; $string['hidechartdata'] = 'Hide chart data'; +$string['hideonfrontpage'] = 'Hide from front page'; $string['hidepicture'] = 'Hide picture'; $string['hidesection'] = 'Hide section {$a}'; $string['hidesettings'] = 'Hide settings'; @@ -1084,8 +1088,10 @@ $string['mailstudents'] = 'Inform students'; $string['mailteachers'] = 'Inform teachers'; $string['maincoursepage'] = 'Main course page'; $string['makeafolder'] = 'Create folder'; +$string['makeavailable'] = 'Make available'; $string['makeeditable'] = 'If you make \'{$a}\' editable by the web server process (eg apache) then you could edit this file directly from this page'; $string['makethismyhome'] = 'Make this my default home page'; +$string['makeunavailable'] = 'Make unavailable'; $string['manageblocks'] = 'Blocks'; $string['managecategorythis'] = 'Manage this category'; $string['managecourses'] = 'Manage courses'; @@ -1640,6 +1646,7 @@ $string['secondstotime86400'] = '1 day'; $string['secretalreadyused'] = 'Change password confirmation link was already used, password was not changed.'; $string['secs'] = 'secs'; $string['section'] = 'Section'; +$string['sectionactionnotsupported'] = 'Section action "{$a}" is not supported here'; $string['sectionname'] = 'Section name'; $string['sections'] = 'Sections'; $string['sectionusedefaultname'] = 'Use default section name'; diff --git a/lib/db/install.xml b/lib/db/install.xml index b64a07fa29c..1c86d11c497 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -291,6 +291,7 @@ + @@ -3442,4 +3443,4 @@ - \ No newline at end of file + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 316b3392dbb..0dc1482fcf2 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2541,5 +2541,19 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2017021300.00); } + if ($oldversion < 2017021400.00) { + // Define field visibleoncoursepage to be added to course_modules. + $table = new xmldb_table('course_modules'); + $field = new xmldb_field('visibleoncoursepage', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '1', 'visible'); + + // Conditionally launch add field visibleoncoursepage. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2017021400.00); + } + return true; } diff --git a/lib/form/modvisible.php b/lib/form/modvisible.php index ddc4a6fba94..d8b68dcb4fd 100644 --- a/lib/form/modvisible.php +++ b/lib/form/modvisible.php @@ -40,6 +40,15 @@ require_once "$CFG->libdir/form/select.php"; */ class MoodleQuickForm_modvisible extends MoodleQuickForm_select{ + /** @var int activity state: visible=0, visibleoncoursepage=any */ + const HIDE = 0; + + /** @var int activity state: visible=1, visibleoncoursepage=1 */ + const SHOW = 1; + + /** @var int activity state: visible=1, visibleoncoursepage=0 */ + const STEALTH = -1; + /** * Class constructor * @@ -49,8 +58,7 @@ class MoodleQuickForm_modvisible extends MoodleQuickForm_select{ * @param array $options ignored */ public function __construct($elementName=null, $elementLabel=null, $attributes=null, $options=null) { - // TODO MDL-52313 Replace with the call to parent::__construct(). - HTML_QuickForm_element::__construct($elementName, $elementLabel, $attributes, null); + parent::__construct($elementName, $elementLabel, null, $attributes); $this->_type = 'modvisible'; } @@ -72,17 +80,79 @@ class MoodleQuickForm_modvisible extends MoodleQuickForm_select{ * @param object $caller calling object * @return bool */ - function onQuickFormEvent($event, $arg, &$caller) - { + public function onQuickFormEvent($event, $arg, &$caller) { switch ($event) { case 'createElement': - $choices=array(); - $choices[1] = get_string('show'); - $choices[0] = get_string('hide'); + $options = is_array($arg[3]) ? $arg[3] : []; + $sectionvisible = array_key_exists('sectionvisible', $options) ? $options['sectionvisible'] : 1; + $cm = !empty($options['cm']) ? cm_info::create($options['cm']) : null; + $choices = array(); + if (!$sectionvisible) { + // If section is not visible the activity is hidden by default but it can also be made available. + $choices[self::HIDE] = get_string('hiddenfromstudents'); + if (!$cm || $cm->has_view()) { + $choices[self::SHOW] = get_string('hiddenoncoursepage'); + } + } else if (!empty($options['allowstealth']) && (!$cm || $cm->has_view())) { + // If allowed in this course/section, add a third visibility option + // "Available but not displayed on course page". + $choices[self::SHOW] = get_string('show'); + $choices[self::HIDE] = get_string('hiddenfromstudents'); + $choices[self::STEALTH] = get_string('hiddenoncoursepage'); + } else { + // In the visible section without "stealth" activities allowed it's just "Show" or "Hide". + $choices[self::SHOW] = get_string('show'); + $choices[self::HIDE] = get_string('hide'); + } $this->load($choices); break; + case 'updateValue': + // Given two bool values of 'visible' and 'visibleoncoursepage' convert to a single + // three-state value (show, hide, hide-on-course-page). + $name = $this->getName(); + $value = $this->_findValue($caller->_constantValues); + if (!empty($value) && isset($caller->_constantValues[$name.'oncoursepage']) && + !$caller->_constantValues[$name.'oncoursepage']) { + $value = self::STEALTH; + } + if (null === $value) { + if ($caller->isSubmitted()) { + break; + } + $value = $this->_findValue($caller->_defaultValues); + if (!empty($value) && isset($caller->_defaultValues[$name.'oncoursepage']) && + !$caller->_defaultValues[$name.'oncoursepage']) { + $value = self::STEALTH; + } + } + if ($value !== null) { + $this->setSelected($value); + } + return true; } return parent::onQuickFormEvent($event, $arg, $caller); } + + /** + * As usual, to get the group's value we access its elements and call + * their exportValue() methods + * + * @param array $submitvalues submitted values + * @param bool $assoc if true the retured value is associated array + * @return mixed + */ + public function exportValue(&$submitvalues, $assoc = false) { + if ($assoc) { + $value = parent::exportValue($submitvalues, $assoc); + $key = key($value); + $v = $value[$key]; + // Convert three-state dropdown value (show, hide, hide-on-course-page) into the array of two bool values: + // array('visible' => x, 'visibleoncoursepage' => y). + return array($key => ($v == self::HIDE ? 0 : 1), + $key . 'oncoursepage' => ($v == self::STEALTH ? 0 : 1)); + } else { + return parent::exportValue($submitvalues, $assoc); + } + } } diff --git a/lib/modinfolib.php b/lib/modinfolib.php index a3664589392..f6f9a13428c 100644 --- a/lib/modinfolib.php +++ b/lib/modinfolib.php @@ -690,6 +690,8 @@ class course_modinfo { * @property-read int $added Time that this course-module was added (unix time) - from course_modules table * @property-read int $visible Visible setting (0 or 1; if this is 0, students cannot see/access the activity) - from * course_modules table + * @property-read int $visibleoncoursepage Visible on course page setting - from course_modules table, adjusted to + * whether course format allows this module to have the "stealth" mode * @property-read int $visibleold Old visible setting (if the entire section is hidden, the previous value for * visible is stored in this field) - from course_modules table * @property-read int $groupmode Group mode (one of the constants NOGROUPS, SEPARATEGROUPS, or VISIBLEGROUPS) - from @@ -833,6 +835,12 @@ class cm_info implements IteratorAggregate { */ private $visible; + /** + * Visible on course page setting - from course_modules table + * @var int + */ + private $visibleoncoursepage; + /** * Old visible setting (if the entire section is hidden, the previous value for * visible is stored in this field) - from course_modules table @@ -998,6 +1006,12 @@ class cm_info implements IteratorAggregate { */ private $uservisible; + /** + * True if this course-module is visible to the CURRENT user on the course page + * @var bool + */ + private $uservisibleoncoursepage; + /** * @var moodle_url */ @@ -1093,6 +1107,7 @@ class cm_info implements IteratorAggregate { 'showdescription' => false, 'uservisible' => 'get_user_visible', 'visible' => false, + 'visibleoncoursepage' => false, 'visibleold' => false, 'deletioninprogress' => false ); @@ -1388,7 +1403,8 @@ class cm_info implements IteratorAggregate { */ public function get_grouping_label($textclasses = '') { $groupinglabel = ''; - if (!empty($this->groupingid) && has_capability('moodle/course:managegroups', context_course::instance($this->course))) { + if ($this->effectivegroupmode != NOGROUPS && !empty($this->groupingid) && + has_capability('moodle/course:managegroups', context_course::instance($this->course))) { $groupings = groups_get_all_groupings($this->course); $groupinglabel = html_writer::tag('span', '('.format_string($groupings[$this->groupingid]->name).')', array('class' => 'groupinglabel '.$textclasses)); @@ -1427,6 +1443,15 @@ class cm_info implements IteratorAggregate { return $this->modinfo; } + /** + * Returns the section this module belongs to + * + * @return section_info + */ + public function get_section_info() { + return $this->modinfo->get_section_info($this->sectionnum); + } + /** * Returns course object that was used in the first {@link get_fast_modinfo()} call. * @@ -1509,7 +1534,7 @@ class cm_info implements IteratorAggregate { // Standard fields from table course_modules. static $cmfields = array('id', 'course', 'module', 'instance', 'section', 'idnumber', 'added', - 'score', 'indent', 'visible', 'visibleold', 'groupmode', 'groupingid', + 'score', 'indent', 'visible', 'visibleoncoursepage', 'visibleold', 'groupmode', 'groupingid', 'completion', 'completiongradeitemnumber', 'completionview', 'completionexpected', 'showdescription', 'availability', 'deletioninprogress'); foreach ($cmfields as $key) { @@ -1685,6 +1710,7 @@ class cm_info implements IteratorAggregate { $this->idnumber = isset($mod->idnumber) ? $mod->idnumber : ''; $this->name = $mod->name; $this->visible = $mod->visible; + $this->visibleoncoursepage = $mod->visibleoncoursepage; $this->sectionnum = $mod->section; // Note weirdness with name here $this->groupmode = isset($mod->groupmode) ? $mod->groupmode : 0; $this->groupingid = isset($mod->groupingid) ? $mod->groupingid : 0; @@ -1823,6 +1849,34 @@ class cm_info implements IteratorAggregate { return $this->uservisible; } + /** + * Returns whether this module is visible to the current user on course page + * + * Activity may be visible on the course page but not available, for example + * when it is hidden conditionally but the condition information is displayed. + * + * @return bool + */ + public function is_visible_on_course_page() { + $this->obtain_dynamic_data(); + return $this->uservisibleoncoursepage; + } + + /** + * Whether this module is available but hidden from course page + * + * "Stealth" modules are the ones that are not shown on course page but available by following url. + * They are normally also displayed in grade reports and other reports. + * Module will be stealth either if visibleoncoursepage=0 or it is a visible module inside the hidden + * section. + * + * @return bool + */ + public function is_stealth() { + return !$this->visibleoncoursepage || + ($this->visible && ($section = $this->get_section_info()) && !$section->visible); + } + /** * Getter method for property $available, ensures that dynamic data is retrieved * @return bool @@ -1889,6 +1943,16 @@ class cm_info implements IteratorAggregate { // Ensure activity is completely hidden from the user. $this->availableinfo = ''; } + + $this->uservisibleoncoursepage = $this->uservisible && + ($this->visibleoncoursepage || + has_capability('moodle/course:manageactivities', $this->get_context(), $userid) || + has_capability('moodle/course:activityvisibility', $this->get_context(), $userid)); + // Activity that is not available, not hidden from course page and has availability + // info is actually visible on the course page (with availability info and without a link). + if (!$this->uservisible && $this->visibleoncoursepage && $this->availableinfo) { + $this->uservisibleoncoursepage = true; + } } /** diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 966112fa563..7b0e5a66b9d 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -2893,7 +2893,8 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $ } else { $url = new moodle_url('/'); } - redirect($url, get_string('activityiscurrentlyhidden')); + redirect($url, get_string('activityiscurrentlyhidden'), null, + \core\output\notification::NOTIFY_ERROR); } // Set the global $COURSE. diff --git a/lib/navigationlib.php b/lib/navigationlib.php index e5955b40956..3467ecc29a4 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -1991,7 +1991,7 @@ class global_navigation extends navigation_node { $activity->display = false; } else { $activity->url = $url->out(); - $activity->display = $cm->uservisible ? true : false; + $activity->display = $cm->is_visible_on_course_page() ? true : false; if (self::module_extends_navigation($cm->modname)) { $activity->nodetype = navigation_node::NODETYPE_BRANCH; } @@ -2139,7 +2139,7 @@ class global_navigation extends navigation_node { $activitynode = $coursenode->add(format_string($cm->name), $url, navigation_node::TYPE_ACTIVITY, null, $cm->id, $icon); $activitynode->title(get_string('modulename', $cm->modname)); $activitynode->hidden = (!$cm->visible); - if (!$cm->uservisible) { + if (!$cm->is_visible_on_course_page()) { // Do not show any error here, let the page handle exception that activity is not visible for the current user. // Also there may be no exception at all in case when teacher is logged in as student. $activitynode->display = false; diff --git a/lib/testing/generator/module_generator.php b/lib/testing/generator/module_generator.php index 81e68a5e629..5400648bc56 100644 --- a/lib/testing/generator/module_generator.php +++ b/lib/testing/generator/module_generator.php @@ -187,6 +187,7 @@ abstract class testing_module_generator extends component_generator_base { $defaults = array( 'section' => 0, 'visible' => 1, + 'visibleoncoursepage' => 1, 'cmidnumber' => '', 'groupmode' => 0, 'groupingid' => 0, diff --git a/mod/glossary/import.php b/mod/glossary/import.php index f377d0765e7..136e57e0465 100644 --- a/mod/glossary/import.php +++ b/mod/glossary/import.php @@ -144,6 +144,7 @@ if ($xml = glossary_read_imported_file($result)) { // New glossary is to be inserted in section 0, it is always visible. $glossary->section = 0; $glossary->visible = 1; + $glossary->visibleoncoursepage = 1; // Include new glossary and return the new ID if ( !($glossary = add_moduleinfo($glossary, $course)) ) { diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss index cfa91469252..c7dd4ec0c87 100644 --- a/theme/boost/scss/moodle/core.scss +++ b/theme/boost/scss/moodle/core.scss @@ -77,10 +77,6 @@ a.dimmed_text:visited, @extend .text-muted; } -.activity.label .dimmed_text { - opacity: 0.5; -} - .unlist, .unlist li, .inline-list, diff --git a/theme/boost/scss/moodle/course.scss b/theme/boost/scss/moodle/course.scss index 7e1d7e55923..03eead18521 100644 --- a/theme/boost/scss/moodle/course.scss +++ b/theme/boost/scss/moodle/course.scss @@ -96,6 +96,14 @@ } } } + + .stealth { + @extend .text-muted; + } + a.stealth, + a.stealth:hover { + color: lighten($link-color, 25%) !important; /* stylelint-disable-line declaration-no-important */ + } } .label { @@ -305,15 +313,9 @@ .course-content ul li.section.hidden { .sectionname > span, - .content > div, + .content > div.summary, .activity .activityinstance { - opacity: .5; - } - - .sectionname > span, - .activity .activityinstance { - margin-left: 10px; - margin-right: 10px; + @extend .text-muted; } } @@ -329,6 +331,11 @@ margin: 0; padding: 0; } + + .summary, + .content > .availabilityinfo { + margin-left: 25px; + } } } @@ -336,10 +343,6 @@ margin-top: 0; } -.course-content ul.topics li.section .summary { - margin-left: 25px; -} - .course-content .hidden { display: none; } diff --git a/theme/bootstrapbase/less/moodle/core.less b/theme/bootstrapbase/less/moodle/core.less index bc20577399b..101740b8b50 100644 --- a/theme/bootstrapbase/less/moodle/core.less +++ b/theme/bootstrapbase/less/moodle/core.less @@ -109,9 +109,6 @@ a.dimmed_text:visited, .dimmed_category a { .muted } -.activity.label .dimmed_text { - .opacity(50) -} .unlist, .unlist li, .inline-list, diff --git a/theme/bootstrapbase/less/moodle/course.less b/theme/bootstrapbase/less/moodle/course.less index a42c49f4bb9..7e64257bcfe 100644 --- a/theme/bootstrapbase/less/moodle/course.less +++ b/theme/bootstrapbase/less/moodle/course.less @@ -71,13 +71,6 @@ display: table-cell; padding-right: 4px; min-height: 2em; - - .dimmed { - img.activityicon { - opacity: 0.5; - filter: alpha(opacity=50); - } - } } } .label { @@ -119,10 +112,24 @@ } } -.activity img.activityicon { - margin-right: 6px; - vertical-align: text-bottom; +.activity { + img.activityicon { + margin-right: 6px; + vertical-align: text-bottom; + } + + .dimmed { + img.activityicon { + opacity: 0.5; + filter: alpha(opacity=50); + } + } + + .stealth:not(.dimmed) { + opacity: .5; + } } + .section .activity .activityinstance, .section .activity .activityinstance div { display: inline-block; @@ -245,36 +252,33 @@ } .course-content ul li.section.hidden { .sectionname > span, - .content > div, - /* All the divs but the activities which are in a UL. */ - .activity .activityinstance { + .content > div.summary { opacity: .5; } - .sectionname > span, - .activity .activityinstance { - margin-left: 10px; - margin-right: 10px; - } } -.course-content ul.topics li.section .content, -.course-content ul.weeks li.section .content { - margin-right: 20px; - margin-left: 20px; - padding: 0; +.course-content ul.topics, +.course-content ul.weeks { + + li.section { + padding-bottom: 20px; + + .content { + margin-right: 20px; + margin-left: 20px; + padding: 0; + } + + .summary, + .content > .availabilityinfo { + margin-left: 25px; + } + } } .course-content { margin-top: 0; } -.course-content ul.topics li.section { - padding-bottom: 20px; -} - -.course-content ul.topics li.section .summary { - margin-left: 25px; -} - .course-content li { &.section { ul { diff --git a/theme/bootstrapbase/style/moodle.css b/theme/bootstrapbase/style/moodle.css index 68f1319a7b3..fefb0e8d655 100644 --- a/theme/bootstrapbase/style/moodle.css +++ b/theme/bootstrapbase/style/moodle.css @@ -79,10 +79,6 @@ a.dimmed_text:visited, .dimmed_category a { color: #999; } -.activity.label .dimmed_text { - opacity: 0.5; - filter: alpha(opacity=50); -} .unlist, .unlist li, .inline-list, @@ -3544,11 +3540,6 @@ img.iconsmall { padding-right: 4px; min-height: 2em; } -.section .activity .contentwithoutlink .dimmed img.activityicon, -.section .activity .activityinstance .dimmed img.activityicon { - opacity: 0.5; - filter: alpha(opacity=50); -} .section .label .contentwithoutlink, .section .label .activityinstance { padding-right: 32px; @@ -3582,6 +3573,13 @@ img.iconsmall { margin-right: 6px; vertical-align: text-bottom; } +.activity .dimmed img.activityicon { + opacity: 0.5; + filter: alpha(opacity=50); +} +.activity .stealth:not(.dimmed) { + opacity: .5; +} .section .activity .activityinstance, .section .activity .activityinstance div { display: inline-block; @@ -3695,14 +3693,12 @@ img.iconsmall { margin-top: 0; } .course-content ul li.section.hidden .sectionname > span, -.course-content ul li.section.hidden .content > div, -.course-content ul li.section.hidden .activity .activityinstance { +.course-content ul li.section.hidden .content > div.summary { opacity: .5; } -.course-content ul li.section.hidden .sectionname > span, -.course-content ul li.section.hidden .activity .activityinstance { - margin-left: 10px; - margin-right: 10px; +.course-content ul.topics li.section, +.course-content ul.weeks li.section { + padding-bottom: 20px; } .course-content ul.topics li.section .content, .course-content ul.weeks li.section .content { @@ -3710,15 +3706,15 @@ img.iconsmall { margin-left: 20px; padding: 0; } +.course-content ul.topics li.section .summary, +.course-content ul.weeks li.section .summary, +.course-content ul.topics li.section .content > .availabilityinfo, +.course-content ul.weeks li.section .content > .availabilityinfo { + margin-left: 25px; +} .course-content { margin-top: 0; } -.course-content ul.topics li.section { - padding-bottom: 20px; -} -.course-content ul.topics li.section .summary { - margin-left: 25px; -} .course-content li.section ul { list-style: disc; } diff --git a/version.php b/version.php index 2e742eccc81..60fbcfafd93 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2017021300.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2017021400.00; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. From 3f9503468fba8bbfc7bfac9b5a2167220ff289ab Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Fri, 3 Feb 2017 09:30:49 +0800 Subject: [PATCH 6/6] MDL-4782 course: Behat tests for stealth mode support --- .../tests/behat/add_url.feature | 2 +- .../behat/behat_block_site_main_menu.php | 157 ++++++++++++++++++ .../tests/behat/edit_activities.feature | 50 +++++- .../behat/behat_block_social_activities.php | 157 ++++++++++++++++++ .../tests/behat/edit_activities.feature | 65 +++++++- .../behat/restrict_activity_by_date.feature | 3 +- .../behat/restrict_activity_by_grade.feature | 3 +- .../behat/activities_visibility_icons.feature | 133 ++++++++++++++- course/tests/behat/behat_course.php | 145 +++++++++++++++- lib/tests/behat/behat_action_menu.php | 7 + .../tests/behat/label_visibility.feature | 69 +++++++- .../behat_theme_boost_behat_action_menu.php | 6 + 12 files changed, 771 insertions(+), 26 deletions(-) create mode 100644 blocks/site_main_menu/tests/behat/behat_block_site_main_menu.php create mode 100644 blocks/social_activities/tests/behat/behat_block_social_activities.php diff --git a/blocks/site_main_menu/tests/behat/add_url.feature b/blocks/site_main_menu/tests/behat/add_url.feature index 62054a04951..a9cfb61f388 100644 --- a/blocks/site_main_menu/tests/behat/add_url.feature +++ b/blocks/site_main_menu/tests/behat/add_url.feature @@ -1,4 +1,4 @@ -@block @block_main_menu +@block @block_site_main_menu Feature: Add URL to main menu block In order to add helpful resources for students As a admin diff --git a/blocks/site_main_menu/tests/behat/behat_block_site_main_menu.php b/blocks/site_main_menu/tests/behat/behat_block_site_main_menu.php new file mode 100644 index 00000000000..f5231162059 --- /dev/null +++ b/blocks/site_main_menu/tests/behat/behat_block_site_main_menu.php @@ -0,0 +1,157 @@ +. + +/** + * Behat steps definitions for block site main menu + * + * @package block_site_main_menu + * @category test + * @copyright 2016 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. + +require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); + +use Behat\Mink\Exception\ExpectationException as ExpectationException, + Behat\Mink\Exception\DriverException as DriverException, + Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException; + +/** + * Behat steps definitions for block site main menu + * + * @package block_site_main_menu + * @category test + * @copyright 2016 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_block_site_main_menu extends behat_base { + + /** + * Returns the DOM node of the activity in the site menu block + * + * @throws ElementNotFoundException Thrown by behat_base::find + * @param string $activityname The activity name + * @return NodeElement + */ + protected function get_site_menu_activity_node($activityname) { + $activityname = behat_context_helper::escape($activityname); + $xpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]//li[contains(., $activityname)]"; + + return $this->find('xpath', $xpath); + } + + /** + * Checks that the specified activity's action menu contains an item. + * + * @Then /^"(?P(?:[^"]|\\")*)" activity in site main menu block should have "(?P(?:[^"]|\\")*)" editing icon$/ + * @param string $activityname + * @param string $iconname + */ + public function activity_in_site_main_menu_block_should_have_editing_icon($activityname, $iconname) { + $activitynode = $this->get_site_menu_activity_node($activityname); + + $notfoundexception = new ExpectationException('"' . $activityname . '" doesn\'t have a "' . + $iconname . '" editing icon', $this->getSession()); + $this->find('named_partial', array('link', $iconname), $notfoundexception, $activitynode); + } + + /** + * Checks that the specified activity's action menu contains an item. + * + * @Then /^"(?P(?:[^"]|\\")*)" activity in site main menu block should not have "(?P(?:[^"]|\\")*)" editing icon$/ + * @param string $activityname + * @param string $iconname + */ + public function activity_in_site_main_menu_block_should_not_have_editing_icon($activityname, $iconname) { + $activitynode = $this->get_site_menu_activity_node($activityname); + + try { + $this->find('named_partial', array('link', $iconname), false, $activitynode); + throw new ExpectationException('"' . $activityname . '" has a "' . $iconname . + '" editing icon when it should not', $this->getSession()); + } catch (ElementNotFoundException $e) { + // This is good, the menu item should not be there. + } + } + + /** + * Clicks on the specified element of the activity. You should be in the course page with editing mode turned on. + * + * @Given /^I click on "(?P(?:[^"]|\\")*)" "(?P(?:[^"]|\\")*)" in the "(?P(?:[^"]|\\")*)" activity in site main menu block$/ + * @param string $element + * @param string $selectortype + * @param string $activityname + */ + public function i_click_on_in_the_activity_in_site_main_menu_block($element, $selectortype, $activityname) { + $element = $this->get_site_menu_activity_element($element, $selectortype, $activityname); + $element->click(); + } + + /** + * Clicks on the specified element inside the activity container. + * + * @throws ElementNotFoundException + * @param string $element + * @param string $selectortype + * @param string $activityname + * @return NodeElement + */ + protected function get_site_menu_activity_element($element, $selectortype, $activityname) { + $activitynode = $this->get_site_menu_activity_node($activityname); + + // Transforming to Behat selector/locator. + list($selector, $locator) = $this->transform_selector($selectortype, $element); + $exception = new ElementNotFoundException($this->getSession(), '"' . $element . '" "' . + $selectortype . '" in "' . $activityname . '" '); + + return $this->find($selector, $locator, $exception, $activitynode); + } + + /** + * Checks that the specified activity is hidden. + * + * @Then /^"(?P(?:[^"]|\\")*)" activity in site main menu block should be hidden$/ + * @param string $activityname + */ + public function activity_in_site_main_menu_block_should_be_hidden($activityname) { + $this->get_site_menu_activity_element("a.dimmed", "css_element", $activityname); + } + + /** + * Checks that the specified activity is hidden. + * + * @Then /^"(?P(?:[^"]|\\")*)" activity in site main menu block should be available but hidden from course page$/ + * @param string $activityname + */ + public function activity_in_site_main_menu_block_should_be_available_but_hidden_from_course_page($activityname) { + $this->get_site_menu_activity_element("a.stealth", "css_element", $activityname); + } + + /** + * Opens an activity actions menu if it is not already opened. + * + * @Given /^I open "(?P(?:[^"]|\\")*)" actions menu in site main menu block$/ + * @throws DriverException The step is not available when Javascript is disabled + * @param string $activityname + */ + public function i_open_actions_menu_in_site_main_menu_block($activityname) { + $activityname = behat_context_helper::escape($activityname); + $xpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]//li[contains(., $activityname)]"; + $this->execute('behat_action_menu::i_open_the_action_menu_in', [$xpath, 'xpath_element']); + } +} diff --git a/blocks/site_main_menu/tests/behat/edit_activities.feature b/blocks/site_main_menu/tests/behat/edit_activities.feature index 47994952f45..6cb07df4712 100644 --- a/blocks/site_main_menu/tests/behat/edit_activities.feature +++ b/blocks/site_main_menu/tests/behat/edit_activities.feature @@ -1,4 +1,4 @@ -@block @block_main_menu +@block @block_site_main_menu Feature: Edit activities in main menu block In order to use main menu block As an admin @@ -12,7 +12,7 @@ Feature: Edit activities in main menu block And I add the "Main menu" block When I add a "Forum" to section "0" and I fill the form with: | Forum name | My forum name | - And I click on "Edit title" "link" in the "//*[contains(@class,'block_site_main_menu')]//li[contains(.,'My forum name')]" "xpath_element" + And I click on "Edit title" "link" in the "My forum name" activity in site main menu block And I set the field "New name for activity My forum name" to "New forum name" And I press key "13" in the field "New name for activity My forum name" Then I should not see "My forum name" @@ -20,3 +20,49 @@ Feature: Edit activities in main menu block And I follow "New forum name" And I should not see "My forum name" And I should see "New forum name" + + @javascript + Scenario: Activities in main menu block can be made available but not visible on a course page + And I log in as "admin" + And I set the following administration settings values: + | allowstealth | 1 | + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "Main menu" block + When I add a "Forum" to section "0" and I fill the form with: + | Forum name | Visible forum | + When I add a "Forum" to section "0" and I fill the form with: + | Forum name | My forum name | + And "My forum name" activity in site main menu block should have "Hide" editing icon + And "My forum name" activity in site main menu block should not have "Show" editing icon + And "My forum name" activity in site main menu block should not have "Make available" editing icon + And "My forum name" activity in site main menu block should not have "Make unavailable" editing icon + And I open "My forum name" actions menu in site main menu block + And I click on "Hide" "link" in the "My forum name" activity in site main menu block + And "My forum name" activity in site main menu block should be hidden + And "My forum name" activity in site main menu block should not have "Hide" editing icon + And "My forum name" activity in site main menu block should have "Show" editing icon + And "My forum name" activity in site main menu block should have "Make available" editing icon + And "My forum name" activity in site main menu block should not have "Make unavailable" editing icon + And I open "My forum name" actions menu in site main menu block + And I click on "Make available" "link" in the "My forum name" activity in site main menu block + And "My forum name" activity in site main menu block should be available but hidden from course page + And "My forum name" activity in site main menu block should not have "Hide" editing icon + And "My forum name" activity in site main menu block should have "Show" editing icon + And "My forum name" activity in site main menu block should not have "Make available" editing icon + And "My forum name" activity in site main menu block should have "Make unavailable" editing icon + # Make sure that "Visible" dropdown in the edit menu has three options. + And I open "My forum name" actions menu in site main menu block + And I click on "Edit settings" "link" in the "My forum name" activity in site main menu block + And I expand all fieldsets + And the "Visible" select box should contain "Show" + And the "Visible" select box should contain "Hidden from students" + And the "Visible" select box should not contain "Hide" + And the field "Visible" matches value "Available but not displayed on course page" + And I press "Save and return to course" + And "My forum name" activity in site main menu block should be available but hidden from course page + And I navigate to "Turn editing off" node in "Front page settings" + And "My forum name" activity in site main menu block should be available but hidden from course page + And I log out + And I should not see "My forum name" in the "Main menu" "block" + And I should see "Visible forum" in the "Main menu" "block" diff --git a/blocks/social_activities/tests/behat/behat_block_social_activities.php b/blocks/social_activities/tests/behat/behat_block_social_activities.php new file mode 100644 index 00000000000..2fe5526d473 --- /dev/null +++ b/blocks/social_activities/tests/behat/behat_block_social_activities.php @@ -0,0 +1,157 @@ +. + +/** + * Behat steps definitions for block social activities + * + * @package block_social_activities + * @category test + * @copyright 2016 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. + +require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); + +use Behat\Mink\Exception\ExpectationException as ExpectationException, + Behat\Mink\Exception\DriverException as DriverException, + Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException; + +/** + * Behat steps definitions for block social activities + * + * @package block_social_activities + * @category test + * @copyright 2016 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_block_social_activities extends behat_base { + + /** + * Returns the DOM node of the activity in the social activities block + * + * @throws ElementNotFoundException Thrown by behat_base::find + * @param string $activityname The activity name + * @return NodeElement + */ + protected function get_social_block_activity_node($activityname) { + $activityname = behat_context_helper::escape($activityname); + $xpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_social_activities ')]//li[contains(., $activityname)]"; + + return $this->find('xpath', $xpath); + } + + /** + * Checks that the specified activity's action menu contains an item. + * + * @Then /^"(?P(?:[^"]|\\")*)" activity in social activities block should have "(?P(?:[^"]|\\")*)" editing icon$/ + * @param string $activityname + * @param string $iconname + */ + public function activity_in_social_activities_block_should_have_editing_icon($activityname, $iconname) { + $activitynode = $this->get_social_block_activity_node($activityname); + + $notfoundexception = new ExpectationException('"' . $activityname . '" doesn\'t have a "' . + $iconname . '" editing icon', $this->getSession()); + $this->find('named_partial', array('link', $iconname), $notfoundexception, $activitynode); + } + + /** + * Checks that the specified activity's action menu contains an item. + * + * @Then /^"(?P(?:[^"]|\\")*)" activity in social activities block should not have "(?P(?:[^"]|\\")*)" editing icon$/ + * @param string $activityname + * @param string $iconname + */ + public function activity_in_social_activities_block_should_not_have_editing_icon($activityname, $iconname) { + $activitynode = $this->get_social_block_activity_node($activityname); + + try { + $this->find('named_partial', array('link', $iconname), false, $activitynode); + throw new ExpectationException('"' . $activityname . '" has a "' . $iconname . + '" editing icon when it should not', $this->getSession()); + } catch (ElementNotFoundException $e) { + // This is good, the menu item should not be there. + } + } + + /** + * Clicks on the specified element of the activity. You should be in the course page with editing mode turned on. + * + * @Given /^I click on "(?P(?:[^"]|\\")*)" "(?P(?:[^"]|\\")*)" in the "(?P(?:[^"]|\\")*)" activity in social activities block$/ + * @param string $element + * @param string $selectortype + * @param string $activityname + */ + public function i_click_on_in_the_activity_in_social_activities_block($element, $selectortype, $activityname) { + $element = $this->get_social_block_activity_element($element, $selectortype, $activityname); + $element->click(); + } + + /** + * Clicks on the specified element inside the activity container. + * + * @throws ElementNotFoundException + * @param string $element + * @param string $selectortype + * @param string $activityname + * @return NodeElement + */ + protected function get_social_block_activity_element($element, $selectortype, $activityname) { + $activitynode = $this->get_social_block_activity_node($activityname); + + // Transforming to Behat selector/locator. + list($selector, $locator) = $this->transform_selector($selectortype, $element); + $exception = new ElementNotFoundException($this->getSession(), '"' . $element . '" "' . + $selectortype . '" in "' . $activityname . '" '); + + return $this->find($selector, $locator, $exception, $activitynode); + } + + /** + * Checks that the specified activity is hidden. + * + * @Then /^"(?P(?:[^"]|\\")*)" activity in social activities block should be hidden$/ + * @param string $activityname + */ + public function activity_in_social_activities_block_should_be_hidden($activityname) { + $this->get_social_block_activity_element("a.dimmed", "css_element", $activityname); + } + + /** + * Checks that the specified activity is hidden. + * + * @Then /^"(?P(?:[^"]|\\")*)" activity in social activities block should be available but hidden from course page$/ + * @param string $activityname + */ + public function activity_in_social_activities_block_should_be_available_but_hidden_from_course_page($activityname) { + $this->get_social_block_activity_element("a.stealth", "css_element", $activityname); + } + + /** + * Opens an activity actions menu if it is not already opened. + * + * @Given /^I open "(?P(?:[^"]|\\")*)" actions menu in social activities block$/ + * @throws DriverException The step is not available when Javascript is disabled + * @param string $activityname + */ + public function i_open_actions_menu_in_social_activities_block($activityname) { + $activityname = behat_context_helper::escape($activityname); + $xpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_social_activities ')]//li[contains(., $activityname)]"; + $this->execute('behat_action_menu::i_open_the_action_menu_in', [$xpath, 'xpath_element']); + } +} diff --git a/blocks/social_activities/tests/behat/edit_activities.feature b/blocks/social_activities/tests/behat/edit_activities.feature index 07b58e391e0..a29b0666fe3 100644 --- a/blocks/social_activities/tests/behat/edit_activities.feature +++ b/blocks/social_activities/tests/behat/edit_activities.feature @@ -1,27 +1,31 @@ -@block @block_social_activities +@block @block_social_activities @format_social Feature: Edit activities in social activities block In order to use social activities block As a teacher I need to add and edit activities there - @javascript - Scenario: Edit name of acitivity in-place in social activities block + Background: Given the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | social | And the following "users" exist: | username | firstname | lastname | | user1 | User | One | + | student1 | Student | One | And the following "course enrolments" exist: | user | course | role | | user1 | C1 | editingteacher | + | student1 | C1 | student | + + @javascript + Scenario: Edit name of acitivity in-place in social activities block Given I log in as "user1" And I follow "Course 1" And I turn editing mode on And I set the field "Add an activity to section 'section 0'" to "Forum" And I set the field "Forum name" to "My forum name" And I press "Save and return to course" - And I click on "Edit title" "link" in the "//*[contains(@class,'block_social_activities')]//li[contains(.,'My forum name')]" "xpath_element" + And I click on "Edit title" "link" in the "My forum name" activity in social activities block And I set the field "New name for activity My forum name" to "New forum name" And I press key "13" in the field "New name for activity My forum name" Then I should not see "My forum name" in the "Social activities" "block" @@ -29,3 +33,56 @@ Feature: Edit activities in social activities block And I follow "New forum name" And I should not see "My forum name" And I should see "New forum name" + + @javascript + Scenario: Activities in social activities block can be made available but not visible on a course page + And I log in as "admin" + And I set the following administration settings values: + | allowstealth | 1 | + And I log out + And I log in as "user1" + And I follow "Course 1" + And I turn editing mode on + And I add the "Recent activity" block + And I set the field "Add an activity to section 'section 0'" to "Forum" + And I set the field "Forum name" to "My forum name" + And I press "Save and return to course" + And "My forum name" activity in social activities block should have "Hide" editing icon + And "My forum name" activity in social activities block should not have "Show" editing icon + And "My forum name" activity in social activities block should not have "Make available" editing icon + And "My forum name" activity in social activities block should not have "Make unavailable" editing icon + And I wait until the page is ready + And I open "My forum name" actions menu in social activities block + And I click on "Hide" "link" in the "My forum name" activity in social activities block + And "My forum name" activity in social activities block should be hidden + And "My forum name" activity in social activities block should not have "Hide" editing icon + And "My forum name" activity in social activities block should have "Show" editing icon + And "My forum name" activity in social activities block should have "Make available" editing icon + And "My forum name" activity in social activities block should not have "Make unavailable" editing icon + And I open "My forum name" actions menu in social activities block + And I click on "Make available" "link" in the "My forum name" activity in social activities block + And "My forum name" activity in social activities block should be available but hidden from course page + And "My forum name" activity in social activities block should not have "Hide" editing icon + And "My forum name" activity in social activities block should have "Show" editing icon + And "My forum name" activity in social activities block should not have "Make available" editing icon + And "My forum name" activity in social activities block should have "Make unavailable" editing icon + # Make sure that "Visible" dropdown in the edit menu has three options. + And I open "My forum name" actions menu in social activities block + And I click on "Edit settings" "link" in the "My forum name" activity in social activities block + And I expand all fieldsets + And the "Visible" select box should contain "Show" + And the "Visible" select box should contain "Hidden from students" + And the "Visible" select box should not contain "Hide" + And the field "Visible" matches value "Available but not displayed on course page" + And I press "Save and return to course" + And "My forum name" activity in social activities block should be available but hidden from course page + And I turn editing mode off + And "My forum name" activity in social activities block should be available but hidden from course page + And I log out + # Student will not see the module on the course page but can access it from other reports and blocks: + And I log in as "student1" + And I follow "Course 1" + And I should not see "My forum name" in the "Social activities" "block" + And I click on "My forum name" "link" in the "Recent activity" "block" + And I should see "My forum name" in the ".breadcrumb" "css_element" + And I log out diff --git a/completion/tests/behat/restrict_activity_by_date.feature b/completion/tests/behat/restrict_activity_by_date.feature index 5d19955bf37..3f94122bc0f 100644 --- a/completion/tests/behat/restrict_activity_by_date.feature +++ b/completion/tests/behat/restrict_activity_by_date.feature @@ -42,7 +42,8 @@ Feature: Restrict activity availability through date conditions And I am on site homepage And I follow "Course 1" Then I should see "Available from 31 December 2037" - And "Test assignment 1" activity should be hidden + And "Test assignment 1" activity should be dimmed + And "Test assignment 1" "link" should not exist And I log out @javascript diff --git a/completion/tests/behat/restrict_activity_by_grade.feature b/completion/tests/behat/restrict_activity_by_grade.feature index 040bf16b1d9..6c970e87da2 100644 --- a/completion/tests/behat/restrict_activity_by_grade.feature +++ b/completion/tests/behat/restrict_activity_by_grade.feature @@ -44,7 +44,8 @@ Feature: Restrict activity availability through grade conditions And I am on site homepage And I follow "Course 1" Then I should see "Not available unless: You achieve a required score in Grade assignment" - And "Test page name" activity should be hidden + And "Test page name" activity should be dimmed + And "Test page name" "link" should not exist And I follow "Grade assignment" And I press "Add submission" And I set the following fields to these values: diff --git a/course/tests/behat/activities_visibility_icons.feature b/course/tests/behat/activities_visibility_icons.feature index b9af5b12a46..a89880c5a3a 100644 --- a/course/tests/behat/activities_visibility_icons.feature +++ b/course/tests/behat/activities_visibility_icons.feature @@ -25,35 +25,62 @@ Feature: Toggle activities visibility from the course page | Description | Test forum description | | Visible | Show | When I open "Test forum name" actions menu + Then "Test forum name" actions menu should not have "Show" item + And "Test forum name" actions menu should not have "Make available" item + And "Test forum name" actions menu should not have "Make unavailable" item And I click on "Hide" "link" in the "Test forum name" activity - Then "Test forum name" activity should be hidden + And "Test forum name" activity should be hidden And I open "Test forum name" actions menu + And "Test forum name" actions menu should not have "Hide" item + # Stealth behaviour is not available by default: + And "Test forum name" actions menu should not have "Make available" item + And "Test forum name" actions menu should not have "Make unavailable" item And I click on "Show" "link" in the "Test forum name" activity And "Test forum name" activity should be visible And I open "Test forum name" actions menu + And "Test forum name" actions menu should not have "Show" item + And "Test forum name" actions menu should not have "Make available" item + And "Test forum name" actions menu should not have "Make unavailable" item And I click on "Hide" "link" in the "Test forum name" activity And "Test forum name" activity should be hidden And I reload the page And "Test forum name" activity should be hidden + # Make sure that "Visible" dropdown in the edit menu has two options: Show/Hide. + And I open "Test forum name" actions menu + And I click on "Edit settings" "link" in the "Test forum name" activity + And I expand all fieldsets + And the "Visible" select box should not contain "Hidden from students" + And the "Visible" select box should not contain "Available but not displayed on course page" + And the "Visible" select box should contain "Show" + And the field "Visible" matches value "Hide" + And I press "Save and return to course" + And "Test forum name" activity should be hidden + And I turn editing mode off + And "Test forum name" activity should be hidden And I log out + # Student should not see this activity. And I log in as "student1" And I follow "Course 1" - And "Test forum name" activity should be hidden + And I should not see "Test forum name" + And I log out @javascript - Scenario: Activities can be shown and hidden inside a hidden section + Scenario: Activities can be made available and unavailable inside a hidden section Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | format | numsections | | Course 1 | C1 | topics | 2 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | + | student1 | C1 | student | And I log in as "teacher1" And I follow "Course 1" And I turn editing mode on + And I add the "Recent activity" block And I add a "Forum" to section "2" and I fill the form with: | Forum name | Test forum name | | Description | Test forum description | @@ -61,11 +88,39 @@ Feature: Toggle activities visibility from the course page When I hide section "2" Then "Test forum name" activity should be hidden And I open "Test forum name" actions menu - And I click on "Show" "link" in the "Test forum name" activity - And "Test forum name" activity should be visible + And "Test forum name" actions menu should not have "Show" item + And "Test forum name" actions menu should not have "Hide" item + And "Test forum name" actions menu should not have "Make unavailable" item + And I click on "Make available" "link" in the "Test forum name" activity + And "Test forum name" activity should be available but hidden from course page And I open "Test forum name" actions menu - And I click on "Hide" "link" in the "Test forum name" activity + And "Test forum name" actions menu should not have "Show" item + And "Test forum name" actions menu should not have "Hide" item + And "Test forum name" actions menu should not have "Make available" item + And I click on "Make unavailable" "link" in the "Test forum name" activity And "Test forum name" activity should be hidden + # Make sure that "Visible" dropdown in the edit menu has three options. + And I open "Test forum name" actions menu + And I click on "Edit settings" "link" in the "Test forum name" activity + And I expand all fieldsets + And the "Visible" select box should contain "Hidden from students" + And the "Visible" select box should contain "Available but not displayed on course page" + And the "Visible" select box should not contain "Hide" + And the "Visible" select box should not contain "Show" + And I set the field "Visible" to "Available but not displayed on course page" + And I press "Save and return to course" + And "Test forum name" activity should be available but hidden from course page + And I turn editing mode off + And "Test forum name" activity should be available but hidden from course page + And I log out + # Student will not see the module on the course page but can access it from other reports and blocks: + And I log in as "student1" + And I follow "Course 1" + And "Test forum name" activity should be hidden + And I click on "Test forum name" "link" in the "Recent activity" "block" + And I should see "Test forum name" + And I should see "(There are no discussion topics yet in this forum)" + And I log out @javascript Scenario: Activities can be shown and hidden inside an orphaned section @@ -88,8 +143,74 @@ Feature: Toggle activities visibility from the course page When I click on ".reduce-sections" "css_element" Then "Test forum name" activity should be visible And I open "Test forum name" actions menu + And "Test forum name" actions menu should not have "Show" item + And "Test forum name" actions menu should not have "Make available" item + And "Test forum name" actions menu should not have "Make unavailable" item And I click on "Hide" "link" in the "Test forum name" activity And "Test forum name" activity should be hidden And I open "Test forum name" actions menu + And "Test forum name" actions menu should not have "Hide" item + And "Test forum name" actions menu should not have "Make available" item + And "Test forum name" actions menu should not have "Make unavailable" item And I click on "Show" "link" in the "Test forum name" activity And "Test forum name" activity should be visible + + @javascript + Scenario: Activities can be made available but not visible on a course page + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | format | numsections | + | Course 1 | C1 | topics | 2 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And I log in as "admin" + And I set the following administration settings values: + | allowstealth | 1 | + And I log out + And I log in as "teacher1" + And I follow "Course 1" + And I turn editing mode on + And I add the "Recent activity" block + And I add a "Assignment" to section "2" and I fill the form with: + | Assignment name | Test assignment name | + | Description | Test assignment description | + | Visible | Show | + When I open "Test assignment name" actions menu + Then "Test assignment name" actions menu should not have "Show" item + And "Test assignment name" actions menu should have "Hide" item + And "Test assignment name" actions menu should not have "Make available" item + And "Test assignment name" actions menu should not have "Make unavailable" item + And I click on "Hide" "link" in the "Test assignment name" activity + And "Test assignment name" activity should be hidden + And I open "Test assignment name" actions menu + And "Test assignment name" actions menu should have "Show" item + And "Test assignment name" actions menu should not have "Hide" item + And "Test assignment name" actions menu should not have "Make unavailable" item + And I click on "Make available" "link" in the "Test assignment name" activity + And "Test assignment name" activity should be available but hidden from course page + # Make sure that "Visible" dropdown in the edit menu has three options. + And I open "Test assignment name" actions menu + And I click on "Edit settings" "link" in the "Test assignment name" activity + And I expand all fieldsets + And the "Visible" select box should contain "Show" + And the "Visible" select box should contain "Hidden from students" + And the "Visible" select box should not contain "Hide" + And the field "Visible" matches value "Available but not displayed on course page" + And I press "Save and return to course" + And "Test assignment name" activity should be available but hidden from course page + And I turn editing mode off + And "Test assignment name" activity should be available but hidden from course page + And I log out + # Student will not see the module on the course page but can access it from other reports and blocks: + And I log in as "student1" + And I follow "Course 1" + And "Test assignment name" activity should be hidden + And I click on "Test assignment name" "link" in the "Recent activity" "block" + And I should see "Test assignment name" + And I should see "Submission status" + And I log out diff --git a/course/tests/behat/behat_course.php b/course/tests/behat/behat_course.php index df95cce26ff..237bc7ff629 100644 --- a/course/tests/behat/behat_course.php +++ b/course/tests/behat/behat_course.php @@ -623,9 +623,67 @@ class behat_course extends behat_base { // All ok. } - // The 'Hide' button should be available. - $nohideexception = new ExpectationException('"' . $activityname . '" don\'t have a "' . get_string('hide') . '" icon', $this->getSession()); - $this->find('named_partial', array('link', get_string('hide')), $nohideexception, $activitynode); + // Additional check if this is a teacher in editing mode. + if ($this->is_editing_on()) { + // The 'Hide' button should be available. + $nohideexception = new ExpectationException('"' . $activityname . '" doesn\'t have a "' . + get_string('hide') . '" icon', $this->getSession()); + $this->find('named_partial', array('link', get_string('hide')), $nohideexception, $activitynode); + } + } + } + + /** + * Checks that the specified activity is visible. You need to be in the course page. + * It can be used being logged as a student and as a teacher on editing mode. + * + * @Then /^"(?P(?:[^"]|\\")*)" activity should be available but hidden from course page$/ + * @param string $activityname + * @throws ExpectationException + */ + public function activity_should_be_available_but_hidden_from_course_page($activityname) { + + if ($this->is_course_editor()) { + + // The activity must exists and be visible. + $activitynode = $this->get_activity_node($activityname); + + // The activity should not be dimmed. + try { + $xpath = "/descendant-or-self::a[contains(concat(' ', normalize-space(@class), ' '), ' dimmed ')] | " . + "/descendant-or-self::div[contains(concat(' ', normalize-space(@class), ' '), ' dimmed_text ')]"; + $this->find('xpath', $xpath, false, $activitynode); + throw new ExpectationException('"' . $activityname . '" is hidden', $this->getSession()); + } catch (ElementNotFoundException $e) { + // All ok. + } + + // Should has "stealth" class. + $exception = new ExpectationException('"' . $activityname . '" does not have CSS class "stealth"', $this->getSession()); + $xpath = "/descendant-or-self::a[contains(concat(' ', normalize-space(@class), ' '), ' stealth ')]"; + $this->find('xpath', $xpath, $exception, $activitynode); + + // Additional check if this is a teacher in editing mode. + if ($this->is_editing_on()) { + // Also has either 'Hide' or 'Make unavailable' edit control. + $nohideexception = new ExpectationException('"' . $activityname . '" has neither "' . get_string('hide') . + '" nor "' . get_string('makeunavailable') . '" icons', $this->getSession()); + try { + $this->find('named_partial', array('link', get_string('hide')), false, $activitynode); + } catch (ElementNotFoundException $e) { + $this->find('named_partial', array('link', get_string('makeunavailable')), $nohideexception, $activitynode); + } + } + + } else { + + // Student should not see the activity at all. + try { + $this->get_activity_node($activityname); + throw new ExpectationException('The "' . $activityname . '" should not appear', $this->getSession()); + } catch (ElementNotFoundException $e) { + // This is good, the activity should not be there. + } } } @@ -649,15 +707,23 @@ class behat_course extends behat_base { "/descendant-or-self::div[contains(concat(' ', normalize-space(@class), ' '), ' dimmed_text ')]"; $this->find('xpath', $xpath, $exception, $activitynode); - // Also 'Show' icon. - $noshowexception = new ExpectationException('"' . $activityname . '" don\'t have a "' . get_string('show') . '" icon', $this->getSession()); - $this->find('named_partial', array('link', get_string('show')), $noshowexception, $activitynode); + // Additional check if this is a teacher in editing mode. + if ($this->is_editing_on()) { + // Also has either 'Show' or 'Make available' edit control. + $noshowexception = new ExpectationException('"' . $activityname . '" has neither "' . get_string('show') . + '" nor "' . get_string('makeavailable') . '" icons', $this->getSession()); + try { + $this->find('named_partial', array('link', get_string('show')), false, $activitynode); + } catch (ElementNotFoundException $e) { + $this->find('named_partial', array('link', get_string('makeavailable')), $noshowexception, $activitynode); + } + } } else { // It should not exist at all. try { - $this->find_link($activityname); + $this->get_activity_node($activityname); throw new ExpectationException('The "' . $activityname . '" should not appear', $this->getSession()); } catch (ElementNotFoundException $e) { // This is good, the activity should not be there. @@ -666,6 +732,26 @@ class behat_course extends behat_base { } + /** + * Checks that the specified activity is dimmed. You need to be in the course page. + * + * @Then /^"(?P(?:[^"]|\\")*)" activity should be dimmed$/ + * @param string $activityname + * @throws ExpectationException + */ + public function activity_should_be_dimmed($activityname) { + + // The activity should exist. + $activitynode = $this->get_activity_node($activityname); + + // Should be hidden. + $exception = new ExpectationException('"' . $activityname . '" is not dimmed', $this->getSession()); + $xpath = "/descendant-or-self::a[contains(concat(' ', normalize-space(@class), ' '), ' dimmed ')] | ". + "/descendant-or-self::div[contains(concat(' ', normalize-space(@class), ' '), ' dimmed_text ')]"; + $this->find('xpath', $xpath, $exception, $activitynode); + + } + /** * Moves the specified activity to the first slot of a section. This step is experimental when using it in Javascript tests. Editing mode should be on. * @@ -803,6 +889,42 @@ class behat_course extends behat_base { } } + /** + * Checks that the specified activity's action menu contains an item. + * + * @Then /^"(?P(?:[^"]|\\")*)" actions menu should have "(?P(?:[^"]|\\")*)" item$/ + * @throws DriverException The step is not available when Javascript is disabled + * @param string $activityname + * @param string $menuitem + */ + public function actions_menu_should_have_item($activityname, $menuitem) { + $activitynode = $this->get_activity_node($activityname); + + $notfoundexception = new ExpectationException('"' . $activityname . '" doesn\'t have a "' . + $menuitem . '" item', $this->getSession()); + $this->find('named_partial', array('link', $menuitem), $notfoundexception, $activitynode); + } + + /** + * Checks that the specified activity's action menu does not contains an item. + * + * @Then /^"(?P(?:[^"]|\\")*)" actions menu should not have "(?P(?:[^"]|\\")*)" item$/ + * @throws DriverException The step is not available when Javascript is disabled + * @param string $activityname + * @param string $menuitem + */ + public function actions_menu_should_not_have_item($activityname, $menuitem) { + $activitynode = $this->get_activity_node($activityname); + + try { + $this->find('named_partial', array('link', $menuitem), false, $activitynode); + throw new ExpectationException('"' . $activityname . '" has a "' . $menuitem . + '" item when it should not', $this->getSession()); + } catch (ElementNotFoundException $e) { + // This is good, the menu item should not be there. + } + } + /** * Indents to the right the activity or resource specified by it's name. Editing mode should be on. * @@ -1159,6 +1281,15 @@ class behat_course extends behat_base { return true; } + /** + * Returns whether the user can edit the course contents and the editing mode is on. + * + * @return bool + */ + protected function is_editing_on() { + return $this->getSession()->getPage()->findButton(get_string('turneditingoff')) ? true : false; + } + /** * Returns the id of the category with the given idnumber. * diff --git a/lib/tests/behat/behat_action_menu.php b/lib/tests/behat/behat_action_menu.php index d90900d8465..dca01b7edee 100644 --- a/lib/tests/behat/behat_action_menu.php +++ b/lib/tests/behat/behat_action_menu.php @@ -55,6 +55,13 @@ class behat_action_menu extends behat_base { } // Gets the node based on the requested selector type and locator. $node = $this->get_node_in_container("css_element", "[role=menuitem][aria-haspopup=true]", $selectortype, $element); + + // Check if it is not already opened. + $menunode = $this->find('css', '[aria-labelledby='.$node->getAttribute('id').']'); + if ($menunode->getAttribute('aria-hidden') === 'false') { + return; + } + $this->ensure_node_is_visible($node); $node->click(); } diff --git a/mod/label/tests/behat/label_visibility.feature b/mod/label/tests/behat/label_visibility.feature index 99920a1bcd1..326dc8c0e9c 100644 --- a/mod/label/tests/behat/label_visibility.feature +++ b/mod/label/tests/behat/label_visibility.feature @@ -1,4 +1,5 @@ @mod @mod_label + Feature: Check label visibility works In order to check label visibility works As a teacher @@ -9,29 +10,89 @@ Feature: Check label visibility works Given the following "courses" exist: | fullname | shortname | category | | Test | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher | Teacher | Frist | teacher1@example.com | + | student | Student | First | student1@example.com | And the following "course enrolments" exist: | user | course | role | - | admin | C1 | editingteacher | - Given I log in as "admin" + | teacher | C1 | editingteacher | + | student | C1 | student | + Given I log in as "teacher" And I follow "Test" And I turn editing mode on When I add a "label" to section "1" and I fill the form with: | Label text | Swanky label | | Visible | Hide | Then "Swanky label" activity should be hidden + And I turn editing mode off + And "Swanky label" activity should be hidden + And I log out + And I log in as "student" + And I follow "Test" + And I should not see "Swanky label" + And I log out @javascript Scenario: Visible label activity should be shown as visible. Given the following "courses" exist: | fullname | shortname | category | | Test | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher | Teacher | Frist | teacher1@example.com | + | student | Student | First | student1@example.com | And the following "course enrolments" exist: | user | course | role | - | admin | C1 | editingteacher | - Given I log in as "admin" + | teacher | C1 | editingteacher | + | student | C1 | student | + Given I log in as "teacher" And I follow "Test" And I turn editing mode on When I add a "label" to section "1" and I fill the form with: | Label text | Swanky label | | Visible | Show | Then "Swanky label" activity should be visible + And I log out + And I log in as "student" + And I follow "Test" + And "Swanky label" activity should be visible + And I log out + + @javascript + Scenario: Teacher can not show label inside the hidden section + Given the following "courses" exist: + | fullname | shortname | category | + | Test | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher | Teacher | Frist | teacher1@example.com | + | student | Student | First | student1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher | C1 | editingteacher | + | student | C1 | student | + Given I log in as "teacher" + And I follow "Test" + And I turn editing mode on + When I add a "label" to section "1" and I fill the form with: + | Label text | Swanky label | + | Visible | Show | + And I hide section "1" + Then "Swanky label" activity should be dimmed + And I open "Swanky label" actions menu + And "Swanky label" actions menu should not have "Show" item + And "Swanky label" actions menu should not have "Hide" item + And "Swanky label" actions menu should not have "Make available" item + And "Swanky label" actions menu should not have "Make unavailable" item + And I click on "Edit settings" "link" in the "Swanky label" activity + And I expand all fieldsets + And the "Visible" select box should contain "Hidden from students" + And the "Visible" select box should not contain "Available but not displayed on course page" + And the "Visible" select box should not contain "Hide" + And the "Visible" select box should not contain "Show" + And I log out + And I log in as "student" + And I follow "Test" + And I should not see "Swanky label" + And I log out diff --git a/theme/boost/tests/behat/behat_theme_boost_behat_action_menu.php b/theme/boost/tests/behat/behat_theme_boost_behat_action_menu.php index 38746180083..a1ab4ea5c80 100644 --- a/theme/boost/tests/behat/behat_theme_boost_behat_action_menu.php +++ b/theme/boost/tests/behat/behat_theme_boost_behat_action_menu.php @@ -40,6 +40,12 @@ class behat_theme_boost_behat_action_menu extends behat_action_menu { public function i_open_the_action_menu_in($element, $selectortype) { // Gets the node based on the requested selector type and locator. $node = $this->get_node_in_container("css_element", "[role=button][aria-haspopup=true]", $selectortype, $element); + + // Check if it is not already opened. + if ($node->getAttribute('aria-expanded') === 'true') { + return; + } + $this->ensure_node_is_visible($node); $node->click(); }