//Database types
$databases = array('mysqli' => moodle_database::get_driver_instance('mysqli', 'native'),
+ 'mariadb'=> moodle_database::get_driver_instance('mariadb', 'native'),
'pgsql' => moodle_database::get_driver_instance('pgsql', 'native'),
'oci' => moodle_database::get_driver_instance('oci', 'native'),
'sqlsrv' => moodle_database::get_driver_instance('sqlsrv', 'native'), // MS SQL*Server PHP driver
cli_error(get_string('pathserrcreatedataroot', 'install', $a));
}
}
-$CFG->tempdir = $CFG->dataroot.'/temp';
-$CFG->cachedir = $CFG->dataroot.'/cache';
+$CFG->tempdir = $CFG->dataroot.'/temp';
+$CFG->cachedir = $CFG->dataroot.'/cache';
+$CFG->localcachedir = $CFG->dataroot.'/localcache';
// download required lang packs
if ($CFG->lang !== 'en') {
</FEEDBACK>
</UNICODE>
<DATABASE level="required">
+ <VENDOR name="mariadb" version="5.3.5" />
<VENDOR name="mysql" version="5.1.33" />
<VENDOR name="postgres" version="8.3" />
<VENDOR name="mssql" version="9.0" />
<ON_ERROR message="ziprequired" />
</FEEDBACK>
</PHP_EXTENSION>
+ <PHP_EXTENSION name="zlib" level="optional">
+ </PHP_EXTENSION>
<PHP_EXTENSION name="gd" level="required">
<FEEDBACK>
<ON_ERROR message="gdrequired" />
'128M' => '128M',
'256M' => '256M',
'512M' => '512M',
- '1024M' => '1024M'
+ '1024M' => '1024M',
+ '2048M' => '2048M',
)));
$temp->add(new admin_setting_configtext('curlcache', new lang_string('curlcache', 'admin'),
new lang_string('configcurlcache', 'admin'), 120, PARAM_INT));
}
// Search by label.
- $searchbox = $this->find_field('Search in settings');
+ $searchbox = $this->find_field(get_string('searchinsettings', 'admin'));
$searchbox->setValue($label);
$submitsearch = $this->find('css', 'form.adminsearchform input[type=submit]');
$submitsearch->press();
// 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.
$exception = new ElementNotFoundException($this->getSession(), '"' . $label . '" administration setting ');
+
+ // The argument should be converted to an xpath literal.
+ $label = $this->getSession()->getSelectorsHandler()->xpathLiteral($label);
+
$fieldxpath = "//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]" .
- "[@id=//label[contains(normalize-space(string(.)), '" . $label . "')]/@for]";
+ "[@id=//label[contains(normalize-space(.), $label)]/@for]";
$fieldnode = $this->find('xpath', $fieldxpath, $exception);
$formfieldtypenode = $this->find('xpath', $fieldxpath . "/ancestor::div[@class='form-setting']" .
"/child::div[contains(concat(' ', @class, ' '), ' form-')]/child::*/parent::div");
$field = behat_field_manager::get_field_instance($type, $fieldnode, $this->getSession());
$field->set_value($value);
- $this->find_button('Save changes')->press();
+ $this->find_button(get_string('savechanges'))->press();
}
}
And I should not see "C_shortname Course fullname"
Scenario: Courses list with extended course names
- Given I click on "Courses" "link" in the "//div[@id='settingsnav']//descendant::li[contains(concat(' ', @class, ' '), ' type_setting ')][not(contains(., 'Site administration'))][contains(., 'Appearance')]" "xpath_element"
+ Given I click on "Courses" "link" in the "//div[@id='settingsnav']/descendant::li[contains(concat(' ', normalize-space(@class), ' '), ' type_setting ')][not(contains(., 'Site administration'))][contains(., 'Appearance')]" "xpath_element"
And I check "Display extended course names"
When I press "Save changes"
And I am on homepage
$CFG->{$var} = $CFG->{'behat_' . $var};
}
+// Clean $CFG extra values before performing any action.
+behat_clean_init_config();
+
$CFG->noemailever = true;
$CFG->passwordsaltmain = 'moodle';
// Unset cache and temp directories to reset them again with the new $CFG->dataroot.
unset($CFG->cachedir);
+unset($CFG->localcachedir);
unset($CFG->tempdir);
// Continues setup.
$string['viewsteps'] = 'Filter';
$string['wheninfo'] = 'When. Actions that provokes an event';
$string['wrongbehatsetup'] = 'Something is wrong with behat setup, ensure:<ul>
-<li>You ran "curl http://getcomposer.org/installer | php"</li>
-<li>You ran "php composer.phar install --dev"</li>
+<li>You ran "php admin/tool/behat/cli/init.php" from your moodle root directory</li>
<li>vendor/bin/behat file has execution permissions</li></ul>';
And I follow "Course 1"
When I click on "Move this to the dock" "button" in the ".block_settings" "css_element"
Then I should not see "Question bank"
- And I click on "//div[@id='dock']/descendant::*[contains(., 'Administration')]/h2" "xpath_element"
+ And I click on "//div[@id='dock']/descendant::h2[normalize-space(.)='Administration']" "xpath_element"
@javascript
Scenario: Locators inside specific DOM nodes using XPath
| 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 "//*[contains(concat(' ', normalize-space(@class), ' '), ' block_settings ')]" "xpath_element"
+ When I click on "Move this to the dock" "button" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' block_settings ')]" "xpath_element"
Then I should not see "Turn editing on"
Then I should see "Course 1"
And I should see "Course 2"
And I should see "Course 3"
- When I go to the courses management page
+ And I go to the courses management page
And I follow "Cat 1"
- Then I should see "Cat 2"
+ And I should see "Cat 2"
And I should see "Cat 3"
- When I follow "Cat 3"
- Then I should see "Course 1"
+ And I follow "Cat 3"
+ And I should see "Course 1"
And I should see "Course 2"
- When I select "Cat 2" from "Course categories:"
- Then I should see "No courses in this category"
- When I select "Miscellaneous" from "Course categories:"
- Then I should see "Course 3"
+ And I select "Cat 1 / Cat 2" from "Course categories:"
+ And I should see "No courses in this category"
+ And I select "Miscellaneous" from "Course categories:"
+ And I should see "Course 3"
@javascript
Scenario: Add a bunch of groups and groupings
And I follow "Course 1"
Then I should see "Topic 1"
+ Scenario: Add role assigns
+ Given the following "users" exists:
+ | username | firstname | lastname | email |
+ | user1 | User | 1 | user1@moodlemoodle.com |
+ | user2 | User | 2 | user2@moodlemoodle.com |
+ | user3 | User | 3 | user3@moodlemoodle.com |
+ And the following "categories" exists:
+ | name | category | idnumber |
+ | Cat 1 | 0 | CAT1 |
+ And the following "courses" exists:
+ | fullname | shortname | category |
+ | Course 1 | C1 | CAT1 |
+ And the following "role assigns" exists:
+ | user | role | contextlevel | reference |
+ | user1 | manager | System | |
+ | user2 | editingteacher | Category | CAT1 |
+ | user3 | editingteacher | Course | C1 |
+ When I log in as "user1"
+ Then I should see "Front page settings"
+ And I log out
+ And I log in as "user2"
+ And I follow "Course 1"
+ And I should see "Turn editing on"
+ And I log out
+ And I log in as "user3"
+ And I follow "Course 1"
+ And I should see "Turn editing on"
+
+ Scenario: Add modules
+ Given the following "courses" exists:
+ | fullname | shortname |
+ | Course 1 | C1 |
+ And the following "activities" exists:
+ | activity | name | intro | course | idnumber |
+ | assign | Test assignment name | Test assignment description | C1 | assign1 |
+ | data | Test database name | Test database description | C1 | data1 |
+ When I log in as "admin"
+ And I follow "Course 1"
+ Then I should see "Test assignment name"
+ And I should see "Test database name"
+ And I follow "Test assignment name"
+ And I should see "Test assignment description"
+
@javascript
Scenario: Add relations between users and groups
Given the following "users" exists:
fwrite($f, ";\n");
}
fclose($f);
+ @chmod($filepath, $CFG->filepermissions);
}
/**
$prevreferences = $DB->get_records_select('profiling',
'url = ? AND runreference = 1 AND timecreated < ?',
array($run->url, $run->timecreated),
- 'timecreated DESC', 'runid', 0, 1);
- $prevrunid = $prevreferences ? reset($prevreferences)->runid : false;
+ 'timecreated DESC', 'runid, runcomment, timecreated', 0, 10);
echo $OUTPUT->box_start('generalbox boxwidthwide boxaligncenter');
$header = get_string('lastrunof', 'tool_profiling', $script);
echo $OUTPUT->heading($header);
- $table = profiling_print_run($run, $prevrunid);
+ $table = profiling_print_run($run, $prevreferences);
echo $table;
echo $OUTPUT->box_end();
$prevreferences = $DB->get_records_select('profiling',
'url = ? AND runreference = 1 AND timecreated < ?',
array($run->url, $run->timecreated),
- 'timecreated DESC', 'runid', 0, 1);
- $prevrunid = $prevreferences ? reset($prevreferences)->runid : false;
+ 'timecreated DESC', 'runid, runcomment, timecreated', 0, 10);
echo $OUTPUT->box_start('generalbox boxwidthwide boxaligncenter');
$header = get_string('summaryof', 'tool_profiling', $run->url);
echo $OUTPUT->heading($header);
- $table = profiling_print_run($run, $prevrunid);
+ $table = profiling_print_run($run, $prevreferences);
echo $table;
echo $OUTPUT->box_end();
$string['runid'] = 'Run ID';
$string['summaryof'] = 'Summary of {$a}';
$string['viewdetails'] = 'View profiling details';
-$string['viewdiff'] = 'View profiling differences with last reference run';
+$string['viewdiff'] = 'View profiling differences with:';
$string['viewdiffdetails'] = 'View profiling diff details';
if (isset($rowcols['city'])) {
$rowcols['city'] = trim($rowcols['city']);
- if (empty($rowcols['city'])) {
- $rowcols['status'][] = get_string('fieldrequired', 'error', 'city');
- }
}
// Check if rowcols have custom profile field with correct data and update error state.
$noerror = uu_check_custom_profile_data($rowcols) && $noerror;
} else {
$mform->setDefault('city', $CFG->defaultcity);
}
- $mform->addRule('city', get_string('required'), 'required');
- $mform->addElement('select', 'country', get_string('selectacountry'), get_string_manager()->get_list_of_countries());
+ $choices = get_string_manager()->get_list_of_countries();
+ $choices = array(''=>get_string('selectacountry').'...') + $choices;
+ $mform->addElement('select', 'country', get_string('selectacountry'), $choices);
if (empty($CFG->country)) {
$mform->setDefault('country', $templateuser->country);
} else {
--- /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/>.
+
+/**
+ * User login event.
+ *
+ * @package core_auth
+ * @copyright 2013 Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_auth\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * User login event class.
+ *
+ * @package core_auth
+ * @copyright 2013 Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_loggedin extends \core\event\base {
+
+ /**
+ * Returns localised description of what happened.
+ *
+ * @return \lang_string.
+ */
+ public function get_description() {
+ return new \lang_string('event_user_loggedin_desc', '', $this->get_username());
+ }
+
+ /**
+ * Return legacy data for add_to_log().
+ *
+ * @return array
+ */
+ public function get_legacy_logdata() {
+ return array(SITEID, 'user', 'login', "view.php?id=" . $this->data['objectid'] . "&course=".SITEID,
+ $this->data['objectid'], 0, $this->data['objectid']);
+ }
+
+ /**
+ * Return localised event name.
+ *
+ * @return \lang_string
+ */
+ public static function get_name() {
+ return new \lang_string('event_user_loggedin');
+ }
+
+ /**
+ * Get URL related to the action
+ *
+ * @return \moodle_url
+ */
+ public function get_url() {
+ return new \moodle_url('/user/profile.php', array('id' => $this->data['objectid']));
+ }
+
+ /**
+ * Return the username of the logged in user.
+ *
+ * @return string
+ */
+ public function get_username() {
+ return $this->data['other']['username'];
+ }
+
+ /**
+ * Init method.
+ *
+ * @return void
+ */
+ protected function init() {
+ $this->context = \context_system::instance();
+ $this->data['crud'] = 'r';
+ $this->data['level'] = 50; // TODO MDL-37658.
+ $this->data['objecttable'] = 'user';
+ }
+
+ /**
+ * Custom validation.
+ *
+ * @throws coding_exception when validation does not pass.
+ * @return void
+ */
+ protected function validate_data() {
+ if (!isset($this->data['objectid'])) {
+ throw new \coding_exception("objectid has to be specified.");
+ } else if (!isset($this->data['other']['username'])) {
+ throw new \coding_exception("other['username'] has to be specified.");
+ }
+ }
+
+}
set_config('sybasequoting', '1', 'auth/db');
break;
+ case 'mariadb_native_moodle_database':
case 'mysqli_native_moodle_database':
set_config('type', 'mysqli', 'auth/db');
set_config('setupsql', "SET NAMES 'UTF-8'", 'auth/db');
--- /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/>.
+
+/**
+ * Tests for auth.
+ *
+ * @package core_auth
+ * @copyright 2013 Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/authlib.php');
+
+/**
+ * Auth testcase class.
+ *
+ * @package core_auth
+ * @copyright 2013 Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class auth_testcase extends advanced_testcase {
+
+ public function test_user_loggedin_event() {
+ global $USER;
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+
+ $sink = $this->redirectEvents();
+ $user = clone($USER);
+ login_attempt_valid($user);
+ $events = $sink->get_events();
+ $sink->close();
+
+ $this->assertCount(1, $events);
+ $event = reset($events);
+ $this->assertInstanceOf('\core_auth\event\user_loggedin', $event);
+ $this->assertEquals('user', $event->objecttable);
+ $this->assertEquals('2', $event->objectid);
+ $this->assertEquals(context_system::instance()->id, $event->contextid);
+ $this->assertEquals($user, $event->get_record_snapshot('user', 2));
+ }
+
+ public function test_user_loggedin_event_exceptions() {
+ try {
+ $event = \core_auth\event\user_loggedin::create(array('objectid' => 1));
+ $this->fail('\core_auth\event\user_loggedin requires other[\'username\']');
+ } catch(Exception $e) {
+ $this->assertInstanceOf('coding_exception', $e);
+ }
+
+ try {
+ $event = \core_auth\event\user_loggedin::create(array('other' => array('username' => 'test')));
+ $this->fail('\core_auth\event\user_loggedin requires objectid');
+ } catch(Exception $e) {
+ $this->assertInstanceOf('coding_exception', $e);
+ }
+ }
+
+}
public function i_log_in_as($username) {
return array(new Given('I am on homepage'),
- new Given('I follow "Log in"'),
- new Given('I fill in "Username" with "'.$username.'"'),
- new Given('I fill in "Password" with "'.$username.'"'),
- new Given('I press "Log in"')
+ new Given('I follow "' . get_string('login') . '"'),
+ new Given('I fill in "' . get_string('username') . '" with "' . $this->escape($username) . '"'),
+ new Given('I fill in "' . get_string('password') . '" with "'. $this->escape($username) . '"'),
+ new Given('I press "' . get_string('login') . '"')
);
}
* @Given /^I log out$/
*/
public function i_log_out() {
- return new When('I follow "Log out"');
+ return new When('I follow "' . get_string('logout') . '"');
}
}
<?php
-
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
require_once($CFG->libdir.'/formslib.php');
class backup_files_edit_form extends moodleform {
- function definition() {
+
+ /**
+ * Form definition.
+ */
+ public function definition() {
$mform =& $this->_form;
- $contextid = $this->_customdata['contextid'];
- $options = array('subdirs'=>0, 'maxfiles'=>-1, 'accepted_types'=>'*', 'return_types'=>FILE_INTERNAL | FILE_REFERENCE);
+
+ $options = array('subdirs' => 0, 'maxfiles' => -1, 'accepted_types' => '*', 'return_types' => FILE_INTERNAL | FILE_REFERENCE);
+
$mform->addElement('filemanager', 'files_filemanager', get_string('files'), null, $options);
+
$mform->addElement('hidden', 'contextid', $this->_customdata['contextid']);
+ $mform->setType('contextid', PARAM_INT);
+
$mform->addElement('hidden', 'currentcontext', $this->_customdata['currentcontext']);
+ $mform->setType('currentcontext', PARAM_INT);
+
$mform->addElement('hidden', 'filearea', $this->_customdata['filearea']);
+ $mform->setType('filearea', PARAM_AREA);
+
$mform->addElement('hidden', 'component', $this->_customdata['component']);
+ $mform->setType('component', PARAM_COMPONENT);
+
$mform->addElement('hidden', 'returnurl', $this->_customdata['returnurl']);
+ $mform->setType('returnurl', PARAM_URL);
+
$this->add_action_buttons(true, get_string('savechanges'));
$this->set_data($this->_customdata['data']);
}
$cdir = static::$path_to_manifest_folder . DIRECTORY_SEPARATOR . 'course_files';
if (!file_exists($cdir)) {
- mkdir($cdir);
+ mkdir($cdir, $CFG->directorypermissions, true);
}
$sheet_base = static::loadsheet(SHEET_BASE);
$cdir = static::$path_to_manifest_folder . DIRECTORY_SEPARATOR . 'course_files';
if (!file_exists($cdir)) {
- mkdir($cdir);
+ mkdir($cdir, $CFG->directorypermissions, true);
}
$sheet_base = static::loadsheet(SHEET_BASE);
* @return mixed - directory short name or false in case of faliure
*/
public static function randomdir($where, $prefix = '', $suffix = '') {
+ global $CFG;
+
$dirname = false;
$randomname = self::uuidgen($prefix, $suffix, false);
$newdirname = $where.DIRECTORY_SEPARATOR.$randomname;
if (mkdir($newdirname)) {
- chmod($newdirname, 0755);
+ chmod($newdirname, $CFG->directorypermissions);
$dirname = $randomname;
}
return $dirname;
}
public static function add_files(cc_i_manifest &$manifest, $packageroot, $outdir, $allinone = true) {
+ global $CFG;
+
if (pkg_static_resources::instance()->finished) {
return;
}
//let us try to recreate them
$justdir = $rdir->fullpath(false).$values[7];
if (!file_exists($justdir)) {
- if (!mkdir($justdir, 0777, true)) {
+ if (!mkdir($justdir, $CFG->directorypermissions, true)) {
throw new RuntimeException('Unable to create directories!');
}
}
*/
function copyr($source, $dest)
{
+ global $CFG;
// Simple copy for a file
if (is_file($source)) {
return copy($source, $dest);
// Make destination directory
if (!is_dir($dest)) {
- mkdir($dest);
+ mkdir($dest, $CFG->directorypermissions, true);
}
// Loop through the folder
$fpath = $dpath . DIRECTORY_SEPARATOR . $rfpath;
if (!file_exists($dpath)) {
- mkdir($dpath);
+ mkdir($dpath, $CFG->directorypermissions, true);
}
//copy the folder.gif file
$folder_gif = "{$CFG->dirroot}/pix/i/files.gif";
$rfpath = 'files.gif';
$fpath = $dpath . DIRECTORY_SEPARATOR . 'files.gif';
if (!file_exists($dpath)) {
- mkdir($dpath);
+ mkdir($dpath, $CFG->directorypermissions, true);
}
//copy the folder.gif file
$folder_gif = "{$CFG->dirroot}/pix/i/files.gif";
$tempdir = $CFG->dataroot . '/temp/backup/' . uniqid('', true);
- if (mkdir($tempdir, 0777, true)) {
+ if (mkdir($tempdir, $CFG->directorypermissions, true)) {
cc_convert_moodle2::convert($basepath, $tempdir);
//Switch the directories
// Get all the block_position objects pending to match
$params = array('backupid' => $this->get_restoreid(), 'itemname' => 'block_position');
- $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid');
+ $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
// Process block positions, creating them or accumulating for final step
foreach($rs as $posrec) {
- // Get the complete position object (stored as info)
- $position = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'block_position', $posrec->itemid)->info;
+ // Get the complete position object out of the info field.
+ $position = backup_controller_dbops::decode_backup_temp_info($posrec->info);
// If position is for one already mapped (known) contextid
// process it now, creating the position, else nothing to
// do, position finally discarded
// Get all the module_availability objects to process
$params = array('backupid' => $this->get_restoreid(), 'itemname' => 'module_availability');
- $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid');
+ $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
// Process availabilities, creating them if everything matches ok
foreach($rs as $availrec) {
$allmatchesok = true;
// Get the complete availabilityobject
- $availability = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'module_availability', $availrec->itemid)->info;
+ $availability = backup_controller_dbops::decode_backup_temp_info($availrec->info);
// Map the sourcecmid if needed and possible
if (!empty($availability->sourcecmid)) {
$newcm = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'course_module', $availability->sourcecmid);
// Iterate over aliases in the queue.
foreach ($rs as $record) {
- $info = unserialize(base64_decode($record->info));
+ $info = restore_dbops::decode_backup_temp_info($record->info);
// Try to pick a repository instance that should serve the alias.
$repository = $this->choose_repository($info);
$source = null;
foreach ($candidates as $candidate) {
- $candidateinfo = unserialize(base64_decode($candidate->info));
+ $candidateinfo = backup_controller_dbops::decode_backup_temp_info($candidate->info);
if ($candidateinfo->filename === $reference['filename']
and $candidateinfo->filepath === $reference['filepath']
and !is_null($candidate->newcontextid)
method is not available anymore. Temp tables must be created
inline always.
+* Using the info field from backup_ids_temp or backup_files_temp
+ must now go via backup_controller_dbops::decode_backup_temp_info() and
+ backup_controller_dbops::encode_backup_temp_info(). The implementation
+ of the encoding has changed. These new functions encapsulate any future
+ changes to the encoding.
+
=== 2.5 ===
* New optional param $sortby in backup set_source_table() allows to
$dbman->drop_table($table); // And drop it
}
+ /**
+ * Decode the info field from backup_ids_temp or backup_files_temp.
+ *
+ * @param mixed $info The info field data to decode, may be an object or a simple integer.
+ * @return mixed The decoded information. For simple types it returns, for complex ones we decode.
+ */
+ public static function decode_backup_temp_info($info) {
+ // We encode all data except null.
+ if ($info != null) {
+ if (extension_loaded('zlib')) {
+ return unserialize(gzuncompress(base64_decode($info)));
+ } else {
+ return unserialize(base64_decode($info));
+ }
+ }
+ return $info;
+ }
+
+ /**
+ * Encode the info field for backup_ids_temp or backup_files_temp.
+ *
+ * @param mixed $info string The info field data to encode.
+ * @return string An encoded string of data or null if the input is null.
+ */
+ public static function encode_backup_temp_info($info) {
+ // We encode if there is any information to keep the translations simpler.
+ if ($info != null) {
+ // We compress if possible. It reduces db, network and memory storage. The saving is greater than CPU compression cost.
+ // Compression level 1 is chosen has it produces good compression with the smallest possible overhead, see MDL-40618.
+ if (extension_loaded('zlib')) {
+ return base64_encode(gzcompress(serialize($info), 1));
+ } else {
+ return base64_encode(serialize($info));
+ }
+ }
+ return $info;
+ }
+
/**
* Given one type and id from controller, return the corresponding courseid
*/
$problems = array(); // To store warnings/errors
// Get loaded roles from backup_ids
- $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid');
+ $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid, info');
foreach ($rs as $recrole) {
// If the rolemappings->modified flag is set, that means that we are coming from
// manually modified mappings (by UI), so accept those mappings an put them to backup_ids
// Else, we haven't any info coming from UI, let's calculate the mappings, matching
// in multiple ways and checking permissions. Note mapping to 0 means "skip"
} else {
- $role = (object)self::get_backup_ids_record($restoreid, 'role', $recrole->itemid)->info;
+ $role = (object)backup_controller_dbops::decode_backup_temp_info($recrole->info);
$match = self::get_best_assignable_role($role, $courseid, $userid, $samesite);
// Send match to backup_ids
self::set_backup_ids_record($restoreid, 'role', $recrole->itemid, $match);
// Build the rolemappings element for controller
unset($role->id);
unset($role->nameincourse);
- unset($role->nameincourse);
$role->targetroleid = $match;
$rolemappings->mappings[$recrole->itemid] = $role;
// Prepare warning if no match found
} else {
self::set_backup_ids_record($restoreid, 'question_category', $category->id, $matchcat->id, $targetcontext->id);
$questions = self::restore_get_questions($restoreid, $category->id);
+
+ // Collect all the questions for this category into memory so we only talk to the DB once.
+ $questioncache = $DB->get_records_sql_menu("SELECT ".$DB->sql_concat('stamp', "' '", 'version').", id
+ FROM {question}
+ WHERE category = ?", array($matchcat->id));
+
foreach ($questions as $question) {
- $matchq = $DB->get_record('question', array(
- 'category' => $matchcat->id,
- 'stamp' => $question->stamp,
- 'version' => $question->version));
+ if (isset($questioncache[$question->stamp." ".$question->version])) {
+ $matchqid = $questioncache[$question->stamp." ".$question->version];
+ } else {
+ $matchqid = false;
+ }
// 5a) No match, check if user can add q
- if (!$matchq) {
+ if (!$matchqid) {
// 6a) User can, mark the q to be created
if ($canadd) {
// Nothing to mark, newitemid means create
// 5b) Match, mark q to be mapped
} else {
- self::set_backup_ids_record($restoreid, 'question', $question->id, $matchq->id);
+ self::set_backup_ids_record($restoreid, 'question', $question->id, $matchqid);
}
}
}
global $DB;
$results = array();
- $qcats = $DB->get_records_sql("SELECT itemid, parentitemid AS contextid
+ $qcats = $DB->get_recordset_sql("SELECT itemid, parentitemid AS contextid, info
FROM {backup_ids_temp}
WHERE backupid = ?
AND itemname = 'question_category'", array($restoreid));
foreach ($qcats as $qcat) {
// If this qcat context haven't been acummulated yet, do that
if (!isset($results[$qcat->contextid])) {
- $temprec = self::get_backup_ids_record($restoreid, 'question_category', $qcat->itemid);
+ $info = backup_controller_dbops::decode_backup_temp_info($qcat->info);
// Filter by contextlevel if necessary
- if (is_null($contextlevel) || $contextlevel == $temprec->info->contextlevel) {
- $results[$qcat->contextid] = $temprec->info->contextlevel;
+ if (is_null($contextlevel) || $contextlevel == $info->contextlevel) {
+ $results[$qcat->contextid] = $info->contextlevel;
}
}
}
+ $qcats->close();
// Sort by value (contextlevel from CONTEXT_SYSTEM downto CONTEXT_MODULE)
asort($results);
return $results;
global $DB;
$results = array();
- $qcats = $DB->get_records_sql("SELECT itemid
+ $qcats = $DB->get_recordset_sql("SELECT itemid, info
FROM {backup_ids_temp}
WHERE backupid = ?
AND itemname = 'question_category'
AND parentitemid = ?", array($restoreid, $contextid));
foreach ($qcats as $qcat) {
- $temprec = self::get_backup_ids_record($restoreid, 'question_category', $qcat->itemid);
- $results[$qcat->itemid] = $temprec->info;
+ $results[$qcat->itemid] = backup_controller_dbops::decode_backup_temp_info($qcat->info);
}
+ $qcats->close();
+
return $results;
}
global $DB;
$results = array();
- $qs = $DB->get_records_sql("SELECT itemid
+ $qs = $DB->get_recordset_sql("SELECT itemid, info
FROM {backup_ids_temp}
WHERE backupid = ?
AND itemname = 'question'
AND parentitemid = ?", array($restoreid, $qcatid));
foreach ($qs as $q) {
- $temprec = self::get_backup_ids_record($restoreid, 'question', $q->itemid);
- $results[$q->itemid] = $temprec->info;
+ $results[$q->itemid] = backup_controller_dbops::decode_backup_temp_info($q->info);
}
+ $qs->close();
return $results;
}
$basepath = $basepath . '/files/';// Get backup file pool base
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $rec) {
- $file = (object)unserialize(base64_decode($rec->info));
+ $file = (object)backup_controller_dbops::decode_backup_temp_info($rec->info);
// ignore root dirs (they are created automatically)
if ($file->filepath == '/' && $file->filename == '.') {
$themes = get_list_of_themes(); // Get themes for quick search later
// Iterate over all the included users with newitemid = 0, have to create them
- $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user', 'newitemid' => 0), '', 'itemid, parentitemid');
+ $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user', 'newitemid' => 0), '', 'itemid, parentitemid, info');
foreach ($rs as $recuser) {
- $user = (object)self::get_backup_ids_record($restoreid, 'user', $recuser->itemid)->info;
+ $user = (object)backup_controller_dbops::decode_backup_temp_info($recuser->info);
// if user lang doesn't exist here, use site default
if (!array_key_exists($user->lang, $languages)) {
}
// Iterate over all the included users
- $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user'), '', 'itemid');
+ $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user'), '', 'itemid, info');
foreach ($rs as $recuser) {
- $user = (object)self::get_backup_ids_record($restoreid, 'user', $recuser->itemid)->info;
+ $user = (object)backup_controller_dbops::decode_backup_temp_info($recuser->info);
// Find the correct mnethostid for user before performing any further check
if (empty($user->mnethosturl) || $user->mnethosturl === $CFG->wwwroot) {
global $DB;
// Store external files info in `info` field
- $filerec->info = base64_encode(serialize($filerec)); // Serialize the whole rec in info
+ $filerec->info = backup_controller_dbops::encode_backup_temp_info($filerec); // Encode the whole record into info.
$filerec->backupid = $restoreid;
$DB->insert_record('backup_files_temp', $filerec);
}
$extrarecord['parentitemid'] = $parentitemid;
}
if ($info != null) {
- $extrarecord['info'] = base64_encode(serialize($info));
+ $extrarecord['info'] = backup_controller_dbops::encode_backup_temp_info($info);
}
self::set_backup_ids_cached($restoreid, $itemname, $itemid, $extrarecord);
public static function get_backup_ids_record($restoreid, $itemname, $itemid) {
$dbrec = self::get_backup_ids_cached($restoreid, $itemname, $itemid);
+ // We must test if info is a string, as the cache stores info in object form.
if ($dbrec && isset($dbrec->info) && is_string($dbrec->info)) {
- $dbrec->info = unserialize(base64_decode($dbrec->info));
+ $dbrec->info = backup_controller_dbops::decode_backup_temp_info($dbrec->info);
}
return $dbrec;
// Get the course context
$coursectx = context_course::instance($courseid);
// Get all the mapped roles we have
- $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid');
+ $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid, info, newitemid');
foreach ($rs as $recrole) {
- // Get the complete temp_ids record
- $role = (object)self::get_backup_ids_record($restoreid, 'role', $recrole->itemid);
+ $info = backup_controller_dbops::decode_backup_temp_info($recrole->info);
// If it's one mapped role and we have one name for it
- if (!empty($role->newitemid) && !empty($role->info['nameincourse'])) {
+ if (!empty($recrole->newitemid) && !empty($info['nameincourse'])) {
// If role name doesn't exist, add it
$rolename = new stdclass();
- $rolename->roleid = $role->newitemid;
+ $rolename->roleid = $recrole->newitemid;
$rolename->contextid = $coursectx->id;
if (!$DB->record_exists('role_names', (array)$rolename)) {
- $rolename->name = $role->info['nameincourse'];
+ $rolename->name = $info['nameincourse'];
$DB->insert_record('role_names', $rolename);
}
}
// Drop and check it doesn't exists anymore
backup_controller_dbops::drop_backup_ids_temp_table('testingid');
$this->assertFalse($dbman->table_exists('backup_ids_temp'));
+
+ // Test encoding/decoding of backup_ids_temp,backup_files_temp encode/decode functions.
+ // We need to handle both objects and data elements.
+ $object = new stdClass();
+ $object->item1 = 10;
+ $object->item2 = 'a String';
+ $testarray = array($object, 10, null, 'string', array('a' => 'b', 1 => 1));
+ foreach ($testarray as $item) {
+ $encoded = backup_controller_dbops::encode_backup_temp_info($item);
+ $decoded = backup_controller_dbops::decode_backup_temp_info($encoded);
+ $this->assertEquals($item, $decoded);
+ }
}
/**
if (@rename($filepath, $filedest)) {
return null;
}
- umask(0000);
+ umask($CFG->umaskpermissions);
if (copy($filepath, $filedest)) {
@chmod($filedest, $CFG->filepermissions); // may fail because the permissions may not make sense outside of dataroot
unlink($filepath);
And I should not see "Section 3"
And I press "Continue"
And I click on "Continue" "button" in the ".bcs-current-course" "css_element"
- And I click on "//div[contains(concat(' ', @class, ' '), ' fitem ')][contains(., 'Include calendar events')]/descendant::img" "xpath_element"
- And I click on "setting_root_logs" "checkbox" in the "//div[contains(@class, 'fitem')][contains(., 'Include course logs')]" "xpath_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 I press "Cancel"
And I click on "Cancel" "button" in the ".confirmation-dialogue" "css_element"
$this->find_link($backupcourse)->click();
// Click the backup link.
- $this->find_link('Backup')->click();
+ $this->find_link(get_string('backup'))->click();
// Initial settings.
$this->fill_backup_restore_form($options);
- $this->find_button('Next')->press();
+ $this->find_button(get_string('backupstage1action', 'backup'))->press();
// Schema settings.
$this->fill_backup_restore_form($options);
- $this->find_button('Next')->press();
+ $this->find_button(get_string('backupstage2action', 'backup'))->press();
// Confirmation and review, backup filename can also be specified.
$this->fill_backup_restore_form($options);
- $this->find_button('Perform backup')->press();
+ $this->find_button(get_string('backupstage4action', 'backup'))->press();
// Waiting for it to finish.
$this->wait(10);
// Last backup continue button.
- $this->find_button('Continue')->press();
+ $this->find_button(get_string('backupstage16action', 'backup'))->press();
}
/**
// Click the course link.
$this->find_link($tocourse)->click();
- // Click the backup link.
- $this->find_link('Import')->click();
+ // Click the import link.
+ $this->find_link(get_string('import'))->click();
// Select the course.
$exception = new ExpectationException('"' . $fromcourse . '" course not found in the list of courses to import from', $this->getSession());
- $fromcourse = str_replace("'", "\'", $fromcourse);
- $xpath = "//div[contains(concat(' ', @class, ' '), ' ics-results ')]" .
- "/descendant::tr[contains(., '" . $fromcourse . "')]" .
+ // The argument should be converted to an xpath literal.
+ $fromcourse = $this->getSession()->getSelectorsHandler()->xpathLiteral($fromcourse);
+ $xpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' ics-results ')]" .
+ "/descendant::tr[contains(., $fromcourse)]" .
"/descendant::input[@type='radio']";
$radionode = $this->find('xpath', $xpath, $exception);
$radionode->check();
$radionode->click();
- $this->find_button('Continue')->press();
+ $this->find_button(get_string('continue'))->press();
// Initial settings.
$this->fill_backup_restore_form($options);
- $this->find_button('Next')->press();
+ $this->find_button(get_string('importbackupstage1action', 'backup'))->press();
// Schema settings.
$this->fill_backup_restore_form($options);
- $this->find_button('Next')->press();
+ $this->find_button(get_string('importbackupstage2action', 'backup'))->press();
// Run it.
- $this->find_button('Perform import')->press();
+ $this->find_button(get_string('importbackupstage4action', 'backup'))->press();
$this->wait();
// Continue and redirect to 'to' course.
- $this->find_button('Continue')->press();
+ $this->find_button(get_string('continue'))->press();
}
/**
// Confirm restore.
$this->select_backup($backupfilename);
+ // The argument should be converted to an xpath literal.
+ $existingcourse = $this->getSession()->getSelectorsHandler()->xpathLiteral($existingcourse);
+
// Selecting the specified course (we can not call behat_forms::select_radio here as is in another behat subcontext).
- $existingcourse = str_replace("'", "\'", $existingcourse);
- $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-existing-course')]" .
+ $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-existing-course ')]" .
"/descendant::div[@class='restore-course-search']" .
- "/descendant::tr[contains(., '" . $existingcourse . "')]" .
+ "/descendant::tr[contains(., $existingcourse)]" .
"/descendant::input[@type='radio']");
$radionode->check();
$radionode->click();
// Pressing the continue button of the restore into an existing course section.
- $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-existing-course')]/descendant::input[@type='submit'][@value='Continue']");
+ $continuenode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-existing-course ')]" .
+ "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']");
$continuenode->click();
$this->wait();
$this->select_backup($backupfilename);
// The first category in the list.
- $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-new-course')]" .
+ $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-new-course ')]" .
"/descendant::div[@class='restore-course-search']" .
"/descendant::input[@type='radio']");
$radionode->check();
$radionode->click();
// Pressing the continue button of the restore into an existing course section.
- $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-new-course')]/descendant::input[@type='submit'][@value='Continue']");
+ $continuenode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-new-course ')]" .
+ "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']");
$continuenode->click();
$this->wait();
$this->select_backup($backupfilename);
// Merge without deleting radio option.
- $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" .
+ $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" .
"/descendant::input[@type='radio'][@name='target'][@value='1']");
$radionode->check();
$radionode->click();
// Pressing the continue button of the restore merging section.
- $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" .
- "/descendant::input[@type='submit'][@value='Continue']");
+ $continuenode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" .
+ "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']");
$continuenode->click();
$this->wait();
$this->select_backup($backupfilename);
// Delete contents radio option.
- $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" .
+ $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" .
"/descendant::input[@type='radio'][@name='target'][@value='0']");
$radionode->check();
$radionode->click();
// Pressing the continue button of the restore merging section.
- $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" .
- "/descendant::input[@type='submit'][@value='Continue']");
+ $continuenode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" .
+ "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']");
$continuenode->click();
$this->wait();
// Using xpath as there are other restore links before this one.
$exception = new ExpectationException('The "' . $backupfilename . '" backup file can not be found in this page', $this->getSession());
- $xpath = "//tr[contains(., '" . $backupfilename . "')]/descendant::a[contains(., 'Restore')]";
+
+ // The argument should be converted to an xpath literal.
+ $backupfilename = $this->getSession()->getSelectorsHandler()->xpathLiteral($backupfilename);
+
+ $xpath = "//tr[contains(., $backupfilename)]/descendant::a[contains(., '" . get_string('restore') . "')]";
$restorelink = $this->find('xpath', $xpath, $exception);
$restorelink->click();
// Confirm the backup contents.
- $restore = $this->find_button('Continue')->press();
+ $restore = $this->find_button(get_string('continue'))->press();
}
/**
// Settings.
$this->fill_backup_restore_form($options);
- $this->find_button('Next')->press();
+ $this->find_button(get_string('restorestage4action', 'backup'))->press();
// Schema.
$this->fill_backup_restore_form($options);
- $this->find_button('Next')->press();
+ $this->find_button(get_string('restorestage8action', 'backup'))->press();
// Review, no options here.
- $this->find_button('Perform restore')->press();
+ $this->find_button(get_string('restorestage16action', 'backup'))->press();
$this->wait(10);
// Last restore continue button, redirected to restore course after this.
- $this->find_button('Continue')->press();
+ $this->find_button(get_string('restorestage32action', 'backup'))->press();
}
/**
*/
protected function wait($timeout = false) {
+ if (!$this->running_javascript()) {
+ return;
+ }
+
if (!$timeout) {
$timeout = self::TIMEOUT;
}
protected $groupedpaths; // Paths we are requesting grouped
protected $currentdata; // Where we'll be acummulating data
+ /**
+ * Keep cache of parent directory paths for XML parsing.
+ * @var array
+ */
+ protected $parentcache = array();
+
+ /**
+ * Remaining space for parent directory paths.
+ * @var integer
+ */
+ protected $parentcacheavailablesize = 2048;
+
public function __construct(array $paths = array()) {
$this->groupedpaths = array();
$this->currentdata = null;
$a->child = $found;
throw new progressive_parser_exception('xml_grouped_child_found', $a);
}
- $this->groupedpaths[] = $path;
+ $this->groupedpaths[$path] = true;
}
parent::add_path($path);
}
}
protected function path_is_grouped($path) {
- return in_array($path, $this->groupedpaths);
+ return isset($this->groupedpaths[$path]);
}
/**
* false if not
*/
protected function grouped_parent_exists($path) {
- $parentpath = progressive_parser::dirname($path);
+ $parentpath = $this->get_parent_path($path);
+
while ($parentpath != '/') {
if ($this->path_is_grouped($parentpath)) {
return $parentpath;
}
- $parentpath = progressive_parser::dirname($parentpath);
+ $parentpath = $this->get_parent_path($parentpath);
}
return false;
}
+ /**
+ * Get the parent path using a local cache for performance.
+ *
+ * @param $path string The pathname you wish to obtain the parent name for.
+ * @return string The parent pathname.
+ */
+ protected function get_parent_path($path) {
+ if (!isset($this->parentcache[$path])) {
+ $this->parentcache[$path] = progressive_parser::dirname($path);
+ $this->parentcacheavailablesize--;
+ if ($this->parentcacheavailablesize < 0) {
+ // Older first is cheaper than LRU. We use 10% as items are grouped together and the large quiz
+ // restore from MDL-40585 used only 600 parent paths. This is an XML heirarchy, so common paths
+ // are grouped near each other. eg; /question_bank/question_category/question/element. After keeping
+ // question_bank paths in the cache when we move to another area and the question_bank cache is not
+ // useful any longer.
+ $this->parentcache = array_slice($this->parentcache, 200, null, true);
+ $this->parentcacheavailablesize += 200;
+ }
+ }
+ return $this->parentcache[$path];
+ }
+
+
/**
* Function that will look for any grouped
* child for the given path, returning it if found,
*/
protected function grouped_child_exists($path) {
$childpath = $path . '/';
- foreach ($this->groupedpaths as $groupedpath) {
+ foreach ($this->groupedpaths as $groupedpath => $set) {
if (strpos($groupedpath, $childpath) === 0) {
return $groupedpath;
}
* Push badges to backpack.
*/
function addtobackpack(event, args) {
- OpenBadges.issue([args.assertion], function(errors, successes) { });
+ var badgetable = Y.one('#issued-badge-table');
+ var errordiv = Y.one('#addtobackpack-error');
+ var errortext = M.util.get_string('error:backpackproblem', 'badges');
+ var errorhtml = '<div id="addtobackpack-error" class="box boxaligncenter notifyproblem">' + errortext + '</div>';
+
+ if (typeof OpenBadges !== 'undefined') {
+ OpenBadges.issue([args.assertion], function(errors, successes) { });
+ } else {
+ // Add error div if it doesn't exist yet.
+ if (!errordiv) {
+ var badgerror = Y.Node.create(errorhtml);
+ badgetable.insert(badgerror, 'before');
+ }
+ }
}
/**
navigation_node::override_active_url($url);
}
-// TODO: Better way of pushing badges to Mozilla backpack?
-if (!empty($CFG->badges_allowexternalbackpack)) {
- $PAGE->requires->js(new moodle_url('http://backpack.openbadges.org/issuer.js'), true);
-}
+// Include JS files for backpack support.
+badges_setup_backpack_js();
echo $OUTPUT->header();
$PAGE->set_heading($title);
$PAGE->set_pagelayout('mydashboard');
-// TODO: Better way of pushing badges to Mozilla backpack?
-if (!empty($CFG->badges_allowexternalbackpack)) {
- $PAGE->requires->js(new moodle_url('http://backpack.openbadges.org/issuer.js'), true);
- $PAGE->requires->js('/badges/backpack.js', true);
-}
+// Include JS files for backpack support.
+badges_setup_backpack_js();
$output = $PAGE->get_renderer('core', 'badges');
$badges = badges_get_user_badges($USER->id);
$today = strtotime($today_date);
$table = new html_table();
+ $table->id = 'issued-badge-table';
$imagetable = new html_table();
$imagetable->attributes = array('class' => 'clearfix badgeissuedimage');
$expiration = isset($issued['expires']) ? strtotime($issued['expires']) : $today + 1;
if (!empty($CFG->badges_allowexternalbackpack) && ($expiration > $today) && badges_user_has_backpack($USER->id)) {
$assertion = new moodle_url('/badges/assertion.php', array('b' => $ibadge->hash));
+ $action = new component_action('click', 'addtobackpack', array('assertion' => $assertion->out(false)));
$attributes = array(
- 'type' => 'button',
- 'value' => get_string('addtobackpack', 'badges'),
- 'onclick' => 'OpenBadges.issue(["' . $assertion->out(false) . '"], function(errors, successes) { })');
+ 'type' => 'button',
+ 'id' => 'addbutton',
+ 'value' => get_string('addtobackpack', 'badges'));
$tobackpack = html_writer::tag('input', '', $attributes);
+ $this->output->add_action_handler($action, 'addbutton');
$imagetable->data[] = array($tobackpack);
}
}
get_string('downloadall'), 'POST', array('class' => 'activatebadge'));
// Local badges.
- $localhtml = html_writer::start_tag('fieldset', array('class' => 'generalbox'));
+ $localhtml = html_writer::start_tag('fieldset', array('id' => 'issued-badge-table', 'class' => 'generalbox'));
$localhtml .= html_writer::tag('legend',
$this->output->heading_with_help(get_string('localbadges', 'badges', $SITE->fullname), 'localbadgesh', 'badges'));
if ($badges->badges) {
$commentstextarea = $this->find('css', '.comment-area textarea', $exception);
$commentstextarea->setValue($comment);
- $this->find_link('Save comment')->click();
+ $this->find_link(get_string('savecomment'))->click();
// Wait for the AJAX request.
$this->getSession()->wait(4 * 1000, false);
$exception = new ElementNotFoundException($this->getSession(), '"' . $comment . '" comment ');
- $commentxpath = "//div[contains(concat(' ', @class, ' '), ' block_comments ')]" .
- "/descendant::div[@class='comment-message'][contains(., '" . $comment . "')]";
+ // Using xpath liternal to avoid possible problems with comments containing quotes.
+ $commentliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($comment);
+
+ $commentxpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' block_comments ')]" .
+ "/descendant::div[@class='comment-message'][contains(., $commentliteral)]";
$commentnode = $this->find('xpath', $commentxpath, $exception);
// Click on delete icon.
$deleteicon->click();
// Yes confirm.
- $confirmnode = $this->find('xpath', "//div[@class='comment-delete-confirm']/descendant::a[contains(., 'Yes')]");
+ $confirmnode = $this->find('xpath', "//div[@class='comment-delete-confirm']/descendant::a[contains(., '" . get_string('yes') . "')]");
$confirmnode->click();
// Wait for the AJAX request.
// No need to pass title through s() here as it will be done automatically by html_writer.
$attributes = array('title' => $course->fullname);
- if (empty($course->visible)) {
- $attributes['class'] = 'dimmed';
- }
if ($course->id > 0) {
+ if (empty($course->visible)) {
+ $attributes['class'] = 'dimmed';
+ }
$courseurl = new moodle_url('/course/view.php', array('id' => $course->id));
$coursefullname = format_string($course->fullname, true, $course->id);
$link = html_writer::link($courseurl, $coursefullname, $attributes);
}
/**
- * Returns all recent enrollments
+ * Returns all recent enrolments.
+ *
+ * This function previously used get_recent_enrolments located in lib/deprecatedlib.php which would
+ * return an empty array which was identified in MDL-36993. The use of this function outside the
+ * deprecated lib was removed in MDL-40649.
*
* @todo MDL-36993 this function always return empty array
* @return array array of entries from {user} table
*/
protected function get_recent_enrolments() {
- return get_recent_enrolments($this->page->course->id, $this->get_timestart());
+ return array();
}
/**
* @param string $blockname
*/
public function i_add_the_block($blockname) {
- $steps = new Given('I select "' . $blockname . '" from "bui_addblock"');
+ $steps = new Given('I select "' . $this->escape($blockname) . '" from "bui_addblock"');
// If we are running without javascript we need to submit the form.
if (!$this->running_javascript()) {
$steps = array(
$steps,
- new Given('I click on "Go" "button" in the "#add_block" "css_element"')
+ new Given('I click on "' . get_string('go') . '" "button" in the "#add_block" "css_element"')
);
}
return $steps;
And I press "Save changes"
And I follow "Course 1"
# The first block matching the pattern should be top-left block
- And I should see "Comments" in the "//*[@id='region-pre']/descendant::div[contains(concat(' ', @class, ' '), ' block ')]" "xpath_element"
+ And I should see "Comments" in the "//*[@id='region-pre']/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' block ')]" "xpath_element"
fflush($handle);
fclose($handle);
$locking->unlock('configwrite', 'config');
+ @chmod($cachefile, $CFG->filepermissions);
// Tell PHP to recompile the script.
core_component::invalidate_opcode_php_cache($cachefile);
} else {
$this->definition = $definition;
$hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id());
$this->path = $this->filestorepath.'/'.$hash;
- make_writable_directory($this->path);
+ make_writable_directory($this->path, false);
if ($this->prescan && $definition->get_mode() !== self::MODE_REQUEST) {
$this->prescan = false;
}
return $this->path . '/' . $key . '.cache';
} else {
// We are using a single subdirectory to achieve 1 level.
- $subdir = substr($key, 0, 3);
+ // We suffix the subdir so it does not clash with any windows
+ // reserved filenames like 'con'.
+ $subdir = substr($key, 0, 3) . '-cache';
$dir = $this->path . '/' . $subdir;
if ($create) {
// Create the directory. This function does it recursivily!
- make_writable_directory($dir);
+ make_writable_directory($dir, false);
}
return $dir . '/' . $key . '.cache';
}
// Finally rename the temp file to the desired file, returning the true|false result.
$result = rename($tempfile, $file);
+ @chmod($file, $this->cfg->filepermissions);
if (!$result) {
// Failed to rename, don't leave files lying around.
@unlink($tempfile);
$form->addElement('text', 'prefix', get_string('prefix', 'cachestore_memcache'),
array('maxlength' => 5, 'size' => 5));
$form->addHelpButton('prefix', 'prefix', 'cachestore_memcache');
- $form->setType('prefix', PARAM_ALPHAEXT);
+ $form->setType('prefix', PARAM_TEXT); // We set to text but we have a rule to limit to alphanumext.
$form->setDefault('prefix', 'mdl_');
+ $form->addRule('prefix', get_string('prefixinvalid', 'cachestore_memcache'), 'regex', '#^[a-zA-Z0-9\-_]+$#');
}
}
\ No newline at end of file
$string['prefix_help'] = 'This prefix is used for all key names on the memcache server.
* If you only have one Moodle instance using this server, you can leave this value default.
* Due to key length restrictions, a maximum of 5 characters is permitted.';
+$string['prefixinvalid'] = 'Invalid prefix. You can only use a-z A-Z 0-9-_.';
$string['servers'] = 'Servers';
$string['servers_help'] = 'This sets the servers that should be utilised by this memcache adapter.
Servers should be defined one per line and consist of a server address and optionally a port and weight.
* Generates an instance of the cache store that can be used for testing.
*
* @param cache_definition $definition
- * @return false
+ * @return cachestore_memcache|false
*/
public static function initialise_test_instance(cache_definition $definition) {
if (!self::are_requirements_met()) {
protected function get_class_name() {
return 'cachestore_memcache';
}
-}
\ No newline at end of file
+
+ /**
+ * Tests the valid keys to ensure they work.
+ */
+ public function test_valid_keys() {
+ $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcache', 'phpunit_test');
+ $instance = cachestore_memcache::initialise_test_instance($definition);
+
+ if (!$instance) { // Something prevented memcache store to be inited (extension, TEST_CACHESTORE_MEMCACHE_TESTSERVERS...).
+ $this->markTestSkipped();
+ }
+
+ $keys = array(
+ // Alphanumeric.
+ 'abc', 'ABC', '123', 'aB1', '1aB',
+ // Hyphens.
+ 'a-1', '1-a', '-a1', 'a1-',
+ // Underscores.
+ 'a_1', '1_a', '_a1', 'a1_'
+ );
+ foreach ($keys as $key) {
+ $this->assertTrue($instance->set($key, $key), "Failed to set key `$key`");
+ }
+ foreach ($keys as $key) {
+ $this->assertEquals($key, $instance->get($key), "Failed to get key `$key`");
+ }
+ $values = $instance->get_many($keys);
+ foreach ($values as $key => $value) {
+ $this->assertEquals($key, $value);
+ }
+ }
+}
$form->addElement('select', 'serialiser', get_string('useserialiser', 'cachestore_memcached'), $serialiseroptions);
$form->addHelpButton('serialiser', 'useserialiser', 'cachestore_memcached');
$form->setDefault('serialiser', Memcached::SERIALIZER_PHP);
- $form->setType('serialiser', PARAM_NUMBER);
+ $form->setType('serialiser', PARAM_INT);
$form->addElement('text', 'prefix', get_string('prefix', 'cachestore_memcached'), array('size' => 16));
- $form->setType('prefix', PARAM_ALPHANUM);
+ $form->setType('prefix', PARAM_TEXT); // We set to text but we have a rule to limit to alphanumext.
$form->addHelpButton('prefix', 'prefix', 'cachestore_memcached');
+ $form->addRule('prefix', get_string('prefixinvalid', 'cachestore_memcached'), 'regex', '#^[a-zA-Z0-9\-_]+$#');
$hashoptions = cachestore_memcached::config_get_hash_options();
$form->addElement('select', 'hash', get_string('hash', 'cachestore_memcached'), $hashoptions);
$form->setDefault('bufferwrites', 0);
$form->setType('bufferwrites', PARAM_BOOL);
}
-}
\ No newline at end of file
+}
$string['pluginname'] = 'Memcached';
$string['prefix'] = 'Prefix key';
$string['prefix_help'] = 'This can be used to create a "domain" for your item keys allowing you to create multiple memcached stores on a single memcached installation. It cannot be longer than 16 characters in order to ensure key length issues are not encountered.';
+$string['prefixinvalid'] = 'Invalid prefix. You can only use a-z A-Z 0-9-_.';
$string['serialiser_igbinary'] = 'The igbinary serializer.';
$string['serialiser_json'] = 'The JSON serializer.';
$string['serialiser_php'] = 'The default PHP serializer.';
$options[Memcached::SERIALIZER_JSON] = get_string('serialiser_json', 'cachestore_memcached');
}
if (Memcached::HAVE_IGBINARY) {
- $options[Memcached::SERIALIZER_IGBINARY] = get_string('serialiser_php', 'cachestore_memcached');
+ $options[Memcached::SERIALIZER_IGBINARY] = get_string('serialiser_igbinary', 'cachestore_memcached');
}
return $options;
}
* Generates an instance of the cache store that can be used for testing.
*
* @param cache_definition $definition
- * @return false
+ * @return cachestore_memcached|false
*/
public static function initialise_test_instance(cache_definition $definition) {
protected function get_class_name() {
return 'cachestore_memcached';
}
-}
\ No newline at end of file
+
+ /**
+ * Tests the valid keys to ensure they work.
+ */
+ public function test_valid_keys() {
+ $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcached', 'phpunit_test');
+ $instance = cachestore_memcached::initialise_test_instance($definition);
+
+ if (!$instance) { // Something prevented memcached store to be inited (extension, TEST_CACHESTORE_MEMCACHED_TESTSERVERS...).
+ $this->markTestSkipped();
+ }
+
+ $keys = array(
+ // Alphanumeric.
+ 'abc', 'ABC', '123', 'aB1', '1aB',
+ // Hyphens.
+ 'a-1', '1-a', '-a1', 'a1-',
+ // Underscores.
+ 'a_1', '1_a', '_a1', 'a1_'
+ );
+ foreach ($keys as $key) {
+ $this->assertTrue($instance->set($key, $key), "Failed to set key `$key`");
+ }
+ foreach ($keys as $key) {
+ $this->assertEquals($key, $instance->get($key), "Failed to get key `$key`");
+ }
+ $values = $instance->get_many($keys);
+ foreach ($values as $key => $value) {
+ $this->assertEquals($key, $value);
+ }
+ }
+}
*/
protected $ttl = 0;
+ /**
+ * The maximum size for the store, or false if there isn't one.
+ * @var bool
+ */
+ protected $maxsize = false;
+
+ /**
+ * The number of items currently being stored.
+ * @var int
+ */
+ protected $storecount = 0;
+
/**
* Constructs the store instance.
*
$this->storeid = $definition->generate_definition_hash();
$this->store = &self::register_store_id($definition->get_id());
$this->ttl = $definition->get_ttl();
+ $maxsize = $definition->get_maxsize();
+ if ($maxsize !== null) {
+ // Must be a positive int.
+ $this->maxsize = abs((int)$maxsize);
+ $this->storecount = count($this->store);
+ }
}
/**
*
* @param string $key The key to use.
* @param mixed $data The data to set.
+ * @param bool $testmaxsize If set to true then we test the maxsize arg and reduce if required.
* @return bool True if the operation was a success false otherwise.
*/
- public function set($key, $data) {
+ public function set($key, $data, $testmaxsize = true) {
+ $testmaxsize = ($testmaxsize && $this->maxsize !== false);
+ if ($testmaxsize) {
+ $increment = (!isset($this->store[$key]));
+ }
if ($this->ttl == 0) {
$this->store[$key][0] = $data;
} else {
$this->store[$key] = array($data, cache::now());
}
+ if ($testmaxsize && $increment) {
+ $this->storecount++;
+ if ($this->storecount > $this->maxsize) {
+ $this->reduce_for_maxsize();
+ }
+ }
return true;
}
public function set_many(array $keyvaluearray) {
$count = 0;
foreach ($keyvaluearray as $pair) {
- $this->set($pair['key'], $pair['value']);
+ $this->set($pair['key'], $pair['value'], false);
$count++;
}
+ if ($this->maxsize !== false) {
+ $this->storecount += $count;
+ if ($this->storecount > $this->maxsize) {
+ $this->reduce_for_maxsize();
+ }
+ }
return $count;
}
public function delete($key) {
$result = isset($this->store[$key]);
unset($this->store[$key]);
+ if ($this->maxsize !== false) {
+ $this->storecount--;
+ }
return $result;
}
}
unset($this->store[$key]);
}
+ if ($this->maxsize !== false) {
+ $this->storecount -= $count;
+ }
return $count;
}
*/
public function purge() {
$this->store = array();
+ // Don't worry about checking if we're using max size just set it as thats as fast as the check.
+ $this->storecount = 0;
return true;
}
+ /**
+ * Reduces the size of the array if maxsize has been hit.
+ *
+ * This function reduces the size of the store reducing it by 10% of its maxsize.
+ * It removes the oldest items in the store when doing this.
+ * The reason it does this an doesn't use a least recently used system is purely the overhead such a system
+ * requires. The current approach is focused on speed, MUC already adds enough overhead to static/session caches
+ * and avoiding more is of benefit.
+ *
+ * @return int
+ */
+ protected function reduce_for_maxsize() {
+ $diff = $this->storecount - $this->maxsize;
+ if ($diff < 1) {
+ return 0;
+ }
+ // Reduce it by an extra 10% to avoid calling this repetitively if we are in a loop.
+ $diff += floor($this->maxsize / 10);
+ $this->store = array_slice($this->store, $diff, null, true);
+ $this->storecount -= $diff;
+ return $diff;
+ }
+
/**
* Returns true if the user can add an instance of the store plugin.
*
* Generates an instance of the cache store that can be used for testing.
*
* @param cache_definition $definition
- * @return false
+ * @return cachestore_session
*/
public static function initialise_test_instance(cache_definition $definition) {
// Do something here perhaps.
protected function get_class_name() {
return 'cachestore_session';
}
+
+ /**
+ * Test the maxsize option.
+ */
+ public function test_maxsize() {
+ $defid = 'phpunit/testmaxsize';
+ $config = cache_config_phpunittest::instance();
+ $config->phpunit_add_definition($defid, array(
+ 'mode' => cache_store::MODE_SESSION,
+ 'component' => 'phpunit',
+ 'area' => 'testmaxsize',
+ 'maxsize' => 3
+ ));
+ $definition = cache_definition::load($defid, $config->get_definition_by_id($defid));
+ $instance = cachestore_session::initialise_test_instance($definition);
+
+ $this->assertTrue($instance->set('key1', 'value1'));
+ $this->assertTrue($instance->set('key2', 'value2'));
+ $this->assertTrue($instance->set('key3', 'value3'));
+
+ $this->assertTrue($instance->has('key1'));
+ $this->assertTrue($instance->has('key2'));
+ $this->assertTrue($instance->has('key3'));
+
+ $this->assertTrue($instance->set('key4', 'value4'));
+ $this->assertTrue($instance->set('key5', 'value5'));
+
+ $this->assertFalse($instance->has('key1'));
+ $this->assertFalse($instance->has('key2'));
+ $this->assertTrue($instance->has('key3'));
+ $this->assertTrue($instance->has('key4'));
+ $this->assertTrue($instance->has('key5'));
+
+ $this->assertFalse($instance->get('key1'));
+ $this->assertFalse($instance->get('key2'));
+ $this->assertEquals('value3', $instance->get('key3'));
+ $this->assertEquals('value4', $instance->get('key4'));
+ $this->assertEquals('value5', $instance->get('key5'));
+
+ // Test adding one more.
+ $this->assertTrue($instance->set('key6', 'value6'));
+ $this->assertFalse($instance->get('key3'));
+
+ // Test reducing and then adding to make sure we don't lost one.
+ $this->assertTrue($instance->delete('key6'));
+ $this->assertTrue($instance->set('key7', 'value7'));
+ $this->assertEquals('value4', $instance->get('key4'));
+
+ // Set the same key three times to make sure it doesn't count overrides.
+ for ($i = 0; $i < 3; $i++) {
+ $this->assertTrue($instance->set('key8', 'value8'));
+ }
+ $this->assertEquals('value7', $instance->get('key7'), 'Overrides are incorrectly incrementing size');
+
+ // Test adding many.
+ $this->assertEquals(3, $instance->set_many(array(
+ array('key' => 'keyA', 'value' => 'valueA'),
+ array('key' => 'keyB', 'value' => 'valueB'),
+ array('key' => 'keyC', 'value' => 'valueC')
+ )));
+ $this->assertEquals(array(
+ 'key4' => false,
+ 'key5' => false,
+ 'key6' => false,
+ 'key7' => false,
+ 'keyA' => 'valueA',
+ 'keyB' => 'valueB',
+ 'keyC' => 'valueC'
+ ), $instance->get_many(array(
+ 'key4', 'key5', 'key6', 'key7', 'keyA', 'keyB', 'keyC'
+ )));
+ }
}
\ No newline at end of file
*/
protected $ttl = 0;
+ /**
+ * The maximum size for the store, or false if there isn't one.
+ * @var bool
+ */
+ protected $maxsize = false;
+
+ /**
+ * The number of items currently being stored.
+ * @var int
+ */
+ protected $storecount = 0;
+
/**
* Constructs the store instance.
*
$this->storeid = $definition->generate_definition_hash();
$this->store = &self::register_store_id($this->storeid);
$this->ttl = $definition->get_ttl();
+ $maxsize = $definition->get_maxsize();
+ if ($maxsize !== null) {
+ // Must be a positive int.
+ $this->maxsize = abs((int)$maxsize);
+ $this->storecount = count($this->store);
+ }
}
/**
*
* @param string $key The key to use.
* @param mixed $data The data to set.
+ * @param bool $testmaxsize If set to true then we test the maxsize arg and reduce if required.
* @return bool True if the operation was a success false otherwise.
*/
- public function set($key, $data) {
+ public function set($key, $data, $testmaxsize = true) {
+ $testmaxsize = ($testmaxsize && $this->maxsize !== false);
+ if ($testmaxsize) {
+ $increment = (!isset($this->store[$key]));
+ }
if ($this->ttl == 0) {
$this->store[$key][0] = $data;
} else {
$this->store[$key] = array($data, cache::now());
}
+ if ($testmaxsize && $increment) {
+ $this->storecount++;
+ if ($this->storecount > $this->maxsize) {
+ $this->reduce_for_maxsize();
+ }
+ }
return true;
}
public function set_many(array $keyvaluearray) {
$count = 0;
foreach ($keyvaluearray as $pair) {
- $this->set($pair['key'], $pair['value']);
+ // Don't test the maxsize here. We'll do it once when we are done.
+ $this->set($pair['key'], $pair['value'], false);
$count++;
}
+ if ($this->maxsize !== false) {
+ $this->storecount += $count;
+ if ($this->storecount > $this->maxsize) {
+ $this->reduce_for_maxsize();
+ }
+ }
return $count;
}
public function delete($key) {
$result = isset($this->store[$key]);
unset($this->store[$key]);
+ if ($this->maxsize !== false) {
+ $this->storecount--;
+ }
return $result;
}
}
unset($this->store[$key]);
}
+ if ($this->maxsize !== false) {
+ $this->storecount -= $count;
+ }
return $count;
}
public function purge() {
$this->flush_store_by_id($this->storeid);
$this->store = &self::register_store_id($this->storeid);
+ // Don't worry about checking if we're using max size just set it as thats as fast as the check.
+ $this->storecount = 0;
return true;
}
+ /**
+ * Reduces the size of the array if maxsize has been hit.
+ *
+ * This function reduces the size of the store reducing it by 10% of its maxsize.
+ * It removes the oldest items in the store when doing this.
+ * The reason it does this an doesn't use a least recently used system is purely the overhead such a system
+ * requires. The current approach is focused on speed, MUC already adds enough overhead to static/session caches
+ * and avoiding more is of benefit.
+ *
+ * @return int
+ */
+ protected function reduce_for_maxsize() {
+ $diff = $this->storecount - $this->maxsize;
+ if ($diff < 1) {
+ return 0;
+ }
+ // Reduce it by an extra 10% to avoid calling this repetitively if we are in a loop.
+ $diff += floor($this->maxsize / 10);
+ $this->store = array_slice($this->store, $diff, null, true);
+ $this->storecount -= $diff;
+ return $diff;
+ }
+
/**
* Returns true if the user can add an instance of the store plugin.
*
* Generates an instance of the cache store that can be used for testing.
*
* @param cache_definition $definition
- * @return false
+ * @return cachestore_static
*/
public static function initialise_test_instance(cache_definition $definition) {
// Do something here perhaps.
<?php
-// This static is part of Moodle - http://moodle.org/
+// 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
protected function get_class_name() {
return 'cachestore_static';
}
+
+ /**
+ * Test the maxsize option.
+ */
+ public function test_maxsize() {
+ $defid = 'phpunit/testmaxsize';
+ $config = cache_config_phpunittest::instance();
+ $config->phpunit_add_definition($defid, array(
+ 'mode' => cache_store::MODE_REQUEST,
+ 'component' => 'phpunit',
+ 'area' => 'testmaxsize',
+ 'maxsize' => 3
+ ));
+ $definition = cache_definition::load($defid, $config->get_definition_by_id($defid));
+ $instance = cachestore_static::initialise_test_instance($definition);
+
+ $this->assertTrue($instance->set('key1', 'value1'));
+ $this->assertTrue($instance->set('key2', 'value2'));
+ $this->assertTrue($instance->set('key3', 'value3'));
+
+ $this->assertTrue($instance->has('key1'));
+ $this->assertTrue($instance->has('key2'));
+ $this->assertTrue($instance->has('key3'));
+
+ $this->assertTrue($instance->set('key4', 'value4'));
+ $this->assertTrue($instance->set('key5', 'value5'));
+
+ $this->assertFalse($instance->has('key1'));
+ $this->assertFalse($instance->has('key2'));
+ $this->assertTrue($instance->has('key3'));
+ $this->assertTrue($instance->has('key4'));
+ $this->assertTrue($instance->has('key5'));
+
+ $this->assertFalse($instance->get('key1'));
+ $this->assertFalse($instance->get('key2'));
+ $this->assertEquals('value3', $instance->get('key3'));
+ $this->assertEquals('value4', $instance->get('key4'));
+ $this->assertEquals('value5', $instance->get('key5'));
+
+ // Test adding one more.
+ $this->assertTrue($instance->set('key6', 'value6'));
+ $this->assertFalse($instance->get('key3'));
+
+ // Test reducing and then adding to make sure we don't lost one.
+ $this->assertTrue($instance->delete('key6'));
+ $this->assertTrue($instance->set('key7', 'value7'));
+ $this->assertEquals('value4', $instance->get('key4'));
+
+ // Set the same key three times to make sure it doesn't count overrides.
+ for ($i = 0; $i < 3; $i++) {
+ $this->assertTrue($instance->set('key8', 'value8'));
+ }
+
+ $this->assertEquals('value7', $instance->get('key7'), 'Overrides are incorrectly incrementing size');
+
+ // Test adding many.
+ $this->assertEquals(3, $instance->set_many(array(
+ array('key' => 'keyA', 'value' => 'valueA'),
+ array('key' => 'keyB', 'value' => 'valueB'),
+ array('key' => 'keyC', 'value' => 'valueC')
+ )));
+ $this->assertEquals(array(
+ 'key4' => false,
+ 'key5' => false,
+ 'key6' => false,
+ 'key7' => false,
+ 'keyA' => 'valueA',
+ 'keyB' => 'valueB',
+ 'keyC' => 'valueC'
+ ), $instance->get_many(array(
+ 'key4', 'key5', 'key6', 'key7', 'keyA', 'keyB', 'keyC'
+ )));
+ }
}
\ No newline at end of file
}
}
+ /**
+ * Tests for cache keys that would break on windows.
+ */
+ public function test_windows_nasty_keys() {
+ $instance = cache_config_phpunittest::instance();
+ $instance->phpunit_add_definition('phpunit/windowskeytest', array(
+ 'mode' => cache_store::MODE_APPLICATION,
+ 'component' => 'phpunit',
+ 'area' => 'windowskeytest',
+ 'simplekeys' => true,
+ 'simpledata' => true
+ ));
+ $cache = cache::make('phpunit', 'windowskeytest');
+ $this->assertTrue($cache->set('contest', 'test data 1'));
+ $this->assertEquals('test data 1', $cache->get('contest'));
+ }
+
/**
* Tests the default application cache
*/
* @param cache_loader $cache
*/
protected function run_on_cache(cache_loader $cache) {
- $key = 'testkey';
+ $key = 'contestkey';
$datascalars = array('test data', null);
- $dataarray = array('test' => 'data', 'part' => 'two');
+ $dataarray = array('contest' => 'data', 'part' => 'two');
$dataobject = (object)$dataarray;
foreach ($datascalars as $datascalar) {
// OK data added, data invalidated, and invalidation time has been set.
// Now we need to manually add back the data and adjust the invalidation time.
$hash = md5(cache_store::MODE_APPLICATION.'/phpunit/eventinvalidationtest/'.$CFG->wwwroot.'phpunit');
- $timefile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/las/lastinvalidation-$hash.cache";
+ $timefile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/las-cache/lastinvalidation-$hash.cache";
// Make sure the file is correct.
$this->assertTrue(file_exists($timefile));
$timecont = serialize(cache::now() - 60); // Back 60sec in the past to force it to re-invalidate.
file_put_contents($timefile, $timecont);
$this->assertTrue(file_exists($timefile));
- $datafile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/tes/testkey1-$hash.cache";
+ $datafile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/tes-cache/testkey1-$hash.cache";
$datacont = serialize("test data 1");
make_writable_directory(dirname($datafile));
file_put_contents($datafile, $datacont);
}
}
if ($modes & cache_store::MODE_SESSION) {
- $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, $class, 'phpunit_test');
+ $definition = cache_definition::load_adhoc(cache_store::MODE_SESSION, $class, 'phpunit_test');
$instance = $class::initialise_test_instance($definition);
if (!$instance) {
$this->markTestSkipped('Could not test '.$class.'. No test instance configured for session caches.');
}
}
if ($modes & cache_store::MODE_REQUEST) {
- $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, $class, 'phpunit_test');
+ $definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, $class, 'phpunit_test');
$instance = $class::initialise_test_instance($definition);
if (!$instance) {
$this->markTestSkipped('Could not test '.$class.'. No test instance configured for request caches.');
return new external_multiple_structure(
new external_single_structure(
array(
- 'id' => new external_value(PARAM_NUMBER, 'ID of the cohort'),
+ 'id' => new external_value(PARAM_INT, 'ID of the cohort'),
'name' => new external_value(PARAM_RAW, 'cohort name'),
'idnumber' => new external_value(PARAM_RAW, 'cohort idnumber'),
'description' => new external_value(PARAM_RAW, 'cohort description'),
'cohorts' => new external_multiple_structure(
new external_single_structure(
array(
- 'id' => new external_value(PARAM_NUMBER, 'ID of the cohort'),
+ 'id' => new external_value(PARAM_INT, 'ID of the cohort'),
'categorytype' => new external_single_structure(
array(
'type' => new external_value(PARAM_TEXT, 'the name of the field: id (numeric value
$userid = $DB->get_field('user', 'id', array('username' => $username));
$steps = array(
- new Given('I click on "Assign" "link" in the "//table[@id=\'cohorts\']//tr[contains(., \'' . $cohortidnumber . '\')]" "xpath_element"'),
- new Given('I select "' . $userid . '" from "Potential users"'),
- new Given('I press "Add"'),
- new Given('I press "Back to cohorts"')
+ new Given('I click on "' . get_string('assign', 'cohort') . '" "link" in the "' . $this->escape($cohortidnumber) . '" table row'),
+ new Given('I select "' . $userid . '" from "' . get_string('potusers', 'cohort') . '"'),
+ new Given('I press "' . get_string('add') . '"'),
+ new Given('I press "' . get_string('backtocohorts', 'cohort') . '"')
);
// If we are not in the cohorts management we should move there before anything else.
$steps = array_merge(
array(
new Given('I am on homepage'),
- new Given('I collapse "Front page settings" node'),
- new Given('I expand "Site administration" node'),
- new Given('I expand "Users" node'),
- new Given('I expand "Accounts" node'),
- new Given('I follow "Cohorts"')
+ new Given('I collapse "' . get_string('frontpagesettings', 'admin') . '" node'),
+ new Given('I expand "' . get_string('administrationsite') . '" node'),
+ new Given('I expand "' . get_string('users', 'admin') . '" node'),
+ new Given('I expand "' . get_string('accounts', 'admin') . '" node'),
+ new Given('I follow "' . get_string('cohorts', 'cohort') . '"')
),
$steps
);
And I press "Upload users"
And I press "Continue"
And I follow "Cohorts"
- And I click on "Assign" "link" in the "//table[@id='cohorts']//tr[contains(., 'Cohort 1')]" "xpath_element"
+ And I click on "Assign" "link" in the "Cohort 1" table row
Then the "Current users" select box should contain "Tom Jones (tomjones@example.com)"
And the "Current users" select box should contain "Bob Jones (bobjones@example.com)"
And I press "Back to cohorts"
- And I click on "Assign" "link" in the "//table[@id='cohorts']//tr[contains(., 'Cohort 2')]" "xpath_element"
+ And I click on "Assign" "link" in the "Cohort 2" table row
And the "Current users" select box should contain "Mary Smith (marysmith@example.com)"
And the "Current users" select box should contain "Alice Smith (alicesmith@example.com)"
And I am on homepage
// Check we retrieve the good total number of enrolled cohorts + no error on capability.
$this->assertEquals(2, count($returnedcohorts));
- // Call the external function.
- $returnedcohorts = core_cohort_external::get_cohorts(array(
- $cohort1->id, $cohort2->id));
-
foreach ($returnedcohorts as $enrolledcohort) {
if ($enrolledcohort['idnumber'] == $cohort1->idnumber) {
$this->assertEquals($cohort1->name, $enrolledcohort['name']);
core_cohort_external::update_cohorts(array($cohort1));
}
+ /**
+ * Verify handling of 'id' param.
+ */
+ public function test_update_cohorts_invalid_id_param() {
+ $this->resetAfterTest(true);
+ $cohort = self::getDataGenerator()->create_cohort();
+
+ $cohort1 = array(
+ 'id' => 'THIS IS NOT AN ID',
+ 'name' => 'Changed cohort name',
+ 'categorytype' => array('type' => 'id', 'value' => '1'),
+ 'idnumber' => $cohort->idnumber,
+ );
+
+ try {
+ core_cohort_external::update_cohorts(array($cohort1));
+ $this->fail('Expecting invalid_parameter_exception exception, none occured');
+ } catch (invalid_parameter_exception $e1) {
+ $this->assertContains('Invalid external api parameter: the value is "THIS IS NOT AN ID"', $e1->debuginfo);
+ }
+
+ $cohort1['id'] = 9.999; // Also not a valid id of a cohort.
+ try {
+ core_cohort_external::update_cohorts(array($cohort1));
+ $this->fail('Expecting invalid_parameter_exception exception, none occured');
+ } catch (invalid_parameter_exception $e2) {
+ $this->assertContains('Invalid external api parameter: the value is "9.999"', $e2->debuginfo);
+ }
+ }
+
/**
* Test update_cohorts without permission on the dest category.
*/
--- /dev/null
+This files describes API changes in /cohort/ information provided here is intended
+especially for developers.
+
+=== 2.6 ===
+* Webservice core_cohort_update_cohorts was incorrectly specifiying float as the parameter type
+ for cohort id. This field is actually int and input is now reported and processed as such.
+* Webservice core_cohort_get_cohorts was incorrectly specifiying float as the return
+ type for cohort id. The actual return type is int and is now reported as such.
+
public function user_has_completed_activity($userfullname, $activityname) {
// Will throw an exception if the element can not be hovered.
+ $titleliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($userfullname . ", " . $activityname . ": Completed");
$xpath = "//table[@id='completion-progress']" .
- "/descendant::img[contains(@title, '" . $userfullname . ", " . $activityname . ": Completed')]";
+ "/descendant::img[contains(@title, $titleliteral)]";
return array(
new Given('I go to the current course activity completion report'),
- new Given('I hover "' . $xpath . '" "xpath_element"')
+ new Given('I hover "' . $this->escape($xpath) . '" "xpath_element"')
);
}
*/
public function user_has_not_completed_activity($userfullname, $activityname) {
+ // Will throw an exception if the element can not be hovered.
+ $titleliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($userfullname . ", " . $activityname . ": Not completed");
$xpath = "//table[@id='completion-progress']" .
- "/descendant::img[contains(@title, '" . $userfullname . ", " . $activityname . ": Not completed')]";
+ "/descendant::img[contains(@title, $titleliteral)]";
return array(
new Given('I go to the current course activity completion report'),
- new Given('I hover "' . $xpath . '" "xpath_element"')
+ new Given('I hover "' . $this->escape($xpath) . '" "xpath_element"')
);
return $steps;
// Expand reports node if we can't see the link.
try {
- $this->find('xpath', "//*[@id='settingsnav']" .
+ $this->find('xpath', "//div[@id='settingsnav']" .
"/descendant::li" .
- "/descendant::li[not(contains(@class,'collapsed'))]" .
- "/descendant::p[contains(., 'Activity completion')]");
+ "/descendant::li[not(contains(concat(' ', normalize-space(@class), ' '), ' collapsed '))]" .
+ "/descendant::p[contains(., '" . get_string('pluginname', 'report_progress') . "')]");
} catch (ElementNotFoundException $e) {
- $steps[] = new Given('I expand "Reports" node');
+ $steps[] = new Given('I expand "' . get_string('reports') . '" node');
}
- $steps[] = new Given('I follow "Activity completion"');
+ $steps[] = new Given('I follow "' . get_string('pluginname', 'report_progress') . '"');
return $steps;
}
// will be stored. This database must already have been created //
// and a username/password created to access it. //
-$CFG->dbtype = 'pgsql'; // 'pgsql', 'mysqli', 'mssql', 'sqlsrv' or 'oci'
+$CFG->dbtype = 'pgsql'; // 'pgsql', 'mariadb', 'mysqli', 'mssql', 'sqlsrv' or 'oci'
$CFG->dblibrary = 'native'; // 'native' only at the moment
$CFG->dbhost = 'localhost'; // eg 'localhost' or 'db.isp.com' or IP
$CFG->dbname = 'moodle'; // database name, eg moodle
// $CFG->xsendfilealiases = array(
// '/dataroot/' => $CFG->dataroot,
// '/cachedir/' => '/var/www/moodle/cache', // for custom $CFG->cachedir locations
+// '/localcachedir/' => '/var/local/cache', // for custom $CFG->localcachedir locations
// '/tempdir/' => '/var/www/moodle/temp', // for custom $CFG->tempdir locations
// '/filedir' => '/var/www/moodle/filedir', // for custom $CFG->filedir locations
// );
//
// It is possible to specify different cache and temp directories, use local fast filesystem
// for normal web servers. Server clusters MUST use shared filesystem for cachedir!
+// Localcachedir is intended for server clusters, it does not have to be shared by cluster nodes.
// The directories must not be accessible via web.
//
-// $CFG->tempdir = '/var/www/moodle/temp';
-// $CFG->cachedir = '/var/www/moodle/cache';
+// $CFG->tempdir = '/var/www/moodle/temp'; // Files used during one HTTP request only.
+// $CFG->cachedir = '/var/www/moodle/cache'; // Directory MUST BE SHARED by all cluster nodes, locking required.
+// $CFG->localcachedir = '/var/local/cache'; // Intended for local node caching.
//
// Some filesystems such as NFS may not support file locking operations.
// Locking resolves race conditions and is strongly recommended for production servers.
// memory limit to something higher.
// The value for the settings should be a valid PHP memory value. e.g. 512M, 1G
//
-// $CFG->extramemorylimit = '1G';
+// $CFG->extramemorylimit = '1024M';
//
// Moodle 2.4 introduced a new cache API.
// The cache API stores a configuration file within the Moodle data directory and
// Example:
// $CFG->behat_restart_browser_after = 7200; // Restarts the browser session after 2 hours
//
+// All this page's extra Moodle settings are compared against a white list of allowed settings
+// (the basic and behat_* ones) to avoid problems with production environments. This setting can be
+// used to expand the default white list with an array of extra settings.
+// Example:
+// $CFG->behat_extraallowedsettings = array('logsql', 'dblogerror');
//=========================================================================
// ALL DONE! To continue installation, visit your main page with a browser
$mform->addElement('select', 'visible', get_string('visible'), $choices);
$mform->addHelpButton('visible', 'visible');
$mform->setDefault('visible', $courseconfig->visible);
- if (!has_capability('moodle/course:visibility', $context)) {
- $mform->hardFreeze('visible');
- if (!empty($course->id)) {
+ if (!empty($course->id)) {
+ if (!has_capability('moodle/course:visibility', $coursecontext)) {
+ $mform->hardFreeze('visible');
$mform->setConstant('visible', $course->visible);
- } else {
+ }
+ } else {
+ if (!guess_if_creator_will_have_course_capability('moodle/course:visibility', $categorycontext)) {
+ $mform->hardFreeze('visible');
$mform->setConstant('visible', $courseconfig->visible);
}
}
--- /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/>.
+
+/**
+ * format.php - course format featuring single activity included from view.php
+ *
+ * if we are not redirected before this point this means we want to
+ * either manage orphaned activities - i.e. display section 1,
+ * or the activity is not setup, does not have url or is not accessible at the
+ * moment
+ *
+ * @package format_singleactivity
+ * @copyright 2012 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$courserenderer = $PAGE->get_renderer('format_singleactivity');
+echo $courserenderer->display($course, $section != 0);
--- /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/>.
+
+/**
+ * Strings for component 'format_singleactivity'
+ *
+ * @package format_singleactivity
+ * @copyright 2012 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['activitytype'] = 'Type of activity';
+$string['activitytype_help'] = 'Choose the type of activity or resource to use in this course';
+$string['defactivitytype'] = 'Default type of activity';
+$string['defactivitytypedesc'] = 'Specify the activity type that will be selected by default when creating a new course';
+$string['erroractivitytype'] = 'Type of activity is not set up in Course settings';
+$string['orphaned'] = 'Orphaned';
+$string['orphanedwarning'] = 'These activities are unreachable by users!';
+$string['pluginname'] = 'Single activity format';
+$string['sectionname'] = '';
+$string['warningchangeformat'] = 'When changing the existing course format to "Single activity" make sure that you removed all extra activities from the course including the "News forum". Note that sections structure may be modified.';
--- /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/>.
+
+/**
+ * This file contains main class for the course format singleactivity
+ *
+ * @package format_singleactivity
+ * @copyright 2012 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->dirroot. '/course/format/lib.php');
+
+/**
+ * Main class for the singleactivity course format
+ *
+ * @package format_singleactivity
+ * @copyright 2012 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class format_singleactivity extends format_base {
+ /** @var cm_info the current activity. Use get_activity() to retrieve it. */
+ private $activity = false;
+
+ /**
+ * The URL to use for the specified course
+ *
+ * @param int|stdClass $section Section object from database or just field course_sections.section
+ * if null the course view page is returned
+ * @param array $options options for view URL. At the moment core uses:
+ * 'navigation' (bool) if true and section has no separate page, the function returns null
+ * 'sr' (int) used by multipage formats to specify to which section to return
+ * @return null|moodle_url
+ */
+ public function get_view_url($section, $options = array()) {
+ $sectionnum = $section;
+ if (is_object($sectionnum)) {
+ $sectionnum = $section->section;
+ }
+ if ($sectionnum == 1) {
+ return new moodle_url('/course/view.php', array('id' => $this->courseid, 'section' => 1));
+ }
+ if (!empty($options['navigation']) && $section !== null) {
+ return null;
+ }
+ return new moodle_url('/course/view.php', array('id' => $this->courseid));
+ }
+
+ /**
+ * Loads all of the course sections into the navigation
+ *
+ * @param global_navigation $navigation
+ * @param navigation_node $node The course node within the navigation
+ */
+ public function extend_course_navigation($navigation, navigation_node $node) {
+ // Display orphaned activities for the users who can see them.
+ $context = context_course::instance($this->courseid);
+ if (has_all_capabilities(array('moodle/course:viewhiddensections',
+ 'moodle/course:viewhiddenactivities'), $context)) {
+ $modinfo = get_fast_modinfo($this->courseid);
+ if (!empty($modinfo->sections[1])) {
+ $section1 = $modinfo->get_section_info(1);
+ // Show orphaned activities.
+ $orphanednode = $node->add(get_string('orphaned', 'format_singleactivity'),
+ $this->get_view_url(1), navigation_node::TYPE_SECTION, null, $section1->id);
+ $orphanednode->nodetype = navigation_node::NODETYPE_BRANCH;
+ $orphanednode->add_class('orphaned');
+ foreach ($modinfo->sections[1] as $cmid) {
+ $this->navigation_add_activity($orphanednode, $modinfo->cms[$cmid]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds a course module to the navigation node
+ *
+ * This is basically copied from function global_navigation::load_section_activities()
+ * because it is not accessible from outside.
+ *
+ * @param navigation_node $node
+ * @param cm_info $cm
+ * @return null|navigation_node
+ */
+ protected function navigation_add_activity(navigation_node $node, $cm) {
+ if (!$cm->uservisible) {
+ return null;
+ }
+ $action = $cm->get_url();
+ if (!$action) {
+ // Do not add to navigation activity without url (i.e. labels).
+ return null;
+ }
+ $activityname = format_string($cm->name, true, array('context' => context_module::instance($cm->id)));
+ if ($cm->icon) {
+ $icon = new pix_icon($cm->icon, $cm->modfullname, $cm->iconcomponent);
+ } else {
+ $icon = new pix_icon('icon', $cm->modfullname, $cm->modname);
+ }
+ $activitynode = $node->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $cm->id, $icon);
+ if (global_navigation::module_extends_navigation($cm->modname)) {
+ $activitynode->nodetype = navigation_node::NODETYPE_BRANCH;
+ } else {
+ $activitynode->nodetype = navigation_node::NODETYPE_LEAF;
+ }
+ return $activitynode;
+ }
+
+ /**
+ * Returns the list of blocks to be automatically added for the newly created course
+ *
+ * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT
+ * each of values is an array of block names (for left and right side columns)
+ */
+ public function get_default_blocks() {
+ // No blocks for this format because course view page is not displayed anyway.
+ return array(
+ BLOCK_POS_LEFT => array(),
+ BLOCK_POS_RIGHT => array()
+ );
+ }
+
+ /**
+ * Definitions of the additional options that this course format uses for course
+ *
+ * Singleactivity course format uses one option 'activitytype'
+ *
+ * @param bool $foreditform
+ * @return array of options
+ */
+ public function course_format_options($foreditform = false) {
+ static $courseformatoptions = false;
+ if ($courseformatoptions === false) {
+ $config = get_config('format_singleactivity');
+ $courseformatoptions = array(
+ 'activitytype' => array(
+ 'default' => $config->activitytype,
+ 'type' => PARAM_TEXT,
+ ),
+ );
+ }
+ if ($foreditform && !isset($courseformatoptions['activitytype']['label'])) {
+ $availabletypes = get_module_types_names();
+ $courseformatoptionsedit = array(
+ 'activitytype' => array(
+ 'label' => new lang_string('activitytype', 'format_singleactivity'),
+ 'help' => 'activitytype',
+ 'help_component' => 'format_singleactivity',
+ 'element_type' => 'select',
+ 'element_attributes' => array($availabletypes),
+ ),
+ );
+ $courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit);
+ }
+ return $courseformatoptions;
+ }
+
+ /**
+ * Adds format options elements to the course/section edit form
+ *
+ * This function is called from {@link course_edit_form::definition_after_data()}
+ *
+ * Format singleactivity adds a warning when format of the course is about to be changed.
+ *
+ * @param MoodleQuickForm $mform form the elements are added to
+ * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form
+ * @return array array of references to the added form elements
+ */
+ public function create_edit_form_elements(&$mform, $forsection = false) {
+ global $PAGE;
+ $elements = parent::create_edit_form_elements($mform, $forsection);
+ if (!$forsection && ($course = $PAGE->course) && !empty($course->format) &&
+ $course->format !== 'site' && $course->format !== 'singleactivity') {
+ // This is the existing course in other format, display a warning.
+ $element = $mform->addElement('static', '', '',
+ html_writer::tag('span', get_string('warningchangeformat', 'format_singleactivity'),
+ array('class' => 'error')));
+ array_unshift($elements, $element);
+ }
+ return $elements;
+ }
+
+ /**
+ * Make sure that current active activity is in section 0
+ *
+ * All other activities are moved to section 1 that will be displayed as 'Orphaned'.
+ * It may be needed after the course format was changed or activitytype in
+ * course settings has been changed.
+ *
+ * @return null|cm_info current activity
+ */
+ public function reorder_activities() {
+ course_create_sections_if_missing($this->courseid, array(0, 1));
+ foreach ($this->get_sections() as $sectionnum => $section) {
+ if (($sectionnum && $section->visible) ||
+ (!$sectionnum && !$section->visible)) {
+ // Make sure that 0 section is visible and all others are hidden.
+ set_section_visible($this->courseid, $sectionnum, $sectionnum == 0);
+ }
+ }
+ $modinfo = get_fast_modinfo($this->courseid);
+
+ // Find the current activity (first activity with the specified type in all course activities).
+ $activitytype = $this->get_activitytype();
+ $activity = null;
+ if (!empty($activitytype)) {
+ foreach ($modinfo->sections as $sectionnum => $cmlist) {
+ foreach ($cmlist as $cmid) {
+ if ($modinfo->cms[$cmid]->modname === $activitytype) {
+ $activity = $modinfo->cms[$cmid];
+ break 2;
+ }
+ }
+ }
+ }
+
+ // Make sure the current activity is in the 0-section.
+ if ($activity && $activity->sectionnum != 0) {
+ moveto_module($activity, $modinfo->get_section_info(0));
+ // Cache was reset so get modinfo again.
+ $modinfo = get_fast_modinfo($this->courseid);
+ }
+
+ // Move all other activities into section 1 (the order must be kept).
+ $hasvisibleactivities = false;
+ $firstorphanedcm = null;
+ foreach ($modinfo->sections as $sectionnum => $cmlist) {
+ if ($sectionnum && !empty($cmlist) && $firstorphanedcm === null) {
+ $firstorphanedcm = reset($cmlist);
+ }
+ foreach ($cmlist as $cmid) {
+ if ($sectionnum > 1) {
+ moveto_module($modinfo->get_cm($cmid), $modinfo->get_section_info(1));
+ } else if (!$hasvisibleactivities && $sectionnum == 1 && $modinfo->get_cm($cmid)->visible) {
+ $hasvisibleactivities = true;
+ }
+ }
+ }
+ if (!empty($modinfo->sections[0])) {
+ foreach ($modinfo->sections[0] as $cmid) {
+ if (!$activity || $cmid != $activity->id) {
+ moveto_module($modinfo->get_cm($cmid), $modinfo->get_section_info(1), $firstorphanedcm);
+ }
+ }
+ }
+ if ($hasvisibleactivities) {
+ set_section_visible($this->courseid, 1, false);
+ }
+ return $activity;
+ }
+
+ /**
+ * Returns the name of activity type used for this course
+ *
+ * @return string|null
+ */
+ protected function get_activitytype() {
+ $options = $this->get_format_options();
+ $availabletypes = get_module_types_names();
+ if (!empty($options['activitytype']) &&
+ array_key_exists($options['activitytype'], $availabletypes)) {
+ return $options['activitytype'];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the current activity if exists
+ *
+ * @return null|cm_info
+ */
+ protected function get_activity() {
+ if ($this->activity === false) {
+ $this->activity = $this->reorder_activities();
+ }
+ return $this->activity;
+ }
+
+ /**
+ * Checks if the current user can add the activity of the specified type to this course.
+ *
+ * @return bool
+ */
+ protected function can_add_activity() {
+ global $CFG;
+ if (!($modname = $this->get_activitytype())) {
+ return false;
+ }
+ if (!has_capability('moodle/course:manageactivities', context_course::instance($this->courseid))) {
+ return false;
+ }
+ if (!course_allowed_module($this->get_course(), $modname)) {
+ return false;
+ }
+ $libfile = "$CFG->dirroot/mod/$modname/lib.php";
+ if (!file_exists($libfile)) {
+ return null;
+ }
+ return true;
+ }
+
+ /**
+ * Checks if the activity type requires subtypes.
+ *
+ * @return bool|null (null if the check is not possible)
+ */
+ public function activity_has_subtypes() {
+ global $CFG;
+ if (!($modname = $this->get_activitytype())) {
+ return null;
+ }
+ $libfile = "$CFG->dirroot/mod/$modname/lib.php";
+ if (!file_exists($libfile)) {
+ return null;
+ }
+ include_once($libfile);
+ return function_exists($modname. '_get_types');
+ }
+
+ /**
+ * Allows course format to execute code on moodle_page::set_course()
+ *
+ * This function is executed before the output starts.
+ *
+ * If everything is configured correctly, user is redirected from the
+ * default course view page to the activity view page.
+ *
+ * "Section 1" is the administrative page to manage orphaned activities
+ *
+ * If user is on course view page and there is no module added to the course
+ * and the user has 'moodle/course:manageactivities' capability, redirect to create module
+ * form.
+ *
+ * @param moodle_page $page instance of page calling set_course
+ */
+ public function page_set_course(moodle_page $page) {
+ global $PAGE;
+ $page->add_body_class('format-'. $this->get_format());
+ if ($PAGE == $page && $page->has_set_url() &&
+ $page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) {
+ $edit = optional_param('edit', -1, PARAM_BOOL);
+ if (($edit == 0 || $edit == 1) && confirm_sesskey()) {
+ // This is a request to turn editing mode on or off, do not redirect here, /course/view.php will do redirection.
+ return;
+ }
+ $cm = $this->get_activity();
+ $cursection = optional_param('section', null, PARAM_INT);
+ if (!empty($cursection) && has_capability('moodle/course:viewhiddensections',
+ context_course::instance($this->courseid))) {
+ // Display orphaned activities (course view page, section 1).
+ return;
+ }
+ if (!$this->get_activitytype()) {
+ if (has_capability('moodle/course:update', context_course::instance($this->courseid))) {
+ // Teacher is redirected to edit course page.
+ $url = new moodle_url('/course/edit.php', array('id' => $this->courseid));
+ redirect($url, get_string('erroractivitytype', 'format_singleactivity'));
+ } else {
+ // Student sees an empty course page.
+ return;
+ }
+ }
+ if ($cm === null) {
+ if ($this->can_add_activity()) {
+ // This is a user who has capability to create an activity.
+ if ($this->activity_has_subtypes()) {
+ // Activity that requires subtype can not be added automatically.
+ if (optional_param('addactivity', 0, PARAM_INT)) {
+ return;
+ } else {
+ $url = new moodle_url('/course/view.php', array('id' => $this->courseid, 'addactivity' => 1));
+ redirect($url);
+ }
+ }
+ // Redirect to the add activity form.
+ $url = new moodle_url('/course/mod.php', array('id' => $this->courseid,
+ 'section' => 0, 'sesskey' => sesskey(), 'add' => $this->get_activitytype()));
+ redirect($url);
+ } else {
+ // Student views an empty course page.
+ return;
+ }
+ } else if (!$cm->uservisible || !$cm->get_url()) {
+ // Activity is set but not visible to current user or does not have url.
+ // Display course page (either empty or with availability restriction info).
+ return;
+ } else {
+ // Everything is set up and accessible, redirect to the activity page!
+ redirect($cm->get_url());
+ }
+ }
+ }
+
+ /**
+ * Allows course format to execute code on moodle_page::set_cm()
+ *
+ * If we are inside the main module for this course, remove extra node level
+ * from navigation: substitute course node with activity node, move all children
+ *
+ * @param moodle_page $page instance of page calling set_cm
+ */
+ public function page_set_cm(moodle_page $page) {
+ global $PAGE;
+ parent::page_set_cm($page);
+ if ($PAGE == $page && ($cm = $this->get_activity()) &&
+ $cm->uservisible &&
+ ($cm->id === $page->cm->id) &&
+ ($activitynode = $page->navigation->find($cm->id, navigation_node::TYPE_ACTIVITY)) &&
+ ($node = $page->navigation->find($page->course->id, navigation_node::TYPE_COURSE))) {
+ // Substitute course node with activity node, move all children.
+ $node->action = $activitynode->action;
+ $node->type = $activitynode->type;
+ $node->id = $activitynode->id;
+ $node->key = $activitynode->key;
+ $node->isactive = $node->isactive || $activitynode->isactive;
+ $node->icon = null;
+ if ($activitynode->children->count()) {
+ foreach ($activitynode->children as &$child) {
+ $child->remove();
+ $node->add_node($child);
+ }
+ } else {
+ $node->search_for_active_node();
+ }
+ $activitynode->remove();
+ }
+ }
+}
--- /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/>.
+
+/**
+ * Renderer for outputting the singleactivity course format.
+ *
+ * @package format_singleactivity
+ * @copyright 2013 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Basic renderer for singleactivity format.
+ *
+ * @package format_singleactivity
+ * @copyright 2013 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class format_singleactivity_renderer extends plugin_renderer_base {
+
+ /**
+ * Displays the activities list in cases when course view page is not
+ * redirected to the activity page.
+ *
+ * @param stdClass $course record from table course
+ * @param bool $orphaned if false displays the main activity (if present)
+ * if true displays all other activities
+ */
+ public function display($course, $orphaned) {
+ $courserenderer = $this->page->get_renderer('core', 'course');
+ $output = '';
+ $modinfo = get_fast_modinfo($course);
+ if ($orphaned) {
+ if (!empty($modinfo->sections[1])) {
+ $output .= $this->output->heading(get_string('orphaned', 'format_singleactivity'), 3, 'sectionname');
+ $output .= $this->output->box(get_string('orphanedwarning', 'format_singleactivity'));
+ $output .= $courserenderer->course_section_cm_list($course, 1, 1);
+ }
+ } else {
+ $output .= $courserenderer->course_section_cm_list($course, 0, 0);
+ if (empty($modinfo->sections[0]) && course_get_format($course)->activity_has_subtypes()) {
+ // Course format was unable to automatically redirect to add module page.
+ $output .= $courserenderer->course_section_add_cm_control($course, 0, 0);
+ }
+ }
+ return $output;
+ }
+}
--- /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/>.
+
+/**
+ * Settings for format_singleactivity
+ *
+ * @package format_singleactivity
+ * @copyright 2012 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+require_once($CFG->dirroot. '/course/format/singleactivity/settingslib.php');
+
+if ($ADMIN->fulltree) {
+ $settings->add(new format_singleactivity_admin_setting_activitytype('format_singleactivity/activitytype',
+ new lang_string('defactivitytype', 'format_singleactivity'),
+ new lang_string('defactivitytypedesc', 'format_singleactivity'),
+ 'forum', null));
+}
--- /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/>.
+
+/**
+ * Settings class for format_singleactivity
+ *
+ * @package format_singleactivity
+ * @copyright 2013 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Admin settings class for the format singleactivity activitytype choice
+ *
+ * @package format_singleactivity
+ * @copyright 2013 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class format_singleactivity_admin_setting_activitytype extends admin_setting_configselect {
+ /**
+ * This function may be used in ancestors for lazy loading of choices
+ *
+ * Override this method if loading of choices is expensive, such
+ * as when it requires multiple db requests.
+ *
+ * @return bool true if loaded, false if error
+ */
+ public function load_choices() {
+ global $CFG;
+ require_once($CFG->dirroot. '/course/lib.php');
+ if (is_array($this->choices)) {
+ return true;
+ }
+ $this->choices = get_module_types_names();
+ return true;
+ }
+}
--- /dev/null
+/* Hide confusing form elements "Display description on course page" and
+"Save and return to course" from module edit form because they
+are not applicable in single activity course format */
+body.format-singleactivity.path-mod.pagelayout-admin form.mform #fitem_id_showdescription,
+body.format-singleactivity.path-mod.pagelayout-admin form.mform .fitem_actionbuttons#fgroup_id_buttonar #id_submitbutton {display:none;}
+
+/* In mod_quiz hide "Back to course" button */
+body.format-singleactivity.path-mod-quiz .quizattempt .continuebutton {display:none;}
+
+body.format-singleactivity .tree_item.orphaned a {color:red;}
--- /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/>.
+
+/**
+ * Version details
+ *
+ * @package format_singleactivity
+ * @copyright 2012 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version = 2013071200; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires = 2013070800; // Requires this Moodle version (2.6)
+$plugin->component = 'format_singleactivity'; // Full name of the plugin (used for diagnostics).
//Accessibility: for files get description via icon, this is very ugly hack!
$instancename = $mod->get_formatted_name();
- $altname = '';
$altname = $mod->modfullname;
// Avoid unnecessary duplication: if e.g. a forum name already
// includes the word forum (or Forum, etc) then it is unhelpful
}
if ($mod->uservisible) {
// show accessibility note only if user can access the module himself
- $accesstext = get_accesshide(get_string('hiddenfromstudents').': ');
+ $accesstext = get_accesshide(get_string('hiddenfromstudents').':'. $mod->modfullname);
}
}
// Display link itself.
$activitylink = html_writer::empty_tag('img', array('src' => $mod->get_icon_url(),
- 'class' => 'iconlarge activityicon', 'alt' => $mod->modfullname)) . $accesstext .
+ 'class' => 'iconlarge activityicon', 'alt' => ' ', 'role' => 'presentation')) . $accesstext .
html_writer::tag('span', $instancename . $altname, array('class' => 'instancename'));
if ($mod->uservisible) {
$output .= html_writer::link($url, $activitylink, array('class' => $linkclasses, 'onclick' => $onclick)) .
| Force group mode | No |
When I press "Save changes"
Then "No groups (Click to change)" "link" should exists
- And ".//a//img[contains(@src, 'groupn')]" "xpath_element" 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//img[contains(@src, 'groups')]" "xpath_element" 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//img[contains(@src, 'groups')]" "xpath_element" 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//img[contains(@src, 'groupv')]" "xpath_element" should exists
+ And "//a/child::img[contains(@src, 'groupv')]" "xpath_element" should exists
And I reload the page
And "Visible groups (Click to change)" "link" should exists
- And ".//a//img[contains(@src, 'groupv')]" "xpath_element" should exists
+ And "//a/child::img[contains(@src, 'groupv')]" "xpath_element" should exists
And I click on "Visible groups (Click to change)" "link" in the "Test forum name" activity
And "No groups (Click to change)" "link" should exists
- And ".//a//img[contains(@src, 'groupn')]" "xpath_element" should exists
+ And "//a/child::img[contains(@src, 'groupn')]" "xpath_element" should exists
When I indent right "Test glossary name" activity
Then "#section-1 li.glossary div.mod-indent-1" "css_element" should exists
And I indent right "Test glossary name" activity
- And "//*[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should exists
+ And "//li[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should exists
And "#section-1 li.glossary div.mod-indent-2" "css_element" should exists
And I reload the page
And "#section-1 li.glossary div.mod-indent-2" "css_element" should exists
And I indent left "Test glossary name" activity
And "#section-1 li.glossary div.mod-indent-2" "css_element" should not exists
And "#section-1 li.glossary div.mod-indent-1" "css_element" should not exists
- And "//*[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should not exists
+ And "//li[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should not exists
* @Given /^I turn editing mode on$/
*/
public function i_turn_editing_mode_on() {
- return new Given('I press "Turn editing on"');
+ return new Given('I press "' . get_string('turneditingon') . '"');
}
/**
* @Given /^I turn editing mode off$/
*/
public function i_turn_editing_mode_off() {
- return new Given('I press "Turn editing off"');
+ return new Given('I press "' . get_string('turneditingoff') . '"');
}
/**
public function i_create_a_course_with(TableNode $table) {
return array(
new Given('I go to the courses management page'),
- new Given('I press "Add a new course"'),
+ new Given('I press "' . get_string('addnewcourse') . '"'),
new Given('I fill the moodle form with:', $table),
- new Given('I press "Save changes"')
+ new Given('I press "' . get_string('savechanges') . '"')
);
}
return array(
new Given('I am on homepage'),
- new Given('I expand "Site administration" node'),
- new Given('I expand "Courses" node'),
- new Given('I follow "Add/edit courses"'),
+ new Given('I expand "' . get_string('administrationsite') . '" node'),
+ new Given('I expand "' . get_string('courses', 'admin') . '" node'),
+ new Given('I follow "' . get_string('coursemgmt', 'admin') . '"'),
);
}
public function i_add_to_section_and_i_fill_the_form_with($activity, $section, TableNode $data) {
return array(
- new Given('I add a "'.$activity.'" to section "'.$section.'"'),
+ new Given('I add a "' . $this->escape($activity) . '" to section "' . $this->escape($section) . '"'),
new Given('I fill the moodle form with:', $data),
- new Given('I press "Save and return to course"')
+ new Given('I press "' . get_string('savechangesandreturntocourse') . '"')
);
}
*/
public function i_add_to_section($activity, $section) {
- $sectionxpath = "//*[@id='section-" . $section . "']";
+ $sectionxpath = "//li[@id='section-" . $section . "']";
+
+ $activityliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral(ucfirst($activity));
if ($this->running_javascript()) {
$sectionnode->click();
// Clicks the selected activity if it exists.
- $activity = ucfirst($activity);
$activityxpath = "//div[@id='chooseform']/descendant::label" .
- "/descendant::span[contains(concat(' ', @class, ' '), ' typename ')][contains(.,'" . $activity . "')]" .
+ "/descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' typename ')]" .
+ "[contains(., $activityliteral)]" .
"/parent::label/child::input";
$activitynode = $this->find('xpath', $activityxpath);
$activitynode->doubleClick();
// Without Javascript.
// Selecting the option from the select box which contains the option.
- $selectxpath = $sectionxpath . "/descendant::div[contains(concat(' ', @class, ' '), ' section_add_menus ')]" .
- "/descendant::select[contains(., '" . $activity . "')]";
+ $selectxpath = $sectionxpath . "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' section_add_menus ')]" .
+ "/descendant::select[contains(., $activityliteral)]";
$selectnode = $this->find('xpath', $selectxpath);
$selectnode->selectOption($activity);
$xpath = $this->section_exists($sectionnumber);
return array(
- new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $xpath . '" "xpath_element"'),
+ new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"'),
new Given('I wait "2" seconds')
);
}
$xpath = $this->section_exists($sectionnumber);
return array(
- new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $xpath . '" "xpath_element"'),
+ new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"'),
new Given('I wait "2" seconds')
);
}
// Section should be hidden.
$exception = new ExpectationException('The section is not hidden', $this->getSession());
- $this->find('xpath', $sectionxpath . "[contains(concat(' ', @class, ' '), ' hidden ')]", $exception);
+ $this->find('xpath', $sectionxpath . "[contains(concat(' ', normalize-space(@class), ' '), ' hidden ')]", $exception);
// The checking are different depending on user permissions.
if ($this->is_course_editor()) {
foreach ($activities as $activity) {
// Dimmed.
- $this->find('xpath', "//div[contains(concat(' ', @class, ' '), ' activityinstance ')]" .
- "/a[contains(concat(' ', @class, ' '), ' dimmed ')]", $dimmedexception, $activity);
+ $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' activityinstance ')]" .
+ "/a[contains(concat(' ', normalize-space(@class), ' '), ' dimmed ')]", $dimmedexception, $activity);
// Non-JS browsers can not click on img elements.
if ($this->running_javascript()) {
$sectionxpath = $this->section_exists($sectionnumber);
// Section should not be hidden.
- if (!$this->getSession()->getPage()->find('xpath', $sectionxpath . "[not(contains(concat(' ', @class, ' '), ' hidden '))]")) {
+ $xpath = $sectionxpath . "[not(contains(concat(' ', normalize-space(@class), ' '), ' hidden '))]";
+ if (!$this->getSession()->getPage()->find('xpath', $xpath)) {
throw new ExpectationException('The section is hidden', $this->getSession());
}
// JS enabled.
if ($this->running_javascript()) {
- $destinationxpath = $sectionxpath . "/descendant::ul[contains(@class, 'yui3-dd-drop')]";
+ $destinationxpath = $sectionxpath . "/descendant::ul[contains(concat(' ', normalize-space(@class), ' '), ' yui3-dd-drop ')]";
return array(
- new Given('I drag "' . $activitynode->getXpath() . '" "xpath_element" and I drop it in "' . $destinationxpath . '" "xpath_element"'),
+ new Given('I drag "' . $this->escape($activitynode->getXpath()) . '" "xpath_element" ' .
+ 'and I drop it in "' . $this->escape($destinationxpath) . '" "xpath_element"'),
);
} else {
// Moving to the fist spot of the section (before all other section's activities).
return array(
- new Given('I click on "a.editing_move" "css_element" in the "' . $activityname . '" activity'),
- new Given('I click on "li.movehere a" "css_element" in the "' . $sectionxpath . '" "xpath_element"'),
+ new Given('I click on "a.editing_move" "css_element" in the "' . $this->escape($activityname) . '" activity'),
+ new Given('I click on "li.movehere a" "css_element" in the "' . $this->escape($sectionxpath) . '" "xpath_element"'),
);
}
}
// Adding chr(10) to save changes.
return array(
- new Given('I click on "' . get_string('edittitle') . '" "link" in the "' . $activityname .'" activity'),
- new Given('I fill in "title" with "' . $newactivityname . chr(10) . '"'),
+ new Given('I click on "' . get_string('edittitle') . '" "link" in the "' . $this->escape($activityname) .'" activity'),
+ new Given('I fill in "title" with "' . $this->escape($newactivityname) . chr(10) . '"'),
new Given('I wait "2" seconds')
);
}
public function i_indent_right_activity($activityname) {
$steps = array(
- new Given('I click on "' . get_string('moveright') . '" "link" in the "' . $activityname . '" activity')
+ new Given('I click on "' . get_string('moveright') . '" "link" in the "' . $this->escape($activityname) . '" activity')
);
if ($this->running_javascript()) {
public function i_indent_left_activity($activityname) {
$steps = array(
- new Given('I click on "' . get_string('moveleft') . '" "link" in the "' . $activityname . '" activity')
+ new Given('I click on "' . get_string('moveleft') . '" "link" in the "' . $this->escape($activityname) . '" activity')
);
if ($this->running_javascript()) {
// With JS disabled.
$steps = array(
- new Given('I click on "' . $deletestring . '" "link" in the "' . $activityname . '" activity'),
+ new Given('I click on "' . $this->escape($deletestring) . '" "link" in the "' . $this->escape($activityname) . '" activity'),
new Given('I press "' . get_string('yes') . '"')
);
*/
public function i_duplicate_activity($activityname) {
return array(
- new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activityname . '" activity'),
+ new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $this->escape($activityname) . '" activity'),
new Given('I press "' . get_string('continue') .'"'),
new Given('I press "' . get_string('duplicatecontcourse') .'"')
);
*/
public function i_duplicate_activity_editing_the_new_copy_with($activityname, TableNode $data) {
return array(
- new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activityname . '" activity'),
+ new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $this->escape($activityname) . '" activity'),
new Given('I press "' . get_string('continue') .'"'),
new Given('I press "' . get_string('duplicatecontedit') . '"'),
new Given('I fill the moodle form with:', $data),
$courseformat = $this->get_course_format();
// Checking the show button alt text and show icon.
- $showtext = get_string('showfromothers', $courseformat);
- $linkxpath = $xpath . "/descendant::a[@title='". $showtext ."']";
- $imgxpath = $linkxpath . "/descendant::img[@alt='". $showtext ."'][contains(@src, 'show')]";
+ $showtext = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('showfromothers', $courseformat));
+ $linkxpath = $xpath . "/descendant::a[@title=$showtext]";
+ $imgxpath = $linkxpath . "/descendant::img[@alt=$showtext][contains(@src, 'show')]";
$exception = new ElementNotFoundException($this->getSession(), 'Show section icon ');
$this->find('xpath', $imgxpath, $exception);
$courseformat = $this->get_course_format();
// Checking the hide button alt text and hide icon.
- $hidetext = get_string('hidefromothers', $courseformat);
- $linkxpath = $xpath . "/descendant::a[@title='" . $hidetext . "']";
- $imgxpath = $linkxpath . "/descendant::img[@alt='" . $hidetext ."'][contains(@src, 'hide')]";
+ $hidetext = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('hidefromothers', $courseformat));
+ $linkxpath = $xpath . "/descendant::a[@title=$hidetext]";
+ $imgxpath = $linkxpath . "/descendant::img[@alt=$hidetext][contains(@src, 'hide')]";
$exception = new ElementNotFoundException($this->getSession(), 'Hide section icon ');
$this->find('xpath', $imgxpath, $exception);
*/
protected function get_section_activities($sectionxpath) {
- $xpath = $sectionxpath . "/descendant::li[contains(concat(' ', @class, ' '), ' activity ')]";
+ $xpath = $sectionxpath . "/descendant::li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]";
// We spin here, as activities usually require a lot of time to load.
try {
*/
protected function get_activity_node($activityname) {
- $activityname = str_replace("'", "\'", $activityname);
- $xpath = "//li[contains(concat(' ', @class, ' '), ' activity ')][contains(., '" .$activityname. "')]";
+ $activityname = $this->getSession()->getSelectorsHandler()->xpathLiteral($activityname);
+ $xpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][contains(., $activityname)]";
return $this->find('xpath', $xpath);
}
protected function is_course_editor() {
// We don't need to behat_base::spin() here as all is already loaded.
- if (!$this->getSession()->getPage()->findButton('Turn editing off') &&
- !$this->getSession()->getPage()->findButton('Turn editing on')) {
+ if (!$this->getSession()->getPage()->findButton(get_string('turneditingoff')) &&
+ !$this->getSession()->getPage()->findButton(get_string('turneditingon'))) {
return false;
}
| Group mode | Separate groups |
| Force group mode | Yes |
When I press "Save changes"
- Then ".//a//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should not exists
- And ".//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should exists
- And I click on "//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element"
- And ".//a//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should not exists
- And ".//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should exists
+ Then "//a/child::img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" should not exists
+ And "//img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" should exists
+ And I click on "//img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element"
+ And "//a/child::img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" should not exists
+ And "//img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" should exists
@javascript
Scenario: Forced group mode using visible groups
| Group mode | Visible groups |
| Force group mode | Yes |
And I press "Save changes"
- Then ".//a//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should not exists
- And ".//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should exists
- And I click on "//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element"
- And ".//a//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should not exists
- And ".//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should exists
+ Then "//a/child::img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" should not exists
+ And "//img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" should exists
+ And I click on "//img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element"
+ And "//a/child::img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" should not exists
+ And "//img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" should exists
@javascript
Scenario: Forced group mode without groups