Merge branch 'wip-MDL-4782-master-3' of https://github.com/marinaglancy/moodle

This commit is contained in:
David Monllao
2017-02-14 10:00:32 +01:00
67 changed files with 2405 additions and 3276 deletions
+6
View File
@@ -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));
}
+1 -1
View File
@@ -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'));
@@ -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');
}
@@ -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
@@ -0,0 +1,157 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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_name_string>(?:[^"]|\\")*)" activity in site main menu block should have "(?P<icon_name_string>(?:[^"]|\\")*)" 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_name_string>(?:[^"]|\\")*)" activity in site main menu block should not have "(?P<icon_name_string>(?:[^"]|\\")*)" 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<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" in the "(?P<activity_name_string>(?:[^"]|\\")*)" 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_name_string>(?:[^"]|\\")*)" 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_name_string>(?:[^"]|\\")*)" 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<activity_name_string>(?:[^"]|\\")*)" 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']);
}
}
@@ -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"
@@ -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 {
@@ -0,0 +1,157 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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_name_string>(?:[^"]|\\")*)" activity in social activities block should have "(?P<icon_name_string>(?:[^"]|\\")*)" 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_name_string>(?:[^"]|\\")*)" activity in social activities block should not have "(?P<icon_name_string>(?:[^"]|\\")*)" 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<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" in the "(?P<activity_name_string>(?:[^"]|\\")*)" 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_name_string>(?:[^"]|\\")*)" 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_name_string>(?:[^"]|\\")*)" 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<activity_name_string>(?:[^"]|\\")*)" 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']);
}
}
@@ -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
@@ -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
@@ -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:
File diff suppressed because one or more lines are too long
+594
View File
@@ -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 <http://www.gnu.org/licenses/>.
/**
* 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 <a> 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.
$('<div>' + data + '</div>').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) {
$('<div>' + activityHTML + '</div>').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);
}
};
});
+226
View File
@@ -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)');
}
}
+74
View File
@@ -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;
}
}
+47 -31
View File
@@ -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,13 +228,11 @@ 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');
$o .= $this->section_availability($section);
$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;
}
@@ -305,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();
@@ -325,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',
@@ -346,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);
@@ -355,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'));
}
}
@@ -398,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));
}
}
@@ -452,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');
@@ -548,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)
*
+12
View File
@@ -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;
}
}
+30
View File
@@ -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;
}
}
/**
+4 -2
View File
@@ -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'));
}
}
+9
View File
@@ -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.
+23
View File
@@ -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;
}
}
/**
+87 -49
View File
@@ -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('<br>', $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;
}
+19 -16
View File
@@ -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()) {
+3 -1
View File
@@ -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;
+5 -2
View File
@@ -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)) {
+92 -69
View File
@@ -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;
}
@@ -890,15 +913,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.
+29 -116
View File
@@ -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));
}
@@ -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
+138 -7
View File
@@ -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_or_resource_string>(?:[^"]|\\")*)" 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_or_resource_string>(?:[^"]|\\")*)" 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<activity_name_string>(?:[^"]|\\")*)" actions menu should have "(?P<menu_item_string>(?:[^"]|\\")*)" 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<activity_name_string>(?:[^"]|\\")*)" actions menu should not have "(?P<menu_item_string>(?:[^"]|\\")*)" 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.
*
+2
View File
@@ -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';
+2 -2
View File
@@ -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']);
+1
View File
@@ -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;
@@ -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);
},
File diff suppressed because one or more lines are too long
@@ -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);
},
@@ -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"]});
File diff suppressed because one or more lines are too long
@@ -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"]});
+2 -1
View File
@@ -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);
},
-12
View File
@@ -1,12 +0,0 @@
{
"name": "moodle-course-toolboxes",
"builds": {
"moodle-course-toolboxes": {
"jsfiles": [
"toolbox.js",
"resource.js",
"section.js"
]
}
}
}
-552
View File
@@ -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;
};
-189
View File
@@ -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);
};
View File
-201
View File
@@ -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': {}
}
}
}
);
@@ -1,13 +0,0 @@
{
"moodle-course-toolboxes": {
"requires": [
"node",
"base",
"event-key",
"node",
"io",
"moodle-course-coursebase",
"moodle-course-util"
]
}
}
+7
View File
@@ -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';
+2 -1
View File
@@ -291,6 +291,7 @@
<FIELD NAME="score" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="indent" TYPE="int" LENGTH="5" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="visible" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
<FIELD NAME="visibleoncoursepage" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="If stealth visibility is allowed for the course, this controls whether activity is visible on course page"/>
<FIELD NAME="visibleold" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
<FIELD NAME="groupmode" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="groupingid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
@@ -3442,4 +3443,4 @@
</INDEXES>
</TABLE>
</TABLES>
</XMLDB>
</XMLDB>
+24
View File
@@ -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',
+14
View File
@@ -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;
}
+77 -7
View File
@@ -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);
}
}
}
+66 -2
View File
@@ -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;
}
}
/**
+2 -1
View File
@@ -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.
+2 -2
View File
@@ -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;
+33
View File
@@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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}}
<div class="availabilityinfo {{classes}}">{{{text}}}</div>
{{/text}}
@@ -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,
+7
View File
@@ -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();
}
+1
View File
@@ -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)) ) {
+65 -4
View File
@@ -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
-4
View File
@@ -77,10 +77,6 @@ a.dimmed_text:visited,
@extend .text-muted;
}
.activity.label .dimmed_text {
opacity: 0.5;
}
.unlist,
.unlist li,
.inline-list,
+15 -12
View File
@@ -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;
}
@@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
Availability info.
Example context (json):
{ "classes": "", "text": "This activity is not available" }
}}
{{#text}}
<div class="availabilityinfo {{classes}}">
<span class="tag tag-info">{{{text}}}</span>
</div>
{{/text}}
@@ -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();
}
@@ -109,9 +109,6 @@ a.dimmed_text:visited,
.dimmed_category a {
.muted
}
.activity.label .dimmed_text {
.opacity(50)
}
.unlist,
.unlist li,
.inline-list,
+35 -31
View File
@@ -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 {
+17 -21
View File
@@ -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;
}
@@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
Availability info.
Example context (json):
{ "classes": "", "text": "This activity is not available" }
}}
{{#text}}
<div class="availabilityinfo {{classes}}">
<span class="label label-info">{{{text}}}</span>
</div>
{{/text}}
+1 -1
View File
@@ -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.