diff --git a/admin/environment.xml b/admin/environment.xml index 19997a449b3..66e4337c0de 100644 --- a/admin/environment.xml +++ b/admin/environment.xml @@ -957,6 +957,8 @@ + + diff --git a/admin/tests/behat/behat_admin.php b/admin/tests/behat/behat_admin.php index 0cd23064ad4..6b4f88f69ae 100644 --- a/admin/tests/behat/behat_admin.php +++ b/admin/tests/behat/behat_admin.php @@ -62,7 +62,7 @@ class behat_admin extends behat_base { } // 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(); @@ -72,8 +72,12 @@ class behat_admin extends behat_base { // 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"); @@ -90,7 +94,7 @@ class behat_admin extends behat_base { $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(); } } diff --git a/admin/tests/behat/display_short_names.feature b/admin/tests/behat/display_short_names.feature index b6c825812e8..f17f092191f 100644 --- a/admin/tests/behat/display_short_names.feature +++ b/admin/tests/behat/display_short_names.feature @@ -15,7 +15,7 @@ Feature: Display extended course names 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 diff --git a/admin/tool/behat/cli/util.php b/admin/tool/behat/cli/util.php index b28f4fcb6b0..5c24cf8c05d 100644 --- a/admin/tool/behat/cli/util.php +++ b/admin/tool/behat/cli/util.php @@ -142,6 +142,9 @@ foreach ($vars as $var) { $CFG->{$var} = $CFG->{'behat_' . $var}; } +// Clean $CFG extra values before performing any action. +behat_clean_init_config(); + $CFG->noemailever = true; $CFG->passwordsaltmain = 'moodle'; diff --git a/admin/tool/behat/lang/en/tool_behat.php b/admin/tool/behat/lang/en/tool_behat.php index 086e78c54f3..9f426cc6a01 100644 --- a/admin/tool/behat/lang/en/tool_behat.php +++ b/admin/tool/behat/lang/en/tool_behat.php @@ -41,6 +41,5 @@ $string['theninfo'] = 'Then. Checkings to ensure the outcomes are the expected o $string['viewsteps'] = 'Filter'; $string['wheninfo'] = 'When. Actions that provokes an event'; $string['wrongbehatsetup'] = 'Something is wrong with behat setup, ensure:
    -
  • You ran "curl http://getcomposer.org/installer | php"
  • -
  • You ran "php composer.phar install --dev"
  • +
  • You ran "php admin/tool/behat/cli/init.php" from your moodle root directory
  • vendor/bin/behat file has execution permissions
