"sub": false,
"supernew": false,
"maxerr": 500,
- "maxlen": 150,
+ "maxlen": 180,
"passfail": false,
"latedef": true
}
define('NO_OUTPUT_BUFFERING', true);
-if (empty($_GET['cache']) and empty($_POST['cache']) and empty($_GET['sesskey']) and empty($_POST['sesskey'])) {
+if ((isset($_GET['cache']) and $_GET['cache'] === '0')
+ or (isset($_POST['cache']) and $_POST['cache'] === '0')
+ or (!isset($_POST['cache']) and !isset($_GET['cache']) and empty($_GET['sesskey']) and empty($_POST['sesskey']))) {
// Prevent caching at all cost when visiting this page directly,
// we redirect to self once we known no upgrades are necessary.
// Note: $_GET and $_POST are used here intentionally because our param cleaning is not loaded yet.
// Set up PAGE.
$url = new moodle_url('/admin/index.php');
-if ($cache) {
- $url->param('cache', 1);
-}
+$url->param('cache', $cache);
$PAGE->set_url($url);
unset($url);
$PAGE->set_pagelayout('maintenance');
$PAGE->set_popup_notification_allowed(false);
+ /** @var core_admin_renderer $output */
+ $output = $PAGE->get_renderer('core', 'admin');
+
if (upgrade_stale_php_files_present()) {
$PAGE->set_title($stradministration);
$PAGE->set_cacheable(false);
- /** @var core_admin_renderer $output */
- $output = $PAGE->get_renderer('core', 'admin');
echo $output->upgrade_stale_php_files_page();
die();
}
$PAGE->set_heading($strdatabasechecking);
$PAGE->set_cacheable(false);
- /** @var core_admin_renderer $output */
- $output = $PAGE->get_renderer('core', 'admin');
echo $output->upgrade_confirm_page($a->newversion, $maturity, $testsite);
die();
$PAGE->set_heading($strcurrentrelease);
$PAGE->set_cacheable(false);
- /** @var core_admin_renderer $output */
- $output = $PAGE->get_renderer('core', 'admin');
echo $output->upgrade_environment_page($release, $envstatus, $environment_results);
die();
$PAGE->set_heading($strplugincheck);
$PAGE->set_cacheable(false);
- $reloadurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1));
-
- /** @var core_admin_renderer $output */
- $output = $PAGE->get_renderer('core', 'admin');
-
- // check plugin dependencies first
- $failed = array();
- if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
- echo $output->unsatisfied_dependencies_page($version, $failed, $reloadurl);
- die();
- }
- unset($failed);
+ $reloadurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0));
if ($fetchupdates) {
- // no sesskey support guaranteed here
- if (empty($CFG->disableupdatenotifications)) {
- \core\update\checker::instance()->fetch();
+ // No sesskey support guaranteed here, because sessions might not work yet.
+ $updateschecker = \core\update\checker::instance();
+ if ($updateschecker->enabled()) {
+ $updateschecker->fetch();
}
redirect($reloadurl);
}
$deploydata = $deployer->submitted_data();
if (!empty($deploydata)) {
+ // No sesskey support guaranteed here, because sessions might not work yet.
echo $output->upgrade_plugin_confirm_deploy_page($deployer, $deploydata);
die();
}
echo $output->upgrade_plugin_check_page(core_plugin_manager::instance(), \core\update\checker::instance(),
$version, $showallplugins, $reloadurl,
- new moodle_url('/admin/index.php', array('confirmupgrade'=>1, 'confirmrelease'=>1, 'confirmplugincheck'=>1)));
+ new moodle_url('/admin/index.php', array('confirmupgrade'=>1, 'confirmrelease'=>1, 'confirmplugincheck'=>1, 'cache'=>0)));
die();
} else {
- // Launch main upgrade
+ // Always verify plugin dependencies!
+ $failed = array();
+ if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+ $PAGE->set_pagelayout('maintenance');
+ $PAGE->set_popup_notification_allowed(false);
+ $reloadurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0));
+ echo $output->unsatisfied_dependencies_page($version, $failed, $reloadurl);
+ die();
+ }
+ unset($failed);
+
+ // Launch main upgrade.
upgrade_core($version, true);
}
} else if ($version < $CFG->version) {
if (!$cache and moodle_needs_upgrading()) {
if (!$PAGE->headerprinted) {
// means core upgrade or installation was not already done
+
+ /** @var core_admin_renderer $output */
+ $output = $PAGE->get_renderer('core', 'admin');
+
if (!$confirmplugins) {
$strplugincheck = get_string('plugincheck');
$PAGE->set_cacheable(false);
if ($fetchupdates) {
- // no sesskey support guaranteed here
- \core\update\checker::instance()->fetch();
+ require_sesskey();
+ $updateschecker = \core\update\checker::instance();
+ if ($updateschecker->enabled()) {
+ $updateschecker->fetch();
+ }
redirect($PAGE->url);
}
- $output = $PAGE->get_renderer('core', 'admin');
-
$deployer = \core\update\deployer::instance();
if ($deployer->enabled()) {
$deployer->initialize($PAGE->url, $PAGE->url);
$deploydata = $deployer->submitted_data();
if (!empty($deploydata)) {
+ require_sesskey();
echo $output->upgrade_plugin_confirm_deploy_page($deployer, $deploydata);
die();
}
}
- // check plugin dependencies first
- $failed = array();
- if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
- echo $output->unsatisfied_dependencies_page($version, $failed, $PAGE->url);
- die();
- }
- unset($failed);
-
- // dependencies check passed, let's rock!
+ // Show plugins info.
echo $output->upgrade_plugin_check_page(core_plugin_manager::instance(), \core\update\checker::instance(),
$version, $showallplugins,
new moodle_url($PAGE->url),
- new moodle_url('/admin/index.php', array('confirmplugincheck'=>1)));
+ new moodle_url('/admin/index.php', array('confirmplugincheck'=>1, 'cache'=>0)));
+ die();
+ }
+
+ // Make sure plugin dependencies are always checked.
+ $failed = array();
+ if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+ $PAGE->set_pagelayout('maintenance');
+ $PAGE->set_popup_notification_allowed(false);
+ $reloadurl = new moodle_url('/admin/index.php', array('cache' => 0));
+ echo $output->unsatisfied_dependencies_page($version, $failed, $reloadurl);
die();
}
+ unset($failed);
}
+
// install/upgrade all plugins and other parts
upgrade_noncore(true);
}
upgrade_finished('upgradesettings.php');
}
+if (has_capability('moodle/site:config', context_system::instance())) {
+ if ($fetchupdates) {
+ require_sesskey();
+ $updateschecker = \core\update\checker::instance();
+ if ($updateschecker->enabled()) {
+ $updateschecker->fetch();
+ }
+ redirect(new moodle_url('/admin/index.php', array('cache' => 0)));
+ }
+}
+
// Now we can be sure everything was upgraded and caches work fine,
// redirect if necessary to make sure caching is enabled.
if (!$cache) {
admin_externalpage_setup('adminnotifications');
-if ($fetchupdates) {
- require_sesskey();
- $updateschecker->fetch();
- redirect(new moodle_url('/admin/index.php'));
-}
-
$output = $PAGE->get_renderer('core', 'admin');
echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed,
$cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
public function upgrade_confirm_page($strnewversion, $maturity, $testsite) {
$output = '';
- $continueurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1));
+ $continueurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'cache' => 0));
$continue = new single_button($continueurl, get_string('continue'), 'get');
$cancelurl = new moodle_url('/admin/index.php');
$output .= $this->environment_check_table($envstatus, $environment_results);
if (!$envstatus) {
- $output .= $this->upgrade_reload(new moodle_url('/admin/index.php'), array('confirmupgrade' => 1));
+ $output .= $this->upgrade_reload(new moodle_url('/admin/index.php'), array('confirmupgrade' => 1, 'cache' => 0));
} else {
$output .= $this->notification(get_string('environmentok', 'admin'), 'notifysuccess');
$output .= $this->box(get_string('langpackwillbeupdated', 'admin'), 'generalbox', 'notice');
}
- $output .= $this->continue_button(new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1)));
+ $output .= $this->continue_button(new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0)));
}
$output .= $this->footer();
}
$updateinfo .= $this->container_start('checkforupdates');
- $fetchurl = new moodle_url('/admin/index.php', array('fetchupdates' => 1, 'sesskey' => sesskey(), 'cache' => 1));
+ $fetchurl = new moodle_url('/admin/index.php', array('fetchupdates' => 1, 'sesskey' => sesskey(), 'cache' => 0));
$updateinfo .= $this->single_button($fetchurl, get_string('checkforupdates', 'core_plugin'));
if ($fetch) {
$updateinfo .= $this->container(get_string('checkforupdateslast', 'core_plugin',
$out .= $this->output->heading(get_string('nonehighlighted', 'core_plugin'));
if (empty($options['full'])) {
$out .= html_writer::link(new moodle_url('/admin/index.php',
- array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 1)),
+ array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 1, 'cache' => 0)),
get_string('nonehighlightedinfo', 'core_plugin'));
}
$out .= $this->output->container_end();
$out .= $this->output->heading(get_string('somehighlighted', 'core_plugin', $sumofhighlighted));
if (empty($options['full'])) {
$out .= html_writer::link(new moodle_url('/admin/index.php',
- array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 1)),
+ array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 1, 'cache' => 0)),
get_string('somehighlightedinfo', 'core_plugin'));
} else {
$out .= html_writer::link(new moodle_url('/admin/index.php',
- array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 0)),
+ array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 0, 'cache' => 0)),
get_string('somehighlightedonly', 'core_plugin'));
}
$out .= $this->output->container_end();
}
- // calendar
+ // Calendar settings.
$temp = new admin_settingpage('calendar', new lang_string('calendarsettings','admin'));
+
+ $temp->add(new admin_setting_configselect('calendartype', new lang_string('calendartype', 'admin'),
+ new lang_string('calendartype_desc', 'admin'), 'gregorian', \core_calendar\type_factory::get_list_of_calendar_types()));
$temp->add(new admin_setting_special_adminseesall());
//this is hacky because we do not want to include the stuff from calendar/lib.php
$temp->add(new admin_setting_configselect('calendar_site_timeformat', new lang_string('pref_timeformat', 'calendar'),
// We expect admin block to be visible, otherwise go to homepage.
if (!$this->getSession()->getPage()->find('css', '.block_settings')) {
$this->getSession()->visit($this->locate_path('/'));
- $this->wait(self::TIMEOUT, '(document.readyState === "complete")');
+ $this->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
}
// Search by label.
$submitsearch = $this->find('css', 'form.adminsearchform input[type=submit]');
$submitsearch->press();
- $this->wait(self::TIMEOUT, '(document.readyState === "complete")');
+ $this->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
// Admin settings does not use the same DOM structure than other moodle forms
// but we also need to use lib/behat/form_field/* to deal with the different moodle form elements.
And I expand "Users" node
And I expand "Accounts" node
And I follow "Upload users"
- When I upload "lib/tests/fixtures/upload_users.csv" file to "File" filepicker
+ When I upload "lib/tests/fixtures/upload_users.csv" file to "File" filemanager
And I press "Upload users"
Then I should see "Upload users preview"
And I should see "Tom"
And I expand "Users" node
And I follow "Groups"
And I select "Section 1 (1)" from "groups"
- And I wait "4" seconds
And the "members" select box should contain "Tom Jones"
| Course 1 | C1 | 0 |
And I log in as "admin"
And I follow "Course 1"
- When I click on "Move this to the dock" "button" in the "Administration" "block"
+ When I dock "Administration" block
Then I should not see "Question bank" in the "region-pre" "region"
And I click on "//div[@id='dock']/descendant::h2[normalize-space(.)='Administration']" "xpath_element"
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And I log in as "admin"
- When I click on "Move this to the dock" "button" in the "Administration" "block"
+ When I dock "Administration" block
Then I should not see "Turn editing on" in the "region-pre" "region"
Then the "groups" select box should contain "Group 1 (1)"
And the "groups" select box should contain "Group 2 (1)"
And I select "Group 1 (1)" from "groups"
- And I wait "5" seconds
And the "members" select box should contain "Student 1"
And I select "Group 2 (1)" from "groups"
- And I wait "5" seconds
And the "members" select box should contain "Student 2"
--- /dev/null
+<?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/>.
+
+/**
+ * Site wide search-replace form.
+ *
+ * @package tool_replace
+ * @copyright 2013 Petr Skoda {@link http://skodak.org}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once("$CFG->libdir/formslib.php");
+
+/**
+ * Site wide search-replace form.
+ */
+class tool_replace_form extends moodleform {
+ function definition() {
+ global $CFG, $DB;
+
+ $mform = $this->_form;
+
+ $mform->addElement('header', 'searchhdr', get_string('pluginname', 'tool_replace'));
+ $mform->setExpanded('searchhdr', true);
+
+ $mform->addElement('text', 'search', get_string('searchwholedb', 'tool_replace'), 'size="50"');
+ $mform->setType('search', PARAM_RAW);
+ $mform->addElement('static', 'searchst', '', get_string('searchwholedbhelp', 'tool_replace'));
+ $mform->addRule('search', get_string('required'), 'required', null, 'client');
+
+ $mform->addElement('text', 'replace', get_string('replacewith', 'tool_replace'), 'size="50"', PARAM_RAW);
+ $mform->addElement('static', 'replacest', '', get_string('replacewithhelp', 'tool_replace'));
+ $mform->setType('replace', PARAM_RAW);
+ $mform->addElement('checkbox', 'shorten', get_string('shortenoversized', 'tool_replace'));
+ $mform->addRule('replace', get_string('required'), 'required', null, 'client');
+
+ $mform->addElement('header', 'confirmhdr', get_string('confirm'));
+ $mform->setExpanded('confirmhdr', true);
+ $mform->addElement('checkbox', 'sure', get_string('disclaimer', 'tool_replace'));
+ $mform->addRule('sure', get_string('required'), 'required', null, 'client');
+
+ $this->add_action_buttons(false, get_string('doit', 'tool_replace'));
+ }
+
+ function validation($data, $files) {
+ $errors = parent::validation($data, $files);
+
+ if (empty($data['shorten']) and core_text::strlen($data['search']) < core_text::strlen($data['replace'])) {
+ $errors['shorten'] = get_string('required');
+ }
+
+ return $errors;
+ }
+}
/**
* Search and replace strings throughout all texts in the whole database
*
- * @package tool
- * @subpackage replace
+ * @package tool_replace
* @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
admin_externalpage_setup('toolreplace');
-$search = optional_param('search', '', PARAM_RAW);
-$replace = optional_param('replace', '', PARAM_RAW);
-$sure = optional_param('sure', 0, PARAM_BOOL);
-
-###################################################################
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('pageheader', 'tool_replace'));
-if ($DB->get_dbfamily() !== 'mysql' and $DB->get_dbfamily() !== 'postgres') {
- //TODO: add $DB->text_replace() to DML drivers
+if (!$DB->replace_all_text_supported()) {
echo $OUTPUT->notification(get_string('notimplemented', 'tool_replace'));
echo $OUTPUT->footer();
die;
}
-if (!data_submitted() or !$search or !$replace or !confirm_sesskey() or !$sure) { /// Print a form
- echo $OUTPUT->notification(get_string('notsupported', 'tool_replace'));
- echo $OUTPUT->notification(get_string('excludedtables', 'tool_replace'));
+echo $OUTPUT->box_start();
+echo $OUTPUT->notification(get_string('notsupported', 'tool_replace'));
+echo $OUTPUT->notification(get_string('excludedtables', 'tool_replace'));
+echo $OUTPUT->box_end();
+
+$form = new tool_replace_form();
- echo $OUTPUT->box_start();
- echo '<div class="mdl-align">';
- echo '<form action="index.php" method="post"><div>';
- echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
- echo '<div><label for="search">'.get_string('searchwholedb', 'tool_replace').
- ' </label><input id="search" type="text" name="search" size="40" /> ('.
- get_string('searchwholedbhelp', 'tool_replace').')</div>';
- echo '<div><label for="replace">'.get_string('replacewith', 'tool_replace').
- ' </label><input type="text" id="replace" name="replace" size="40" /> ('.
- get_string('replacewithhelp', 'tool_replace').')</div>';
- echo '<div><label for="sure">'.get_string('disclaimer', 'tool_replace').' </label><input type="checkbox" id="sure" name="sure" value="1" /></div>';
- echo '<div class="buttons"><input type="submit" class="singlebutton" value="Yes, do it now" /></div>';
- echo '</div></form>';
- echo '</div>';
- echo $OUTPUT->box_end();
+if (!$data = $form->get_data()) {
+ $form->display();
echo $OUTPUT->footer();
- die;
+ die();
}
+// Scroll to the end when finished.
+$PAGE->requires->js_init_code("window.scrollTo(0, 5000000);");
+
echo $OUTPUT->box_start();
-db_replace($search, $replace);
+db_replace($data->search, $data->replace);
echo $OUTPUT->box_end();
-/// Rebuild course cache which might be incorrect now
-echo $OUTPUT->notification(get_string('notifyrebuilding', 'tool_replace'), 'notifysuccess');
-rebuild_course_cache();
-echo $OUTPUT->notification(get_string('notifyfinished', 'tool_replace'), 'notifysuccess');
+// Course caches are now rebuilt on the fly.
echo $OUTPUT->continue_button(new moodle_url('/admin/index.php'));
echo $OUTPUT->footer();
-
-
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-$string['disclaimer'] = 'I understand the risks of this operation:';
+$string['cannotfit'] = 'The replacement is longer than original and shortening is not allow, cannot continue.';
+$string['disclaimer'] = 'I understand the risks of this operation';
+$string['doit'] = 'Yes, do it!';
$string['excludedtables'] = 'Several tables are not updated as part of the text replacement. This include configuration, log, events, and session tables.';
$string['pageheader'] = 'Search and replace text throughout the whole database';
$string['notifyfinished'] = '...finished';
$string['notifyrebuilding'] = 'Rebuilding course cache...';
-$string['notimplemented'] = 'Sorry, this feature is implemented only for MySQL and PostgreSQL databases.';
+$string['notimplemented'] = 'Sorry, this feature is not implemented in your database driver.';
$string['notsupported'] ='This script is not supported, always make complete backup before proceeding!<br />This operation can not be reverted!';
$string['pluginname'] = 'DB search and replace';
-$string['replacewith'] = 'Replace with this string:';
+$string['replacewith'] = 'Replace with this string';
$string['replacewithhelp'] = 'usually new server URL';
-$string['searchwholedb'] = 'Search whole database for:';
+$string['searchwholedb'] = 'Search whole database for';
$string['searchwholedbhelp'] = 'usually previous server URL';
+$string['shortenoversized'] = 'Shorten result if necessary';
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2013110500; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version = 2013110501; // The current plugin version (Date: YYYYMMDDXX)
$plugin->requires = 2013110500; // Requires this Moodle version
$plugin->component = 'tool_replace'; // Full name of the plugin (used for diagnostics)
@javascript
Scenario: Creation of unexisting courses
- Given I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filepicker
+ Given I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filemanager
And I click on "Preview" "button"
When I click on "Upload courses" "button"
Then I should see "The course exists and update is not allowed"
@javascript
Scenario: Creation of existing courses
- Given I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filepicker
+ Given I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filemanager
And I select "Create all, increment shortname if needed" from "Upload mode"
And I click on "Preview" "button"
When I click on "Upload courses" "button"
@javascript
Scenario: Updating a course fullname
- Given I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filepicker
+ Given I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filemanager
And I select "Only update existing courses" from "Upload mode"
And I select "Update with CSV data only" from "Update mode"
And I click on "Preview" "button"
if ($usertotal < 500) {
list($sort, $params) = users_order_by_sql('u');
- //user searchable selector - get all users (admin and guest included)
- //user must be confirmed, not deleted, not suspended, not guest
- $sql = "SELECT u.id, u.firstname, u.lastname
- FROM {user} u
- WHERE u.deleted = 0 AND u.confirmed = 1 AND u.suspended = 0 AND u.id != :siteguestid
- ORDER BY $sort";
+ // User searchable selector - return users who are confirmed, not deleted, not suspended and not a guest.
+ $sql = 'SELECT u.id, ' . get_all_user_name_fields(true, 'u') . '
+ FROM {user} u
+ WHERE u.deleted = 0
+ AND u.confirmed = 1
+ AND u.suspended = 0
+ AND u.id != :siteguestid
+ ORDER BY ' . $sort;
$params['siteguestid'] = $CFG->siteguest;
$users = $DB->get_records_sql($sql, $params);
$options = array();
if (empty($user->lang)) {
$user->lang = $CFG->lang;
}
+ if (empty($user->calendartype)) {
+ $user->calendartype = $CFG->calendartype;
+ }
$user->timecreated = time();
$user->timemodified = $user->timecreated;
if ($collision = $DB->get_record_select('user', "username = :username AND mnethostid = :mnethostid AND auth <> :auth", array('username'=>$user->username, 'mnethostid'=>$CFG->mnet_localhost_id, 'auth'=>$this->authtype), 'id,username,auth')) {
require_once($CFG->dirroot.'/user/lib.php');
$user->password = hash_internal_user_password($user->password);
+ if (empty($user->calendartype)) {
+ $user->calendartype = $CFG->calendartype;
+ }
$user->id = user_create_user($user, false);
if (empty($user->lang)) {
$user->lang = $CFG->lang;
}
+ if (empty($user->calendartype)) {
+ $user->calendartype = $CFG->calendartype;
+ }
$id = user_create_user($user, false);
echo "\t"; print_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)); echo "\n";
// Carry out actual backup.
$backup->execute();
+ // Backup controller gets saved/loaded so the logger object changes and we
+ // have to retrieve it.
+ $logger = $backup->get_controller()->get_logger();
+ while (!is_a($logger, 'core_backup_html_logger')) {
+ $logger = $logger->get_next();
+ }
+
// Get HTML from logger.
$loghtml = $logger->get_html();
backup_file_manager::copy_file_moodle2backup($this->backupid, $values);
} catch (file_exception $e) {
$this->add_result(array('missing_files_in_pool' => true));
- $this->add_log('missing file in pool: ' . $e->debuginfo, backup::LOG_WARNING);
+
+ // Build helpful log message with all information necessary to identify
+ // file location.
+ $context = context::instance_by_id($values->contextid, IGNORE_MISSING);
+ $contextname = '';
+ if ($context) {
+ $contextname = ' \'' . $context->get_context_name() . '\'';
+ }
+ $message = 'Missing file in pool: ' . $values->filepath . $values->filename .
+ ' (context ' . $values->contextid . $contextname . ', component ' .
+ $values->component . ', filearea ' . $values->filearea . ', itemid ' .
+ $values->itemid . ') [' . $e->debuginfo . ']';
+ $this->add_log($message, backup::LOG_WARNING);
}
}
}
if ($includesfiles) {
// The file is not found in the backup.
if (!file_exists($backuppath)) {
- $result = new stdClass();
- $result->code = 'file_missing_in_backup';
- $result->message = sprintf('missing file %s%s in backup', $file->filepath, $file->filename);
- $result->level = backup::LOG_WARNING;
- $results[] = $result;
+ $results[] = self::get_missing_file_result($file);
continue;
}
$fs->create_file_from_storedfile($file_record, $foundfile->id);
} else {
// A matching existing file record was not found in the database.
- $result = new stdClass();
- $result->code = 'file_missing_in_backup';
- $result->message = sprintf('missing file %s%s in backup', $file->filepath, $file->filename);
- $result->level = backup::LOG_WARNING;
- $results[] = $result;
+ $results[] = self::get_missing_file_result($file);
continue;
}
}
return $results;
}
+ /**
+ * Returns suitable entry to include in log when there is a missing file.
+ *
+ * @param stdClass $file File definition
+ * @return stdClass Log entry
+ */
+ protected static function get_missing_file_result($file) {
+ $result = new stdClass();
+ $result->code = 'file_missing_in_backup';
+ $result->message = 'Missing file in backup: ' . $file->filepath . $file->filename .
+ ' (old context ' . $file->contextid . ', component ' . $file->component .
+ ', filearea ' . $file->filearea . ', old itemid ' . $file->itemid . ')';
+ $result->level = backup::LOG_WARNING;
+ return $result;
+ }
+
/**
* Given one restoreid, create in DB all the users present
* in backup_ids having newitemid = 0, as far as
$config->yesLabel = get_string('confirmcancelyes', 'backup');
$config->noLabel = get_string('confirmcancelno', 'backup');
$config->closeButtonTitle = get_string('close', 'editor');
- $PAGE->requires->yui_module('moodle-backup-confirmcancel', 'M.core_backup.watch_cancel_buttons', array($config));
+ $PAGE->requires->yui_module('moodle-backup-confirmcancel', 'M.core_backup.confirmcancel.watch_cancel_buttons', array($config));
// Get list of module types on course.
$modinfo = get_fast_modinfo($COURSE);
$modnames = $modinfo->get_used_module_names(true);
- $PAGE->requires->yui_module('moodle-backup-backupselectall', 'M.core_backup.select_all_init',
+ $PAGE->requires->yui_module('moodle-backup-backupselectall', 'M.core_backup.backupselectall',
array($modnames));
$PAGE->requires->strings_for_js(array('select', 'all', 'none'), 'moodle');
$PAGE->requires->strings_for_js(array('showtypes', 'hidetypes'), 'backup');
And I press "Continue"
And I click on "Continue" "button" in the ".bcs-current-course" "css_element"
And "//div[contains(concat(' ', normalize-space(@class), ' '), ' fitem ')][contains(., 'Include calendar events')]/descendant::img" "xpath_element" should exists
- And I check "Include course logs"
+ And "Include course logs" "checkbox" should exists
And I press "Next"
@javascript
// Go to homepage.
$this->getSession()->visit($this->locate_path('/'));
+ $this->wait();
// Click the course link.
$this->find_link($backupcourse)->click();
+ $this->wait();
// Click the backup link.
$this->find_link(get_string('backup'))->click();
+ $this->wait();
// Initial settings.
$this->fill_backup_restore_form($options);
$this->find_button(get_string('backupstage1action', 'backup'))->press();
+ $this->wait();
// Schema settings.
$this->fill_backup_restore_form($options);
$this->find_button(get_string('backupstage2action', 'backup'))->press();
+ $this->wait();
// Confirmation and review, backup filename can also be specified.
$this->fill_backup_restore_form($options);
$this->find_button(get_string('backupstage4action', 'backup'))->press();
// Waiting for it to finish.
- $this->wait(10);
+ $this->wait(self::EXTENDED_TIMEOUT);
// Last backup continue button.
$this->find_button(get_string('backupstage16action', 'backup'))->press();
// Go to homepage.
$this->getSession()->visit($this->locate_path('/'));
+ $this->wait();
// Click the course link.
$this->find_link($tocourse)->click();
+ $this->wait();
// Click the import link.
$this->find_link(get_string('import'))->click();
+ $this->wait();
// Select the course.
$exception = new ExpectationException('"' . $fromcourse . '" course not found in the list of courses to import from', $this->getSession());
$radionode->click();
$this->find_button(get_string('continue'))->press();
+ $this->wait();
// Initial settings.
$this->fill_backup_restore_form($options);
$this->find_button(get_string('importbackupstage1action', 'backup'))->press();
+ $this->wait();
// Schema settings.
$this->fill_backup_restore_form($options);
$this->find_button(get_string('importbackupstage2action', 'backup'))->press();
+ $this->wait();
// Run it.
$this->find_button(get_string('importbackupstage4action', 'backup'))->press();
- $this->wait();
+ $this->wait(self::EXTENDED_TIMEOUT);
// Continue and redirect to 'to' course.
$this->find_button(get_string('continue'))->press();
// Settings.
$this->fill_backup_restore_form($options);
$this->find_button(get_string('restorestage4action', 'backup'))->press();
+ $this->wait();
// Schema.
$this->fill_backup_restore_form($options);
$this->find_button(get_string('restorestage8action', 'backup'))->press();
+ $this->wait();
// Review, no options here.
$this->find_button(get_string('restorestage16action', 'backup'))->press();
- $this->wait(10);
+ $this->wait();
// Last restore continue button, redirected to restore course after this.
$this->find_button(get_string('restorestage32action', 'backup'))->press();
+
+ // Long wait when waiting for the restore to finish.
+ $this->wait(self::EXTENDED_TIMEOUT);
}
/**
return;
}
+ // Wait for the page to be loaded and the JS ready.
+ $this->wait();
+
// If we find any of the provided options in the current form we should set the value.
$datahash = $options->getRowsHash();
foreach ($datahash as $locator => $value) {
try {
+ // Using $this->find* to enforce stability over speed.
$fieldnode = $this->find_field($locator);
$field = behat_field_manager::get_form_field($fieldnode, $this->getSession());
$field->set_value($value);
}
/**
- * Waits until the DOM is ready.
+ * Waits until the DOM and the page Javascript code is ready.
*
- * @param int To override the default timeout
+ * @param int $timeout The number of seconds that we wait.
* @return void
*/
protected function wait($timeout = false) {
if (!$timeout) {
$timeout = self::TIMEOUT;
}
- $this->getSession()->wait($timeout, '(document.readyState === "complete")');
+
+ $this->getSession()->wait($timeout * 1000, self::PAGE_READY_JS);
}
}
And I add a "Database" to section "1" and I fill the form with:
| Name | Test database name |
| Description | Test database description |
- And I open "Test database name" actions menu
- When I click on "Duplicate" "link" in the "Test database name" activity
+ And I duplicate "Test database name" activity
+ And I wait until section "1" is available
And I open "Test database name" actions menu
And I click on "Edit settings" "link" in the "Test database name" activity
And I fill the moodle form with:
+++ /dev/null
-YUI.add('moodle-backup-confirmcancel', function(Y) {
-
-// Namespace for the backup
-M.core_backup = M.core_backup || {};
-/**
- * Adds confirmation dialogues to the cancel buttons on the page.
- *
- * @param {object} config
- */
-M.core_backup.watch_cancel_buttons = function(config) {
- Y.all('.confirmcancel').each(function(){
- this._confirmationListener = this._confirmationListener || this.on('click', function(e){
- // Prevent the default event (sumbit) from firing
- e.preventDefault();
- // Create the confirm box
- var confirm = new M.core.confirm(config);
- // If the user clicks yes
- confirm.on('complete-yes', function(e){
- // Detach the listener for the confirm box so it doesn't fire again.
- this._confirmationListener.detach();
- // Simulate the original cancel button click
- this.simulate('click');
- }, this);
- // Show the confirm box
- confirm.show();
- }, this);
- });
-}
-
-}, '@VERSION@', {'requires':['base','node','node-event-simulate','moodle-core-notification']});
--- /dev/null
+{
+ "name": "moodle-backup-backupselectall",
+ "builds": {
+ "moodle-backup-backupselectall": {
+ "jsfiles": [
+ "backupselectall.js"
+ ]
+ }
+ }
+}
-YUI.add('moodle-backup-backupselectall', function(Y) {
+/**
+ * Adds select all/none links to the top of the backup/restore/import schema page.
+ *
+ * @module moodle-backup-backupselectall
+ */
// Namespace for the backup
M.core_backup = M.core_backup || {};
/**
* Adds select all/none links to the top of the backup/restore/import schema page.
+ *
+ * @class M.core_backup.backupselectall
*/
-M.core_backup.select_all_init = function(modnames) {
+M.core_backup.backupselectall = function(modnames) {
var formid = null;
var helper = function(e, check, type, mod) {
if (prefix && name.substring(0, prefix.length) !== prefix) {
return;
}
- if (name.substring(name.length - len) == type) {
+ if (name.substring(name.length - len) === type) {
checkbox.set('checked', check);
}
});
var withuserdata = false;
Y.all('input[type="checkbox"]').each(function(checkbox) {
var name = checkbox.get('name');
- if (name.substring(name.length - 9) == '_userdata') {
+ if (name.substring(name.length - 9) === '_userdata') {
withuserdata = '_userdata';
- } else if (name.substring(name.length - 9) == '_userinfo') {
+ } else if (name.substring(name.length - 9) === '_userinfo') {
withuserdata = '_userinfo';
}
});
if (!modnames.hasOwnProperty(mod)) {
continue;
}
- var html = html_generator('include_setting section_level', 'mod_' + mod, modnames[mod]);
+ html = html_generator('include_setting section_level', 'mod_' + mod, modnames[mod]);
if (withuserdata) {
html += html_generator('normal_setting', 'userdata-mod_' + mod, modnames[mod]);
}
modlist.currentlyshown = !modlist.currentlyshown;
// Either hide or show the links.
- var animcfg = { node: modlist, duration: 0.2 };
+ var animcfg = { node: modlist, duration: 0.2 },
+ anim;
if (modlist.currentlyshown) {
// Animate reveal of the module links.
modlist.show();
animcfg.to = { maxHeight: modlist.get('clientHeight') + 'px' };
modlist.setStyle('maxHeight', '0px');
- var anim = new Y.Anim(animcfg);
+ anim = new Y.Anim(animcfg);
anim.on('end', function() { modlist.setStyle('maxHeight', 'none'); });
anim.run();
} else {
// Animate hide of the module links.
animcfg.to = { maxHeight: '0px' };
modlist.setStyle('maxHeight', modlist.get('clientHeight') + 'px');
- var anim = new Y.Anim(animcfg);
+ anim = new Y.Anim(animcfg);
anim.on('end', function() { modlist.hide(); modlist.setStyle('maxHeight', 'none'); });
anim.run();
}
};
- Y.one('#backup-bytype').on('click', function(e) { toggletypes(); });
+ Y.one('#backup-bytype').on('click', function() { toggletypes(); });
Y.one('#backup-all-included').on('click', function(e) { helper(e, true, '_included'); });
Y.one('#backup-none-included').on('click', function(e) { helper(e, false, '_included'); });
Y.one('#backup-all-userdata').on('click', function(e) { helper(e, true, withuserdata); });
Y.one('#backup-none-userdata').on('click', function(e) { helper(e, false, withuserdata); });
}
-}
-
-}, '@VERSION@', {'requires':['base', 'node', 'event', 'node-event-simulate', 'anim']});
+};
--- /dev/null
+{
+ "moodle-backup-backupselectall": {
+ "requires": [
+ "node",
+ "event",
+ "node-event-simulate",
+ "anim"
+ ]
+ }
+}
--- /dev/null
+{
+ "moodle-backup-backupselectall": {
+ "requires": [
+ "node",
+ "event",
+ "node-event-simulate",
+ "anim"
+ ]
+ }
+}
--- /dev/null
+{
+ "name": "moodle-backup-confirmcancel",
+ "builds": {
+ "moodle-backup-confirmcancel": {
+ "jsfiles": [
+ "confirmcancel.js"
+ ]
+ }
+ }
+}
--- /dev/null
+/**
+ * Add a confirmation dialogue when cancelling a backup.
+ *
+ * @module moodle-backup-confirmcancel
+ */
+
+/**
+ * Add a confirmation dialogue when cancelling a backup.
+ *
+ * @class M.core_backup.confirmcancel
+ */
+
+
+// Namespace for the backup.
+M.core_backup = M.core_backup || {};
+
+M.core_backup.confirmcancel = {
+ /**
+ * An array of EventHandlers which call the confirm_cancel dialogue.
+ *
+ * @property listeners
+ * @protected
+ * @type Array
+ */
+ listeners: [],
+
+ /**
+ * The configuration supplied to this instance.
+ *
+ * @property config
+ * @protected
+ * @type Object
+ */
+ config: {},
+
+ /**
+ * Initializer to watch all cancel buttons.
+ *
+ * @method watch_cancel_buttons
+ * @param {Object} config The configuration for the confirmation dialogue.
+ */
+ watch_cancel_buttons: function(config) {
+ this.config = config;
+
+ this.listeners.push(
+ Y.one(Y.config.doc.body).delegate('click', this.confirm_cancel, '.confirmcancel', this)
+ );
+ },
+
+ /**
+ * Display the confirmation dialogue.
+ *
+ * @method confirm_cancel
+ * @protected
+ * @param {EventFacade} e
+ */
+ confirm_cancel: function(e) {
+ // Prevent the default event (submit) from firing.
+ e.preventDefault();
+
+ // Create the confirmation dialogue.
+ var confirm = new M.core.confirm(this.config);
+
+ // If the user clicks yes.
+ confirm.on('complete-yes', function(){
+ // Detach the listeners for the confirm box so they don't fire again.
+ new Y.EventHandle(M.core_backup.confirmcancel.listeners).detach();
+
+ // Simulate the original cancel button click.
+ c.currentTarget.simulate('click');
+ }, this);
+
+
+ // Show the confirm box.
+ confirm.show();
+ }
+};
--- /dev/null
+{
+ "moodle-backup-confirmcancel": {
+ "requires": [
+ "node",
+ "node-event-simulate",
+ "moodle-core-notification-confirm"
+ ]
+ }
+}
| Description | Test badge description |
| issuername | Test Badge Site |
| issuercontact | testuser@test-badge-site.com |
- And I upload "badges/tests/behat/badge.png" file to "Image" filepicker
+ And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
When I press "Create badge"
Then I should see "Edit details"
And I should see "Test Badge"
As an admin
I need to add criteria to badges in the system
- Background:
- Given I am on homepage
- And I log in as "admin"
-
@javascript
Scenario: Award profile badge
- Given I expand "Site administration" node
+ Given I log in as "admin"
+ And I expand "Site administration" node
And I expand "Badges" node
And I follow "Add a new badge"
And I fill the moodle form with:
| Description | Test badge description |
| issuername | Test Badge Site |
| issuercontact | testuser@test-badge-site.com |
- And I upload "badges/tests/behat/badge.png" file to "Image" filepicker
+ And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I select "Profile completion" from "type"
And I check "First name"
| username | firstname | lastname | email |
| teacher | teacher | 1 | teacher1@asd.com |
| student | student | 1 | student1@asd.com |
+ And I log in as "admin"
And I expand "Site administration" node
And I expand "Badges" node
And I follow "Add a new badge"
| Name | Site Badge |
| Description | Site badge description |
| issuername | Tester of site badge |
- And I upload "badges/tests/behat/badge.png" file to "Image" filepicker
+ And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I select "Manual issue by role" from "type"
And I check "Teacher"
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
- And I log out
And I log in as "teacher1"
And I follow "Course 1"
And I click on "//span[text()='Badges']" "xpath_element" in the "Administration" "block"
| Name | Course Badge |
| Description | Course badge description |
| issuername | Tester of course badge |
- And I upload "badges/tests/behat/badge.png" file to "Image" filepicker
+ And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I select "Manual issue by role" from "type"
And I check "Teacher"
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
- And I log out
And I log in as "admin"
And I set the following administration settings values:
| Enable completion tracking | 1 |
| Name | Course Badge |
| Description | Course badge description |
| issuername | Tester of course badge |
- And I upload "badges/tests/behat/badge.png" file to "Image" filepicker
+ And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I select "Activity completion" from "type"
And I check "Test assignment name"
And I follow "Home"
And I follow "Course 1"
And I press "Mark as complete: Test assignment name"
- And I wait "2" seconds
And I expand "My profile" node
And I follow "My badges"
Then I should see "Course Badge"
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
- And I log out
And I log in as "admin"
And I set the following administration settings values:
| Enable completion tracking | 1 |
| Name | Course Badge |
| Description | Course badge description |
| issuername | Tester of course badge |
- And I upload "badges/tests/behat/badge.png" file to "Image" filepicker
+ And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I select "Course completion" from "type"
And I fill the moodle form with:
$this->content = new stdClass();
$this->content->footer = '';
+ $this->content->text = '';
$context = $this->page->context;
$this->find_link(get_string('savecomment'))->click();
- // Wait for the AJAX request.
- $this->getSession()->wait(4 * 1000, false);
-
} else {
$commentstextarea = $this->find('css', '.block_comments form textarea', $exception);
$deleteicon = $this->find('css', '.comment-delete a img', $deleteexception, $commentnode);
$deleteicon->click();
- // Wait for the AJAX request.
+ // Wait for the animation to finish, in theory is just 1 sec, adding 4 just in case.
$this->getSession()->wait(4 * 1000, false);
}
And I should see "cat3" in the "Navigation" "block"
And I should not see "cat2" in the "Navigation" "block"
And I expand "cat3" node
- And I wait "2" seconds
And I should see "cat31" in the "Navigation" "block"
And I should see "cat33" in the "Navigation" "block"
And I should not see "cat32" in the "Navigation" "block"
And I expand "cat31" node
- And I wait "2" seconds
And I should see "c31" in the "Navigation" "block"
And I expand "cat33" node
- And I wait "2" seconds
And I should see "c331" in the "Navigation" "block"
And I should not see "c332" in the "Navigation" "block"
instance : {
value : false,
setter : function(val) {
- return parseInt(val);
+ return parseInt(val, 10);
}
}
}
return $steps;
}
+ /**
+ * Docks a block. Editing mode should be previously enabled.
+ *
+ * @Given /^I dock "(?P<block_name_string>(?:[^"]|\\")*)" block$/
+ * @param string $blockname
+ * @return Given
+ */
+ public function i_dock_block($blockname) {
+
+ // Looking for both title and alt.
+ $xpath = "//input[@type='image'][@title='" . get_string('dockblock', 'block', $blockname) . "' or @alt='" . get_string('addtodock', 'block') . "']";
+ return new Given('I click on " ' . $xpath . '" "xpath_element" in the "' . $this->escape($blockname) . '" "block"');
+ }
+
/**
* Opens a block's actions menu if it is not already opened.
*
And I follow "Comments (0)"
When I fill in "content" with "$My own >nasty< \"string\"!"
And I follow "Save comment"
- And I wait "4" seconds
Then I should see "$My own >nasty< \"string\"!"
And I fill in "content" with "Another $Nasty <string?>"
And I follow "Save comment"
- And I wait "4" seconds
And I should see "Comments (2)" in the ".comment-link" "css_element"
@javascript
And I follow "Comments (0)"
And I fill in "content" with "$My own >nasty< \"string\"!"
And I follow "Save comment"
- And I wait "4" seconds
When I click on ".comment-delete a" "css_element"
+ # Waiting for the animation to finish.
And I wait "4" seconds
Then I should not see "$My own >nasty< \"string\"!"
And I follow "Blog post from user 1"
When I follow "Comments (0)"
And I fill in "content" with "$My own >nasty< \"string\"!"
And I follow "Save comment"
- And I wait "4" seconds
Then I should see "$My own >nasty< \"string\"!"
And I expand "Users" node
And I expand "Accounts" node
When I follow "Upload users"
- And I upload "lib/tests/fixtures/upload_users_cohorts.csv" file to "File" filepicker
+ And I upload "lib/tests/fixtures/upload_users_cohorts.csv" file to "File" filemanager
And I press "Upload users"
And I press "Upload users"
And I press "Continue"
And I log in as "student1"
And I follow "Course 1"
And I press "Mark as complete: Test forum name"
- And I wait "3" seconds
And I log out
And I log in as "teacher1"
And I follow "Course 1"
And I set the following administration settings values:
| Enable conditional access | 1 |
And I log out
-
- @javascript
- Scenario: Show activity greyed-out to students when available from date is in future
- Given I log in as "teacher1"
+ And I log in as "teacher1"
And I follow "Course 1"
And I turn editing mode on
+ # Adding the page like this because id_available*_enabled needs to be clicked to trigger the action.
And I add a "Assignment" to section "1"
+ And I expand all fieldsets
+
+ @javascript
+ Scenario: Show activity greyed-out to students when available from date is in future
+ Given I click on "id_availablefrom_enabled" "checkbox"
And I fill the moodle form with:
| Assignment name | Test assignment 1 |
| Description | This assignment is restricted by date |
| id_availablefrom_month | 12 |
| id_availablefrom_year | 2050 |
| id_showavailability | 1 |
- And I click on "id_availablefrom_enabled" "checkbox"
And I press "Save and return to course"
And I log out
When I log in as "student1"
@javascript
Scenario: Show activity hidden to students when available until date is in past
- Given I log in as "teacher1"
- And I follow "Course 1"
- And I turn editing mode on
- And I add a "Assignment" to section "2"
+ Given I click on "id_availableuntil_enabled" "checkbox"
And I fill the moodle form with:
| Assignment name | Test assignment 2 |
| Description | This assignment is restricted by date |
| id_availableuntil_month | 2 |
| id_availableuntil_year | 2013 |
| id_showavailability | 0 |
- And I click on "id_availableuntil_enabled" "checkbox"
And I press "Save and return to course"
And I log out
When I log in as "student1"
| Description | Grade this assignment to revoke restriction on restricted assignment |
| assignsubmission_onlinetext_enabled | 1 |
| assignsubmission_file_enabled | 0 |
- And I add a "Page" to section "2" and I fill the form with:
+ # Adding the page like this because id_availableform_enabled needs to be clicked to trigger the action.
+ And I add a "Page" to section "2"
+ And I expand all fieldsets
+ And I click on "id_availablefrom_enabled" "checkbox"
+ And I fill the moodle form with:
| Name | Test page name |
| Description | Restricted page, till grades in Grade assignment is at least 20% |
| Page content | Test page contents |
| id_conditiongradegroup_0_conditiongradeitemid | 2 |
| id_conditiongradegroup_0_conditiongrademin | 20 |
| id_showavailability | 1 |
+ And I press "Save and return to course"
And I log out
When I log in as "student1"
And I follow "Course 1"
// Example:
// $CFG->behat_usedeprecated = true;
//
+// Including feature files from directories outside the dirroot is possible if required. The setting
+// requires that the running user has executable permissions on all parent directories in the paths.
+// Example:
+// $CFG->behat_additionalfeatures = array('/home/developer/code/wipfeatures');
+//
//=========================================================================
// 12. DEVELOPER DATA GENERATOR
//=========================================================================
if ($hasmanageactivities) {
$pixicon = 'i/dragdrop';
- if ($mod->course == SITEID) {
+ if (!course_ajax_enabled($mod->get_course())) {
// Override for course frontpage until we get drag/drop working there.
$pixicon = 't/move';
}
Then "No groups (Click to change)" "link" should exists
And "//a/child::img[contains(@src, 'groupn')]" "xpath_element" should exists
And I click on "No groups (Click to change)" "link" in the "Test forum name" activity
- And I wait "3" seconds
And "Separate groups (Click to change)" "link" should exists
And "//a/child::img[contains(@src, 'groups')]" "xpath_element" should exists
And I reload the page
And "Separate groups (Click to change)" "link" should exists
And "//a/child::img[contains(@src, 'groups')]" "xpath_element" should exists
And I click on "Separate groups (Click to change)" "link" in the "Test forum name" activity
- And I wait "3" seconds
And "Visible groups (Click to change)" "link" should exists
And "//a/child::img[contains(@src, 'groupv')]" "xpath_element" should exists
And I reload the page
Scenario: Add an activity without the required fields
When I add a "Database" to section "3" and I fill the form with:
| Name | Test name |
+ And I press "Save and return to course"
Then I should see "Adding a new"
And I should see "Required"
+ And I press "Cancel"
Scenario: Add an activity to a course with Javascript disabled
Then I should see "Add a resource to section 'Topic 1'"
* @return Given[]
*/
public function i_create_a_course_with(TableNode $table) {
- return array(
+
+ $steps = array(
new Given('I go to the courses management page'),
new Given('I should see the "'.get_string('categories').'" management page'),
new Given('I click on category "'.get_string('miscellaneous').'" in the management interface'),
new Given('I should see the "'.get_string('categoriesandcoures').'" management page'),
- new Given('I click on "'.get_string('createnewcourse').'" "link" in the "#course-listing" "css_element"'),
- new Given('I fill the moodle form with:', $table),
- new Given('I press "' . get_string('savechanges') . '"')
+ new Given('I click on "'.get_string('createnewcourse').'" "link" in the "#course-listing" "css_element"')
);
+
+ // If the course format is one of the fields we change how we
+ // fill the form as we need to wait for the form to be set.
+ $rowshash = $table->getRowsHash();
+ $formatfieldrefs = array(get_string('format'), 'format', 'id_format');
+ foreach ($formatfieldrefs as $fieldref) {
+ if (!empty($rowshash[$fieldref])) {
+ $formatfield = $fieldref;
+ }
+ }
+
+ // Setting the format separately.
+ if (!empty($formatfield)) {
+
+ // Removing the format field from the TableNode.
+ $rows = $table->getRows();
+ $formatvalue = $rowshash[$formatfield];
+ foreach ($rows as $key => $row) {
+ if ($row[0] == $formatfield) {
+ unset($rows[$key]);
+ }
+ }
+ $table->setRows($rows);
+
+ // Adding a forced wait until editors are loaded as otherwise selenium sometimes tries clicks on the
+ // format field when the editor is being rendered and the click misses the field coordinates.
+ $steps[] = new Given('I wait until the editors are loaded');
+ $steps[] = new Given('I select "' . $formatvalue . '" from "' . $formatfield . '"');
+ $steps[] = new Given('I fill the moodle form with:', $table);
+ } else {
+ $steps[] = new Given('I fill the moodle form with:', $table);
+ }
+
+ $steps[] = new Given('I press "' . get_string('savechanges') . '"');
+
+ return $steps;
}
/**
// Ensures the section exists.
$xpath = $this->section_exists($sectionnumber);
- return array(
- new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"'),
- new Given('I wait "2" seconds')
- );
+ return new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"');
}
/**
// Ensures the section exists.
$xpath = $this->section_exists($sectionnumber);
- return array(
- new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"'),
- new Given('I wait "2" seconds')
- );
+ return new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"');
}
/**
$showlink = $this->show_section_icon_exists($sectionnumber);
$showlink->click();
- // It requires time.
if ($this->running_javascript()) {
- $this->getSession()->wait(5000, false);
+ $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
+ $this->i_wait_until_section_is_available($sectionnumber);
}
}
$hidelink = $this->hide_section_icon_exists($sectionnumber);
$hidelink->click();
- // It requires time.
if ($this->running_javascript()) {
- $this->getSession()->wait(5000, false);
+ $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
+ $this->i_wait_until_section_is_available($sectionnumber);
}
}
$sectionxpath = $this->section_exists($sectionnumber);
+ // Preventive in case there is any action in progress.
+ // Adding it here because we are interacting (click) with
+ // the elements, not necessary when we just find().
+ $this->i_wait_until_section_is_available($sectionnumber);
+
// Section should be hidden.
$exception = new ExpectationException('The section is not hidden', $this->getSession());
$this->find('xpath', $sectionxpath . "[contains(concat(' ', normalize-space(@class), ' '), ' hidden ')]", $exception);
// Non-JS browsers can not click on img elements.
if ($this->running_javascript()) {
- // Expanding the actions menu.
- $actionsmenu = $this->find('css', "a[role='menuitem']", false, $activity);
- $actionsmenu->click();
+ // Expanding the actions menu if it is not shown.
+ $classes = array_flip(explode(' ', $activity->getAttribute('class')));
+ if (empty($classes['action-menu-shown'])) {
+ $actionsmenu = $this->find('css', "a[role='menuitem']", false, $activity);
+ $actionsmenu->click();
+ }
// To check that the visibility is not clickable we check the funcionality rather than the applied style.
$visibilityiconnode = $this->find('css', 'a.editing_show img', false, $activity);
// We ensure that we still see the show icon.
$visibilityiconnode = $this->find('css', 'a.editing_show img', $visibilityexception, $activity);
+
+ // It is there only when running JS scenarios.
+ if ($this->running_javascript()) {
+
+ // Collapse the actions menu if it is displayed.
+ $classes = array_flip(explode(' ', $activity->getAttribute('class')));
+ if (!empty($classes['action-menu-shown'])) {
+ $actionsmenu = $this->find('css', "a[role='menuitem']", false, $activity);
+ $actionsmenu->click();
+ }
+ }
}
}
$activity = $this->escape($activityname);
return array(
new Given('I click on "' . get_string('edittitle') . '" "link" in the "' . $activity .'" activity'),
- new Given('I fill in "title" with "' . $this->escape($newactivityname) . chr(10) . '"'),
- new Given('I wait "2" seconds')
+ new Given('I fill in "title" with "' . $this->escape($newactivityname) . chr(10) . '"')
);
}
return new Given('I click on "a[role=\'menuitem\']" "css_element" in the "' . $this->escape($activityname) . '" activity');
}
+ /**
+ * Closes an activity actions menu if it is not already closed.
+ *
+ * @Given /^I close "(?P<activity_name_string>(?:[^"]|\\")*)" actions menu$/
+ * @throws DriverException The step is not available when Javascript is disabled
+ * @param string $activityname
+ * @return Given
+ */
+ public function i_close_actions_menu($activityname) {
+
+ if (!$this->running_javascript()) {
+ throw new DriverException('Activities actions menu not available when Javascript is disabled');
+ }
+
+ // If it is already closed we do nothing.
+ $activitynode = $this->get_activity_node($activityname);
+ $classes = array_flip(explode(' ', $activitynode->getAttribute('class')));
+ if (empty($classes['action-menu-shown'])) {
+ return;
+ }
+
+ return new Given('I click on "a[role=\'menuitem\']" "css_element" in the "' . $this->escape($activityname) . '" activity');
+ }
+
/**
* Indents to the right the activity or resource specified by it's name. Editing mode should be on.
*
}
$steps[] = new Given('I click on "' . get_string('moveright') . '" "link" in the "' . $activity . '" activity');
- if ($this->running_javascript()) {
- $steps[] = new Given('I wait "2" seconds');
- }
-
return $steps;
}
}
$steps[] = new Given('I click on "' . get_string('moveleft') . '" "link" in the "' . $activity . '" activity');
- if ($this->running_javascript()) {
- $steps[] = new Given('I wait "2" seconds');
- }
-
return $steps;
}
$this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
- $this->getSession()->wait(2 * 1000, false);
-
} else {
// With JS disabled.
$steps[] = new Given('I open "' . $activity . '" actions menu');
}
$steps[] = new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activity . '" activity');
- if ($this->running_javascript()) {
- // Temporary wait until MDL-41030 lands.
- $steps[] = new Given('I wait "4" seconds');
- } else {
+ if (!$this->running_javascript()) {
$steps[] = new Given('I press "' . get_string('continue') .'"');
$steps[] = new Given('I press "' . get_string('duplicatecontcourse') .'"');
}
$steps = array();
$activity = $this->escape($activityname);
+ $activityliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($activityname);
if ($this->running_javascript()) {
$steps[] = new Given('I duplicate "' . $activity . '" activity');
+ // We wait until the AJAX request finishes and the section is visible again.
+ $hiddenlightboxxpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][contains(., $activityliteral)]" .
+ "/ancestor::li[contains(concat(' ', normalize-space(@class), ' '), ' section ')]" .
+ "/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]";
+ $steps[] = new Given('I wait until the page is ready');
+ $steps[] = new Given('I wait until "' . $this->escape($hiddenlightboxxpath) .'" "xpath_element" exists');
+
+ // Close the original activity actions menu.
+ $steps[] = new Given('I close "' . $activity . '" actions menu');
+
// Determine the future new activity xpath from the former one.
- $activityliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($activityname);
$duplicatedxpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][contains(., $activityliteral)]" .
"/following-sibling::li";
$duplicatedactionsmenuxpath = $duplicatedxpath . "/descendant::a[@role='menuitem']";
return $steps;
}
+ /**
+ * Waits until the section is available to interact with it. Useful when the section is performing an action and the section is overlayed with a loading layout.
+ *
+ * Using the protected method as this method will be usually
+ * called by other methods which are not returning a set of
+ * steps and performs the actions directly, so it would not
+ * be executed if it returns another step.
+ *
+ * Hopefully we would not require test writers to use this step
+ * and we will manage it from other step definitions.
+ *
+ * @Given /^I wait until section "(?P<section_number>\d+)" is available$/
+ * @param int $sectionnumber
+ * @return void
+ */
+ public function i_wait_until_section_is_available($sectionnumber) {
+
+ // Looks for a hidden lightbox or a non-existent lightbox in that section.
+ $sectionxpath = $this->section_exists($sectionnumber);
+ $hiddenlightboxxpath = $sectionxpath . "/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]" .
+ " | " .
+ $sectionxpath . "[count(child::div[contains(@class, 'lightbox')]) = 0]";
+
+ $this->ensure_element_exists($hiddenlightboxxpath, 'xpath_element');
+ }
+
/**
* Clicks on the specified element of the activity. You should be in the course page with editing mode turned on.
*
return $this->find('xpath', $xpath);
}
+ /**
+ * Gets the activity instance name from the activity node.
+ *
+ * @throws ElementNotFoundException
+ * @param NodeElement $activitynode
+ * @return string
+ */
+ protected function get_activity_name($activitynode) {
+ $instancenamenode = $this->find('xpath', "//span[contains(concat(' ', normalize-space(@class), ' '), ' instancename ')]", false, $activitynode);
+ return $instancenamenode->getText();
+ }
+
/**
* Returns whether the user can edit the course contents or not.
*
-@core @core_course @test
+@core @core_course
Feature: Course category management interface performs as expected
In order to test JS enhanced display of categories and subcategories.
As a moodle admin
-@core @core_course
+@core @core_course @_alerts
Feature: Course activity controls works as expected
In order to manage my course's activities
As a teacher
And I click on "Edit settings" "link" in the "Test forum name 1" activity
And I should see "Updating Forum"
And I should see "Display description on course page"
- And I press "Save and return to course"
+ And I fill the moodle form with:
+ | Forum name | Just to check that I can edit the name |
+ | Description | Just to check that I can edit the description |
+ | Display description on course page | 1 |
+ And I click on "Cancel" "button"
And "#section-2" "css_element" <should_see_other_sections> exists
And I open "Test forum name 1" actions menu
And I click on "Hide" "link" in the "Test forum name 1" activity
And "#section-2" "css_element" <should_see_other_sections> exists
+ And I close "Test forum name 1" actions menu
And I duplicate "Test forum name 2" activity editing the new copy with:
| Forum name | Edited test forum name 2 |
And "#section-2" "css_element" <should_see_other_sections> exists
--- /dev/null
+{
+ "name": "moodle-course-toolboxes",
+ "builds": {
+ "moodle-course-toolboxes": {
+ "jsfiles": [
+ "toolbox.js",
+ "resource.js",
+ "section.js"
+ ]
+ }
+ }
+}
--- /dev/null
+/**
+ * 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,
+
+ /**
+ * An Array of events added when editing a title.
+ * These should all be detached when editing is complete.
+ *
+ * @property edittitleevents
+ * @protected
+ * @type Array
+ * @protected
+ */
+ edittitleevents: [],
+
+ /**
+ * 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.
+ * @returns {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 'edittitle':
+ // The user wishes to edit the title of the event.
+ this.edit_title(ev, node, activity, action);
+ break;
+ 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.
+ callback = 'change_groupmode';
+ this.change_groupmode(ev, node, activity, action);
+ break;
+ case 'move':
+ case 'update':
+ case 'duplicate':
+ 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);
+ }
+
+ // Confirm element removal
+ if (!confirm(confirmstring)) {
+ return this;
+ }
+
+ // 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();
+ }
+ 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();
+
+ // Return early if the current section is hidden
+ var section = activity.ancestor(M.course.format.get_section_selector(Y));
+ if (section && section.hasClass(CSS.SECTIONHIDDENCLASS)) {
+ return this;
+ }
+
+ // 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'.
+ * @returns {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)) {
+ // Change the UI.
+ dimarea.toggleClass(toggleclass);
+ // We need to toggle dimming on the description too.
+ activity.all(SELECTOR.CONTENTAFTERLINK).toggleClass(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
+ 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;
+ },
+
+ /**
+ * Edit the title for the resource
+ *
+ * @method edit_title
+ * @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
+ */
+ edit_title: function(ev, button, activity) {
+ // Get the element we're working on
+ var activityid = Y.Moodle.core_course.util.cm.getId(activity),
+ instancename = activity.one(SELECTOR.INSTANCENAME),
+ instance = activity.one(SELECTOR.ACTIVITYINSTANCE),
+ currenttitle = instancename.get('firstChild'),
+ oldtitle = currenttitle.get('data'),
+ titletext = oldtitle,
+ thisevent,
+ anchor = instancename.ancestor('a'),// Grab the anchor so that we can swap it with the edit form.
+ data = {
+ 'class': 'resource',
+ 'field': 'gettitle',
+ 'id': activityid
+ };
+
+ // Prevent the default actions.
+ ev.preventDefault();
+
+ this.send_request(data, null, function(response) {
+ if (M.core.actionmenu && M.core.actionmenu.instance) {
+ M.core.actionmenu.instance.hideMenu();
+ }
+
+ // Try to retrieve the existing string from the server
+ if (response.instancename) {
+ titletext = response.instancename;
+ }
+
+ // Create the editor and submit button
+ var editform = Y.Node.create('<form action="#" />');
+ var editinstructions = Y.Node.create('<span class="'+CSS.EDITINSTRUCTIONS+'" id="id_editinstructions" />')
+ .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
+ var editor = Y.Node.create('<input name="title" type="text" class="'+CSS.TITLEEDITOR+'" />').setAttrs({
+ 'value': titletext,
+ 'autocomplete': 'off',
+ 'aria-describedby': 'id_editinstructions',
+ 'maxLength': '255'
+ });
+
+ // Clear the existing content and put the editor in
+ editform.appendChild(activity.one(SELECTOR.ACTIVITYICON).cloneNode());
+ editform.appendChild(editor);
+ editform.setData('anchor', anchor);
+ instance.insert(editinstructions, 'before');
+ anchor.replace(editform);
+
+ // Force the editing instruction to match the mod-indent position.
+ var padside = 'left';
+ if (right_to_left()) {
+ padside = 'right';
+ }
+
+ // We hide various components whilst editing:
+ activity.addClass(CSS.EDITINGTITLE);
+
+ // Focus and select the editor text
+ editor.focus().select();
+
+ // Cancel the edit if we lose focus or the escape key is pressed.
+ thisevent = editor.on('blur', this.edit_title_cancel, this, activity, false);
+ this.edittitleevents.push(thisevent);
+ thisevent = editor.on('key', this.edit_title_cancel, 'esc', this, activity, true);
+ this.edittitleevents.push(thisevent);
+
+ // Handle form submission.
+ thisevent = editform.on('submit', this.edit_title_submit, this, activity, oldtitle);
+ this.edittitleevents.push(thisevent);
+ });
+ return this;
+ },
+
+ /**
+ * Handles the submit event when editing the activity or resources title.
+ *
+ * @method edit_title_submit
+ * @protected
+ * @param {EventFacade} ev The event that triggered this.
+ * @param {Node} activity The activity whose title we are altering.
+ * @param {String} originaltitle The original title the activity or resource had.
+ */
+ edit_title_submit: function(ev, activity, originaltitle) {
+ // We don't actually want to submit anything
+ ev.preventDefault();
+
+ var newtitle = Y.Lang.trim(activity.one(SELECTOR.ACTIVITYFORM + ' ' + SELECTOR.ACTIVITYTITLE).get('value'));
+ this.edit_title_clear(activity);
+ var spinner = this.add_spinner(activity);
+ if (newtitle !== null && newtitle !== "" && newtitle !== originaltitle) {
+ var data = {
+ 'class': 'resource',
+ 'field': 'updatetitle',
+ 'title': newtitle,
+ 'id': Y.Moodle.core_course.util.cm.getId(activity)
+ };
+ this.send_request(data, spinner, function(response) {
+ if (response.instancename) {
+ activity.one(SELECTOR.INSTANCENAME).setContent(response.instancename);
+ }
+ });
+ }
+ },
+
+ /**
+ * Handles the cancel event when editing the activity or resources title.
+ *
+ * @method edit_title_cancel
+ * @protected
+ * @param {EventFacade} ev The event that triggered this.
+ * @param {Node} activity The activity whose title we are altering.
+ * @param {Boolean} preventdefault If true we should prevent the default action from occuring.
+ */
+ edit_title_cancel: function(ev, activity, preventdefault) {
+ if (preventdefault) {
+ ev.preventDefault();
+ }
+ this.edit_title_clear(activity);
+ },
+
+ /**
+ * Handles clearing the editing UI and returning things to the original state they were in.
+ *
+ * @method edit_title_clear
+ * @protected
+ * @param {Node} activity The activity whose title we were altering.
+ */
+ edit_title_clear: function(activity) {
+ // Detach all listen events to prevent duplicate triggers
+ new Y.EventHandle(this.edittitleevents).detach();
+
+ var editform = activity.one(SELECTOR.ACTIVITYFORM),
+ instructions = activity.one('#id_editinstructions');
+ if (editform) {
+ editform.replace(editform.getData('anchor'));
+ }
+ if (instructions) {
+ instructions.remove();
+ }
+
+ // Remove the editing class again to revert the display.
+ activity.removeClass(CSS.EDITINGTITLE);
+
+ // Refocus the link which was clicked originally so the user can continue using keyboard nav.
+ Y.later(100, this, function() {
+ activity.one(SELECTOR.EDITTITLE).focus();
+ });
+ },
+
+ /**
+ * Set the visibility of the current resource (identified by the element) to match the hidden parameter (this is not
+ * a toggle).
+ *
+ * Only changes the visibility in the browser (no ajax update).
+ *
+ * @method set_visibility_resource_ui
+ * @param {Object} args An object with 'element' being the A node containing the resource and 'visible' being the
+ * state that the visibility should be set to.
+ */
+ set_visibility_resource_ui: function(args) {
+ var element = args.element,
+ shouldbevisible = args.visible,
+ buttonnode = element.one(SELECTOR.SHOW),
+ visible = (buttonnode === null),
+ action = 'show';
+ if (visible) {
+ buttonnode = element.one(SELECTOR.HIDE);
+ action = 'hide';
+ }
+ if (visible !== shouldbevisible) {
+ 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;
+};
--- /dev/null
+/**
+ * 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'),
+
+ // 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);
+
+ // Change the highlight 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');
+
+ // 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');
+ Y.one(SELECTOR.PAGECONTENT)
+ .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT)
+ .set('title', old_string);
+ Y.one(SELECTOR.PAGECONTENT)
+ .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.
+ Y.one(SELECTOR.PAGECONTENT).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'));
+ }
+
+ // 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);
+};
--- /dev/null
+/**
+ * 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',
+ EDITINGTITLE: 'editor_displayed',
+ HIDE : 'hide',
+ MODINDENTCOUNT : 'mod-indent-',
+ MODINDENTHUGE : 'mod-indent-huge',
+ MODULEIDPREFIX : 'module-',
+ SECTIONHIDDENCLASS : 'hidden',
+ SECTIONIDPREFIX : 'section-',
+ SHOW : 'editing_show',
+ TITLEEDITOR : 'titleeditor'
+ },
+ // The CSS selectors we use.
+ SELECTOR = {
+ ACTIONAREA: '.actions',
+ ACTIONLINKTEXT : '.actionlinktext',
+ ACTIVITYACTION : 'a.cm-edit-action[data-action], a.editing_title',
+ ACTIVITYFORM : '.' + CSS.ACTIVITYINSTANCE + ' form',
+ ACTIVITYICON : 'img.activityicon',
+ ACTIVITYINSTANCE : '.' + CSS.ACTIVITYINSTANCE,
+ ACTIVITYLINK: '.' + CSS.ACTIVITYINSTANCE + ' > a',
+ ACTIVITYLI : 'li.activity',
+ ACTIVITYTITLE : 'input[name=title]',
+ COMMANDSPAN : '.commands',
+ CONTENTAFTERLINK : 'div.contentafterlink',
+ CONTENTWITHOUTLINK : 'div.contentwithoutlink',
+ EDITTITLE: 'a.editing_title',
+ HIDE : 'a.editing_hide',
+ HIGHLIGHT : 'a.editing_highlight',
+ INSTANCENAME : 'span.instancename',
+ MODINDENTDIV : '.mod-indent',
+ MODINDENTOUTER : '.mod-indent-outer',
+ PAGECONTENT : 'div#page-content',
+ 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) {}
+
+ // 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': {}
+ }
+ }
+}
+);
+
--- /dev/null
+{
+ "moodle-course-toolboxes": {
+ "requires": [
+ "node",
+ "base",
+ "event-key",
+ "node",
+ "io",
+ "moodle-course-coursebase",
+ "moodle-course-util"
+ ]
+ }
+}
+++ /dev/null
-YUI.add('moodle-course-toolboxes', function(Y) {
-
- // The following properties contain common strings.
- // We separate them out here because when this JS is minified the content is less as
- // Variables get compacted to single/double characters and the full length of the string
- // exists only once.
-
- // The CSS classes we use.
- var CSS = {
- ACTIVITYINSTANCE : 'activityinstance',
- AVAILABILITYINFODIV : 'div.availabilityinfo',
- CONTENTWITHOUTLINK : 'contentwithoutlink',
- CONDITIONALHIDDEN : 'conditionalhidden',
- DIMCLASS : 'dimmed',
- DIMMEDTEXT : 'dimmed_text',
- EDITINSTRUCTIONS : 'editinstructions',
- EDITINGTITLE: 'editor_displayed',
- HIDE : 'hide',
- MODINDENTCOUNT : 'mod-indent-',
- MODINDENTHUGE : 'mod-indent-huge',
- MODULEIDPREFIX : 'module-',
- SECTIONHIDDENCLASS : 'hidden',
- SECTIONIDPREFIX : 'section-',
- SHOW : 'editing_show',
- TITLEEDITOR : 'titleeditor'
- },
- // The CSS selectors we use.
- SELECTOR = {
- ACTIONAREA: '.actions',
- ACTIONLINKTEXT : '.actionlinktext',
- ACTIVITYACTION : 'a.cm-edit-action[data-action], a.editing_title',
- ACTIVITYFORM : '.' + CSS.ACTIVITYINSTANCE + ' form',
- ACTIVITYICON : 'img.activityicon',
- ACTIVITYINSTANCE : '.' + CSS.ACTIVITYINSTANCE,
- ACTIVITYLINK: '.' + CSS.ACTIVITYINSTANCE + ' > a',
- ACTIVITYLI : 'li.activity',
- ACTIVITYTITLE : 'input[name=title]',
- COMMANDSPAN : '.commands',
- CONTENTAFTERLINK : 'div.contentafterlink',
- CONTENTWITHOUTLINK : 'div.contentwithoutlink',
- EDITTITLE: 'a.editing_title',
- HIDE : 'a.editing_hide',
- HIGHLIGHT : 'a.editing_highlight',
- INSTANCENAME : 'span.instancename',
- MODINDENTDIV : '.mod-indent',
- MODINDENTOUTER : '.mod-indent-outer',
- PAGECONTENT : 'div#page-content',
- SECTIONLI : 'li.section',
- SHOW : 'a.'+CSS.SHOW,
- SHOWHIDE : 'a.editing_showhide'
- },
- INDENTLIMITS = {
- MIN: 0,
- MAX: 16
- },
- BODY = Y.one(document.body);
-
- /**
- * The toolbox classes
- *
- * TOOLBOX is a generic class which should never be directly instantiated
- * RESOURCETOOLBOX is a class extending TOOLBOX containing code specific to resources
- * SECTIONTOOLBOX is a class extending TOOLBOX containing code specific to sections
- */
- 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 data The data to submit
- * @param statusspinner (optional) A statusspinner which may contain a section loader
- * @param {Function} success_callback The callback to use on success
- * @param optionalconfig (optional) Any additional configuration to submit
- * @return response responseText field from response - please use success_callback instead
- */
- 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) {}
-
- // 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 responsetext;
- }
- },
- {
- NAME : 'course-toolbox',
- ATTRS : {
- // The ID of the current course
- courseid : {
- 'value' : 0
- },
- ajaxurl : {
- 'value' : 0
- },
- config : {
- 'value' : 0
- }
- }
- }
- );
-
- /**
- * Resource and activity toolbox class.
- *
- * This class is responsible for managing AJAX interactions with activities and resources
- * when viewing a course in editing mode.
- *
- * @namespace M.course.toolbox
- * @class ResourceToolbox
- * @constructor
- */
- var RESOURCETOOLBOX = function() {
- RESOURCETOOLBOX.superclass.constructor.apply(this, arguments);
- }
-
- Y.extend(RESOURCETOOLBOX, TOOLBOX, {
- /**
- * No groups are being used.
- * @static
- * @const GROUPS_NONE
- * @type Number
- */
- GROUPS_NONE : 0,
- /**
- * Separate groups are being used.
- * @static
- * @const GROUPS_SEPARATE
- * @type Number
- */
- GROUPS_SEPARATE : 1,
- /**
- * Visible groups are being used.
- * @static
- * @const GROUPS_VISIBLE
- * @type Number
- */
- GROUPS_VISIBLE : 2,
-
- /**
- * Events that were added when editing a title.
- * These should all be detached when editing is complete.
- * @property edittitleevents
- * @type {Event[]}
- * @protected
- */
- edittitleevents : [],
-
- /**
- * 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
- */
- 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.
- * @returns {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;
- }
- Y.log(ev.type);
-
- // Switch based upon the action and do the desired thing.
- switch (action) {
- case 'edittitle' :
- // The user wishes to edit the title of the event.
- this.edit_title(ev, node, activity, action);
- break;
- 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.
- callback = 'change_groupmode';
- this.change_groupmode(ev, node, activity, action);
- break;
- case 'move' :
- case 'update' :
- case 'duplicate' :
- case 'assignroles' :
- default:
- // Nothing to do here!
- break;
- }
- },
- add_spinner: function(activity) {
- var actionarea = activity.one(SELECTOR.ACTIONAREA);
- return M.util.add_spinner(Y, actionarea);
- },
-
- /**
- * Change the indent of the activity or resource.
- *
- * @protected
- * @method change_indent
- * @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.
- * @return Boolean
- */
- 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)
- var confirmstring = '';
- var 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)
- }
-
- // Confirm element removal
- if (!confirm(confirmstring)) {
- return false;
- }
-
- // 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();
- }
- },
-
- /**
- * Duplicates the activity
- *
- * @protected
- * @method duplicate
- * @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.
- * @return Boolean
- */
- 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);
- }
- });
- },
-
- /**
- * Changes the visibility of this activity or resource.
- *
- * @protected
- * @method change_visibility
- * @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.
- * @return Boolean
- */
- change_visibility : function(ev, button, activity, action) {
- // Prevent the default button action
- ev.preventDefault();
-
- // Return early if the current section is hidden
- var section = activity.ancestor(M.course.format.get_section_selector(Y));
- if (section && section.hasClass(CSS.SECTIONHIDDENCLASS)) {
- return;
- }
-
- // 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 false; // Need to return false to stop the delegate for the new state firing
- },
-
- /**
- * Handles the UI aspect of dimming the activity or resource.
- *
- * @protected
- * @method handle_resource_dim
- * @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'.
- * @returns {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)) {
- // Change the UI.
- dimarea.toggleClass(toggleclass);
- // We need to toggle dimming on the description too.
- activity.all(SELECTOR.CONTENTAFTERLINK).toggleClass(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.
- *
- * @protected
- * @method change_groupmode
- * @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.
- * @return Boolean
- */
- change_groupmode : function(ev, button, activity, action) {
- // 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(newtitle, 'moodle'),
- newtitlestr = M.util.get_string('clicktochangeinbrackets', 'moodle', newtitlestr);
-
- // Change the UI
- 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 false; // Need to return false to stop the delegate for the new state firing
- },
-
- /**
- * Edit the title for the resource
- *
- * @protected
- * @method edit_title
- * @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.
- * @return Boolean
- */
- edit_title : function(ev, button, activity) {
- // Get the element we're working on
- var activityid = Y.Moodle.core_course.util.cm.getId(activity),
- instancename = activity.one(SELECTOR.INSTANCENAME),
- instance = activity.one(SELECTOR.ACTIVITYINSTANCE),
- currenttitle = instancename.get('firstChild'),
- oldtitle = currenttitle.get('data'),
- titletext = oldtitle,
- thisevent,
- anchor = instancename.ancestor('a'),// Grab the anchor so that we can swap it with the edit form.
- data = {
- 'class' : 'resource',
- 'field' : 'gettitle',
- 'id' : activityid
- };
-
- // Prevent the default actions.
- ev.preventDefault();
-
- this.send_request(data, null, function(response) {
- if (M.core.actionmenu && M.core.actionmenu.instance) {
- M.core.actionmenu.instance.hideMenu();
- }
-
- // Try to retrieve the existing string from the server
- if (response.instancename) {
- titletext = response.instancename;
- }
-
- // Create the editor and submit button
- var editform = Y.Node.create('<form action="#" />');
- var editinstructions = Y.Node.create('<span class="'+CSS.EDITINSTRUCTIONS+'" id="id_editinstructions" />')
- .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
- var editor = Y.Node.create('<input name="title" type="text" class="'+CSS.TITLEEDITOR+'" />').setAttrs({
- 'value' : titletext,
- 'autocomplete' : 'off',
- 'aria-describedby' : 'id_editinstructions',
- 'maxLength' : '255'
- });
-
- // Clear the existing content and put the editor in
- editform.appendChild(activity.one(SELECTOR.ACTIVITYICON).cloneNode());
- editform.appendChild(editor);
- editform.setData('anchor', anchor);
- instance.insert(editinstructions, 'before');
- anchor.replace(editform);
-
- // Force the editing instruction to match the mod-indent position.
- var padside = 'left';
- if (right_to_left()) {
- padside = 'right';
- }
-
- // We hide various components whilst editing:
- activity.addClass(CSS.EDITINGTITLE);
-
- // Focus and select the editor text
- editor.focus().select();
-
- // Cancel the edit if we lose focus or the escape key is pressed.
- thisevent = editor.on('blur', this.edit_title_cancel, this, activity, false);
- this.edittitleevents.push(thisevent);
- thisevent = editor.on('key', this.edit_title_cancel, 'esc', this, activity, true);
- this.edittitleevents.push(thisevent);
-
- // Handle form submission.
- thisevent = editform.on('submit', this.edit_title_submit, this, activity, oldtitle);
- this.edittitleevents.push(thisevent);
- });
- },
-
- /**
- * Handles the submit event when editing the activity or resources title.
- *
- * @protected
- * @method edit_title_submit
- * @param {EventFacade} ev The event that triggered this.
- * @param {Node} activity The activity whose title we are altering.
- * @param {String} originaltitle The original title the activity or resource had.
- */
- edit_title_submit : function(ev, activity, originaltitle) {
- // We don't actually want to submit anything
- ev.preventDefault();
-
- var newtitle = Y.Lang.trim(activity.one(SELECTOR.ACTIVITYFORM + ' ' + SELECTOR.ACTIVITYTITLE).get('value'));
- this.edit_title_clear(activity);
- var spinner = this.add_spinner(activity);
- if (newtitle != null && newtitle != "" && newtitle != originaltitle) {
- var data = {
- 'class' : 'resource',
- 'field' : 'updatetitle',
- 'title' : newtitle,
- 'id' : Y.Moodle.core_course.util.cm.getId(activity)
- };
- this.send_request(data, spinner, function(response) {
- if (response.instancename) {
- activity.one(SELECTOR.INSTANCENAME).setContent(response.instancename);
- }
- });
- }
- },
-
- /**
- * Handles the cancel event when editing the activity or resources title.
- *
- * @protected
- * @method edit_title_cancel
- * @param {EventFacade} ev The event that triggered this.
- * @param {Node} activity The activity whose title we are altering.
- * @param {Boolean} preventdefault If true we should prevent the default action from occuring.
- */
- edit_title_cancel : function(ev, activity, preventdefault) {
- if (preventdefault) {
- ev.preventDefault();
- }
- this.edit_title_clear(activity);
- },
-
- /**
- * Handles clearing the editing UI and returning things to the original state they were in.
- *
- * @protected
- * @method edit_title_clear
- * @param {Node} activity The activity whose title we were altering.
- */
- edit_title_clear : function(activity) {
- // Detach all listen events to prevent duplicate triggers
- var thisevent;
- while (thisevent = this.edittitleevents.shift()) {
- thisevent.detach();
- }
- var editform = activity.one(SELECTOR.ACTIVITYFORM),
- instructions = activity.one('#id_editinstructions');
- if (editform) {
- editform.replace(editform.getData('anchor'));
- }
- if (instructions) {
- instructions.remove();
- }
-
- // Remove the editing class again to revert the display.
- activity.removeClass(CSS.EDITINGTITLE);
-
- // Refocus the link which was clicked originally so the user can continue using keyboard nav.
- Y.later(100, this, function() {
- activity.one(SELECTOR.EDITTITLE).focus();
- });
- },
-
- /**
- * Set the visibility of the current resource (identified by the element)
- * to match the hidden parameter (this is not a toggle).
- * Only changes the visibility in the browser (no ajax update).
- *
- * @public This method is used by other modules.
- * @method set_visibility_resource_ui
- * @param args An object with 'element' being the A node containing the resource
- * and 'visible' being the state that the visibility should be set to.
- */
- set_visibility_resource_ui: function(args) {
- var element = args.element,
- shouldbevisible = args.visible,
- buttonnode = element.one(SELECTOR.SHOW),
- visible = (buttonnode === null),
- action = 'show';
- if (visible) {
- buttonnode = element.one(SELECTOR.HIDE);
- action = 'hide';
- }
- if (visible != shouldbevisible) {
- this.handle_resource_dim(buttonnode, element, action);
- }
- }
- }, {
- NAME : 'course-resource-toolbox',
- ATTRS : {
- courseid : {
- 'value' : 0
- },
- format : {
- 'value' : 'topics'
- }
- }
- });
-
- var SECTIONTOOLBOX = function() {
- SECTIONTOOLBOX.superclass.constructor.apply(this, arguments);
- }
-
- Y.extend(SECTIONTOOLBOX, TOOLBOX, {
- /**
- * Initialize the toolboxes module
- *
- * Updates all span.commands with relevant handlers and other required changes
- */
- initializer : function(config) {
- this.setup_for_section();
- 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);
- },
- /**
- * Update any section areas within the scope of the specified
- * selector with AJAX equivelants
- *
- * @param baseselector The selector to limit scope to
- * @return void
- */
- setup_for_section : function(baseselector) {
- // Left here for potential future use - not currently needed due to YUI delegation in initializer()
- /*if (!baseselector) {
- var baseselector = SELECTOR.PAGECONTENT;
- }
-
- Y.all(baseselector).each(this._setup_for_section, this);*/
- },
- _setup_for_section : function(toolboxtarget) {
- // Left here for potential future use - not currently needed due to YUI delegation in initializer()
- },
- 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));
- var button = e.target.ancestor('a', true);
- var hideicon = button.one('img');
-
- // The value to submit
- var value;
- // The text for strings and images. Also determines the icon to display.
- var 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);
-
- // Change the highlight 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_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');
-
- // 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');
- Y.one(SELECTOR.PAGECONTENT)
- .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT)
- .set('title', old_string);
- Y.one(SELECTOR.PAGECONTENT)
- .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
- var allsections = Y.one(SELECTOR.PAGECONTENT).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'));
- }
-
- // 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 : {
- courseid : {
- 'value' : 0
- },
- format : {
- 'value' : 'topics'
- }
- }
- });
-
- M.course = M.course || {};
- M.course.resource_toolbox = null;
- M.course.init_resource_toolbox = function(config) {
- M.course.resource_toolbox = new RESOURCETOOLBOX(config);
- return M.course.resource_toolbox;
- };
-
- M.course.init_section_toolbox = function(config) {
- return new SECTIONTOOLBOX(config);
- };
-
-},
-'@VERSION@', {
- requires : ['base', 'event-key', 'node', 'io', 'moodle-course-coursebase', 'moodle-course-util']
-}
-);
$string['allowguests'] = 'This course allows guest users to enter';
$string['guest:config'] = 'Configure guest access instances';
+$string['guestaccess_withpassword'] = 'Guest access requires password';
+$string['guestaccess_withoutpassword'] = 'Guest access';
$string['password'] = 'Password';
$string['password_help'] = 'A password allows guest access to the course to be restricted to only those who know the password. Guests will be required to supply the password each time they access the course.';
$string['passwordinvalid'] = 'Incorrect access password, please try again';
public function get_info_icons(array $instances) {
foreach ($instances as $instance) {
if ($instance->password !== '') {
- return array(new pix_icon('withpassword', get_string('pluginname', 'enrol_guest'), 'enrol_guest'));
+ return array(new pix_icon('withpassword', get_string('guestaccess_withpassword', 'enrol_guest'), 'enrol_guest'));
} else {
- return array(new pix_icon('withoutpassword', get_string('pluginname', 'enrol_guest'), 'enrol_guest'));
+ return array(new pix_icon('withoutpassword', get_string('guestaccess_withoutpassword', 'enrol_guest'), 'enrol_guest'));
}
}
}
+++ /dev/null
-/**
- * This module provides backwards compatability and should be removed
- * entirely in Moodle 2.5
- */
-YUI.add('moodle-enrol-notification', function(Y) {
- console.log("You are using a deprecated name. Please update your YUI module to use moodle-core-notification instead of moodle-enrol-notification");
-}, '@VERSION@', {requires:['base','node','overlay','event-key', 'moodle-core-notification']});
this.courseId = courseId;
this.connectCallback = {
- success: function(o) {
+ success: function(t, o) {
if (o.responseText !== undefined) {
var selectEl = document.getElementById("members");
removeLoaderImgs("membersloader", "memberslabel");
},
- failure: function(o) {
+ failure: function() {
removeLoaderImgs("membersloader", "memberslabel");
}
if(singleSelection) {
var sUrl = this.wwwRoot+"/group/index.php?id="+this.courseId+"&group="+groupId+"&act_ajax_getmembersingroup";
- var callback = this.connectCallback;
- YUI().use('yui2-connection', function (Y) {
- Y.YUI2.util.Connect.asyncRequest("GET", sUrl, callback, null);
+ var self = this;
+ YUI().use('io', function (Y) {
+ Y.io(sUrl, {
+ method: 'GET',
+ context: this,
+ on: self.connectCallback
+ });
});
}
};
addselect = document.getElementById('addselect');
addselect.onchange = updateUserSummary;
-}
\ No newline at end of file
+}
$this->find_button(get_string('adduserstogroup', 'group'))->click();
// Wait for add/remove members page to be loaded.
- $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")');
+ $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
// Getting the option and selecting it.
$select = $this->find_field('addselect');
$this->find_button(get_string('add'))->click();
// Wait for the page to load.
- $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")');
+ $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
// Returning to the main groups page.
$this->find_button(get_string('backtogroups', 'group'))->click();
And I add "student2" user to "Group 2" group
And I add "student3" user to "Group 2" group
Then I select "Group 1 (2)" from "groups"
- And I wait "5" seconds
And the "members" select box should contain "Student 0"
And the "members" select box should contain "Student 1"
And the "members" select box should not contain "Student 2"
And I select "Group 2 (2)" from "groups"
- And I wait "5" seconds
And the "members" select box should contain "Student 2"
And the "members" select box should contain "Student 3"
And the "members" select box should not contain "Student 0"
-@core @core_group
+@core @core_group @_only_local
Feature: Importing of groups and groupings
In order to import groups and grouping
As a teacher
And I expand "Users" node
And I follow "Groups"
And I press "Import groups"
- When I upload "group/tests/fixtures/groups_import.csv" file to "Import" filepicker
+ When I upload "group/tests/fixtures/groups_import.csv" file to "Import" filemanager
And I press "Import groups"
And I press "Continue"
Then I should see "group-id-1"
And I expand "Users" node
And I follow "Groups"
And I press "Import groups"
- When I upload "group/tests/fixtures/groups_import.csv" file to "Import" filepicker
+ When I upload "group/tests/fixtures/groups_import.csv" file to "Import" filemanager
And I press "Import groups"
Then I should see "Group group-id-1 added successfully"
And I should see "Group group-id-2 added successfully"
And I expand "Users" node
And I follow "Groups"
And I press "Import groups"
- When I upload "group/tests/fixtures/groups_import.csv" file to "Import" filepicker
+ When I upload "group/tests/fixtures/groups_import.csv" file to "Import" filemanager
And I press "Import groups"
And I press "Continue"
Then I select "group-id-1" from "groups"
$string['cachetext'] = 'Text cache lifetime';
$string['calendarexportsalt'] = 'Calendar export salt';
$string['calendarsettings'] = 'Calendar';
+$string['calendartype'] = 'Calendar type';
+$string['calendartype_desc'] = 'Choose a default calendar type for the whole site. This setting can be overridden in the course settings or by users in their personal profile.';
$string['calendar_weekend'] = 'Weekend days';
$string['cannotdeletemodfilter'] = 'You cannot uninstall the \'{$a->filter}\' because it is part of the \'{$a->module}\' module.';
$string['cannotuninstall'] = '{$a} can not be uninstalled.';
$string['eventcourserestored'] = 'Course restored';
$string['eventcourseupdated'] = 'Course updated';
$string['eventcoursesectionupdated'] = ' Course section updated';
+$string['eventcoursemoduleinstancelistviewed'] = 'Course module instance list viewed';
$string['eventusercreated'] = 'User created';
$string['eventuserdeleted'] = 'User deleted';
$string['eventuserlistviewed'] = 'User list viewed';
$string['ok'] = 'OK';
$string['oldpassword'] = 'Current password';
$string['olduserdirectory'] = 'This is the OLD users directory, and is no longer needed. You may safely delete it. The files it contains have been copied to the NEW user directory.';
-$string['opentoguests'] = 'Guest access';
$string['optional'] = 'optional';
$string['options'] = 'options';
$string['order'] = 'Order';
if ($columns = $DB->get_columns($table)) {
$DB->set_debug(true);
- foreach ($columns as $column => $data) {
- if (in_array($data->meta_type, array('C', 'X'))) { // Text stuff only
- //TODO: this should be definitively moved to DML driver to do the actual replace, this is not going to work for MSSQL and Oracle...
- $DB->execute("UPDATE {".$table."} SET $column = REPLACE($column, ?, ?)", array($search, $replace));
- }
+ foreach ($columns as $column) {
+ $DB->replace_all_text($table, $column, $search, $replace);
}
$DB->set_debug(false);
}
echo $OUTPUT->notification("...finished", 'notifysuccess');
}
+ purge_all_caches();
+
return true;
}
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
use Behat\Mink\Exception\ExpectationException as ExpectationException,
- Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
+ Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
+ Behat\Mink\Element\NodeElement as NodeElement;
/**
* Steps definitions base class.
* It can not contain steps definitions to avoid duplicates, only utility
* methods shared between steps.
*
+ * @method NodeElement find_field(string $locator) Finds a form element
+ * @method NodeElement find_button(string $locator) Finds a form input submit element or a button
+ * @method NodeElement find_link(string $locator) Finds a link on a page
+ * @method NodeElement find_file(string $locator) Finds a forum input file element
+ *
* @package core
* @category test
* @copyright 2012 David Monllaó
/**
* The timeout for each Behat step (load page, wait for an element to load...).
*/
- const TIMEOUT = 6;
+ const TIMEOUT = 3;
+
+ /**
+ * And extended timeout for specific cases.
+ */
+ const EXTENDED_TIMEOUT = 10;
+
+ /**
+ * The JS code to check that the page is ready.
+ */
+ const PAGE_READY_JS = '(M && M.util && M.util.pending_js && !Boolean(M.util.pending_js.length)) && (document.readyState === "complete")';
/**
* Locates url, based on provided path.
return get_class($this->getSession()->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver';
}
+ /**
+ * Spins around an element until it exists
+ *
+ * @throws ExpectationException
+ * @param string $element
+ * @param string $selectortype
+ * @return void
+ */
+ protected function ensure_element_exists($element, $selectortype) {
+
+ // Getting the behat selector & locator.
+ list($selector, $locator) = $this->transform_selector($selectortype, $element);
+
+ // Exception if it timesout and the element is still there.
+ $msg = 'The "' . $element . '" element does not exist and should exist';
+ $exception = new ExpectationException($msg, $this->getSession());
+
+ // It will stop spinning once the find() method returns true.
+ $this->spin(
+ function($context, $args) {
+ // We don't use behat_base::find as it is already spinning.
+ if ($context->getSession()->getPage()->find($args['selector'], $args['locator'])) {
+ return true;
+ }
+ return false;
+ },
+ array('selector' => $selector, 'locator' => $locator),
+ self::EXTENDED_TIMEOUT,
+ $exception,
+ true
+ );
+
+ }
+
+ /**
+ * Spins until the element does not exist
+ *
+ * @throws ExpectationException
+ * @param string $element
+ * @param string $selectortype
+ * @return void
+ */
+ protected function ensure_element_does_not_exist($element, $selectortype) {
+
+ // Getting the behat selector & locator.
+ list($selector, $locator) = $this->transform_selector($selectortype, $element);
+
+ // Exception if it timesout and the element is still there.
+ $msg = 'The "' . $element . '" element exists and should not exist';
+ $exception = new ExpectationException($msg, $this->getSession());
+
+ // It will stop spinning once the find() method returns false.
+ $this->spin(
+ function($context, $args) {
+ // We don't use behat_base::find() as we are already spinning.
+ if (!$context->getSession()->getPage()->find($args['selector'], $args['locator'])) {
+ return true;
+ }
+ return false;
+ },
+ array('selector' => $selector, 'locator' => $locator),
+ self::EXTENDED_TIMEOUT,
+ $exception,
+ true
+ );
+ }
+
+ /**
+ * Ensures that the provided node is visible and we can interact with it.
+ *
+ * @throws ExpectationException
+ * @param NodeElement $node
+ * @return void Throws an exception if it times out without the element being visible
+ */
+ protected function ensure_node_is_visible($node) {
+
+ if (!$this->running_javascript()) {
+ return;
+ }
+
+ // Exception if it timesout and the element is still there.
+ $msg = 'The "' . $node->getXPath() . '" xpath node is not visible and it should be visible';
+ $exception = new ExpectationException($msg, $this->getSession());
+
+ // It will stop spinning once the isVisible() method returns true.
+ $this->spin(
+ function($context, $args) {
+ if ($args->isVisible()) {
+ return true;
+ }
+ return false;
+ },
+ $node,
+ self::EXTENDED_TIMEOUT,
+ $exception,
+ true
+ );
+ }
+
+ /**
+ * Ensures that the provided element is visible and we can interact with it.
+ *
+ * Returns the node in case other actions are interested in using it.
+ *
+ * @throws ExpectationException
+ * @param string $element
+ * @param string $selectortype
+ * @return NodeElement Throws an exception if it times out without being visible
+ */
+ protected function ensure_element_is_visible($element, $selectortype) {
+
+ if (!$this->running_javascript()) {
+ return;
+ }
+
+ $node = $this->get_selected_node($selectortype, $element);
+ $this->ensure_node_is_visible($node);
+
+ return $node;
+ }
+
+ /**
+ * Ensures that all the page's editors are loaded.
+ *
+ * This method is expensive as it waits for .mceEditor CSS
+ * so use with caution and only where there will be editors.
+ *
+ * @throws ElementNotFoundException
+ * @throws ExpectationException
+ * @return void
+ */
+ protected function ensure_editors_are_loaded() {
+
+ if (!$this->running_javascript()) {
+ return;
+ }
+
+ // If there are no editors we don't need to wait.
+ try {
+ $this->find('css', '.mceEditor');
+ } catch (ElementNotFoundException $e) {
+ return;
+ }
+
+ // Exception if it timesout and the element is not appearing.
+ $msg = 'The editors are not completely loaded';
+ $exception = new ExpectationException($msg, $this->getSession());
+
+ // Here we know that there are .mceEditor editors in the page and we will
+ // probably need to interact with them, if we use tinyMCE JS var before
+ // it exists it will throw an exception and we want to catch it until all
+ // the page's editors are ready to interact with them.
+ $this->spin(
+ function($context) {
+
+ // It may return 0 if tinyMCE is loaded but not the instances, so we just loop again.
+ $neditors = $context->getSession()->evaluateScript('return tinyMCE.editors.length;');
+ if ($neditors == 0) {
+ return false;
+ }
+
+ // It may be there but not ready.
+ $iframeready = $context->getSession()->evaluateScript('
+ var readyeditors = new Array;
+ for (editorid in tinyMCE.editors) {
+ if (tinyMCE.editors[editorid].getDoc().readyState === "complete") {
+ readyeditors[editorid] = editorid;
+ }
+ }
+ if (tinyMCE.editors.length === readyeditors.length) {
+ return "complete";
+ }
+ return "";
+ ');
+
+ // Now we know that the editors are there.
+ if ($iframeready) {
+ return true;
+ }
+
+ // Loop again if it is not ready.
+ return false;
+ },
+ false,
+ self::EXTENDED_TIMEOUT,
+ $exception,
+ true
+ );
+ }
+
}
require_once(__DIR__ . '/behat_base.php');
use Behat\Mink\Exception\ExpectationException as ExpectationException,
- Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
+ Behat\Mink\Element\NodeElement as NodeElement;
/**
* Files-related actions.
class behat_files extends behat_base {
/**
- * Gets the filepicker NodeElement.
+ * Gets the NodeElement for filepicker of filemanager moodleform element.
*
- * The filepicker field label is pointing to a hidden input which is
+ * The filepicker/filemanager element label is pointing to a hidden input which is
* not recognized as a named selector, as it is hidden...
*
* @throws ExpectationException Thrown by behat_base::find
}
/**
- * Performs $action on a filepicker container element (file or folder).
+ * Performs $action on a filemanager container element (file or folder).
*
* It works together with open_element_contextual_menu
* as this method needs the contextual menu to be opened.
$classname = 'fp-file-' . $action;
$button = $this->find('css', '.moodle-dialogue-focused button.' . $classname, $exception);
+ $this->ensure_node_is_visible($button);
$button->click();
}
/**
* Opens the contextual menu of a folder or a file.
*
- * Works both in filepicker elements and when dealing with repository
- * elements inside modal windows.
+ * Works both in filemanager elements and when dealing with repository
+ * elements inside filepicker modal window.
*
* @throws ExpectationException Thrown by behat_base::find
* @param string $name The name of the folder/file
- * @param string $filepickerelement The filepicker locator, the whole DOM if false
+ * @param string $filemanagerelement The filemanager form element locator, the repository items are in filepicker modal window if false
* @return void
*/
- protected function open_element_contextual_menu($name, $filepickerelement = false) {
+ protected function open_element_contextual_menu($name, $filemanagerelement = false) {
- // If a filepicker is specified we restrict the search to the filepicker descendants.
+ // If a filemanager is specified we restrict the search to the descendants of this particular filemanager form element.
$containernode = false;
$exceptionmsg = '"'.$name.'" element can not be found';
- if ($filepickerelement) {
- $containernode = $this->get_filepicker_node($filepickerelement);
- $exceptionmsg = 'The "'.$filepickerelement.'" filepicker ' . $exceptionmsg;
+ if ($filemanagerelement) {
+ $containernode = $this->get_filepicker_node($filemanagerelement);
+ $exceptionmsg = 'The "'.$filemanagerelement.'" filemanager ' . $exceptionmsg;
+ $locatorprefix = "//div[@class='fp-content']";
+ } else {
+ $locatorprefix = "//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-items ')]//descendant::div[@class='fp-content']";
}
$exception = new ExpectationException($exceptionmsg, $this->getSession());
// Avoid quote-related problems.
$name = $this->getSession()->getSelectorsHandler()->xpathLiteral($name);
- // Get a filepicker element (folder or file).
+ // Get a filepicker/filemanager element (folder or file).
try {
// First we look at the folder as we need to click on the contextual menu otherwise it would be opened.
$node = $this->find(
'xpath',
- "//div[@class='fp-content']" .
+ $locatorprefix .
"//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')]" .
"[contains(concat(' ', normalize-space(@class), ' '), ' fp-folder ')]" .
"[normalize-space(.)=$name]" .
// Here the contextual menu is hidden, we click on the thumbnail.
$node = $this->find(
'xpath',
- "//div[@class='fp-content']" .
+ $locatorprefix .
"//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')]" .
"[normalize-space(.)=$name]" .
- "//descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-thumbnail ')]",
+ "//descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-filename-field ')]",
false,
$containernode
);
}
// Click opens the contextual menu when clicking on files.
+ $this->ensure_node_is_visible($node);
$node->click();
}
/**
- * Opens the 'add file' modal window and selects the repository.
+ * Opens the filepicker modal window and selects the repository.
*
* @throws ExpectationException Thrown by behat_base::find
- * @param NodeElement $filepickernode The filepicker DOM node.
+ * @param NodeElement $filemanagernode The filemanager or filepicker form element DOM node.
* @param mixed $repositoryname The repo name.
* @return void
*/
- protected function open_add_file_window($filepickernode, $repositoryname) {
+ protected function open_add_file_window($filemanagernode, $repositoryname) {
- $exception = new ExpectationException('No files can be added to the specified filepicker', $this->getSession());
+ $exception = new ExpectationException('No files can be added to the specified filemanager', $this->getSession());
- // We should deal with single-file and multiple-file filepickers,
+ // We should deal with single-file and multiple-file filemanagers,
// catching the exception thrown by behat_base::find() in case is not multiple
try {
- // Looking for the add button inside the specified filepicker.
- $add = $this->find('css', 'div.fp-btn-add a', $exception, $filepickernode);
+ // Looking for the add button inside the specified filemanager.
+ $add = $this->find('css', 'div.fp-btn-add a', $exception, $filemanagernode);
} catch (Exception $e) {
- // Otherwise should be a single-file filepicker.
- $add = $this->find('css', 'input.fp-btn-choose', $exception, $filepickernode);
+ // Otherwise should be a single-file filepicker form element.
+ $add = $this->find('css', 'input.fp-btn-choose', $exception, $filemanagernode);
}
+ $this->ensure_node_is_visible($add);
$add->click();
// Getting the repository link and opening it.
// Avoid problems with both double and single quotes in the same string.
$repositoryname = $this->getSession()->getSelectorsHandler()->xpathLiteral($repositoryname);
- // Here we don't need to look inside the selected filepicker because there can only be one modal window.
+ // Here we don't need to look inside the selected element because there can only be one modal window.
$repositorylink = $this->find(
'xpath',
"//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-area ')]" .
);
// Selecting the repo.
+ $this->ensure_node_is_visible($repositorylink);
$repositorylink->click();
}
/**
* Waits until the file manager modal windows are closed.
*
+ * This method is not used by any of our step definitions,
+ * keeping it here for users already using it.
+ *
* @throws ExpectationException
* @return void
*/
/**
* Checks that the file manager contents are not being updated.
*
+ * This method is not used by any of our step definitions,
+ * keeping it here for users already using it.
+ *
* @throws ExpectationException
* @param NodeElement $filepickernode The file manager DOM node
* @return void
$exception,
$filepickernode
);
-
- // After removing the class FileManagerHelper.view_files() performs other actions.
- $this->getSession()->wait(4 * 1000, false);
}
}
$features = array_values($featurespaths);
}
+ // Optionally include features from additional directories.
+ if (!empty($CFG->behat_additionalfeatures)) {
+ $features = array_merge($features, array_map("realpath", $CFG->behat_additionalfeatures));
+ }
+
// Gets all the components with steps definitions.
$stepsdefinitions = array();
$steps = self::get_components_steps_definitions();
*/
public function set_value($value) {
- // Get tinyMCE editor id if it exists.
- if ($editorid = $this->get_editor_id()) {
-
- // Set the value to the iframe and save it to the textarea.
- $this->session->executeScript('
- tinyMCE.get("'.$editorid.'").setContent("' . $value . '");
- tinyMCE.get("'.$editorid.'").save();
- ');
-
- } else {
- // Set the value to a textarea otherwise.
- parent::set_value($value);
+ $lastexception = null;
+
+ // We want the editor to be ready, otherwise the value can not
+ // be set and an exception is thrown.
+ for ($i = 0; $i < behat_base::EXTENDED_TIMEOUT; $i++) {
+ try {
+ // Get tinyMCE editor id if it exists.
+ if ($editorid = $this->get_editor_id()) {
+
+ // Set the value to the iframe and save it to the textarea.
+ $this->session->executeScript('
+ tinyMCE.get("'.$editorid.'").setContent("' . $value . '");
+ tinyMCE.get("'.$editorid.'").save();
+ ');
+
+ } else {
+ // Set the value to a textarea otherwise.
+ parent::set_value($value);
+ }
+ return;
+
+ } catch (Exception $e) {
+ // Catching any kind of exception and ignoring it until times out.
+ $lastexception = $e;
+
+ // Waiting 0.1 seconds.
+ usleep(100000);
+ }
}
+
+ // If it is not available we throw the last exception.
+ throw $lastexception;
}
/**
*/
public function get_value() {
- // Get tinyMCE editor id if it exists.
- if ($editorid = $this->get_editor_id()) {
+ // Can be be a string value or an exception depending whether the editor loads or not.
+ $lastoutcome = '';
+
+ // We want the editor to be ready to return the correct value, sometimes the
+ // page loads too fast and the returned value may be '' if the editor didn't
+ // have enough time to load completely despite having a different value.
+ for ($i = 0; $i < behat_base::EXTENDED_TIMEOUT; $i++) {
+ try {
+
+ // Get tinyMCE editor id if it exists.
+ if ($editorid = $this->get_editor_id()) {
+
+ // Save the current iframe value in case default value has been edited.
+ $this->session->executeScript('tinyMCE.get("'.$editorid.'").save();');
+ }
+
+ $lastoutcome = $this->field->getValue();
+
+ // We only want to wait until it times out if the value is empty.
+ if ($lastoutcome != '') {
+ return $lastoutcome;
+ }
+
+ } catch (Exception $e) {
+ // Catching any kind of exception and ignoring it until times out.
+ $lastoutcome = $e;
+
+ // Waiting 0.1 seconds.
+ usleep(100000);
+ }
+ }
- // Save the current iframe value in case default value has been edited.
- $this->session->executeScript('tinyMCE.get("'.$editorid.'").save();');
+ // If it is not available we throw the last exception.
+ if (is_a($lastoutcome, 'Exception')) {
+ throw $lastoutcome;
}
- return $this->field->getValue();
+ // Return the value if there are no exceptions it will be '' at this point
+ return $lastoutcome;
}
/**
* can not execute Javascript, also some Moodle settings disables the HTML
* editor.
*
- * @return mixed The id of the editor of false if is not available
+ * @return mixed The id of the editor of false if it is not available
*/
protected function get_editor_id() {
try {
$available = $this->session->evaluateScript('return (typeof tinyMCE != "undefined")');
- // Also checking that it exist a tinyMCE editor for the requested field.
+ // Also checking that it exists a tinyMCE editor for the requested field.
$editorid = $this->field->getAttribute('id');
$available = $this->session->evaluateScript('return (typeof tinyMCE.get("'.$editorid.'") != "undefined")');
$classname = 'behat_form_select';
} else {
+ // We can not provide a closer field type.
return false;
}
return get_class($this->session->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver';
}
+ /**
+ * Gets the field internal id used by selenium wire protocol.
+ *
+ * Only available when running_javascript().
+ *
+ * @throws coding_exception
+ * @return int
+ */
+ protected function get_internal_field_id() {
+
+ if (!$this->running_javascript()) {
+ throw new coding_exception('You can only get an internal ID using the selenium driver.');
+ }
+
+ return $this->session->
+ getDriver()->
+ getWebDriverSession()->
+ element('xpath', $this->field->getXPath())->
+ getID();
+ }
}
/**
* Sets the value of a single select.
*
+ * Seems an easy select, but there are lots of combinations
+ * of browsers and operative systems and each one manages the
+ * autosubmits and the multiple option selects in a diferent way.
+ *
* @param string $value
* @return void
*/
public function set_value($value) {
- $this->field->selectOption($value);
- // Adding a click as Selenium requires it to fire some JS events.
+ // In some browsers we select an option and it triggers all the
+ // autosubmits and works as expected but not in all of them, so we
+ // try to catch all the possibilities to make this function work as
+ // expected.
+
+ // Get the internal id of the element we are going to click.
+ // This kind of internal IDs are only available in the selenium wire
+ // protocol, so only available using selenium drivers, phantomjs and family.
if ($this->running_javascript()) {
+ $currentelementid = $this->get_internal_field_id();
+ }
- // In some browsers the selectOption actions can perform a page reload
- // so we need to ensure the element is still available to continue interacting
- // with it. We don't wait here.
- if (!$this->session->getDriver()->find($this->field->getXpath())) {
- return;
- }
+ // Here we select an option.
+ $this->field->selectOption($value);
- // Single select needs an extra click in the option.
- if (!$this->field->hasAttribute('multiple')) {
+ // With JS disabled this is enough and we finish here.
+ if (!$this->running_javascript()) {
+ return;
+ }
- $value = $this->session->getSelectorsHandler()->xpathLiteral($value);
+ // With JS enabled we add more clicks as some selenium
+ // drivers requires it to fire JS events.
- // Using the driver direcly because Element methods are messy when dealing
- // with elements inside containers.
- $optionxpath = $this->field->getXpath() .
- "/descendant::option[(./@value=$value or normalize-space(.)=$value)]";
- $optionnodes = $this->session->getDriver()->find($optionxpath);
- if ($optionnodes) {
- current($optionnodes)->click();
- }
+ // In some browsers the selectOption actions can perform a form submit or reload page
+ // so we need to ensure the element is still available to continue interacting
+ // with it. We don't wait here.
+ $selectxpath = $this->field->getXpath();
+ if (!$this->session->getDriver()->find($selectxpath)) {
+ return;
+ }
- } else {
- // Multiple ones needs the click in the select.
- $this->field->click();
+ // We also check the selenium internal element id, if it have changed
+ // we are dealing with an autosubmit that was already executed, and we don't to
+ // execute anything else as the action we wanted was already performed.
+ if ($currentelementid != $this->get_internal_field_id()) {
+ return;
+ }
+
+ // We also check that the option is still there. We neither wait.
+ $valueliteral = $this->session->getSelectorsHandler()->xpathLiteral($value);
+ $optionxpath = $selectxpath . "/descendant::option[(./@value=$valueliteral or normalize-space(.)=$valueliteral)]";
+ if (!$this->session->getDriver()->find($optionxpath)) {
+ return;
+ }
+
+ // Single select sometimes needs an extra click in the option.
+ if (!$this->field->hasAttribute('multiple')) {
+
+ // Using the driver direcly because Element methods are messy when dealing
+ // with elements inside containers.
+ $optionnodes = $this->session->getDriver()->find($optionxpath);
+ if ($optionnodes) {
+ current($optionnodes)->click();
}
+
+ } else {
+ // Multiple ones needs the click in the select.
+ $this->field->click();
+
+ // We ensure that the option is still there.
+ if (!$this->session->getDriver()->find($optionxpath)) {
+ return;
+ }
+
+ // Repeating the select as some drivers (chrome that I know) are moving
+ // to another option after the general select field click above.
+ $this->field->selectOption($value);
}
}
self::load_classes('core', "$CFG->dirroot/lib/classes");
foreach (self::$subsystems as $subsystem => $fulldir) {
+ if (!$fulldir) {
+ continue;
+ }
self::load_classes('core_'.$subsystem, "$fulldir/classes");
}
*/
private final function __construct() {
$this->data = array_fill_keys(self::$fields, null);
+
+ // Define some basic details.
+ $classname = get_called_class();
+ $parts = explode('\\', $classname);
+ if (count($parts) !== 3 or $parts[1] !== 'event') {
+ throw new \coding_exception("Invalid event class name '$classname', it must be defined in component\\event\\
+ namespace");
+ }
+ $this->data['eventname'] = '\\'.$classname;
+ $this->data['component'] = $parts[0];
+
+ $pos = strrpos($parts[2], '_');
+ if ($pos === false) {
+ throw new \coding_exception("Invalid event class name '$classname', there must be at least one underscore separating
+ object and action words");
+ }
+ $this->data['target'] = substr($parts[2], 0, $pos);
+ $this->data['action'] = substr($parts[2], $pos + 1);
}
/**
// Set automatic data.
$event->data['timecreated'] = time();
- $classname = get_class($event);
- $parts = explode('\\', $classname);
- if (count($parts) !== 3 or $parts[1] !== 'event') {
- throw new \coding_exception("Invalid event class name '$classname', it must be defined in component\\event\\ namespace");
- }
- $event->data['eventname'] = '\\'.$classname;
- $event->data['component'] = $parts[0];
-
- $pos = strrpos($parts[2], '_');
- if ($pos === false) {
- throw new \coding_exception("Invalid event class name '$classname', there must be at least one underscore separating object and action words");
- }
- $event->data['target'] = substr($parts[2], 0, $pos);
- $event->data['action'] = substr($parts[2], $pos+1);
-
// Set optional data or use defaults.
$event->data['objectid'] = isset($data['objectid']) ? $data['objectid'] : null;
$event->data['courseid'] = isset($data['courseid']) ? $data['courseid'] : null;
debugging('Number of event data fields must not be changed in event classes', DEBUG_DEVELOPER);
}
$encoded = json_encode($this->data['other']);
- if ($encoded === false or $this->data['other'] !== json_decode($encoded, true)) {
+ // The comparison here is not set to strict as whole float numbers will be converted to integers through JSON encoding /
+ // decoding and send an unwanted debugging message.
+ if ($encoded === false or $this->data['other'] != json_decode($encoded, true)) {
debugging('other event data must be compatible with json encoding', DEBUG_DEVELOPER);
}
if ($this->data['userid'] and !is_number($this->data['userid'])) {
--- /dev/null
+<?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/>.
+
+/**
+ * Course module instance list viewed event.
+ *
+ * @package core
+ * @copyright 2013 onwards Ankit Agarwal
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Course module instance list viewed event class.
+ *
+ * This is an abstract to guide the developers in using this event name for their events.
+ * It is intended to be used when the user viewes the list of all the instances of a module
+ * in a course. This replaces the historical 'view all' log entry generated in mod/somemod/index.php.
+ *
+ * Example:
+ *
+ * \mod_chat\event\course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed
+ *
+ * @package core
+ * @copyright 2013 onwards Ankit Agarwal
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class course_module_instance_list_viewed extends base{
+
+ /** @var string private var to store mod name */
+ private $modname;
+
+ /**
+ * Init method.
+ *
+ * @return void
+ */
+ protected function init() {
+ $this->data['crud'] = 'r';
+ $this->data['level'] = self::LEVEL_OTHER;
+ if (strstr($this->component, 'mod_') === false) {
+ throw new \coding_exception('The event name or namespace is invalid.');
+ } else {
+ $this->modname = str_replace('mod_', '', $this->component);
+ }
+ }
+
+ /**
+ * Returns description of what happened.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return "User with id '$this->userid' viewed instance list for module '$this->modname' in course with id
+ '$this->courseid'";
+ }
+
+ /**
+ * Return localised event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('eventcoursemoduleinstancelistviewed', 'core');
+ }
+
+ /**
+ * Get URL related to the action.
+ *
+ * @return \moodle_url
+ */
+ public function get_url() {
+ return new \moodle_url("/mod/$this->modname/index.php", array('id' => $this->courseid));
+ }
+
+ /**
+ * Return the legacy event log data.
+ *
+ * @return array|null
+ */
+ protected function get_legacy_logdata() {
+ return array($this->courseid, $this->modname, 'view all', 'index.php?id=' . $this->courseid, '');
+ }
+
+
+ /**
+ * Custom validation.
+ *
+ * @throws \coding_exception
+ * @return void
+ */
+ protected function validate_data() {
+ if ($this->context->contextlevel !== CONTEXT_COURSE) {
+ throw new \coding_exception('The context must be a course level context.');
+ }
+ }
+
+}
/**
* Course module instances list_viewed event.
*
+ * This class has been deprecated, please use \core\event\course_module_instance_list_viewed.
+ *
* @package core
* @copyright 2013 Frédéric Massart
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
defined('MOODLE_INTERNAL') || die();
/**
- * Course module instances list viewed event class.
- *
- * This is an abstract to guide the developers in using this event name for their events.
- * It is intended to be used when the user viewes the list of all the instances of a module
- * in a course. This replaces the historical 'view all' log entry generated in mod/somemod/index.php.
- *
- * Example:
- *
- * \mod_chat\event\instances_list_viewed extends \core\event\course_module_instances_list_viewed
+ * This class has been deprecated, please use \core\event\course_module_instance_list_viewed.
*
+ * @deprecated Since Moodle 2.7
* @package core
* @copyright 2013 Frédéric Massart
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-abstract class course_module_instances_list_viewed extends \core\event\content_viewed {
-
- /**
- * Init method.
- *
- * @return void
- */
- protected function init() {
- $this->data['crud'] = 'r';
- $this->data['level'] = self::LEVEL_OTHER;
- }
-
- /**
- * Set page detail.
- *
- * Override to prevent its use.
- */
- public function set_page_detail() {
- }
-
- /**
- * Custom validation.
- *
- * The parent validation is ignored on purpose.
- *
- * @throws \coding_exception
- * @return void
- */
- protected function validate_data() {
- if ($this->context->contextlevel !== CONTEXT_COURSE) {
- throw new \coding_exception('The context must be a course level context.');
- }
- // Hack to by pass the requirement of the parent class. 'view_all' was the old fashioned-way
- // to describe the page listing all the instances of a module in a course.
- $this->data['other']['content'] = 'view_all';
- parent::validate_data();
- }
-
+abstract class course_module_instances_list_viewed extends course_module_instance_list_viewed {
}
+
+debugging('core\\event\\course_module_instances_list_viewed has been deperecated. Please use
+ core\\event\\course_module_instance_list_viewed instead', DEBUG_DEVELOPER);
}
}
+ /**
+ * Is automatic deployment enabled?
+ *
+ * @return bool
+ */
+ public function enabled() {
+ global $CFG;
+
+ // The feature can be prohibited via config.php.
+ return empty($CFG->disableupdateautodeploy);
+ }
+
/**
* Returns the timestamp of the last execution of {@link fetch()}
*
*/
class deployer {
- const HTTP_PARAM_PREFIX = 'updteautodpldata_'; // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p.
- const HTTP_PARAM_CHECKER = 'datapackagesize'; // Name of the parameter that holds the number of items in the received data items.
-
/** @var \core\update\deployer holds the singleton instance */
protected static $singletoninstance;
/** @var moodle_url URL of a page that includes the deployer UI */
throw new coding_exception('Illegal method call - deployer not initialized.');
}
- $params = $this->data_to_params(array(
- 'updateinfo' => (array)$info, // See http://www.php.net/manual/en/language.types.array.php#language.types.array.casting .
- ));
+ $params = array(
+ 'updateaddon' => $info->component,
+ 'version' =>$info->version,
+ 'sesskey' => sesskey(),
+ );
+
+ // Append some our own data.
+ if (!empty($this->callerurl)) {
+ $params['callerurl'] = $this->callerurl->out(false);
+ }
+ if (!empty($this->returnurl)) {
+ $params['returnurl'] = $this->returnurl->out(false);
+ }
$widget = new \single_button(
new moodle_url($this->callerurl, $params),
* @return array
*/
public function submitted_data() {
+ $component = optional_param('updateaddon', '', PARAM_COMPONENT);
+ $version = optional_param('version', '', PARAM_RAW);
+ if (!$component or !$version) {
+ return false;
+ }
+
+ $plugininfo = \core_plugin_manager::instance()->get_plugin_info($component);
+ if (!$plugininfo) {
+ return false;
+ &n