'; diff --git a/admin/tool/behat/tests/behat/basic_actions.feature b/admin/tool/behat/tests/behat/basic_actions.feature index c2f829bd659..637086f20a8 100644 --- a/admin/tool/behat/tests/behat/basic_actions.feature +++ b/admin/tool/behat/tests/behat/basic_actions.feature @@ -37,7 +37,7 @@ Feature: Page contents assertions 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 @@ -45,5 +45,5 @@ Feature: Page contents assertions | 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" diff --git a/admin/tool/behat/tests/behat/data_generators.feature b/admin/tool/behat/tests/behat/data_generators.feature index a98adcf9f1a..b19db483ed7 100644 --- a/admin/tool/behat/tests/behat/data_generators.feature +++ b/admin/tool/behat/tests/behat/data_generators.feature @@ -30,17 +30,17 @@ Feature: Set up contextual data for tests 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 diff --git a/admin/tool/profiling/index.php b/admin/tool/profiling/index.php index a5e21d3badf..e602e44196b 100644 --- a/admin/tool/profiling/index.php +++ b/admin/tool/profiling/index.php @@ -74,12 +74,11 @@ if (isset($script)) { $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(); @@ -126,12 +125,11 @@ if (isset($script)) { $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(); diff --git a/admin/tool/profiling/lang/en/tool_profiling.php b/admin/tool/profiling/lang/en/tool_profiling.php index 4a687f8f279..7c80c848c3a 100644 --- a/admin/tool/profiling/lang/en/tool_profiling.php +++ b/admin/tool/profiling/lang/en/tool_profiling.php @@ -51,5 +51,5 @@ $string['referencerun'] = 'Reference run/comment'; $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'; diff --git a/auth/tests/behat/behat_auth.php b/auth/tests/behat/behat_auth.php index c6d47aa4958..4ced1542330 100644 --- a/auth/tests/behat/behat_auth.php +++ b/auth/tests/behat/behat_auth.php @@ -49,10 +49,10 @@ class behat_auth extends behat_base { public function i_log_in_as($username) { return array(new Given('I am on homepage'), - new Given('I follow "Login"'), - new Given('I fill in "Username" with "'.$username.'"'), - new Given('I fill in "Password" with "'.$username.'"'), - new Given('I press "Login"') + 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') . '"') ); } @@ -62,7 +62,7 @@ class behat_auth extends behat_base { * @Given /^I log out$/ */ public function i_log_out() { - return new When('I follow "Logout"'); + return new When('I follow "' . get_string('logout') . '"'); } } diff --git a/backup/backupfilesedit_form.php b/backup/backupfilesedit_form.php index 36a4030da07..dce09fe9fa0 100644 --- a/backup/backupfilesedit_form.php +++ b/backup/backupfilesedit_form.php @@ -1,5 +1,4 @@ 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']); } diff --git a/backup/util/dbops/restore_dbops.class.php b/backup/util/dbops/restore_dbops.class.php index 518d7bac1d6..a3dfe1e6161 100644 --- a/backup/util/dbops/restore_dbops.class.php +++ b/backup/util/dbops/restore_dbops.class.php @@ -614,13 +614,20 @@ abstract class restore_dbops { } 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 @@ -645,7 +652,7 @@ abstract class restore_dbops { // 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); } } } diff --git a/backup/util/ui/tests/behat/backup_courses.feature b/backup/util/ui/tests/behat/backup_courses.feature index 488ab6013cb..f8b85d75f0b 100644 --- a/backup/util/ui/tests/behat/backup_courses.feature +++ b/backup/util/ui/tests/behat/backup_courses.feature @@ -31,7 +31,7 @@ Feature: Backup Moodle courses 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" diff --git a/backup/util/ui/tests/behat/behat_backup.php b/backup/util/ui/tests/behat/behat_backup.php index b0d99986caf..d06185ed304 100644 --- a/backup/util/ui/tests/behat/behat_backup.php +++ b/backup/util/ui/tests/behat/behat_backup.php @@ -61,25 +61,25 @@ class behat_backup extends behat_base { $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(); } /** @@ -105,36 +105,37 @@ class behat_backup extends behat_base { // 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(); } /** @@ -150,17 +151,20 @@ class behat_backup extends behat_base { // 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(); @@ -181,14 +185,15 @@ class behat_backup extends behat_base { $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(); @@ -209,14 +214,14 @@ class behat_backup extends behat_base { $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(); @@ -237,14 +242,14 @@ class behat_backup extends behat_base { $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(); @@ -263,12 +268,16 @@ class behat_backup extends behat_base { // 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(); } /** @@ -284,18 +293,18 @@ class behat_backup extends behat_base { // 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(); } /** @@ -339,6 +348,10 @@ class behat_backup extends behat_base { */ protected function wait($timeout = false) { + if (!$this->running_javascript()) { + return; + } + if (!$timeout) { $timeout = self::TIMEOUT; } diff --git a/backup/util/xml/parser/processors/grouped_parser_processor.class.php b/backup/util/xml/parser/processors/grouped_parser_processor.class.php index 48782b7540b..748daace21b 100644 --- a/backup/util/xml/parser/processors/grouped_parser_processor.class.php +++ b/backup/util/xml/parser/processors/grouped_parser_processor.class.php @@ -43,6 +43,18 @@ abstract class grouped_parser_processor extends simplified_parser_processor { 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; @@ -65,7 +77,7 @@ abstract class grouped_parser_processor extends simplified_parser_processor { $a->child = $found; throw new progressive_parser_exception('xml_grouped_child_found', $a); } - $this->groupedpaths[] = $path; + $this->groupedpaths[$path] = true; } parent::add_path($path); } @@ -141,7 +153,7 @@ abstract class grouped_parser_processor extends simplified_parser_processor { } protected function path_is_grouped($path) { - return in_array($path, $this->groupedpaths); + return isset($this->groupedpaths[$path]); } /** @@ -150,16 +162,41 @@ abstract class grouped_parser_processor extends simplified_parser_processor { * 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, @@ -167,7 +204,7 @@ abstract class grouped_parser_processor extends simplified_parser_processor { */ 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; } diff --git a/badges/backpack.js b/badges/backpack.js index 68e736bee08..98678e8f29e 100644 --- a/badges/backpack.js +++ b/badges/backpack.js @@ -2,7 +2,20 @@ * 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 = '
' + errortext + '
'; + + 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'); + } + } } /** diff --git a/badges/badge.php b/badges/badge.php index cd90239502c..339b147313e 100644 --- a/badges/badge.php +++ b/badges/badge.php @@ -56,10 +56,8 @@ if (isloggedin()) { 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(); diff --git a/badges/mybadges.php b/badges/mybadges.php index 46f4c1dce2d..d944e736beb 100644 --- a/badges/mybadges.php +++ b/badges/mybadges.php @@ -90,11 +90,8 @@ $PAGE->set_title($title); $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); diff --git a/badges/renderer.php b/badges/renderer.php index 171bdb3bb9b..0100410eb0a 100644 --- a/badges/renderer.php +++ b/badges/renderer.php @@ -282,6 +282,7 @@ class core_badges_renderer extends plugin_renderer_base { $today = strtotime($today_date); $table = new html_table(); + $table->id = 'issued-badge-table'; $imagetable = new html_table(); $imagetable->attributes = array('class' => 'clearfix badgeissuedimage'); @@ -294,11 +295,13 @@ class core_badges_renderer extends plugin_renderer_base { $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); } } @@ -459,7 +462,7 @@ class core_badges_renderer extends plugin_renderer_base { 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) { diff --git a/blocks/comments/tests/behat/behat_block_comments.php b/blocks/comments/tests/behat/behat_block_comments.php index 548246062a3..3cac07a0d8f 100644 --- a/blocks/comments/tests/behat/behat_block_comments.php +++ b/blocks/comments/tests/behat/behat_block_comments.php @@ -63,7 +63,7 @@ class behat_block_comments extends behat_base { $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); @@ -91,8 +91,11 @@ class behat_block_comments extends behat_base { $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. @@ -101,7 +104,7 @@ class behat_block_comments extends behat_base { $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. diff --git a/blocks/course_overview/renderer.php b/blocks/course_overview/renderer.php index bb2956908ad..6f33899ac12 100644 --- a/blocks/course_overview/renderer.php +++ b/blocks/course_overview/renderer.php @@ -103,10 +103,10 @@ class block_course_overview_renderer extends plugin_renderer_base { // 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); diff --git a/blocks/tests/behat/behat_blocks.php b/blocks/tests/behat/behat_blocks.php index 1c8f70d9aa1..42fe9a117c7 100644 --- a/blocks/tests/behat/behat_blocks.php +++ b/blocks/tests/behat/behat_blocks.php @@ -46,13 +46,13 @@ class behat_blocks extends behat_base { * @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; diff --git a/blocks/tests/behat/configure_block_throughout_site.feature b/blocks/tests/behat/configure_block_throughout_site.feature index fb97f0483d1..9f74446e7ca 100644 --- a/blocks/tests/behat/configure_block_throughout_site.feature +++ b/blocks/tests/behat/configure_block_throughout_site.feature @@ -32,4 +32,4 @@ Feature: Add and configure blocks throughout the site 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" diff --git a/cache/stores/memcache/addinstanceform.php b/cache/stores/memcache/addinstanceform.php index 984966a8b56..eb7c999a1be 100644 --- a/cache/stores/memcache/addinstanceform.php +++ b/cache/stores/memcache/addinstanceform.php @@ -49,7 +49,8 @@ class cachestore_memcache_addinstance_form extends cachestore_addinstance_form { $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 diff --git a/cache/stores/memcache/lang/en/cachestore_memcache.php b/cache/stores/memcache/lang/en/cachestore_memcache.php index 5cba887de78..fb87d289836 100644 --- a/cache/stores/memcache/lang/en/cachestore_memcache.php +++ b/cache/stores/memcache/lang/en/cachestore_memcache.php @@ -31,6 +31,7 @@ $string['prefix'] = 'Key prefix'; $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. diff --git a/cache/stores/memcache/lib.php b/cache/stores/memcache/lib.php index 882ab3775cb..2e104bd6385 100644 --- a/cache/stores/memcache/lib.php +++ b/cache/stores/memcache/lib.php @@ -404,7 +404,7 @@ class cachestore_memcache extends cache_store implements cache_is_configurable { * 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()) { diff --git a/cache/stores/memcache/tests/memcache_test.php b/cache/stores/memcache/tests/memcache_test.php index 023e6cc699b..4e3c2aa9e43 100644 --- a/cache/stores/memcache/tests/memcache_test.php +++ b/cache/stores/memcache/tests/memcache_test.php @@ -59,4 +59,35 @@ class cachestore_memcache_test extends cachestore_tests { 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); + } + } +} diff --git a/cache/stores/memcached/addinstanceform.php b/cache/stores/memcached/addinstanceform.php index a8e98bf0f48..c833a18bbae 100644 --- a/cache/stores/memcached/addinstanceform.php +++ b/cache/stores/memcached/addinstanceform.php @@ -57,11 +57,12 @@ class cachestore_memcached_addinstance_form extends cachestore_addinstance_form $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); @@ -74,4 +75,4 @@ class cachestore_memcached_addinstance_form extends cachestore_addinstance_form $form->setDefault('bufferwrites', 0); $form->setType('bufferwrites', PARAM_BOOL); } -} \ No newline at end of file +} diff --git a/cache/stores/memcached/lang/en/cachestore_memcached.php b/cache/stores/memcached/lang/en/cachestore_memcached.php index 67c3cd22ec2..87713f6755d 100644 --- a/cache/stores/memcached/lang/en/cachestore_memcached.php +++ b/cache/stores/memcached/lang/en/cachestore_memcached.php @@ -42,6 +42,7 @@ $string['hash_murmur'] = 'Murmur'; $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.'; diff --git a/cache/stores/memcached/lib.php b/cache/stores/memcached/lib.php index 6e3834cce0f..a74dc83e053 100644 --- a/cache/stores/memcached/lib.php +++ b/cache/stores/memcached/lib.php @@ -339,7 +339,7 @@ class cachestore_memcached extends cache_store implements cache_is_configurable $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; } @@ -444,7 +444,7 @@ class cachestore_memcached extends cache_store implements cache_is_configurable * 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) { diff --git a/cache/stores/memcached/tests/memcached_test.php b/cache/stores/memcached/tests/memcached_test.php index f0896ca4954..80a918a38c7 100644 --- a/cache/stores/memcached/tests/memcached_test.php +++ b/cache/stores/memcached/tests/memcached_test.php @@ -59,4 +59,35 @@ class cachestore_memcached_test extends cachestore_tests { 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); + } + } +} diff --git a/cache/tests/fixtures/stores.php b/cache/tests/fixtures/stores.php index 2927b4b8f07..a350f6d37fb 100644 --- a/cache/tests/fixtures/stores.php +++ b/cache/tests/fixtures/stores.php @@ -62,7 +62,7 @@ abstract class cachestore_tests extends advanced_testcase { } } 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.'); @@ -71,7 +71,7 @@ abstract class cachestore_tests extends advanced_testcase { } } 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.'); diff --git a/cohort/tests/behat/behat_cohort.php b/cohort/tests/behat/behat_cohort.php index 68ff6073fcd..5349c53e751 100644 --- a/cohort/tests/behat/behat_cohort.php +++ b/cohort/tests/behat/behat_cohort.php @@ -54,10 +54,10 @@ class behat_cohort extends behat_base { $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. @@ -65,11 +65,11 @@ class behat_cohort extends behat_base { $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 ); diff --git a/cohort/tests/behat/upload_cohort_users.feature b/cohort/tests/behat/upload_cohort_users.feature index c40095f16db..47b169a29d7 100644 --- a/cohort/tests/behat/upload_cohort_users.feature +++ b/cohort/tests/behat/upload_cohort_users.feature @@ -32,11 +32,11 @@ Feature: Upload users to a cohort 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 diff --git a/completion/tests/behat/behat_completion.php b/completion/tests/behat/behat_completion.php index ef05aeb328d..89b6344b85f 100644 --- a/completion/tests/behat/behat_completion.php +++ b/completion/tests/behat/behat_completion.php @@ -50,12 +50,13 @@ class behat_completion extends behat_base { 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"') ); } @@ -68,11 +69,13 @@ class behat_completion extends behat_base { */ 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; @@ -89,15 +92,15 @@ class behat_completion extends behat_base { // 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; } diff --git a/course/renderer.php b/course/renderer.php index 9bc279d7a21..d8ea0e58e68 100644 --- a/course/renderer.php +++ b/course/renderer.php @@ -670,7 +670,6 @@ class core_course_renderer extends plugin_renderer_base { //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 @@ -706,7 +705,7 @@ class core_course_renderer extends plugin_renderer_base { } 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); } } @@ -723,7 +722,7 @@ class core_course_renderer extends plugin_renderer_base { // 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)) . diff --git a/course/tests/behat/activities_group_icons.feature b/course/tests/behat/activities_group_icons.feature index 78c342aa7cb..65abea5d721 100644 --- a/course/tests/behat/activities_group_icons.feature +++ b/course/tests/behat/activities_group_icons.feature @@ -27,21 +27,21 @@ Feature: Toggle activities groups mode from the course page | 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 diff --git a/course/tests/behat/activities_indentation.feature b/course/tests/behat/activities_indentation.feature index 5621e49aae9..7cd00794680 100644 --- a/course/tests/behat/activities_indentation.feature +++ b/course/tests/behat/activities_indentation.feature @@ -26,7 +26,7 @@ Feature: Indent items on the course page 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 @@ -34,4 +34,4 @@ Feature: Indent items on the course page 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 diff --git a/course/tests/behat/behat_course.php b/course/tests/behat/behat_course.php index c12a9381941..49145f29859 100644 --- a/course/tests/behat/behat_course.php +++ b/course/tests/behat/behat_course.php @@ -48,7 +48,7 @@ class behat_course extends behat_base { * @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') . '"'); } /** @@ -56,7 +56,7 @@ class behat_course extends behat_base { * @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') . '"'); } /** @@ -68,9 +68,9 @@ class behat_course extends behat_base { 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') . '"') ); } @@ -83,9 +83,9 @@ class behat_course extends behat_base { 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') . '"'), ); } @@ -100,9 +100,9 @@ class behat_course extends behat_base { 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') . '"') ); } @@ -116,7 +116,9 @@ class behat_course extends behat_base { */ 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()) { @@ -126,9 +128,9 @@ class behat_course extends behat_base { $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(); @@ -137,8 +139,8 @@ class behat_course extends behat_base { // 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); @@ -162,7 +164,7 @@ class behat_course extends behat_base { $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') ); } @@ -179,7 +181,7 @@ class behat_course extends behat_base { $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') ); } @@ -268,7 +270,7 @@ class behat_course extends behat_base { // 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()) { @@ -284,8 +286,8 @@ class behat_course extends behat_base { 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()) { @@ -319,7 +321,8 @@ class behat_course extends behat_base { $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()); } @@ -449,10 +452,11 @@ class behat_course extends behat_base { // 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 { @@ -460,8 +464,8 @@ class behat_course extends behat_base { // 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"'), ); } } @@ -482,8 +486,8 @@ class behat_course extends behat_base { // 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') ); } @@ -497,7 +501,7 @@ class behat_course extends behat_base { 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()) { @@ -516,7 +520,7 @@ class behat_course extends behat_base { 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()) { @@ -553,7 +557,7 @@ class behat_course extends behat_base { // 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') . '"') ); @@ -569,7 +573,7 @@ class behat_course extends behat_base { */ 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') .'"') ); @@ -584,7 +588,7 @@ class behat_course extends behat_base { */ 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), @@ -657,9 +661,9 @@ class behat_course extends behat_base { $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); @@ -684,9 +688,9 @@ class behat_course extends behat_base { $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); @@ -730,7 +734,7 @@ class behat_course extends behat_base { */ 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 { @@ -751,8 +755,8 @@ class behat_course extends behat_base { */ 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); } @@ -765,8 +769,8 @@ class behat_course extends behat_base { 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; } diff --git a/course/tests/behat/force_group_mode.feature b/course/tests/behat/force_group_mode.feature index 1d88c8d4a54..59cc3219878 100644 --- a/course/tests/behat/force_group_mode.feature +++ b/course/tests/behat/force_group_mode.feature @@ -28,11 +28,11 @@ Feature: Force group mode in a course | 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 @@ -40,11 +40,11 @@ Feature: Force group mode in a course | 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 @@ -52,9 +52,9 @@ Feature: Force group mode in a course | Group mode | No groups | | Force group mode | Yes | And I press "Save changes" - Then ".//a//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" should not exists - And ".//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" should exists - And I click on "//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element" - And ".//a//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" should not exists - And ".//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" should exists + Then "//a/child::img[contains(@alt, 'No groups (forced mode)')]" "xpath_element" should not exists + And "//img[contains(@alt, 'No groups (forced mode)')]" "xpath_element" should exists + And I click on "//img[contains(@alt, 'No groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element" + And "//a/child::img[contains(@alt, 'No groups (forced mode)')]" "xpath_element" should not exists + And "//img[contains(@alt, 'No groups (forced mode)')]" "xpath_element" should exists diff --git a/enrol/tests/behat/behat_enrol.php b/enrol/tests/behat/behat_enrol.php index 0b18d78b202..7e751d517aa 100644 --- a/enrol/tests/behat/behat_enrol.php +++ b/enrol/tests/behat/behat_enrol.php @@ -50,11 +50,11 @@ class behat_enrol extends behat_base { public function i_add_enrolment_method_with($enrolmethod, TableNode $table) { return array( - new Given('I expand "Users" node'), - new Given('I follow "Enrolment methods"'), - new Given('I select "' . $enrolmethod . '" from "Add method"'), + new Given('I expand "' . get_string('users', 'admin') . '" node'), + new Given('I follow "' . get_string('type_enrol_plural', 'plugin') . '"'), + new Given('I select "' . $this->escape($enrolmethod) . '" from "' . get_string('addinstance', 'enrol') . '"'), new Given('I fill the moodle form with:', $table), - new Given('I press "Add method"') + new Given('I press "' . get_string('addinstance', 'enrol') . '"') ); } diff --git a/grade/grading/form/guide/lib.php b/grade/grading/form/guide/lib.php index c5292ba8eb0..942b19b37fb 100644 --- a/grade/grading/form/guide/lib.php +++ b/grade/grading/form/guide/lib.php @@ -820,10 +820,9 @@ class gradingform_guide_instance extends gradingform_instance { /** * Calculates the grade to be pushed to the gradebook * - * @return int the valid grade from $this->get_controller()->get_grade_range() + * @return float|int the valid grade from $this->get_controller()->get_grade_range() */ public function get_grade() { - global $DB, $USER; $grade = $this->get_guide_filling(); if (!($scores = $this->get_controller()->get_min_max_score()) || $scores['maxscore'] <= $scores['minscore']) { @@ -842,8 +841,12 @@ class gradingform_guide_instance extends gradingform_instance { foreach ($grade['criteria'] as $record) { $curscore += $record['score']; } - return round(($curscore-$scores['minscore'])/($scores['maxscore']-$scores['minscore'])* - ($maxgrade-$mingrade), 0) + $mingrade; + $gradeoffset = ($curscore-$scores['minscore'])/($scores['maxscore']-$scores['minscore'])* + ($maxgrade-$mingrade); + if ($this->get_controller()->get_allow_grade_decimals()) { + return $gradeoffset + $mingrade; + } + return round($gradeoffset, 0) + $mingrade; } /** diff --git a/grade/grading/form/guide/version.php b/grade/grading/form/guide/version.php index 6d92a4fa0cf..48ba85c0ba4 100644 --- a/grade/grading/form/guide/version.php +++ b/grade/grading/form/guide/version.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'gradingform_guide'; -$plugin->version = 2013050100; -$plugin->requires = 2013050100; +$plugin->version = 2013051401.05; +$plugin->requires = 2013051401.05; $plugin->maturity = MATURITY_STABLE; \ No newline at end of file diff --git a/grade/grading/form/lib.php b/grade/grading/form/lib.php index 6c8c8acc5da..574d087d71c 100644 --- a/grade/grading/form/lib.php +++ b/grade/grading/form/lib.php @@ -73,6 +73,9 @@ abstract class gradingform_controller { /** @var array graderange array of valid grades for this area. Use set_grade_range and get_grade_range to access this */ private $graderange = null; + /** @var bool if decimal values are allowed as grades. */ + private $allowgradedecimals = false; + /** @var boolean|null cached result of function has_active_instances() */ protected $hasactiveinstances = null; @@ -622,13 +625,23 @@ abstract class gradingform_controller { /** * Sets the range of grades used in this area. This is usually either range like 0-100 - * or the scale where keys start from 1. Typical use: - * $controller->set_grade_range(make_grades_menu($gradingtype)); + * or the scale where keys start from 1. * - * @param array $graderange + * Typically modules will call it: + * $controller->set_grade_range(make_grades_menu($gradingtype), $gradingtype > 0); + * Negative $gradingtype means that scale is used and the grade must be rounded + * to the nearest int. Positive $gradingtype means that range 0..$gradingtype + * is used for the grades and in this case grade does not have to be rounded. + * + * Sometimes modules always expect grade to be rounded (like mod_assignment does). + * + * @param array $graderange array where first _key_ is the minimum grade and the + * last key is the maximum grade. + * @param bool $allowgradedecimals if decimal values are allowed as grades. */ - public final function set_grade_range(array $graderange) { + public final function set_grade_range(array $graderange, $allowgradedecimals = false) { $this->graderange = $graderange; + $this->allowgradedecimals = $allowgradedecimals; } /** @@ -643,6 +656,15 @@ abstract class gradingform_controller { return $this->graderange; } + /** + * Returns if decimal values are allowed as grades + * + * @return bool + */ + public final function get_allow_grade_decimals() { + return $this->allowgradedecimals; + } + /** * Overridden by sub classes that wish to make definition details available to web services. * When not overridden, only definition data common to all grading methods is made available. @@ -866,7 +888,11 @@ abstract class gradingform_instance { /** * Calculates the grade to be pushed to the gradebook * - * @return int the valid grade from $this->get_controller()->get_grade_range() + * Returned grade must be in range $this->get_controller()->get_grade_range() + * Plugins must returned grade converted to int unless + * $this->get_controller()->get_allow_grade_decimals() is true. + * + * @return float|int */ abstract public function get_grade(); diff --git a/grade/grading/form/rubric/lib.php b/grade/grading/form/rubric/lib.php index ae4f5d507b1..39e6adcbe15 100644 --- a/grade/grading/form/rubric/lib.php +++ b/grade/grading/form/rubric/lib.php @@ -814,10 +814,9 @@ class gradingform_rubric_instance extends gradingform_instance { /** * Calculates the grade to be pushed to the gradebook * - * @return int the valid grade from $this->get_controller()->get_grade_range() + * @return float|int the valid grade from $this->get_controller()->get_grade_range() */ public function get_grade() { - global $DB, $USER; $grade = $this->get_rubric_filling(); if (!($scores = $this->get_controller()->get_min_max_score()) || $scores['maxscore'] <= $scores['minscore']) { @@ -836,7 +835,11 @@ class gradingform_rubric_instance extends gradingform_instance { foreach ($grade['criteria'] as $id => $record) { $curscore += $this->get_controller()->get_definition()->rubric_criteria[$id]['levels'][$record['levelid']]['score']; } - return round(($curscore-$scores['minscore'])/($scores['maxscore']-$scores['minscore'])*($maxgrade-$mingrade), 0) + $mingrade; + $gradeoffset = ($curscore-$scores['minscore'])/($scores['maxscore']-$scores['minscore'])*($maxgrade-$mingrade); + if ($this->get_controller()->get_allow_grade_decimals()) { + return $gradeoffset + $mingrade; + } + return round($gradeoffset, 0) + $mingrade; } /** diff --git a/grade/grading/form/rubric/styles.css b/grade/grading/form/rubric/styles.css index 50a6b37df51..35b22bf83d0 100644 --- a/grade/grading/form/rubric/styles.css +++ b/grade/grading/form/rubric/styles.css @@ -80,7 +80,7 @@ .gradingform_rubric .plainvalue.empty {font-style: italic; color: #AAA;} -.gradingform_rubric.editor .criterion .levels .level .delete {position:absolute;right:0;bottom:0;} +.gradingform_rubric.editor .criterion .levels .level .delete {position:absolute;right:0;} .gradingform_rubric .criterion .levels .level .score {font-style:italic;color:#575;font-weight: bold;margin-top:5px;white-space:nowrap;} .gradingform_rubric .criterion .levels .level .score .scorevalue {padding-right:5px;} @@ -106,7 +106,7 @@ .gradingform_rubric.editor .addcriterion input, .gradingform_rubric.editor .addlevel input {background: transparent url([[pix:t/add]]) no-repeat top left ;display:block;color:#555555;font-weight:bold;text-decoration:none;} .gradingform_rubric.editor .addcriterion input {background-position: 5px 8px;height:30px;line-height:29px;margin-bottom:14px;padding-left:20px;padding-right:10px;} -.gradingform_rubric.editor .addlevel input {background-position: 5px 5px;height:25px;line-height:24px;margin-bottom:10px;padding-left:18px;padding-right:8px;} +.gradingform_rubric.editor .addlevel input {background-position: 5px 5px;height:25px;line-height:24px;margin-bottom:45px;padding-left:18px;padding-right:8px;} .gradingform_rubric .options .optionsheading {font-weight:bold;font-size:1.1em;padding-bottom:5px;} .gradingform_rubric .options .option {padding-bottom:2px;} diff --git a/grade/grading/form/rubric/version.php b/grade/grading/form/rubric/version.php index d69563909bc..020b4ae0d65 100644 --- a/grade/grading/form/rubric/version.php +++ b/grade/grading/form/rubric/version.php @@ -25,8 +25,8 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'gradingform_rubric'; -$plugin->version = 2013050100; +$plugin->version = 2013051401.05; -$plugin->requires = 2013050100; +$plugin->requires = 2013051401.05; $plugin->maturity = MATURITY_STABLE; diff --git a/grade/grading/form/upgrade.txt b/grade/grading/form/upgrade.txt new file mode 100644 index 00000000000..21cc6316240 --- /dev/null +++ b/grade/grading/form/upgrade.txt @@ -0,0 +1,9 @@ +This files describes API changes in /grade/grading/form/* - Advanced grading methods +information provided here is intended especially for developers. + +=== 2.5.2 === + +* Grading methods now can return grade with decimals. See API functions + gradingform_controller::set_grade_range() and + gradingform_controller::get_allow_grade_decimals(), and also examples + in gradingform_rubric_instance::get_grade(). diff --git a/grade/report/grader/lib.php b/grade/report/grader/lib.php index c99f6727d5d..51eddc26a2a 100644 --- a/grade/report/grader/lib.php +++ b/grade/report/grader/lib.php @@ -226,6 +226,10 @@ class grade_report_grader extends grade_report { $changedgrades = true; } else if ($datatype === 'feedback') { + // If quick grading is on, feedback needs to be compared without line breaks. + if ($this->get_pref('quickgrading')) { + $oldvalue->feedback = preg_replace("/\r\n|\r|\n/", "", $oldvalue->feedback); + } if (($oldvalue->feedback === $postedvalue) or ($oldvalue->feedback === NULL and empty($postedvalue))) { continue; } diff --git a/group/tests/behat/behat_groups.php b/group/tests/behat/behat_groups.php index 847727d3b78..2afb6f383b8 100644 --- a/group/tests/behat/behat_groups.php +++ b/group/tests/behat/behat_groups.php @@ -51,36 +51,39 @@ class behat_groups extends behat_base { global $DB; $user = $DB->get_record('user', array('username' => $username)); - $userfullname = fullname($user); + $userfullname = $this->getSession()->getSelectorsHandler()->xpathLiteral(fullname($user)); + + // Using a xpath liternal to avoid problems with quotes and double quotes. + $groupname = $this->getSession()->getSelectorsHandler()->xpathLiteral($groupname); // We don't know the option text as it contains the number of users in the group. $select = $this->find_field('groups'); - $xpath = "//select[@id='groups']/descendant::option[contains(., '" . $groupname . "')]"; + $xpath = "//select[@id='groups']/descendant::option[contains(., $groupname)]"; $groupoption = $this->find('xpath', $xpath); $fulloption = $groupoption->getText(); $select->selectOption($fulloption); // Here we don't need to wait for the AJAX response. - $this->find_button('Add/remove users')->click(); + $this->find_button(get_string('adduserstogroup', 'group'))->click(); // Wait for add/remove members page to be loaded. $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")'); // Getting the option and selecting it. $select = $this->find_field('addselect'); - $xpath = "//select[@id='addselect']/descendant::option[contains(., '" . $userfullname . "')]"; + $xpath = "//select[@id='addselect']/descendant::option[contains(., $userfullname)]"; $memberoption = $this->find('xpath', $xpath); $fulloption = $memberoption->getText(); $select->selectOption($fulloption); // Click add button. - $this->find_button('Add')->click(); + $this->find_button(get_string('add'))->click(); // Wait for the page to load. $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")'); // Returning to the main groups page. - $this->find_button('Back to groups')->click(); + $this->find_button(get_string('backtogroups', 'group'))->click(); } } diff --git a/lang/en/badges.php b/lang/en/badges.php index 437c5dd1ecf..9e5b147dabe 100644 --- a/lang/en/badges.php +++ b/lang/en/badges.php @@ -198,6 +198,7 @@ $string['donotaward'] = 'Currently, this badge is not active, so it cannot be aw $string['editsettings'] = 'Edit settings'; $string['enablebadges'] = 'Enable badges'; $string['error:backpacknotavailable'] = 'Your site is not accessible from the Internet, so any badges issued from this site cannot be verified by external backpack services.'; +$string['error:backpackproblem'] = 'There was a problem connecting to your backpack service provider. Please try again later.'; $string['error:cannotact'] = 'Cannot activate the badge. '; $string['error:cannotawardbadge'] = 'Cannot award badge to a user.'; $string['error:clone'] = 'Cannot clone the badge.'; diff --git a/lib/badgeslib.php b/lib/badgeslib.php index daf2d44f0ce..f4d38cbb467 100644 --- a/lib/badgeslib.php +++ b/lib/badgeslib.php @@ -1348,3 +1348,19 @@ function badges_handle_course_deletion($courseid) { $DB->update_record('badge', $toupdate); } } + +/** + * Loads JS files required for backpack support. + * + * @uses $CFG, $PAGE + * @return void + */ +function badges_setup_backpack_js() { + global $CFG, $PAGE; + if (!empty($CFG->badges_allowexternalbackpack)) { + $PAGE->requires->string_for_js('error:backpackproblem', 'badges'); + $protocol = (strpos($CFG->wwwroot, 'https://') === 0) ? 'https://' : 'http://'; + $PAGE->requires->js(new moodle_url($protocol . 'backpack.openbadges.org/issuer.js'), true); + $PAGE->requires->js('/badges/backpack.js', true); + } +} diff --git a/lib/behat/behat_files.php b/lib/behat/behat_files.php index 4e8b7d171cd..9d12a3a783d 100644 --- a/lib/behat/behat_files.php +++ b/lib/behat/behat_files.php @@ -64,10 +64,12 @@ class behat_files extends behat_base { $exception = new ExpectationException('"' . $filepickerelement . '" filepicker can not be found', $this->getSession()); // Gets the ffilemanager node specified by the locator which contains the filepicker container. + $filepickerelement = $this->getSession()->getSelectorsHandler()->xpathLiteral($filepickerelement); $filepickercontainer = $this->find( 'xpath', - "//input[./@id = //label[contains(normalize-space(string(.)), '" . $filepickerelement . "')]/@for] -//ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' ffilemanager ') or contains(concat(' ', normalize-space(@class), ' '), ' ffilepicker ')]", + "//input[./@id = //label[normalize-space(.)=$filepickerelement]/@for]" . + "//ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' ffilemanager ') or " . + "contains(concat(' ', normalize-space(@class), ' '), ' ffilepicker ')]", $exception ); @@ -117,17 +119,20 @@ class behat_files extends behat_base { $exception = new ExpectationException($exceptionmsg, $this->getSession()); + // Avoid quote-related problems. + $name = $this->getSession()->getSelectorsHandler()->xpathLiteral($name); + // Get a filepicker element (folder or file). try { // First we look at the folder as we need to click on the contextual menu otherwise it would be opened. $node = $this->find( 'xpath', - "//div[@class='fp-content'] -//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')] -[contains(concat(' ', normalize-space(@class), ' '), ' fp-folder ')][contains(normalize-space(string(.)), '" . $name . "')] -//descendant::a[contains(concat(' ', normalize-space(@class), ' '), ' fp-contextmenu ')] -", + "//div[@class='fp-content']" . + "//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')]" . + "[contains(concat(' ', normalize-space(@class), ' '), ' fp-folder ')]" . + "[normalize-space(.)=$name]" . + "//descendant::a[contains(concat(' ', normalize-space(@class), ' '), ' fp-contextmenu ')]", $exception, $containernode ); @@ -137,10 +142,10 @@ class behat_files extends behat_base { // Here the contextual menu is hidden, we click on the thumbnail. $node = $this->find( 'xpath', - "//div[@class='fp-content'] -//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')][contains(normalize-space(string(.)), '" . $name . "')] -//descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-thumbnail ')] -", + "//div[@class='fp-content']" . + "//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')]" . + "[normalize-space(.)=$name]" . + "//descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-thumbnail ')]", false, $containernode ); @@ -176,12 +181,15 @@ class behat_files extends behat_base { // Getting the repository link and opening it. $repoexception = new ExpectationException('The "' . $repositoryname . '" repository has not been found', $this->getSession()); + // Avoid problems with both double and single quotes in the same string. + $repositoryname = $this->getSession()->getSelectorsHandler()->xpathLiteral($repositoryname); + // Here we don't need to look inside the selected filepicker because there can only be one modal window. $repositorylink = $this->find( 'xpath', - "//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-area ')] -//descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-name ')] -[contains(normalize-space(string(.)), '" . $repositoryname . "')]", + "//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-area ')]" . + "//descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-name ')]" . + "[normalize-space(.)=$repositoryname]", $repoexception ); @@ -226,10 +234,10 @@ class behat_files extends behat_base { // only used when accessing the filepicker, there is no filemanager-loading after selecting the file. $this->find( 'xpath', - "//div[contains(concat(' ', @class, ' '), ' filemanager ')]" . - "[not(contains(concat(' ', @class, ' '), ' fm-updating '))]" . + "//div[contains(concat(' ', normalize-space(@class), ' '), ' filemanager ')]" . + "[not(contains(concat(' ', normalize-space(@class), ' '), ' fm-updating '))]" . "|" . - "//div[contains(concat(' ', @class, ' '), ' filemanager-loading ')]" . + "//div[contains(concat(' ', normalize-space(@class), ' '), ' filemanager-loading ')]" . "[contains(@style, 'display: none;')]", $exception, $filepickernode diff --git a/lib/behat/form_field/behat_form_select.php b/lib/behat/form_field/behat_form_select.php index fbde5719266..5be8135633e 100644 --- a/lib/behat/form_field/behat_form_select.php +++ b/lib/behat/form_field/behat_form_select.php @@ -58,10 +58,13 @@ class behat_form_select extends behat_form_field { // Single select needs an extra click in the option. if (!$this->field->hasAttribute('multiple')) { + + $value = $this->session->getSelectorsHandler()->xpathLiteral($value); + // Using the driver direcly because Element methods are messy when dealing // with elements inside containers. $optionxpath = $this->field->getXpath() . - "/descendant::option[(./@value = '" . $value . "' or contains(normalize-space(string(.)), '" . $value . "'))]"; + "/descendant::option[(./@value=$value or normalize-space(.)=$value)]"; $optionnodes = $this->session->getDriver()->find($optionxpath); if ($optionnodes) { current($optionnodes)->click(); diff --git a/lib/behat/lib.php b/lib/behat/lib.php index e35b0e6192c..42ed4418ffd 100644 --- a/lib/behat/lib.php +++ b/lib/behat/lib.php @@ -141,3 +141,29 @@ function behat_error_handler($errno, $errstr, $errfile, $errline, $errcontext) { // Also use the internal error handler so we keep the usual behaviour. return false; } + +/** + * Restrict the config.php settings allowed. + * + * When running the behat features the config.php + * settings should not affect the results. + * + * @return void + */ +function behat_clean_init_config() { + global $CFG; + + $allowed = array_flip(array( + 'wwwroot', 'dataroot', 'dirroot', 'admin', 'directorypermissions', 'filepermissions', + 'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix', 'dboptions', + 'proxyhost', 'proxyport', 'proxytype', 'proxyuser', 'proxypassword', 'proxybypass' + )); + + // Also allowing behat_ prefixed attributes. + foreach ($CFG as $key => $value) { + if (!isset($allowed[$key]) && strpos($key, 'behat_') !== 0) { + unset($CFG->{$key}); + } + } + +} diff --git a/lib/editor/tinymce/plugins/spellchecker/config.php b/lib/editor/tinymce/plugins/spellchecker/config.php index 4f1ac23e6e4..98adf18e4cd 100644 --- a/lib/editor/tinymce/plugins/spellchecker/config.php +++ b/lib/editor/tinymce/plugins/spellchecker/config.php @@ -28,25 +28,18 @@ require('../../../../../config.php'); // General settings $engine = get_config('tinymce_spellchecker', 'spellengine'); -if (!$engine) { - $engine = 'GoogleSpell'; +if (!$engine or $engine === 'GoogleSpell') { + $engine = 'PSpell'; } $config['general.engine'] = $engine; -// GoogleSpell settings -$config['GoogleSpell.proxyhost'] = isset($CFG->proxyhost) ? $CFG->proxyhost : ''; -$config['GoogleSpell.proxyport'] = isset($CFG->proxyport) ? $CFG->proxyport : ''; -$config['GoogleSpell.proxytype'] = isset($CFG->proxytype) ? $CFG->proxytype : 'HTML'; -$config['GoogleSpell.proxyuser'] = isset($CFG->proxyuser) ? $CFG->proxyuser : ''; -$config['GoogleSpell.proxypassword'] = isset($CFG->proxypassword) ? $CFG->proxypassword : ''; - -if ($config['general.engine'] === 'PSpell' || $config['general.engine'] === 'PSpellShell') { +if ($config['general.engine'] === 'PSpell') { // PSpell settings $config['PSpell.mode'] = PSPELL_FAST; $config['PSpell.spelling'] = ""; $config['PSpell.jargon'] = ""; $config['PSpell.encoding'] = ""; - +} else if ($config['general.engine'] === 'PSpellShell') { // PSpellShell settings $config['PSpellShell.mode'] = PSPELL_FAST; $config['PSpellShell.aspell'] = $CFG->aspellpath; diff --git a/lib/editor/tinymce/plugins/spellchecker/lib.php b/lib/editor/tinymce/plugins/spellchecker/lib.php index 3f856656693..8c14e08546d 100644 --- a/lib/editor/tinymce/plugins/spellchecker/lib.php +++ b/lib/editor/tinymce/plugins/spellchecker/lib.php @@ -36,6 +36,12 @@ class tinymce_spellchecker extends editor_tinymce_plugin { return; } + // Check some speller is configured. + $engine = $this->get_config('spellengine', ''); + if (!$engine or $engine === 'GoogleSpell') { + return; + } + // Check at least one language is supported. $spelllanguagelist = $this->get_config('spelllanguagelist', ''); if ($spelllanguagelist !== '') { diff --git a/lib/editor/tinymce/plugins/spellchecker/settings.php b/lib/editor/tinymce/plugins/spellchecker/settings.php index c4081e3a2ea..2be8fde4dd4 100644 --- a/lib/editor/tinymce/plugins/spellchecker/settings.php +++ b/lib/editor/tinymce/plugins/spellchecker/settings.php @@ -26,11 +26,11 @@ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { $options = array( + ''=>get_string('none'), 'PSpell'=>'PSpell', - 'GoogleSpell'=>'Google Spell', 'PSpellShell'=>'PSpellShell'); $settings->add(new admin_setting_configselect('tinymce_spellchecker/spellengine', - get_string('spellengine', 'admin'), '', 'GoogleSpell', $options)); + get_string('spellengine', 'admin'), '', '', $options)); $settings->add(new admin_setting_configtextarea('tinymce_spellchecker/spelllanguagelist', get_string('spelllanguagelist', 'admin'), '', '+English=en,Danish=da,Dutch=nl,Finnish=fi,French=fr,German=de,Italian=it,Polish=pl,' . diff --git a/lib/navigationlib.php b/lib/navigationlib.php index c691a83ef64..0243a88d0b2 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -1266,7 +1266,8 @@ class global_navigation extends navigation_node { // Remove any empty root nodes foreach ($this->rootnodes as $node) { // Dont remove the home node - if ($node->key !== 'home' && !$node->has_children()) { + /** @var navigation_node $node */ + if ($node->key !== 'home' && !$node->has_children() && !$node->isactive) { $node->remove(); } } diff --git a/lib/setup.php b/lib/setup.php index 0b9cbd2f40a..c776a891623 100644 --- a/lib/setup.php +++ b/lib/setup.php @@ -131,6 +131,10 @@ if (!defined('BEHAT_SITE_RUNNING') && !empty($CFG->behat_dataroot) && // actions like reset the site or use data generators. define('BEHAT_SITE_RUNNING', true); + // Clean extra config.php settings. + require_once(__DIR__ . '/../lib/behat/lib.php'); + behat_clean_init_config(); + $CFG->wwwroot = $CFG->behat_wwwroot; $CFG->passwordsaltmain = 'moodle'; $CFG->prefix = $CFG->behat_prefix; diff --git a/lib/tablelib.php b/lib/tablelib.php index ebd4886b0d9..b14231007d0 100644 --- a/lib/tablelib.php +++ b/lib/tablelib.php @@ -1686,6 +1686,7 @@ class table_xhtml_export_format extends table_default_export_format_parent { +