cli_error(get_string('pathserrcreatedataroot', 'install', $a));
}
}
-$CFG->tempdir = $CFG->dataroot.'/temp';
-$CFG->cachedir = $CFG->dataroot.'/cache';
+$CFG->tempdir = $CFG->dataroot.'/temp';
+$CFG->cachedir = $CFG->dataroot.'/cache';
+$CFG->localcachedir = $CFG->dataroot.'/localcache';
// download required lang packs
if ($CFG->lang !== 'en') {
}
// Search by label.
- $searchbox = $this->find_field('Search in settings');
+ $searchbox = $this->find_field(get_string('searchinsettings', 'admin'));
$searchbox->setValue($label);
$submitsearch = $this->find('css', 'form.adminsearchform input[type=submit]');
$submitsearch->press();
// Admin settings does not use the same DOM structure than other moodle forms
// but we also need to use lib/behat/form_field/* to deal with the different moodle form elements.
$exception = new ElementNotFoundException($this->getSession(), '"' . $label . '" administration setting ');
+
+ // The argument should be converted to an xpath literal.
+ $label = $this->getSession()->getSelectorsHandler()->xpathLiteral($label);
+
$fieldxpath = "//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]" .
- "[@id=//label[contains(normalize-space(string(.)), '" . $label . "')]/@for]";
+ "[@id=//label[contains(normalize-space(.), $label)]/@for]";
$fieldnode = $this->find('xpath', $fieldxpath, $exception);
$formfieldtypenode = $this->find('xpath', $fieldxpath . "/ancestor::div[@class='form-setting']" .
"/child::div[contains(concat(' ', @class, ' '), ' form-')]/child::*/parent::div");
$field = behat_field_manager::get_field_instance($type, $fieldnode, $this->getSession());
$field->set_value($value);
- $this->find_button('Save changes')->press();
+ $this->find_button(get_string('savechanges'))->press();
}
}
And I should not see "C_shortname Course fullname"
Scenario: Courses list with extended course names
- Given I click on "Courses" "link" in the "//div[@id='settingsnav']//descendant::li[contains(concat(' ', @class, ' '), ' type_setting ')][not(contains(., 'Site administration'))][contains(., 'Appearance')]" "xpath_element"
+ Given I click on "Courses" "link" in the "//div[@id='settingsnav']/descendant::li[contains(concat(' ', normalize-space(@class), ' '), ' type_setting ')][not(contains(., 'Site administration'))][contains(., 'Appearance')]" "xpath_element"
And I check "Display extended course names"
When I press "Save changes"
And I am on homepage
$CFG->{$var} = $CFG->{'behat_' . $var};
}
+// Clean $CFG extra values before performing any action.
+behat_clean_init_config();
+
$CFG->noemailever = true;
$CFG->passwordsaltmain = 'moodle';
// Unset cache and temp directories to reset them again with the new $CFG->dataroot.
unset($CFG->cachedir);
+unset($CFG->localcachedir);
unset($CFG->tempdir);
// Continues setup.
And I follow "Course 1"
When I click on "Move this to the dock" "button" in the ".block_settings" "css_element"
Then I should not see "Question bank"
- And I click on "//div[@id='dock']/descendant::*[contains(., 'Administration')]/h2" "xpath_element"
+ And I click on "//div[@id='dock']/descendant::h2[normalize-space(.)='Administration']" "xpath_element"
@javascript
Scenario: Locators inside specific DOM nodes using XPath
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And I log in as "admin"
- When I click on "Move this to the dock" "button" in the "//*[contains(concat(' ', normalize-space(@class), ' '), ' block_settings ')]" "xpath_element"
+ When I click on "Move this to the dock" "button" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' block_settings ')]" "xpath_element"
Then I should not see "Turn editing on"
Then I should see "Course 1"
And I should see "Course 2"
And I should see "Course 3"
- When I go to the courses management page
+ And I go to the courses management page
And I follow "Cat 1"
- Then I should see "Cat 2"
+ And I should see "Cat 2"
And I should see "Cat 3"
- When I follow "Cat 3"
- Then I should see "Course 1"
+ And I follow "Cat 3"
+ And I should see "Course 1"
And I should see "Course 2"
- When I select "Cat 2" from "Course categories:"
- Then I should see "No courses in this category"
- When I select "Miscellaneous" from "Course categories:"
- Then I should see "Course 3"
+ And I select "Cat 1 / Cat 2" from "Course categories:"
+ And I should see "No courses in this category"
+ And I select "Miscellaneous" from "Course categories:"
+ And I should see "Course 3"
@javascript
Scenario: Add a bunch of groups and groupings
public function i_log_in_as($username) {
return array(new Given('I am on homepage'),
- new Given('I follow "Log in"'),
- new Given('I fill in "Username" with "'.$username.'"'),
- new Given('I fill in "Password" with "'.$username.'"'),
- new Given('I press "Log in"')
+ new Given('I follow "' . get_string('login') . '"'),
+ new Given('I fill in "' . get_string('username') . '" with "' . $this->escape($username) . '"'),
+ new Given('I fill in "' . get_string('password') . '" with "'. $this->escape($username) . '"'),
+ new Given('I press "' . get_string('login') . '"')
);
}
* @Given /^I log out$/
*/
public function i_log_out() {
- return new When('I follow "Log out"');
+ return new When('I follow "' . get_string('logout') . '"');
}
}
And I should not see "Section 3"
And I press "Continue"
And I click on "Continue" "button" in the ".bcs-current-course" "css_element"
- And I click on "//div[contains(concat(' ', @class, ' '), ' fitem ')][contains(., 'Include calendar events')]/descendant::img" "xpath_element"
- And I click on "setting_root_logs" "checkbox" in the "//div[contains(@class, 'fitem')][contains(., 'Include course logs')]" "xpath_element"
+ And "//div[contains(concat(' ', normalize-space(@class), ' '), ' fitem ')][contains(., 'Include calendar events')]/descendant::img" "xpath_element" should exists
+ And I check "Include course logs"
And I press "Cancel"
And I click on "Cancel" "button" in the ".confirmation-dialogue" "css_element"
$this->find_link($backupcourse)->click();
// Click the backup link.
- $this->find_link('Backup')->click();
+ $this->find_link(get_string('backup'))->click();
// Initial settings.
$this->fill_backup_restore_form($options);
- $this->find_button('Next')->press();
+ $this->find_button(get_string('backupstage1action', 'backup'))->press();
// Schema settings.
$this->fill_backup_restore_form($options);
- $this->find_button('Next')->press();
+ $this->find_button(get_string('backupstage2action', 'backup'))->press();
// Confirmation and review, backup filename can also be specified.
$this->fill_backup_restore_form($options);
- $this->find_button('Perform backup')->press();
+ $this->find_button(get_string('backupstage4action', 'backup'))->press();
// Waiting for it to finish.
$this->wait(10);
// Last backup continue button.
- $this->find_button('Continue')->press();
+ $this->find_button(get_string('backupstage16action', 'backup'))->press();
}
/**
// Click the course link.
$this->find_link($tocourse)->click();
- // Click the backup link.
- $this->find_link('Import')->click();
+ // Click the import link.
+ $this->find_link(get_string('import'))->click();
// Select the course.
$exception = new ExpectationException('"' . $fromcourse . '" course not found in the list of courses to import from', $this->getSession());
- $fromcourse = str_replace("'", "\'", $fromcourse);
- $xpath = "//div[contains(concat(' ', @class, ' '), ' ics-results ')]" .
- "/descendant::tr[contains(., '" . $fromcourse . "')]" .
+ // The argument should be converted to an xpath literal.
+ $fromcourse = $this->getSession()->getSelectorsHandler()->xpathLiteral($fromcourse);
+ $xpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' ics-results ')]" .
+ "/descendant::tr[contains(., $fromcourse)]" .
"/descendant::input[@type='radio']";
$radionode = $this->find('xpath', $xpath, $exception);
$radionode->check();
$radionode->click();
- $this->find_button('Continue')->press();
+ $this->find_button(get_string('continue'))->press();
// Initial settings.
$this->fill_backup_restore_form($options);
- $this->find_button('Next')->press();
+ $this->find_button(get_string('importbackupstage1action', 'backup'))->press();
// Schema settings.
$this->fill_backup_restore_form($options);
- $this->find_button('Next')->press();
+ $this->find_button(get_string('importbackupstage2action', 'backup'))->press();
// Run it.
- $this->find_button('Perform import')->press();
+ $this->find_button(get_string('importbackupstage4action', 'backup'))->press();
$this->wait();
// Continue and redirect to 'to' course.
- $this->find_button('Continue')->press();
+ $this->find_button(get_string('continue'))->press();
}
/**
// Confirm restore.
$this->select_backup($backupfilename);
+ // The argument should be converted to an xpath literal.
+ $existingcourse = $this->getSession()->getSelectorsHandler()->xpathLiteral($existingcourse);
+
// Selecting the specified course (we can not call behat_forms::select_radio here as is in another behat subcontext).
- $existingcourse = str_replace("'", "\'", $existingcourse);
- $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-existing-course')]" .
+ $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-existing-course ')]" .
"/descendant::div[@class='restore-course-search']" .
- "/descendant::tr[contains(., '" . $existingcourse . "')]" .
+ "/descendant::tr[contains(., $existingcourse)]" .
"/descendant::input[@type='radio']");
$radionode->check();
$radionode->click();
// Pressing the continue button of the restore into an existing course section.
- $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-existing-course')]/descendant::input[@type='submit'][@value='Continue']");
+ $continuenode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-existing-course ')]" .
+ "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']");
$continuenode->click();
$this->wait();
$this->select_backup($backupfilename);
// The first category in the list.
- $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-new-course')]" .
+ $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-new-course ')]" .
"/descendant::div[@class='restore-course-search']" .
"/descendant::input[@type='radio']");
$radionode->check();
$radionode->click();
// Pressing the continue button of the restore into an existing course section.
- $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-new-course')]/descendant::input[@type='submit'][@value='Continue']");
+ $continuenode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-new-course ')]" .
+ "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']");
$continuenode->click();
$this->wait();
$this->select_backup($backupfilename);
// Merge without deleting radio option.
- $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" .
+ $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" .
"/descendant::input[@type='radio'][@name='target'][@value='1']");
$radionode->check();
$radionode->click();
// Pressing the continue button of the restore merging section.
- $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" .
- "/descendant::input[@type='submit'][@value='Continue']");
+ $continuenode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" .
+ "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']");
$continuenode->click();
$this->wait();
$this->select_backup($backupfilename);
// Delete contents radio option.
- $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" .
+ $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" .
"/descendant::input[@type='radio'][@name='target'][@value='0']");
$radionode->check();
$radionode->click();
// Pressing the continue button of the restore merging section.
- $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" .
- "/descendant::input[@type='submit'][@value='Continue']");
+ $continuenode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" .
+ "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']");
$continuenode->click();
$this->wait();
// Using xpath as there are other restore links before this one.
$exception = new ExpectationException('The "' . $backupfilename . '" backup file can not be found in this page', $this->getSession());
- $xpath = "//tr[contains(., '" . $backupfilename . "')]/descendant::a[contains(., 'Restore')]";
+
+ // The argument should be converted to an xpath literal.
+ $backupfilename = $this->getSession()->getSelectorsHandler()->xpathLiteral($backupfilename);
+
+ $xpath = "//tr[contains(., $backupfilename)]/descendant::a[contains(., '" . get_string('restore') . "')]";
$restorelink = $this->find('xpath', $xpath, $exception);
$restorelink->click();
// Confirm the backup contents.
- $restore = $this->find_button('Continue')->press();
+ $restore = $this->find_button(get_string('continue'))->press();
}
/**
// Settings.
$this->fill_backup_restore_form($options);
- $this->find_button('Next')->press();
+ $this->find_button(get_string('restorestage4action', 'backup'))->press();
// Schema.
$this->fill_backup_restore_form($options);
- $this->find_button('Next')->press();
+ $this->find_button(get_string('restorestage8action', 'backup'))->press();
// Review, no options here.
- $this->find_button('Perform restore')->press();
+ $this->find_button(get_string('restorestage16action', 'backup'))->press();
$this->wait(10);
// Last restore continue button, redirected to restore course after this.
- $this->find_button('Continue')->press();
+ $this->find_button(get_string('restorestage32action', 'backup'))->press();
}
/**
*/
protected function wait($timeout = false) {
+ if (!$this->running_javascript()) {
+ return;
+ }
+
if (!$timeout) {
$timeout = self::TIMEOUT;
}
* Push badges to backpack.
*/
function addtobackpack(event, args) {
- OpenBadges.issue([args.assertion], function(errors, successes) { });
+ var badgetable = Y.one('#issued-badge-table');
+ var errordiv = Y.one('#addtobackpack-error');
+ var errortext = M.util.get_string('error:backpackproblem', 'badges');
+ var errorhtml = '<div id="addtobackpack-error" class="box boxaligncenter notifyproblem">' + errortext + '</div>';
+
+ if (typeof OpenBadges !== 'undefined') {
+ OpenBadges.issue([args.assertion], function(errors, successes) { });
+ } else {
+ // Add error div if it doesn't exist yet.
+ if (!errordiv) {
+ var badgerror = Y.Node.create(errorhtml);
+ badgetable.insert(badgerror, 'before');
+ }
+ }
}
/**
navigation_node::override_active_url($url);
}
-// TODO: Better way of pushing badges to Mozilla backpack?
-if (!empty($CFG->badges_allowexternalbackpack)) {
- $PAGE->requires->js(new moodle_url('http://backpack.openbadges.org/issuer.js'), true);
-}
+// Include JS files for backpack support.
+badges_setup_backpack_js();
echo $OUTPUT->header();
$PAGE->set_heading($title);
$PAGE->set_pagelayout('mydashboard');
-// TODO: Better way of pushing badges to Mozilla backpack?
-if (!empty($CFG->badges_allowexternalbackpack)) {
- $PAGE->requires->js(new moodle_url('http://backpack.openbadges.org/issuer.js'), true);
- $PAGE->requires->js('/badges/backpack.js', true);
-}
+// Include JS files for backpack support.
+badges_setup_backpack_js();
$output = $PAGE->get_renderer('core', 'badges');
$badges = badges_get_user_badges($USER->id);
$today = strtotime($today_date);
$table = new html_table();
+ $table->id = 'issued-badge-table';
$imagetable = new html_table();
$imagetable->attributes = array('class' => 'clearfix badgeissuedimage');
$expiration = isset($issued['expires']) ? strtotime($issued['expires']) : $today + 1;
if (!empty($CFG->badges_allowexternalbackpack) && ($expiration > $today) && badges_user_has_backpack($USER->id)) {
$assertion = new moodle_url('/badges/assertion.php', array('b' => $ibadge->hash));
+ $action = new component_action('click', 'addtobackpack', array('assertion' => $assertion->out(false)));
$attributes = array(
- 'type' => 'button',
- 'value' => get_string('addtobackpack', 'badges'),
- 'onclick' => 'OpenBadges.issue(["' . $assertion->out(false) . '"], function(errors, successes) { })');
+ 'type' => 'button',
+ 'id' => 'addbutton',
+ 'value' => get_string('addtobackpack', 'badges'));
$tobackpack = html_writer::tag('input', '', $attributes);
+ $this->output->add_action_handler($action, 'addbutton');
$imagetable->data[] = array($tobackpack);
}
}
get_string('downloadall'), 'POST', array('class' => 'activatebadge'));
// Local badges.
- $localhtml = html_writer::start_tag('fieldset', array('class' => 'generalbox'));
+ $localhtml = html_writer::start_tag('fieldset', array('id' => 'issued-badge-table', 'class' => 'generalbox'));
$localhtml .= html_writer::tag('legend',
$this->output->heading_with_help(get_string('localbadges', 'badges', $SITE->fullname), 'localbadgesh', 'badges'));
if ($badges->badges) {
$commentstextarea = $this->find('css', '.comment-area textarea', $exception);
$commentstextarea->setValue($comment);
- $this->find_link('Save comment')->click();
+ $this->find_link(get_string('savecomment'))->click();
// Wait for the AJAX request.
$this->getSession()->wait(4 * 1000, false);
$exception = new ElementNotFoundException($this->getSession(), '"' . $comment . '" comment ');
- $commentxpath = "//div[contains(concat(' ', @class, ' '), ' block_comments ')]" .
- "/descendant::div[@class='comment-message'][contains(., '" . $comment . "')]";
+ // Using xpath liternal to avoid possible problems with comments containing quotes.
+ $commentliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($comment);
+
+ $commentxpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' block_comments ')]" .
+ "/descendant::div[@class='comment-message'][contains(., $commentliteral)]";
$commentnode = $this->find('xpath', $commentxpath, $exception);
// Click on delete icon.
$deleteicon->click();
// Yes confirm.
- $confirmnode = $this->find('xpath', "//div[@class='comment-delete-confirm']/descendant::a[contains(., 'Yes')]");
+ $confirmnode = $this->find('xpath', "//div[@class='comment-delete-confirm']/descendant::a[contains(., '" . get_string('yes') . "')]");
$confirmnode->click();
// Wait for the AJAX request.
// No need to pass title through s() here as it will be done automatically by html_writer.
$attributes = array('title' => $course->fullname);
- if (empty($course->visible)) {
- $attributes['class'] = 'dimmed';
- }
if ($course->id > 0) {
+ if (empty($course->visible)) {
+ $attributes['class'] = 'dimmed';
+ }
$courseurl = new moodle_url('/course/view.php', array('id' => $course->id));
$coursefullname = format_string($course->fullname, true, $course->id);
$link = html_writer::link($courseurl, $coursefullname, $attributes);
}
/**
- * Returns all recent enrollments
+ * Returns all recent enrolments.
+ *
+ * This function previously used get_recent_enrolments located in lib/deprecatedlib.php which would
+ * return an empty array which was identified in MDL-36993. The use of this function outside the
+ * deprecated lib was removed in MDL-40649.
*
* @todo MDL-36993 this function always return empty array
* @return array array of entries from {user} table
*/
protected function get_recent_enrolments() {
- return get_recent_enrolments($this->page->course->id, $this->get_timestart());
+ return array();
}
/**
* @param string $blockname
*/
public function i_add_the_block($blockname) {
- $steps = new Given('I select "' . $blockname . '" from "bui_addblock"');
+ $steps = new Given('I select "' . $this->escape($blockname) . '" from "bui_addblock"');
// If we are running without javascript we need to submit the form.
if (!$this->running_javascript()) {
$steps = array(
$steps,
- new Given('I click on "Go" "button" in the "#add_block" "css_element"')
+ new Given('I click on "' . get_string('go') . '" "button" in the "#add_block" "css_element"')
);
}
return $steps;
And I press "Save changes"
And I follow "Course 1"
# The first block matching the pattern should be top-left block
- And I should see "Comments" in the "//*[@id='region-pre']/descendant::div[contains(concat(' ', @class, ' '), ' block ')]" "xpath_element"
+ And I should see "Comments" in the "//*[@id='region-pre']/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' block ')]" "xpath_element"
$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->setDefault('bufferwrites', 0);
$form->setType('bufferwrites', PARAM_BOOL);
}
-}
\ No newline at end of file
+}
$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;
}
return new external_multiple_structure(
new external_single_structure(
array(
- 'id' => new external_value(PARAM_NUMBER, 'ID of the cohort'),
+ 'id' => new external_value(PARAM_INT, 'ID of the cohort'),
'name' => new external_value(PARAM_RAW, 'cohort name'),
'idnumber' => new external_value(PARAM_RAW, 'cohort idnumber'),
'description' => new external_value(PARAM_RAW, 'cohort description'),
'cohorts' => new external_multiple_structure(
new external_single_structure(
array(
- 'id' => new external_value(PARAM_NUMBER, 'ID of the cohort'),
+ 'id' => new external_value(PARAM_INT, 'ID of the cohort'),
'categorytype' => new external_single_structure(
array(
'type' => new external_value(PARAM_TEXT, 'the name of the field: id (numeric value
$userid = $DB->get_field('user', 'id', array('username' => $username));
$steps = array(
- new Given('I click on "Assign" "link" in the "//table[@id=\'cohorts\']//tr[contains(., \'' . $cohortidnumber . '\')]" "xpath_element"'),
- new Given('I select "' . $userid . '" from "Potential users"'),
- new Given('I press "Add"'),
- new Given('I press "Back to cohorts"')
+ new Given('I click on "' . get_string('assign', 'cohort') . '" "link" in the "' . $this->escape($cohortidnumber) . '" table row'),
+ new Given('I select "' . $userid . '" from "' . get_string('potusers', 'cohort') . '"'),
+ new Given('I press "' . get_string('add') . '"'),
+ new Given('I press "' . get_string('backtocohorts', 'cohort') . '"')
);
// If we are not in the cohorts management we should move there before anything else.
$steps = array_merge(
array(
new Given('I am on homepage'),
- new Given('I collapse "Front page settings" node'),
- new Given('I expand "Site administration" node'),
- new Given('I expand "Users" node'),
- new Given('I expand "Accounts" node'),
- new Given('I follow "Cohorts"')
+ new Given('I collapse "' . get_string('frontpagesettings', 'admin') . '" node'),
+ new Given('I expand "' . get_string('administrationsite') . '" node'),
+ new Given('I expand "' . get_string('users', 'admin') . '" node'),
+ new Given('I expand "' . get_string('accounts', 'admin') . '" node'),
+ new Given('I follow "' . get_string('cohorts', 'cohort') . '"')
),
$steps
);
And I press "Upload users"
And I press "Continue"
And I follow "Cohorts"
- And I click on "Assign" "link" in the "//table[@id='cohorts']//tr[contains(., 'Cohort 1')]" "xpath_element"
+ And I click on "Assign" "link" in the "Cohort 1" table row
Then the "Current users" select box should contain "Tom Jones (tomjones@example.com)"
And the "Current users" select box should contain "Bob Jones (bobjones@example.com)"
And I press "Back to cohorts"
- And I click on "Assign" "link" in the "//table[@id='cohorts']//tr[contains(., 'Cohort 2')]" "xpath_element"
+ And I click on "Assign" "link" in the "Cohort 2" table row
And the "Current users" select box should contain "Mary Smith (marysmith@example.com)"
And the "Current users" select box should contain "Alice Smith (alicesmith@example.com)"
And I am on homepage
// Check we retrieve the good total number of enrolled cohorts + no error on capability.
$this->assertEquals(2, count($returnedcohorts));
- // Call the external function.
- $returnedcohorts = core_cohort_external::get_cohorts(array(
- $cohort1->id, $cohort2->id));
-
foreach ($returnedcohorts as $enrolledcohort) {
if ($enrolledcohort['idnumber'] == $cohort1->idnumber) {
$this->assertEquals($cohort1->name, $enrolledcohort['name']);
core_cohort_external::update_cohorts(array($cohort1));
}
+ /**
+ * Verify handling of 'id' param.
+ */
+ public function test_update_cohorts_invalid_id_param() {
+ $this->resetAfterTest(true);
+ $cohort = self::getDataGenerator()->create_cohort();
+
+ $cohort1 = array(
+ 'id' => 'THIS IS NOT AN ID',
+ 'name' => 'Changed cohort name',
+ 'categorytype' => array('type' => 'id', 'value' => '1'),
+ 'idnumber' => $cohort->idnumber,
+ );
+
+ try {
+ core_cohort_external::update_cohorts(array($cohort1));
+ $this->fail('Expecting invalid_parameter_exception exception, none occured');
+ } catch (invalid_parameter_exception $e1) {
+ $this->assertContains('Invalid external api parameter: the value is "THIS IS NOT AN ID"', $e1->debuginfo);
+ }
+
+ $cohort1['id'] = 9.999; // Also not a valid id of a cohort.
+ try {
+ core_cohort_external::update_cohorts(array($cohort1));
+ $this->fail('Expecting invalid_parameter_exception exception, none occured');
+ } catch (invalid_parameter_exception $e2) {
+ $this->assertContains('Invalid external api parameter: the value is "9.999"', $e2->debuginfo);
+ }
+ }
+
/**
* Test update_cohorts without permission on the dest category.
*/
--- /dev/null
+This files describes API changes in /cohort/ information provided here is intended
+especially for developers.
+
+=== 2.6 ===
+* Webservice core_cohort_update_cohorts was incorrectly specifiying float as the parameter type
+ for cohort id. This field is actually int and input is now reported and processed as such.
+* Webservice core_cohort_get_cohorts was incorrectly specifiying float as the return
+ type for cohort id. The actual return type is int and is now reported as such.
+
public function user_has_completed_activity($userfullname, $activityname) {
// Will throw an exception if the element can not be hovered.
+ $titleliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($userfullname . ", " . $activityname . ": Completed");
$xpath = "//table[@id='completion-progress']" .
- "/descendant::img[contains(@title, '" . $userfullname . ", " . $activityname . ": Completed')]";
+ "/descendant::img[contains(@title, $titleliteral)]";
return array(
new Given('I go to the current course activity completion report'),
- new Given('I hover "' . $xpath . '" "xpath_element"')
+ new Given('I hover "' . $this->escape($xpath) . '" "xpath_element"')
);
}
*/
public function user_has_not_completed_activity($userfullname, $activityname) {
+ // Will throw an exception if the element can not be hovered.
+ $titleliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($userfullname . ", " . $activityname . ": Not completed");
$xpath = "//table[@id='completion-progress']" .
- "/descendant::img[contains(@title, '" . $userfullname . ", " . $activityname . ": Not completed')]";
+ "/descendant::img[contains(@title, $titleliteral)]";
return array(
new Given('I go to the current course activity completion report'),
- new Given('I hover "' . $xpath . '" "xpath_element"')
+ new Given('I hover "' . $this->escape($xpath) . '" "xpath_element"')
);
return $steps;
// Expand reports node if we can't see the link.
try {
- $this->find('xpath', "//*[@id='settingsnav']" .
+ $this->find('xpath', "//div[@id='settingsnav']" .
"/descendant::li" .
- "/descendant::li[not(contains(@class,'collapsed'))]" .
- "/descendant::p[contains(., 'Activity completion')]");
+ "/descendant::li[not(contains(concat(' ', normalize-space(@class), ' '), ' collapsed '))]" .
+ "/descendant::p[contains(., '" . get_string('pluginname', 'report_progress') . "')]");
} catch (ElementNotFoundException $e) {
- $steps[] = new Given('I expand "Reports" node');
+ $steps[] = new Given('I expand "' . get_string('reports') . '" node');
}
- $steps[] = new Given('I follow "Activity completion"');
+ $steps[] = new Given('I follow "' . get_string('pluginname', 'report_progress') . '"');
return $steps;
}
// $CFG->xsendfilealiases = array(
// '/dataroot/' => $CFG->dataroot,
// '/cachedir/' => '/var/www/moodle/cache', // for custom $CFG->cachedir locations
+// '/localcachedir/' => '/var/local/cache', // for custom $CFG->localcachedir locations
// '/tempdir/' => '/var/www/moodle/temp', // for custom $CFG->tempdir locations
// '/filedir' => '/var/www/moodle/filedir', // for custom $CFG->filedir locations
// );
//
// It is possible to specify different cache and temp directories, use local fast filesystem
// for normal web servers. Server clusters MUST use shared filesystem for cachedir!
+// Localcachedir is intended for server clusters, it does not have to be shared by cluster nodes.
// The directories must not be accessible via web.
//
-// $CFG->tempdir = '/var/www/moodle/temp';
-// $CFG->cachedir = '/var/www/moodle/cache';
+// $CFG->tempdir = '/var/www/moodle/temp'; // Files used during one HTTP request only.
+// $CFG->cachedir = '/var/www/moodle/cache'; // Directory MUST BE SHARED by all cluster nodes, locking required.
+// $CFG->localcachedir = '/var/local/cache'; // Intended for local node caching.
//
// Some filesystems such as NFS may not support file locking operations.
// Locking resolves race conditions and is strongly recommended for production servers.
// Example:
// $CFG->behat_restart_browser_after = 7200; // Restarts the browser session after 2 hours
//
+// All this page's extra Moodle settings are compared against a white list of allowed settings
+// (the basic and behat_* ones) to avoid problems with production environments. This setting can be
+// used to expand the default white list with an array of extra settings.
+// Example:
+// $CFG->behat_extraallowedsettings = array('logsql', 'dblogerror');
//=========================================================================
// ALL DONE! To continue installation, visit your main page with a browser
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * format.php - course format featuring single activity included from view.php
+ *
+ * if we are not redirected before this point this means we want to
+ * either manage orphaned activities - i.e. display section 1,
+ * or the activity is not setup, does not have url or is not accessible at the
+ * moment
+ *
+ * @package format_singleactivity
+ * @copyright 2012 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$courserenderer = $PAGE->get_renderer('format_singleactivity');
+echo $courserenderer->display($course, $section != 0);
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for component 'format_singleactivity'
+ *
+ * @package format_singleactivity
+ * @copyright 2012 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['activitytype'] = 'Type of activity';
+$string['activitytype_help'] = 'Choose the type of activity or resource to use in this course';
+$string['defactivitytype'] = 'Default type of activity';
+$string['defactivitytypedesc'] = 'Specify the activity type that will be selected by default when creating a new course';
+$string['erroractivitytype'] = 'Type of activity is not set up in Course settings';
+$string['orphaned'] = 'Orphaned';
+$string['orphanedwarning'] = 'These activities are unreachable by users!';
+$string['pluginname'] = 'Single activity format';
+$string['sectionname'] = '';
+$string['warningchangeformat'] = 'When changing the existing course format to "Single activity" make sure that you removed all extra activities from the course including the "News forum". Note that sections structure may be modified.';
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains main class for the course format singleactivity
+ *
+ * @package format_singleactivity
+ * @copyright 2012 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->dirroot. '/course/format/lib.php');
+
+/**
+ * Main class for the singleactivity course format
+ *
+ * @package format_singleactivity
+ * @copyright 2012 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class format_singleactivity extends format_base {
+ /** @var cm_info the current activity. Use get_activity() to retrieve it. */
+ private $activity = false;
+
+ /**
+ * The URL to use for the specified course
+ *
+ * @param int|stdClass $section Section object from database or just field course_sections.section
+ * if null the course view page is returned
+ * @param array $options options for view URL. At the moment core uses:
+ * 'navigation' (bool) if true and section has no separate page, the function returns null
+ * 'sr' (int) used by multipage formats to specify to which section to return
+ * @return null|moodle_url
+ */
+ public function get_view_url($section, $options = array()) {
+ $sectionnum = $section;
+ if (is_object($sectionnum)) {
+ $sectionnum = $section->section;
+ }
+ if ($sectionnum == 1) {
+ return new moodle_url('/course/view.php', array('id' => $this->courseid, 'section' => 1));
+ }
+ if (!empty($options['navigation']) && $section !== null) {
+ return null;
+ }
+ return new moodle_url('/course/view.php', array('id' => $this->courseid));
+ }
+
+ /**
+ * Loads all of the course sections into the navigation
+ *
+ * @param global_navigation $navigation
+ * @param navigation_node $node The course node within the navigation
+ */
+ public function extend_course_navigation($navigation, navigation_node $node) {
+ // Display orphaned activities for the users who can see them.
+ $context = context_course::instance($this->courseid);
+ if (has_all_capabilities(array('moodle/course:viewhiddensections',
+ 'moodle/course:viewhiddenactivities'), $context)) {
+ $modinfo = get_fast_modinfo($this->courseid);
+ if (!empty($modinfo->sections[1])) {
+ $section1 = $modinfo->get_section_info(1);
+ // Show orphaned activities.
+ $orphanednode = $node->add(get_string('orphaned', 'format_singleactivity'),
+ $this->get_view_url(1), navigation_node::TYPE_SECTION, null, $section1->id);
+ $orphanednode->nodetype = navigation_node::NODETYPE_BRANCH;
+ $orphanednode->add_class('orphaned');
+ foreach ($modinfo->sections[1] as $cmid) {
+ $this->navigation_add_activity($orphanednode, $modinfo->cms[$cmid]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds a course module to the navigation node
+ *
+ * This is basically copied from function global_navigation::load_section_activities()
+ * because it is not accessible from outside.
+ *
+ * @param navigation_node $node
+ * @param cm_info $cm
+ * @return null|navigation_node
+ */
+ protected function navigation_add_activity(navigation_node $node, $cm) {
+ if (!$cm->uservisible) {
+ return null;
+ }
+ $action = $cm->get_url();
+ if (!$action) {
+ // Do not add to navigation activity without url (i.e. labels).
+ return null;
+ }
+ $activityname = format_string($cm->name, true, array('context' => context_module::instance($cm->id)));
+ if ($cm->icon) {
+ $icon = new pix_icon($cm->icon, $cm->modfullname, $cm->iconcomponent);
+ } else {
+ $icon = new pix_icon('icon', $cm->modfullname, $cm->modname);
+ }
+ $activitynode = $node->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $cm->id, $icon);
+ if (global_navigation::module_extends_navigation($cm->modname)) {
+ $activitynode->nodetype = navigation_node::NODETYPE_BRANCH;
+ } else {
+ $activitynode->nodetype = navigation_node::NODETYPE_LEAF;
+ }
+ return $activitynode;
+ }
+
+ /**
+ * Returns the list of blocks to be automatically added for the newly created course
+ *
+ * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT
+ * each of values is an array of block names (for left and right side columns)
+ */
+ public function get_default_blocks() {
+ // No blocks for this format because course view page is not displayed anyway.
+ return array(
+ BLOCK_POS_LEFT => array(),
+ BLOCK_POS_RIGHT => array()
+ );
+ }
+
+ /**
+ * Definitions of the additional options that this course format uses for course
+ *
+ * Singleactivity course format uses one option 'activitytype'
+ *
+ * @param bool $foreditform
+ * @return array of options
+ */
+ public function course_format_options($foreditform = false) {
+ static $courseformatoptions = false;
+ if ($courseformatoptions === false) {
+ $config = get_config('format_singleactivity');
+ $courseformatoptions = array(
+ 'activitytype' => array(
+ 'default' => $config->activitytype,
+ 'type' => PARAM_TEXT,
+ ),
+ );
+ }
+ if ($foreditform && !isset($courseformatoptions['activitytype']['label'])) {
+ $availabletypes = get_module_types_names();
+ $courseformatoptionsedit = array(
+ 'activitytype' => array(
+ 'label' => new lang_string('activitytype', 'format_singleactivity'),
+ 'help' => 'activitytype',
+ 'help_component' => 'format_singleactivity',
+ 'element_type' => 'select',
+ 'element_attributes' => array($availabletypes),
+ ),
+ );
+ $courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit);
+ }
+ return $courseformatoptions;
+ }
+
+ /**
+ * Adds format options elements to the course/section edit form
+ *
+ * This function is called from {@link course_edit_form::definition_after_data()}
+ *
+ * Format singleactivity adds a warning when format of the course is about to be changed.
+ *
+ * @param MoodleQuickForm $mform form the elements are added to
+ * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form
+ * @return array array of references to the added form elements
+ */
+ public function create_edit_form_elements(&$mform, $forsection = false) {
+ global $PAGE;
+ $elements = parent::create_edit_form_elements($mform, $forsection);
+ if (!$forsection && ($course = $PAGE->course) && !empty($course->format) &&
+ $course->format !== 'site' && $course->format !== 'singleactivity') {
+ // This is the existing course in other format, display a warning.
+ $element = $mform->addElement('static', '', '',
+ html_writer::tag('span', get_string('warningchangeformat', 'format_singleactivity'),
+ array('class' => 'error')));
+ array_unshift($elements, $element);
+ }
+ return $elements;
+ }
+
+ /**
+ * Make sure that current active activity is in section 0
+ *
+ * All other activities are moved to section 1 that will be displayed as 'Orphaned'.
+ * It may be needed after the course format was changed or activitytype in
+ * course settings has been changed.
+ *
+ * @return null|cm_info current activity
+ */
+ public function reorder_activities() {
+ course_create_sections_if_missing($this->courseid, array(0, 1));
+ foreach ($this->get_sections() as $sectionnum => $section) {
+ if (($sectionnum && $section->visible) ||
+ (!$sectionnum && !$section->visible)) {
+ // Make sure that 0 section is visible and all others are hidden.
+ set_section_visible($this->courseid, $sectionnum, $sectionnum == 0);
+ }
+ }
+ $modinfo = get_fast_modinfo($this->courseid);
+
+ // Find the current activity (first activity with the specified type in all course activities).
+ $activitytype = $this->get_activitytype();
+ $activity = null;
+ if (!empty($activitytype)) {
+ foreach ($modinfo->sections as $sectionnum => $cmlist) {
+ foreach ($cmlist as $cmid) {
+ if ($modinfo->cms[$cmid]->modname === $activitytype) {
+ $activity = $modinfo->cms[$cmid];
+ break 2;
+ }
+ }
+ }
+ }
+
+ // Make sure the current activity is in the 0-section.
+ if ($activity && $activity->sectionnum != 0) {
+ moveto_module($activity, $modinfo->get_section_info(0));
+ // Cache was reset so get modinfo again.
+ $modinfo = get_fast_modinfo($this->courseid);
+ }
+
+ // Move all other activities into section 1 (the order must be kept).
+ $hasvisibleactivities = false;
+ $firstorphanedcm = null;
+ foreach ($modinfo->sections as $sectionnum => $cmlist) {
+ if ($sectionnum && !empty($cmlist) && $firstorphanedcm === null) {
+ $firstorphanedcm = reset($cmlist);
+ }
+ foreach ($cmlist as $cmid) {
+ if ($sectionnum > 1) {
+ moveto_module($modinfo->get_cm($cmid), $modinfo->get_section_info(1));
+ } else if (!$hasvisibleactivities && $sectionnum == 1 && $modinfo->get_cm($cmid)->visible) {
+ $hasvisibleactivities = true;
+ }
+ }
+ }
+ if (!empty($modinfo->sections[0])) {
+ foreach ($modinfo->sections[0] as $cmid) {
+ if (!$activity || $cmid != $activity->id) {
+ moveto_module($modinfo->get_cm($cmid), $modinfo->get_section_info(1), $firstorphanedcm);
+ }
+ }
+ }
+ if ($hasvisibleactivities) {
+ set_section_visible($this->courseid, 1, false);
+ }
+ return $activity;
+ }
+
+ /**
+ * Returns the name of activity type used for this course
+ *
+ * @return string|null
+ */
+ protected function get_activitytype() {
+ $options = $this->get_format_options();
+ $availabletypes = get_module_types_names();
+ if (!empty($options['activitytype']) &&
+ array_key_exists($options['activitytype'], $availabletypes)) {
+ return $options['activitytype'];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the current activity if exists
+ *
+ * @return null|cm_info
+ */
+ protected function get_activity() {
+ if ($this->activity === false) {
+ $this->activity = $this->reorder_activities();
+ }
+ return $this->activity;
+ }
+
+ /**
+ * Checks if the current user can add the activity of the specified type to this course.
+ *
+ * @return bool
+ */
+ protected function can_add_activity() {
+ global $CFG;
+ if (!($modname = $this->get_activitytype())) {
+ return false;
+ }
+ if (!has_capability('moodle/course:manageactivities', context_course::instance($this->courseid))) {
+ return false;
+ }
+ if (!course_allowed_module($this->get_course(), $modname)) {
+ return false;
+ }
+ $libfile = "$CFG->dirroot/mod/$modname/lib.php";
+ if (!file_exists($libfile)) {
+ return null;
+ }
+ return true;
+ }
+
+ /**
+ * Checks if the activity type requires subtypes.
+ *
+ * @return bool|null (null if the check is not possible)
+ */
+ public function activity_has_subtypes() {
+ global $CFG;
+ if (!($modname = $this->get_activitytype())) {
+ return null;
+ }
+ $libfile = "$CFG->dirroot/mod/$modname/lib.php";
+ if (!file_exists($libfile)) {
+ return null;
+ }
+ include_once($libfile);
+ return function_exists($modname. '_get_types');
+ }
+
+ /**
+ * Allows course format to execute code on moodle_page::set_course()
+ *
+ * This function is executed before the output starts.
+ *
+ * If everything is configured correctly, user is redirected from the
+ * default course view page to the activity view page.
+ *
+ * "Section 1" is the administrative page to manage orphaned activities
+ *
+ * If user is on course view page and there is no module added to the course
+ * and the user has 'moodle/course:manageactivities' capability, redirect to create module
+ * form.
+ *
+ * @param moodle_page $page instance of page calling set_course
+ */
+ public function page_set_course(moodle_page $page) {
+ global $PAGE;
+ $page->add_body_class('format-'. $this->get_format());
+ if ($PAGE == $page && $page->has_set_url() &&
+ $page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) {
+ $edit = optional_param('edit', -1, PARAM_BOOL);
+ if (($edit == 0 || $edit == 1) && confirm_sesskey()) {
+ // This is a request to turn editing mode on or off, do not redirect here, /course/view.php will do redirection.
+ return;
+ }
+ $cm = $this->get_activity();
+ $cursection = optional_param('section', null, PARAM_INT);
+ if (!empty($cursection) && has_capability('moodle/course:viewhiddensections',
+ context_course::instance($this->courseid))) {
+ // Display orphaned activities (course view page, section 1).
+ return;
+ }
+ if (!$this->get_activitytype()) {
+ if (has_capability('moodle/course:update', context_course::instance($this->courseid))) {
+ // Teacher is redirected to edit course page.
+ $url = new moodle_url('/course/edit.php', array('id' => $this->courseid));
+ redirect($url, get_string('erroractivitytype', 'format_singleactivity'));
+ } else {
+ // Student sees an empty course page.
+ return;
+ }
+ }
+ if ($cm === null) {
+ if ($this->can_add_activity()) {
+ // This is a user who has capability to create an activity.
+ if ($this->activity_has_subtypes()) {
+ // Activity that requires subtype can not be added automatically.
+ if (optional_param('addactivity', 0, PARAM_INT)) {
+ return;
+ } else {
+ $url = new moodle_url('/course/view.php', array('id' => $this->courseid, 'addactivity' => 1));
+ redirect($url);
+ }
+ }
+ // Redirect to the add activity form.
+ $url = new moodle_url('/course/mod.php', array('id' => $this->courseid,
+ 'section' => 0, 'sesskey' => sesskey(), 'add' => $this->get_activitytype()));
+ redirect($url);
+ } else {
+ // Student views an empty course page.
+ return;
+ }
+ } else if (!$cm->uservisible || !$cm->get_url()) {
+ // Activity is set but not visible to current user or does not have url.
+ // Display course page (either empty or with availability restriction info).
+ return;
+ } else {
+ // Everything is set up and accessible, redirect to the activity page!
+ redirect($cm->get_url());
+ }
+ }
+ }
+
+ /**
+ * Allows course format to execute code on moodle_page::set_cm()
+ *
+ * If we are inside the main module for this course, remove extra node level
+ * from navigation: substitute course node with activity node, move all children
+ *
+ * @param moodle_page $page instance of page calling set_cm
+ */
+ public function page_set_cm(moodle_page $page) {
+ global $PAGE;
+ parent::page_set_cm($page);
+ if ($PAGE == $page && ($cm = $this->get_activity()) &&
+ $cm->uservisible &&
+ ($cm->id === $page->cm->id) &&
+ ($activitynode = $page->navigation->find($cm->id, navigation_node::TYPE_ACTIVITY)) &&
+ ($node = $page->navigation->find($page->course->id, navigation_node::TYPE_COURSE))) {
+ // Substitute course node with activity node, move all children.
+ $node->action = $activitynode->action;
+ $node->type = $activitynode->type;
+ $node->id = $activitynode->id;
+ $node->key = $activitynode->key;
+ $node->isactive = $node->isactive || $activitynode->isactive;
+ $node->icon = null;
+ if ($activitynode->children->count()) {
+ foreach ($activitynode->children as &$child) {
+ $child->remove();
+ $node->add_node($child);
+ }
+ } else {
+ $node->search_for_active_node();
+ }
+ $activitynode->remove();
+ }
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Renderer for outputting the singleactivity course format.
+ *
+ * @package format_singleactivity
+ * @copyright 2013 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Basic renderer for singleactivity format.
+ *
+ * @package format_singleactivity
+ * @copyright 2013 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class format_singleactivity_renderer extends plugin_renderer_base {
+
+ /**
+ * Displays the activities list in cases when course view page is not
+ * redirected to the activity page.
+ *
+ * @param stdClass $course record from table course
+ * @param bool $orphaned if false displays the main activity (if present)
+ * if true displays all other activities
+ */
+ public function display($course, $orphaned) {
+ $courserenderer = $this->page->get_renderer('core', 'course');
+ $output = '';
+ $modinfo = get_fast_modinfo($course);
+ if ($orphaned) {
+ if (!empty($modinfo->sections[1])) {
+ $output .= $this->output->heading(get_string('orphaned', 'format_singleactivity'), 3, 'sectionname');
+ $output .= $this->output->box(get_string('orphanedwarning', 'format_singleactivity'));
+ $output .= $courserenderer->course_section_cm_list($course, 1, 1);
+ }
+ } else {
+ $output .= $courserenderer->course_section_cm_list($course, 0, 0);
+ if (empty($modinfo->sections[0]) && course_get_format($course)->activity_has_subtypes()) {
+ // Course format was unable to automatically redirect to add module page.
+ $output .= $courserenderer->course_section_add_cm_control($course, 0, 0);
+ }
+ }
+ return $output;
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Settings for format_singleactivity
+ *
+ * @package format_singleactivity
+ * @copyright 2012 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+require_once($CFG->dirroot. '/course/format/singleactivity/settingslib.php');
+
+if ($ADMIN->fulltree) {
+ $settings->add(new format_singleactivity_admin_setting_activitytype('format_singleactivity/activitytype',
+ new lang_string('defactivitytype', 'format_singleactivity'),
+ new lang_string('defactivitytypedesc', 'format_singleactivity'),
+ 'forum', null));
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Settings class for format_singleactivity
+ *
+ * @package format_singleactivity
+ * @copyright 2013 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Admin settings class for the format singleactivity activitytype choice
+ *
+ * @package format_singleactivity
+ * @copyright 2013 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class format_singleactivity_admin_setting_activitytype extends admin_setting_configselect {
+ /**
+ * This function may be used in ancestors for lazy loading of choices
+ *
+ * Override this method if loading of choices is expensive, such
+ * as when it requires multiple db requests.
+ *
+ * @return bool true if loaded, false if error
+ */
+ public function load_choices() {
+ global $CFG;
+ require_once($CFG->dirroot. '/course/lib.php');
+ if (is_array($this->choices)) {
+ return true;
+ }
+ $this->choices = get_module_types_names();
+ return true;
+ }
+}
--- /dev/null
+/* Hide confusing form elements "Display description on course page" and
+"Save and return to course" from module edit form because they
+are not applicable in single activity course format */
+body.format-singleactivity.path-mod.pagelayout-admin form.mform #fitem_id_showdescription,
+body.format-singleactivity.path-mod.pagelayout-admin form.mform .fitem_actionbuttons#fgroup_id_buttonar #id_submitbutton {display:none;}
+
+/* In mod_quiz hide "Back to course" button */
+body.format-singleactivity.path-mod-quiz .quizattempt .continuebutton {display:none;}
+
+body.format-singleactivity .tree_item.orphaned a {color:red;}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Version details
+ *
+ * @package format_singleactivity
+ * @copyright 2012 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version = 2013071200; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires = 2013070800; // Requires this Moodle version (2.6)
+$plugin->component = 'format_singleactivity'; // Full name of the plugin (used for diagnostics).
| Force group mode | No |
When I press "Save changes"
Then "No groups (Click to change)" "link" should exists
- And ".//a//img[contains(@src, 'groupn')]" "xpath_element" should exists
+ And "//a/child::img[contains(@src, 'groupn')]" "xpath_element" should exists
And I click on "No groups (Click to change)" "link" in the "Test forum name" activity
And I wait "3" seconds
And "Separate groups (Click to change)" "link" should exists
- And ".//a//img[contains(@src, 'groups')]" "xpath_element" should exists
+ And "//a/child::img[contains(@src, 'groups')]" "xpath_element" should exists
And I reload the page
And "Separate groups (Click to change)" "link" should exists
- And ".//a//img[contains(@src, 'groups')]" "xpath_element" should exists
+ And "//a/child::img[contains(@src, 'groups')]" "xpath_element" should exists
And I click on "Separate groups (Click to change)" "link" in the "Test forum name" activity
And I wait "3" seconds
And "Visible groups (Click to change)" "link" should exists
- And ".//a//img[contains(@src, 'groupv')]" "xpath_element" should exists
+ And "//a/child::img[contains(@src, 'groupv')]" "xpath_element" should exists
And I reload the page
And "Visible groups (Click to change)" "link" should exists
- And ".//a//img[contains(@src, 'groupv')]" "xpath_element" should exists
+ And "//a/child::img[contains(@src, 'groupv')]" "xpath_element" should exists
And I click on "Visible groups (Click to change)" "link" in the "Test forum name" activity
And "No groups (Click to change)" "link" should exists
- And ".//a//img[contains(@src, 'groupn')]" "xpath_element" should exists
+ And "//a/child::img[contains(@src, 'groupn')]" "xpath_element" should exists
When I indent right "Test glossary name" activity
Then "#section-1 li.glossary div.mod-indent-1" "css_element" should exists
And I indent right "Test glossary name" activity
- And "//*[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should exists
+ And "//li[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should exists
And "#section-1 li.glossary div.mod-indent-2" "css_element" should exists
And I reload the page
And "#section-1 li.glossary div.mod-indent-2" "css_element" should exists
And I indent left "Test glossary name" activity
And "#section-1 li.glossary div.mod-indent-2" "css_element" should not exists
And "#section-1 li.glossary div.mod-indent-1" "css_element" should not exists
- And "//*[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should not exists
+ And "//li[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should not exists
* @Given /^I turn editing mode on$/
*/
public function i_turn_editing_mode_on() {
- return new Given('I press "Turn editing on"');
+ return new Given('I press "' . get_string('turneditingon') . '"');
}
/**
* @Given /^I turn editing mode off$/
*/
public function i_turn_editing_mode_off() {
- return new Given('I press "Turn editing off"');
+ return new Given('I press "' . get_string('turneditingoff') . '"');
}
/**
public function i_create_a_course_with(TableNode $table) {
return array(
new Given('I go to the courses management page'),
- new Given('I press "Add a new course"'),
+ new Given('I press "' . get_string('addnewcourse') . '"'),
new Given('I fill the moodle form with:', $table),
- new Given('I press "Save changes"')
+ new Given('I press "' . get_string('savechanges') . '"')
);
}
return array(
new Given('I am on homepage'),
- new Given('I expand "Site administration" node'),
- new Given('I expand "Courses" node'),
- new Given('I follow "Add/edit courses"'),
+ new Given('I expand "' . get_string('administrationsite') . '" node'),
+ new Given('I expand "' . get_string('courses', 'admin') . '" node'),
+ new Given('I follow "' . get_string('coursemgmt', 'admin') . '"'),
);
}
public function i_add_to_section_and_i_fill_the_form_with($activity, $section, TableNode $data) {
return array(
- new Given('I add a "'.$activity.'" to section "'.$section.'"'),
+ new Given('I add a "' . $this->escape($activity) . '" to section "' . $this->escape($section) . '"'),
new Given('I fill the moodle form with:', $data),
- new Given('I press "Save and return to course"')
+ new Given('I press "' . get_string('savechangesandreturntocourse') . '"')
);
}
*/
public function i_add_to_section($activity, $section) {
- $sectionxpath = "//*[@id='section-" . $section . "']";
+ $sectionxpath = "//li[@id='section-" . $section . "']";
+
+ $activityliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral(ucfirst($activity));
if ($this->running_javascript()) {
$sectionnode->click();
// Clicks the selected activity if it exists.
- $activity = ucfirst($activity);
$activityxpath = "//div[@id='chooseform']/descendant::label" .
- "/descendant::span[contains(concat(' ', @class, ' '), ' typename ')][contains(.,'" . $activity . "')]" .
+ "/descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' typename ')]" .
+ "[contains(., $activityliteral)]" .
"/parent::label/child::input";
$activitynode = $this->find('xpath', $activityxpath);
$activitynode->doubleClick();
// Without Javascript.
// Selecting the option from the select box which contains the option.
- $selectxpath = $sectionxpath . "/descendant::div[contains(concat(' ', @class, ' '), ' section_add_menus ')]" .
- "/descendant::select[contains(., '" . $activity . "')]";
+ $selectxpath = $sectionxpath . "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' section_add_menus ')]" .
+ "/descendant::select[contains(., $activityliteral)]";
$selectnode = $this->find('xpath', $selectxpath);
$selectnode->selectOption($activity);
$xpath = $this->section_exists($sectionnumber);
return array(
- new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $xpath . '" "xpath_element"'),
+ new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"'),
new Given('I wait "2" seconds')
);
}
$xpath = $this->section_exists($sectionnumber);
return array(
- new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $xpath . '" "xpath_element"'),
+ new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"'),
new Given('I wait "2" seconds')
);
}
// Section should be hidden.
$exception = new ExpectationException('The section is not hidden', $this->getSession());
- $this->find('xpath', $sectionxpath . "[contains(concat(' ', @class, ' '), ' hidden ')]", $exception);
+ $this->find('xpath', $sectionxpath . "[contains(concat(' ', normalize-space(@class), ' '), ' hidden ')]", $exception);
// The checking are different depending on user permissions.
if ($this->is_course_editor()) {
foreach ($activities as $activity) {
// Dimmed.
- $this->find('xpath', "//div[contains(concat(' ', @class, ' '), ' activityinstance ')]" .
- "/a[contains(concat(' ', @class, ' '), ' dimmed ')]", $dimmedexception, $activity);
+ $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' activityinstance ')]" .
+ "/a[contains(concat(' ', normalize-space(@class), ' '), ' dimmed ')]", $dimmedexception, $activity);
// Non-JS browsers can not click on img elements.
if ($this->running_javascript()) {
$sectionxpath = $this->section_exists($sectionnumber);
// Section should not be hidden.
- if (!$this->getSession()->getPage()->find('xpath', $sectionxpath . "[not(contains(concat(' ', @class, ' '), ' hidden '))]")) {
+ $xpath = $sectionxpath . "[not(contains(concat(' ', normalize-space(@class), ' '), ' hidden '))]";
+ if (!$this->getSession()->getPage()->find('xpath', $xpath)) {
throw new ExpectationException('The section is hidden', $this->getSession());
}
// JS enabled.
if ($this->running_javascript()) {
- $destinationxpath = $sectionxpath . "/descendant::ul[contains(@class, 'yui3-dd-drop')]";
+ $destinationxpath = $sectionxpath . "/descendant::ul[contains(concat(' ', normalize-space(@class), ' '), ' yui3-dd-drop ')]";
return array(
- new Given('I drag "' . $activitynode->getXpath() . '" "xpath_element" and I drop it in "' . $destinationxpath . '" "xpath_element"'),
+ new Given('I drag "' . $this->escape($activitynode->getXpath()) . '" "xpath_element" ' .
+ 'and I drop it in "' . $this->escape($destinationxpath) . '" "xpath_element"'),
);
} else {
// Moving to the fist spot of the section (before all other section's activities).
return array(
- new Given('I click on "a.editing_move" "css_element" in the "' . $activityname . '" activity'),
- new Given('I click on "li.movehere a" "css_element" in the "' . $sectionxpath . '" "xpath_element"'),
+ new Given('I click on "a.editing_move" "css_element" in the "' . $this->escape($activityname) . '" activity'),
+ new Given('I click on "li.movehere a" "css_element" in the "' . $this->escape($sectionxpath) . '" "xpath_element"'),
);
}
}
// Adding chr(10) to save changes.
return array(
- new Given('I click on "' . get_string('edittitle') . '" "link" in the "' . $activityname .'" activity'),
- new Given('I fill in "title" with "' . $newactivityname . chr(10) . '"'),
+ new Given('I click on "' . get_string('edittitle') . '" "link" in the "' . $this->escape($activityname) .'" activity'),
+ new Given('I fill in "title" with "' . $this->escape($newactivityname) . chr(10) . '"'),
new Given('I wait "2" seconds')
);
}
public function i_indent_right_activity($activityname) {
$steps = array(
- new Given('I click on "' . get_string('moveright') . '" "link" in the "' . $activityname . '" activity')
+ new Given('I click on "' . get_string('moveright') . '" "link" in the "' . $this->escape($activityname) . '" activity')
);
if ($this->running_javascript()) {
public function i_indent_left_activity($activityname) {
$steps = array(
- new Given('I click on "' . get_string('moveleft') . '" "link" in the "' . $activityname . '" activity')
+ new Given('I click on "' . get_string('moveleft') . '" "link" in the "' . $this->escape($activityname) . '" activity')
);
if ($this->running_javascript()) {
// With JS disabled.
$steps = array(
- new Given('I click on "' . $deletestring . '" "link" in the "' . $activityname . '" activity'),
+ new Given('I click on "' . $this->escape($deletestring) . '" "link" in the "' . $this->escape($activityname) . '" activity'),
new Given('I press "' . get_string('yes') . '"')
);
*/
public function i_duplicate_activity($activityname) {
return array(
- new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activityname . '" activity'),
+ new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $this->escape($activityname) . '" activity'),
new Given('I press "' . get_string('continue') .'"'),
new Given('I press "' . get_string('duplicatecontcourse') .'"')
);
*/
public function i_duplicate_activity_editing_the_new_copy_with($activityname, TableNode $data) {
return array(
- new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activityname . '" activity'),
+ new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $this->escape($activityname) . '" activity'),
new Given('I press "' . get_string('continue') .'"'),
new Given('I press "' . get_string('duplicatecontedit') . '"'),
new Given('I fill the moodle form with:', $data),
$courseformat = $this->get_course_format();
// Checking the show button alt text and show icon.
- $showtext = get_string('showfromothers', $courseformat);
- $linkxpath = $xpath . "/descendant::a[@title='". $showtext ."']";
- $imgxpath = $linkxpath . "/descendant::img[@alt='". $showtext ."'][contains(@src, 'show')]";
+ $showtext = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('showfromothers', $courseformat));
+ $linkxpath = $xpath . "/descendant::a[@title=$showtext]";
+ $imgxpath = $linkxpath . "/descendant::img[@alt=$showtext][contains(@src, 'show')]";
$exception = new ElementNotFoundException($this->getSession(), 'Show section icon ');
$this->find('xpath', $imgxpath, $exception);
$courseformat = $this->get_course_format();
// Checking the hide button alt text and hide icon.
- $hidetext = get_string('hidefromothers', $courseformat);
- $linkxpath = $xpath . "/descendant::a[@title='" . $hidetext . "']";
- $imgxpath = $linkxpath . "/descendant::img[@alt='" . $hidetext ."'][contains(@src, 'hide')]";
+ $hidetext = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('hidefromothers', $courseformat));
+ $linkxpath = $xpath . "/descendant::a[@title=$hidetext]";
+ $imgxpath = $linkxpath . "/descendant::img[@alt=$hidetext][contains(@src, 'hide')]";
$exception = new ElementNotFoundException($this->getSession(), 'Hide section icon ');
$this->find('xpath', $imgxpath, $exception);
*/
protected function get_section_activities($sectionxpath) {
- $xpath = $sectionxpath . "/descendant::li[contains(concat(' ', @class, ' '), ' activity ')]";
+ $xpath = $sectionxpath . "/descendant::li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]";
// We spin here, as activities usually require a lot of time to load.
try {
*/
protected function get_activity_node($activityname) {
- $activityname = str_replace("'", "\'", $activityname);
- $xpath = "//li[contains(concat(' ', @class, ' '), ' activity ')][contains(., '" .$activityname. "')]";
+ $activityname = $this->getSession()->getSelectorsHandler()->xpathLiteral($activityname);
+ $xpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][contains(., $activityname)]";
return $this->find('xpath', $xpath);
}
protected function is_course_editor() {
// We don't need to behat_base::spin() here as all is already loaded.
- if (!$this->getSession()->getPage()->findButton('Turn editing off') &&
- !$this->getSession()->getPage()->findButton('Turn editing on')) {
+ if (!$this->getSession()->getPage()->findButton(get_string('turneditingoff')) &&
+ !$this->getSession()->getPage()->findButton(get_string('turneditingon'))) {
return false;
}
| Group mode | Separate groups |
| Force group mode | Yes |
When I press "Save changes"
- Then ".//a//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should not exists
- And ".//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should exists
- And I click on "//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element"
- And ".//a//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should not exists
- And ".//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should exists
+ Then "//a/child::img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" should not exists
+ And "//img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" should exists
+ And I click on "//img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element"
+ And "//a/child::img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" should not exists
+ And "//img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" should exists
@javascript
Scenario: Forced group mode using visible groups
| Group mode | Visible groups |
| Force group mode | Yes |
And I press "Save changes"
- Then ".//a//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should not exists
- And ".//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should exists
- And I click on "//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element"
- And ".//a//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should not exists
- And ".//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should exists
+ Then "//a/child::img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" should not exists
+ And "//img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" should exists
+ And I click on "//img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element"
+ And "//a/child::img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" should not exists
+ And "//img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" should exists
@javascript
Scenario: Forced group mode without groups
| 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
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Local stuff for category enrolment plugin.
+ *
+ * @package enrol_category
+ * @copyright 2010 Petr Skoda {@link http://skodak.org}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Event handler for category enrolment plugin.
+ *
+ * We try to keep everything in sync via listening to events,
+ * it may fail sometimes, so we always do a full sync in cron too.
+ */
+class enrol_category_observer {
+ /**
+ * Triggered when user is assigned a new role.
+ *
+ * @param \core\event\role_assigned $event
+ */
+ public static function role_assigned(\core\event\role_assigned $event) {
+ global $DB;
+
+ if (!enrol_is_enabled('category')) {
+ return;
+ }
+
+ $ra = new stdClass();
+ $ra->roleid = $event->objectid;
+ $ra->userid = $event->relateduserid;
+ $ra->contextid = $event->contextid;
+
+ //only category level roles are interesting
+ $parentcontext = context::instance_by_id($ra->contextid);
+ if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
+ return;
+ }
+
+ // Make sure the role is to be actually synchronised,
+ // please note we are ignoring overrides of the synchronised capability (for performance reasons in full sync).
+ $syscontext = context_system::instance();
+ if (!$DB->record_exists('role_capabilities', array('contextid'=>$syscontext->id, 'roleid'=>$ra->roleid, 'capability'=>'enrol/category:synchronised', 'permission'=>CAP_ALLOW))) {
+ return;
+ }
+
+ // Add necessary enrol instances.
+ $plugin = enrol_get_plugin('category');
+ $sql = "SELECT c.*
+ FROM {course} c
+ JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
+ LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
+ WHERE e.id IS NULL";
+ $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%');
+ $rs = $DB->get_recordset_sql($sql, $params);
+ foreach ($rs as $course) {
+ $plugin->add_instance($course);
+ }
+ $rs->close();
+
+ // Now look for missing enrolments.
+ $sql = "SELECT e.*
+ FROM {course} c
+ JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
+ JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
+ LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
+ WHERE ue.id IS NULL";
+ $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
+ $rs = $DB->get_recordset_sql($sql, $params);
+ foreach ($rs as $instance) {
+ $plugin->enrol_user($instance, $ra->userid, null, time());
+ }
+ $rs->close();
+ }
+
+ /**
+ * Triggered when user role is unassigned.
+ *
+ * @param \core\event\role_unassigned $event
+ */
+ public static function role_unassigned(\core\event\role_unassigned $event) {
+ global $DB;
+
+ if (!enrol_is_enabled('category')) {
+ return;
+ }
+
+ $ra = new stdClass();
+ $ra->userid = $event->relateduserid;
+ $ra->contextid = $event->contextid;
+
+ // only category level roles are interesting
+ $parentcontext = context::instance_by_id($ra->contextid);
+ if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
+ return;
+ }
+
+ // Now this is going to be a bit slow, take all enrolments in child courses and verify each separately.
+ $syscontext = context_system::instance();
+ if (!$roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext)) {
+ return;
+ }
+
+ $plugin = enrol_get_plugin('category');
+
+ $sql = "SELECT e.*
+ FROM {course} c
+ JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
+ JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
+ JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)";
+ $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
+ $rs = $DB->get_recordset_sql($sql, $params);
+
+ list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
+ $params['userid'] = $ra->userid;
+
+ foreach ($rs as $instance) {
+ $coursecontext = context_course::instance($instance->courseid);
+ $contextids = $coursecontext->get_parent_context_ids();
+ array_pop($contextids); // Remove system context, we are interested in categories only.
+
+ list($contextids, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'c');
+ $params = array_merge($params, $contextparams);
+
+ $sql = "SELECT ra.id
+ FROM {role_assignments} ra
+ WHERE ra.userid = :userid AND ra.contextid $contextids AND ra.roleid $roleids";
+ if (!$DB->record_exists_sql($sql, $params)) {
+ // User does not have any interesting role in any parent context, let's unenrol.
+ $plugin->unenrol_user($instance, $ra->userid);
+ }
+ }
+ $rs->close();
+ }
+}
defined('MOODLE_INTERNAL') || die();
-/* List of handlers */
-$handlers = array (
- 'role_assigned' => array (
- 'handlerfile' => '/enrol/category/locallib.php',
- 'handlerfunction' => array('enrol_category_handler', 'role_assigned'),
- 'schedule' => 'instant',
- 'internal' => 1,
+$observers = array (
+
+ array (
+ 'eventname' => '\core\event\role_assigned',
+ 'callback' => 'enrol_category_observer::role_assigned',
),
- 'role_unassigned' => array (
- 'handlerfile' => '/enrol/category/locallib.php',
- 'handlerfunction' => array('enrol_category_handler', 'role_unassigned'),
- 'schedule' => 'instant',
- 'internal' => 1,
+ array (
+ 'eventname' => '\core\event\role_unassigned',
+ 'callback' => 'enrol_category_observer::role_unassigned',
),
-);
\ No newline at end of file
+);
defined('MOODLE_INTERNAL') || die();
-
-/**
- * Event handler for category enrolment plugin.
- *
- * We try to keep everything in sync via listening to events,
- * it may fail sometimes, so we always do a full sync in cron too.
- */
-class enrol_category_handler {
- /**
- * Triggered when user is assigned a new role.
- * @static
- * @param stdClass $ra
- * @return bool
- */
- public static function role_assigned($ra) {
- global $DB;
-
- if (!enrol_is_enabled('category')) {
- return true;
- }
-
- //only category level roles are interesting
- $parentcontext = context::instance_by_id($ra->contextid);
- if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
- return true;
- }
-
- // Make sure the role is to be actually synchronised,
- // please note we are ignoring overrides of the synchronised capability (for performance reasons in full sync).
- $syscontext = context_system::instance();
- if (!$DB->record_exists('role_capabilities', array('contextid'=>$syscontext->id, 'roleid'=>$ra->roleid, 'capability'=>'enrol/category:synchronised', 'permission'=>CAP_ALLOW))) {
- return true;
- }
-
- // Add necessary enrol instances.
- $plugin = enrol_get_plugin('category');
- $sql = "SELECT c.*
- FROM {course} c
- JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
- LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
- WHERE e.id IS NULL";
- $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%');
- $rs = $DB->get_recordset_sql($sql, $params);
- foreach ($rs as $course) {
- $plugin->add_instance($course);
- }
- $rs->close();
-
- // Now look for missing enrolments.
- $sql = "SELECT e.*
- FROM {course} c
- JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
- JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
- LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
- WHERE ue.id IS NULL";
- $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
- $rs = $DB->get_recordset_sql($sql, $params);
- foreach ($rs as $instance) {
- $plugin->enrol_user($instance, $ra->userid, null, $ra->timemodified);
- }
- $rs->close();
-
- return true;
- }
-
- /**
- * Triggered when user role is unassigned.
- * @static
- * @param stdClass $ra
- * @return bool
- */
- public static function role_unassigned($ra) {
- global $DB;
-
- if (!enrol_is_enabled('category')) {
- return true;
- }
-
- // only category level roles are interesting
- $parentcontext = context::instance_by_id($ra->contextid);
- if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
- return true;
- }
-
- // Now this is going to be a bit slow, take all enrolments in child courses and verify each separately.
- $syscontext = context_system::instance();
- if (!$roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext)) {
- return true;
- }
-
- $plugin = enrol_get_plugin('category');
-
- $sql = "SELECT e.*
- FROM {course} c
- JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
- JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
- JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)";
- $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
- $rs = $DB->get_recordset_sql($sql, $params);
-
- list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
- $params['userid'] = $ra->userid;
-
- foreach ($rs as $instance) {
- $coursecontext = context_course::instance($instance->courseid);
- $contextids = $coursecontext->get_parent_context_ids();
- array_pop($contextids); // Remove system context, we are interested in categories only.
-
- list($contextids, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'c');
- $params = array_merge($params, $contextparams);
-
- $sql = "SELECT ra.id
- FROM {role_assignments} ra
- WHERE ra.userid = :userid AND ra.contextid $contextids AND ra.roleid $roleids";
- if (!$DB->record_exists_sql($sql, $params)) {
- // User does not have any interesting role in any parent context, let's unenrol.
- $plugin->unenrol_user($instance, $ra->userid);
- }
- }
- $rs->close();
-
- return true;
- }
-}
-
/**
* Sync all category enrolments in one course
* @param stdClass $course
defined('MOODLE_INTERNAL') || die();
-global $CFG;
-require_once($CFG->dirroot.'/enrol/category/locallib.php');
-
-class enrol_category_testcase extends advanced_testcase {
+class enrol_category_plugin_testcase extends advanced_testcase {
protected function enable_plugin() {
$enabled = enrol_get_plugins(true);
}
public function test_handler_sync() {
- global $DB;
+ global $DB, $CFG;
+ require_once($CFG->dirroot.'/enrol/category/locallib.php');
$this->resetAfterTest();
}
public function test_sync_course() {
- global $DB;
+ global $DB, $CFG;
+ require_once($CFG->dirroot.'/enrol/category/locallib.php');
$this->resetAfterTest();
}
public function test_sync_full() {
- global $DB;
+ global $DB, $CFG;
+ require_once($CFG->dirroot.'/enrol/category/locallib.php');
$this->resetAfterTest();
'users' => new external_multiple_structure(
new external_single_structure(
array(
- 'id' => new external_value(PARAM_NUMBER, 'ID of the user'),
+ 'id' => new external_value(PARAM_INT, 'ID of the user'),
'username' => new external_value(PARAM_RAW, 'Username', VALUE_OPTIONAL),
'firstname' => new external_value(PARAM_NOTAGS, 'The first name(s) of the user', VALUE_OPTIONAL),
'lastname' => new external_value(PARAM_NOTAGS, 'The family name of the user', VALUE_OPTIONAL),
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') . '"')
);
}
* Enrolment plugin which supports self enrolment should implement can_self_enrol()
* Enrolment plugin should implement get_enrol_info() to expose instance information
with webservice or external interface.
+* Webservice core_enrol_get_enrolled_users_with_capability was incorrectly specifing
+ float as the return type for user id. int is the actual returned type and is now
+ reported as such.
=== 2.5 ===
/**
* 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']) {
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;
}
/**
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'gradingform_guide';
-$plugin->version = 2013050100;
-$plugin->requires = 2013050100;
+$plugin->version = 2013071900.02;
+$plugin->requires = 2013071900.02;
$plugin->maturity = MATURITY_STABLE;
\ No newline at end of file
/** @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;
/**
* 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.
+ *
+ * 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
+ * @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;
}
/**
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.
/**
* 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();
/**
* 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']) {
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;
}
/**
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'gradingform_rubric';
-$plugin->version = 2013050100;
+$plugin->version = 2013071900.02;
-$plugin->requires = 2013050100;
+$plugin->requires = 2013071900.02;
$plugin->maturity = MATURITY_STABLE;
--- /dev/null
+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().
*/
function get_unenrolled_users_in_import($importcode, $courseid) {
global $CFG, $DB;
- $relatedctxcondition = get_related_contexts_string(context_course::instance($courseid));
- //users with a gradeable role
+ $coursecontext = context_course::instance($courseid);
+
+ // We want to query both the current context and parent contexts.
+ list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+
+ // Users with a gradeable role.
list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $CFG->gradebookroles), SQL_PARAMS_NAMED, 'grbr');
- //enrolled users
+ // Enrolled users.
$context = context_course::instance($courseid);
list($enrolledsql, $enrolledparams) = get_enrolled_sql($context);
list($sort, $sortparams) = users_order_by_sql('u');
LEFT JOIN ($enrolledsql) je
ON je.id = u.id
LEFT JOIN {role_assignments} ra
- ON (giv.userid = ra.userid AND ra.roleid $gradebookrolessql AND ra.contextid $relatedctxcondition)
+ ON (giv.userid = ra.userid AND ra.roleid $gradebookrolessql AND ra.contextid $relatedctxsql)
WHERE giv.importcode = :importcode
AND (ra.id IS NULL OR je.id IS NULL)
ORDER BY gradeidnumber, $sort";
- $params = array_merge($gradebookrolesparams, $enrolledparams, $sortparams);
+ $params = array_merge($gradebookrolesparams, $enrolledparams, $sortparams, $relatedctxparams);
$params['importcode'] = $importcode;
return $DB->get_records_sql($sql, $params);
export_verify_grades($this->course->id);
$course_item = grade_item::fetch_course_item($this->course->id);
if ($course_item->needsupdate) {
- // can not calculate all final grades - sorry
+ // Can not calculate all final grades - sorry.
return false;
}
$coursecontext = context_course::instance($this->course->id);
- $relatedcontexts = get_related_contexts_string($coursecontext);
- list($gradebookroles_sql, $params) =
- $DB->get_in_or_equal(explode(',', $CFG->gradebookroles), SQL_PARAMS_NAMED, 'grbr');
+ list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+ list($gradebookroles_sql, $params) = $DB->get_in_or_equal(explode(',', $CFG->gradebookroles), SQL_PARAMS_NAMED, 'grbr');
list($enrolledsql, $enrolledparams) = get_enrolled_sql($coursecontext, '', 0, $this->onlyactive);
- $params = array_merge($params, $enrolledparams);
+ $params = array_merge($params, $enrolledparams, $relatedctxparams);
if ($this->groupid) {
$groupsql = "INNER JOIN {groups_members} gm ON gm.userid = u.id";
}
if (empty($this->sortfield1)) {
- // we must do some sorting even if not specified
+ // We must do some sorting even if not specified.
$ofields = ", u.id AS usrt";
$order = "usrt ASC";
$order .= ", usrt2 $this->sortorder2";
}
if ($this->sortfield1 != 'id' and $this->sortfield2 != 'id') {
- // user order MUST be the same in both queries,
- // must include the only unique user->id if not already present
+ // User order MUST be the same in both queries,
+ // must include the only unique user->id if not already present.
$ofields .= ", u.id AS usrt";
$order .= ", usrt ASC";
}
}
}
- // $params contents: gradebookroles and groupid (for $groupwheresql)
$users_sql = "SELECT $userfields $ofields
FROM {user} u
JOIN ($enrolledsql) je ON je.id = u.id
SELECT DISTINCT ra.userid
FROM {role_assignments} ra
WHERE ra.roleid $gradebookroles_sql
- AND ra.contextid $relatedcontexts
+ AND ra.contextid $relatedctxsql
) rainner ON rainner.userid = u.id
WHERE u.deleted = 0
$groupwheresql
$itemids = array_keys($this->grade_items);
list($itemidsql, $grades_params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED, 'items');
$params = array_merge($params, $grades_params);
- // $params contents: gradebookroles, enrolledparams, groupid (for $groupwheresql) and itemids
$grades_sql = "SELECT g.* $ofields
FROM {grade_grades} g
SELECT DISTINCT ra.userid
FROM {role_assignments} ra
WHERE ra.roleid $gradebookroles_sql
- AND ra.contextid $relatedcontexts
+ AND ra.contextid $relatedctxsql
) rainner ON rainner.userid = u.id
WHERE u.deleted = 0
AND g.itemid $itemidsql
}
function grade_get_graded_users_select($report, $course, $userid, $groupid, $includeall) {
- global $USER;
+ global $USER, $CFG;
if (is_null($userid)) {
$userid = $USER->id;
$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;
}
return;
}
- //limit to users with a gradeable role
+ // Limit to users with a gradeable role.
list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
- //limit to users with an active enrollment
+ // Limit to users with an active enrollment.
list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
- //fields we need from the user table
+ // Fields we need from the user table.
$userfields = user_picture::fields('u', get_extra_user_fields($this->context));
- $sortjoin = $sort = $params = null;
+ // We want to query both the current context and parent contexts.
+ list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
- //if the user has clicked one of the sort asc/desc arrows
+ // If the user has clicked one of the sort asc/desc arrows.
if (is_numeric($this->sortitemid)) {
- $params = array_merge(array('gitemid'=>$this->sortitemid), $gradebookrolesparams, $this->groupwheresql_params, $enrolledparams);
+ $params = array_merge(array('gitemid' => $this->sortitemid), $gradebookrolesparams, $this->groupwheresql_params, $enrolledparams,
+ $relatedctxparams);
$sortjoin = "LEFT JOIN {grade_grades} g ON g.userid = u.id AND g.itemid = $this->sortitemid";
$sort = "g.finalgrade $this->sortorder";
-
} else {
$sortjoin = '';
switch($this->sortitemid) {
break;
}
- $params = array_merge($gradebookrolesparams, $this->groupwheresql_params, $enrolledparams);
+ $params = array_merge($gradebookrolesparams, $this->groupwheresql_params, $enrolledparams, $relatedctxparams);
}
$sql = "SELECT $userfields
SELECT DISTINCT ra.userid
FROM {role_assignments} ra
WHERE ra.roleid IN ($this->gradebookroles)
- AND ra.contextid " . get_related_contexts_string($this->context) . "
+ AND ra.contextid $relatedctxsql
) rainner ON rainner.userid = u.id
AND u.deleted = 0
$this->groupwheresql
ORDER BY $sort";
-
$studentsperpage = $this->get_students_per_page();
$this->users = $DB->get_records_sql($sql, $params, $studentsperpage * $this->page, $studentsperpage);
* @return array Array of rows for the right part of the report
*/
public function get_right_avg_row($rows=array(), $grouponly=false) {
- global $CFG, $USER, $DB, $OUTPUT;
+ global $USER, $DB, $OUTPUT;
if (!$this->canviewhidden) {
- // totals might be affected by hiding, if user can not see hidden grades the aggregations might be altered
- // better not show them at all if user can not see all hidden grades
+ // Totals might be affected by hiding, if user can not see hidden grades the aggregations might be altered
+ // better not show them at all if user can not see all hidden grades.
return $rows;
}
- $showaverages = $this->get_pref('showaverages');
- $showaveragesgroup = $this->currentgroup && $showaverages;
-
$averagesdisplaytype = $this->get_pref('averagesdisplaytype');
$averagesdecimalpoints = $this->get_pref('averagesdecimalpoints');
$meanselection = $this->get_pref('meanselection');
$shownumberofgrades = $this->get_pref('shownumberofgrades');
- $avghtml = '';
- $avgcssclass = 'avg';
-
if ($grouponly) {
- $straverage = get_string('groupavg', 'grades');
$showaverages = $this->currentgroup && $this->get_pref('showaverages');
$groupsql = $this->groupsql;
$groupwheresql = $this->groupwheresql;
$groupwheresqlparams = $this->groupwheresql_params;
- $avgcssclass = 'groupavg';
} else {
- $straverage = get_string('overallaverage', 'grades');
$showaverages = $this->get_pref('showaverages');
$groupsql = "";
$groupwheresql = "";
$groupwheresqlparams = array();
}
- if ($shownumberofgrades) {
- $straverage .= ' (' . get_string('submissions', 'grades') . ') ';
- }
+ if ($showaverages) {
+ $totalcount = $this->get_numusers($grouponly);
- $totalcount = $this->get_numusers($grouponly);
+ // Limit to users with a gradeable role.
+ list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
- //limit to users with a gradeable role
- list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
+ // Limit to users with an active enrollment.
+ list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
- //limit to users with an active enrollment
- list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
+ // We want to query both the current context and parent contexts.
+ list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
- if ($showaverages) {
- $params = array_merge(array('courseid'=>$this->courseid), $gradebookrolesparams, $enrolledparams, $groupwheresqlparams);
+ $params = array_merge(array('courseid' => $this->courseid), $gradebookrolesparams, $enrolledparams, $groupwheresqlparams, $relatedctxparams);
- // find sums of all grade items in course
+ // Find sums of all grade items in course.
$sql = "SELECT g.itemid, SUM(g.finalgrade) AS sum
FROM {grade_items} gi
JOIN {grade_grades} g ON g.itemid = gi.id
SELECT DISTINCT ra.userid
FROM {role_assignments} ra
WHERE ra.roleid $gradebookrolessql
- AND ra.contextid " . get_related_contexts_string($this->context) . "
+ AND ra.contextid $relatedctxsql
) rainner ON rainner.userid = u.id
$groupsql
WHERE gi.courseid = :courseid
$groupsql
WHERE gi.courseid = :courseid
AND ra.roleid $gradebookrolessql
- AND ra.contextid ".get_related_contexts_string($this->context)."
+ AND ra.contextid $relatedctxsql
AND u.deleted = 0
AND g.id IS NULL
$groupwheresql
$meancount = $totalcount;
}
- $decimalpoints = $item->get_decimals();
-
// Determine which display type to use for this average
if ($USER->gradeediting[$this->courseid]) {
$displaytype = GRADE_DISPLAY_TYPE_REAL;
* @return int Count of users
*/
public function get_numusers($groups=true) {
- global $CFG, $DB;
+ global $DB;
$groupsql = "";
$groupwheresql = "";
- //limit to users with a gradeable role
+ // Limit to users with a gradeable role.
list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
- //limit to users with an active enrollment
+ // Limit to users with an active enrollment.
list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
- $params = array_merge($gradebookrolesparams, $enrolledparams);
+ // We want to query both the current context and parent contexts.
+ list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+
+ $params = array_merge($gradebookrolesparams, $enrolledparams, $relatedctxparams);
if ($groups) {
$groupsql = $this->groupsql;
WHERE ra.roleid $gradebookrolessql
AND u.deleted = 0
$groupwheresql
- AND ra.contextid ".get_related_contexts_string($this->context);
+ AND ra.contextid $relatedctxsql";
return $DB->count_records_sql($countsql, $params);
}
/**
* Builds the grade item averages.
- *
*/
function calculate_averages() {
global $USER, $DB;
if ($this->showaverage) {
- // this settings are actually grader report settings (not user report)
+ // This settings are actually grader report settings (not user report)
// however we're using them as having two separate but identical settings the
- // user would have to keep in sync would be annoying
+ // user would have to keep in sync would be annoying.
$averagesdisplaytype = $this->get_pref('averagesdisplaytype');
$averagesdecimalpoints = $this->get_pref('averagesdecimalpoints');
$meanselection = $this->get_pref('meanselection');
$shownumberofgrades = $this->get_pref('shownumberofgrades');
$avghtml = '';
- $avgcssclass = 'avg';
-
- $straverage = get_string('overallaverage', 'grades');
-
$groupsql = $this->groupsql;
$groupwheresql = $this->groupwheresql;
- //$groupwheresqlparams = ;
-
- if ($shownumberofgrades) {
- $straverage .= ' (' . get_string('submissions', 'grades') . ') ';
- }
-
$totalcount = $this->get_numusers(false);
- //limit to users with a gradeable role ie students
+ // We want to query both the current context and parent contexts.
+ list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+
+ // Limit to users with a gradeable role ie students.
list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
- //limit to users with an active enrolment
+ // Limit to users with an active enrolment.
list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
- $params = array_merge($this->groupwheresql_params, $gradebookrolesparams, $enrolledparams);
+ $params = array_merge($this->groupwheresql_params, $gradebookrolesparams, $enrolledparams, $relatedctxparams);
$params['courseid'] = $this->courseid;
// find sums of all grade items in course
SELECT DISTINCT ra.userid
FROM {role_assignments} ra
WHERE ra.roleid $gradebookrolessql
- AND ra.contextid " . get_related_contexts_string($this->context) . "
+ AND ra.contextid $relatedctxsql
) rainner ON rainner.userid = u.id
$groupsql
WHERE gi.courseid = :courseid
SELECT DISTINCT ra.userid
FROM {role_assignments} ra
WHERE ra.roleid $gradebookrolessql
- AND ra.contextid " . get_related_contexts_string($this->context) . "
+ AND ra.contextid $relatedctxsql
) rainner ON rainner.userid = u.id
LEFT JOIN {grade_grades} gg
ON (gg.itemid = gi.id AND gg.userid = u.id AND gg.finalgrade IS NOT NULL AND gg.hidden = 0)
$mean_count = $totalcount;
}
- $decimalpoints = $item->get_decimals();
-
// Determine which display type to use for this average
if (!empty($USER->gradeediting) && $USER->gradeediting[$this->courseid]) {
$displaytype = GRADE_DISPLAY_TYPE_REAL;
// Override grade_item setting if a display preference (not inherit) was set for the averages
if ($averagesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {
$decimalpoints = $item->get_decimals();
-
} else {
$decimalpoints = $averagesdecimalpoints;
}
$context = context_course::instance($courseid);
- // we are looking for all users with this role assigned in this context or higher
- $listofcontexts = get_related_contexts_string($context);
-
list($esql, $params) = get_enrolled_sql($context);
if ($roleid) {
- $params['roleid'] = $roleid;
+ // We are looking for all users with this role assigned in this context or higher.
+ list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+
+ $params = array_merge($params, $relatedctxparams, array('roleid' => $roleid));
$where = "WHERE u.id IN (SELECT userid
FROM {role_assignments}
- WHERE roleid = :roleid AND contextid $listofcontexts)";
+ WHERE roleid = :roleid AND contextid $relatedctxsql)";
} else {
$where = "";
}
*/
function groups_get_members_by_role($groupid, $courseid, $fields='u.*',
$sort=null, $extrawheretest='', $whereorsortparams=array()) {
- global $CFG, $DB;
+ global $DB;
// Retrieve information about all users and their roles on the course or
// parent ('related') contexts
$context = context_course::instance($courseid);
+ // We are looking for all users with this role assigned in this context or higher.
+ list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+
if ($extrawheretest) {
$extrawheretest = ' AND ' . $extrawheretest;
}
$sql = "SELECT r.id AS roleid, u.id AS userid, $fields
FROM {groups_members} gm
JOIN {user} u ON u.id = gm.userid
- LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.contextid ".get_related_contexts_string($context).")
+ LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.contextid $relatedctxsql)
LEFT JOIN {role} r ON r.id = ra.roleid
WHERE gm.groupid=:mgroupid
".$extrawheretest."
ORDER BY r.sortorder, $sort";
- $whereorsortparams['mgroupid'] = $groupid;
+ $whereorsortparams = array_merge($whereorsortparams, $relatedctxparams, array('mgroupid' => $groupid));
$rs = $DB->get_recordset_sql($sql, $whereorsortparams);
return groups_calculate_role_people($rs, $context);
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();
}
}
$CFG->dataroot = $config->dataroot;
$CFG->tempdir = $CFG->dataroot.'/temp';
$CFG->cachedir = $CFG->dataroot.'/cache';
+$CFG->localcachedir = $CFG->dataroot.'/localcache';
$CFG->admin = $config->admin;
$CFG->docroot = 'http://docs.moodle.org';
$CFG->langotherroot = $CFG->dataroot.'/lang';
$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.';
$string['cachedef_groupdata'] = 'Course group information';
$string['cachedef_htmlpurifier'] = 'HTML Purifier - cleaned content';
$string['cachedef_locking'] = 'Locking';
+$string['cachedef_observers'] = 'Event observers';
$string['cachedef_plugininfo_base'] = 'Plugin info - base';
$string['cachedef_plugininfo_block'] = 'Plugin info - blocks';
$string['cachedef_plugininfo_filter'] = 'Plugin info - filters';
reload_all_capabilities();
}
- events_trigger('role_assigned', $ra);
+ $event = \core\event\role_assigned::create(
+ array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
+ 'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
+ $event->add_record_snapshot('role_assignments', $ra);
+ $event->trigger();
return $ra->id;
}
if (!empty($USER->id) && $USER->id == $ra->userid) {
reload_all_capabilities();
}
+ $event = \core\event\role_unassigned::create(
+ array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
+ 'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
+ $event->add_record_snapshot('role_assignments', $ra);
+ $event->trigger();
}
- events_trigger('role_unassigned', $ra);
}
unset($ras);
if (!empty($USER->id) && $USER->id == $ra->userid) {
reload_all_capabilities();
}
- events_trigger('role_unassigned', $ra);
+ $event = \core\event\role_unassigned::create(
+ array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
+ 'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
+ $event->add_record_snapshot('role_assignments', $ra);
+ $event->trigger();
}
}
}
* Is this context part of any course? If yes return course context.
*
* @param bool $strict true means throw exception if not found, false means return false if not found
- * @return course_context context of the enclosing course, null if not found or exception
+ * @return context_course context of the enclosing course, null if not found or exception
*/
public function get_course_context($strict = true) {
return $this->get_parent_context();
", $params);
}
-/**
- * Gets a string for sql calls, searching for stuff in this context or above
- *
- * NOTE: use $DB->get_in_or_equal($context->get_parent_context_ids()...
- *
- * @deprecated since 2.2, $context->use get_parent_context_ids() instead
- * @param context $context
- * @return string
- */
-function get_related_contexts_string(context $context) {
-
- if ($parents = $context->get_parent_context_ids()) {
- return (' IN ('.$context->id.','.implode(',', $parents).')');
- } else {
- return (' ='.$context->id);
- }
-}
-
/**
* Given context and array of users, returns array of users whose enrolment status is suspended,
* or enrolment has expired or has not started. Also removes those users from the given array
$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);
+ }
+}
$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
);
$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
);
// 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
);
// 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
);
// 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
// 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();
// 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',
+ 'theme'
+ ));
+
+ // Add extra allowed settings.
+ if (!empty($CFG->behat_extraallowedsettings)) {
+ $allowed = array_merge($allowed, array_flip($CFG->behat_extraallowedsettings));
+ }
+
+ // Also allowing behat_ prefixed attributes.
+ foreach ($CFG as $key => $value) {
+ if (!isset($allowed[$key]) && strpos($key, 'behat_') !== 0) {
+ unset($CFG->{$key});
+ }
+ }
+
+}
*/
class core_component {
/** @var array list of ignored directories - watch out for auth/db exception */
- protected static $ignoreddirs = array('CVS'=>true, '_vti_cnf'=>true, 'simpletest'=>true, 'db'=>true, 'yui'=>true, 'tests'=>true, 'classes'=>true);
+ protected static $ignoreddirs = array('CVS'=>true, '_vti_cnf'=>true, 'simpletest'=>true, 'db'=>true, 'yui'=>true, 'tests'=>true, 'classes'=>true, 'fonts'=>true);
/** @var array list plugin types that support subplugins, do not add more here unless absolutely necessary */
protected static $supportsubplugins = array('mod', 'editor', 'local');
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+namespace core\event;
+
+/**
+ * Base event class.
+ *
+ * @package core
+ * @copyright 2013 Petr Skoda {@link http://skodak.org}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+/**
+ * All other event classes must extend this class.
+ *
+ * @package core
+ * @copyright 2013 Petr Skoda {@link http://skodak.org}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ *
+ * @property-read string $eventname Name of the event (=== class name with leading \)
+ * @property-read string $component Full frankenstyle component name
+ * @property-read string $action what happened
+ * @property-read string $target what/who was target of the action
+ * @property-read string $objecttable name of database table where is object record stored
+ * @property-read int $objectid optional id of the object
+ * @property-read string $crud letter indicating event type
+ * @property-read int $level log level (number between 1 and 100)
+ * @property-read int $contextid
+ * @property-read int $contextlevel
+ * @property-read int $contextinstanceid
+ * @property-read int $userid who did this?
+ * @property-read int $courseid
+ * @property-read int $relateduserid
+ * @property-read mixed $other array or scalar, can not contain objects
+ * @property-read int $timecreated
+ */
+abstract class base implements \IteratorAggregate {
+ /** @var array event data */
+ protected $data;
+
+ /** @var array the format is standardised by logging API */
+ protected $logextra;
+
+ /** @var \context of this event */
+ protected $context;
+
+ /**
+ * @var bool indicates if event was already triggered,
+ * this prevents second attempt to trigger event.
+ */
+ private $triggered;
+
+ /**
+ * @var bool indicates if event was already dispatched,
+ * this prevents direct calling of manager::dispatch($event).
+ */
+ private $dispatched;
+
+ /**
+ * @var bool indicates if event was restored from storage,
+ * this prevents triggering of restored events.
+ */
+ private $restored;
+
+ /** @var array list of event properties */
+ private static $fields = array(
+ 'eventname', 'component', 'action', 'target', 'objecttable', 'objectid', 'crud', 'level', 'contextid',
+ 'contextlevel', 'contextinstanceid', 'userid', 'courseid', 'relateduserid', 'other',
+ 'timecreated');
+
+ /** @var array simple record cache */
+ private $recordsnapshots = array();
+
+ /**
+ * Private constructor, use create() or restore() methods instead.
+ */
+ private final function __construct() {
+ $this->data = array_fill_keys(self::$fields, null);
+ }
+
+ /**
+ * Create new event.
+ *
+ * The optional data keys as:
+ * 1/ objectid - the id of the object specified in class name
+ * 2/ context - the context of this event
+ * 3/ other - the other data describing the event, can not contain objects
+ * 4/ relateduserid - the id of user which is somehow related to this event
+ *
+ * @param array $data
+ * @return \core\event\base returns instance of new event
+ *
+ * @throws \coding_exception
+ */
+ public static final function create(array $data = null) {
+ global $PAGE, $USER;
+
+ $data = (array)$data;
+
+ /** @var \core\event\base $event */
+ $event = new static();
+ $event->triggered = false;
+ $event->restored = false;
+ $event->dispatched = false;
+
+ // Set static event data specific for child class.
+ $event->init();
+
+ // Set automatic data.
+ $event->data['timecreated'] = time();
+
+ $classname = get_class($event);
+ $parts = explode('\\', $classname);
+ if (count($parts) !== 3 or $parts[1] !== 'event') {
+ throw new \coding_exception("Invalid event class name '$classname', it must be defined in component\\event\\ namespace");
+ }
+ $event->data['eventname'] = '\\'.$classname;
+ $event->data['component'] = $parts[0];
+
+ $pos = strrpos($parts[2], '_');
+ if ($pos === false) {
+ throw new \coding_exception("Invalid event class name '$classname', there must be at least one underscore separating object and action words");
+ }
+ $event->data['target'] = substr($parts[2], 0, $pos);
+ $event->data['action'] = substr($parts[2], $pos+1);
+
+ // Set optional data or use defaults.
+ $event->data['objectid'] = isset($data['objectid']) ? $data['objectid'] : null;
+ $event->data['courseid'] = isset($data['courseid']) ? $data['courseid'] : null;
+ $event->data['userid'] = isset($data['userid']) ? $data['userid'] : $USER->id;
+ $event->data['other'] = isset($data['other']) ? $data['other'] : null;
+ $event->data['relateduserid'] = isset($data['relateduserid']) ? $data['relateduserid'] : null;
+
+ if (isset($event->context)) {
+ if (isset($data['context'])) {
+ debugging('Context was already set in init() method, ignoring context parameter', DEBUG_DEVELOPER);
+ }
+
+ } else if (!empty($data['context'])) {
+ $event->context = $data['context'];
+
+ } else if (!empty($data['contextid'])) {
+ $event->context = \context::instance_by_id($data['contextid'], MUST_EXIST);
+
+ } else {
+ throw new \coding_exception('context (or contextid) is a required event property, system context may be hardcoded in init() method.');
+ }
+
+ $event->data['contextid'] = $event->context->id;
+ $event->data['contextlevel'] = $event->context->contextlevel;
+ $event->data['contextinstanceid'] = $event->context->instanceid;
+
+ if (!isset($event->data['courseid'])) {
+ if ($coursecontext = $event->context->get_course_context(false)) {
+ $event->data['courseid'] = $coursecontext->id;
+ } else {
+ $event->data['courseid'] = 0;
+ }
+ }
+
+ if (!array_key_exists('relateduserid', $data) and $event->context->contextlevel == CONTEXT_USER) {
+ $event->data['relateduserid'] = $event->context->instanceid;
+ }
+
+ // Warn developers if they do something wrong.
+ if (debugging('', DEBUG_DEVELOPER)) { // This should be replaced by new $CFG->slowdebug flag if introduced.
+ static $automatickeys = array('eventname', 'component', 'action', 'target', 'contextlevel', 'contextinstanceid', 'timecreated');
+ static $initkeys = array('crud', 'level', 'objecttable');
+
+ foreach ($data as $key => $ignored) {
+ if ($key === 'context') {
+ continue;
+
+ } else if (in_array($key, $automatickeys)) {
+ debugging("Data key '$key' is not allowed in \\core\\event\\base::create() method, it is set automatically");
+
+ } else if (in_array($key, $initkeys)) {
+ debugging("Data key '$key' is not allowed in \\core\\event\\base::create() method, you need to set it in init() method");
+
+ } else if (!in_array($key, self::$fields)) {
+ debugging("Data key '$key' does not exist in \\core\\event\\base");
+ }
+ }
+ }
+
+ // Let developers validate their custom data (such as $this->data['other'], contextlevel, etc.).
+ $event->validate_data();
+
+ return $event;
+ }
+
+ /**
+ * Override in subclass.
+ *
+ * Set all required data properties:
+ * 1/ crud - letter [crud] TODO: MDL-37658
+ * 2/ level - number 1...100 TODO: MDL-37658
+ * 3/ objecttable - name of database table if objectid specified
+ *
+ * Optionally it can set:
+ * a/ fixed system context
+ *
+ * @return void
+ */
+ protected abstract function init();
+
+ /**
+ * Let developers validate their custom data (such as $this->data['other'], contextlevel, etc.).
+ *
+ * Throw \coding_exception or debugging() notice in case of any problems.
+ */
+ protected function validate_data() {
+ // Override if you want to validate event properties when
+ // creating new events.
+ }
+
+ /**
+ * Returns localised general event name.
+ *
+ * Override in subclass, we can not make it static and abstract at the same time.
+ *
+ * TODO: MDL-37658
+ *
+ * @return string|\lang_string
+ */
+ public static function get_name() {
+ // Override in subclass with real lang string.
+ $parts = explode('\\', __CLASS__);
+ if (count($parts) !== 3) {
+ return 'unknown event';
+ }
+ return $parts[0].': '.str_replace('_', ' ', $parts[2]);
+ }
+
+ /**
+ * Returns localised description of what happened.
+ *
+ * TODO: MDL-37658
+ *
+ * @return string|\lang_string
+ */
+ public function get_description() {
+ return null;
+ }
+
+ /**
+ * Define whether a user can view the event or not.
+ *
+ * @param int|\stdClass $user_or_id ID of the user.
+ * @return bool True if the user can view the event, false otherwise.
+ */
+ public function can_view($user_or_id = null) {
+ return is_siteadmin($user_or_id);
+ }
+
+ /**
+ * Restore event from existing historic data.
+ *
+ * @param array $data
+ * @param array $logextra the format is standardised by logging API
+ * @return bool|\core\event\base
+ */
+ public static final function restore(array $data, array $logextra) {
+ $classname = $data['eventname'];
+ $component = $data['component'];
+ $action = $data['action'];
+ $target = $data['target'];
+
+ // Security: make 100% sure this really is an event class.
+ if ($classname !== "\\{$component}\\event\\{$target}_{$action}") {
+ return false;
+ }
+
+ if (!class_exists($classname)) {
+ return false;
+ }
+ $event = new $classname();
+ if (!($event instanceof \core\event\base)) {
+ return false;
+ }
+
+ $event->restored = true;
+ $event->triggered = true;
+ $event->dispatched = true;
+ $event->logextra = $logextra;
+
+ foreach (self::$fields as $key) {
+ if (!array_key_exists($key, $data)) {
+ debugging("Event restore data must contain key $key");
+ $data[$key] = null;
+ }
+ }
+ if (count($data) != count(self::$fields)) {
+ foreach ($data as $key => $value) {
+ if (!in_array($key, self::$fields)) {
+ debugging("Event restore data cannot contain key $key");
+ unset($data[$key]);
+ }
+ }
+ }
+ $event->data = $data;
+
+ return $event;
+ }
+
+ /**
+ * Returns event context.
+ * @return \context
+ */
+ public function get_context() {
+ if (isset($this->context)) {
+ return $this->context;
+ }
+ $this->context = \context::instance_by_id($this->data['contextid'], false);
+ return $this->context;
+ }
+
+ /**
+ * Returns relevant URL, override in subclasses.
+ * @return \moodle_url
+ */
+ public function get_url() {
+ return null;
+ }
+
+ /**
+ * Return standardised event data as array.
+ *
+ * @return array
+ */
+ public function get_data() {
+ return $this->data;
+ }
+
+ /**
+ * Return auxiliary data that was stored in logs.
+ *
+ * TODO: MDL-37658
+ *
+ * @return array the format is standardised by logging API
+ */
+ public function get_logextra() {
+ return $this->logextra;
+ }
+
+ /**
+ * Does this event replace legacy event?
+ *
+ * Note: do not use directly!
+ *
+ * @return null|string legacy event name
+ */
+ protected function get_legacy_eventname() {
+ return null;
+ }
+
+ /**
+ * Legacy event data if get_legacy_eventname() is not empty.
+ *
+ * Note: do not use directly!
+ *
+ * @return mixed
+ */
+ protected function get_legacy_eventdata() {
+ return null;
+ }
+
+ /**
+ * Doest this event replace add_to_log() statement?
+ *
+ * Note: do not use directly!
+ *
+ * @return null|array of parameters to be passed to legacy add_to_log() function.
+ */
+ protected function get_legacy_logdata() {
+ return null;
+ }
+
+ /**
+ * Validate all properties right before triggering the event.
+ *
+ * This throws coding exceptions for fatal problems and debugging for minor problems.
+ *
+ * @throws \coding_exception
+ */
+ protected final function validate_before_trigger() {
+ global $DB;
+
+ if (empty($this->data['crud'])) {
+ throw new \coding_exception('crud must be specified in init() method of each method');
+ }
+ if (empty($this->data['level'])) {
+ throw new \coding_exception('level must be specified in init() method of each method');
+ }
+ if (!empty($this->data['objectid']) and empty($this->data['objecttable'])) {
+ throw new \coding_exception('objecttable must be specified in init() method if objectid present');
+ }
+
+ if (debugging('', DEBUG_DEVELOPER)) { // This should be replaced by new $CFG->slowdebug flag if introduced.
+ // Ideally these should be coding exceptions, but we need to skip these for performance reasons
+ // on production servers.
+
+ if (!in_array($this->data['crud'], array('c', 'r', 'u', 'd'), true)) {
+ debugging("Invalid event crud value specified.");
+ }
+ if (!is_number($this->data['level'])) {
+ debugging('Event property level must be a number');
+ }
+ if (self::$fields !== array_keys($this->data)) {
+ debugging('Number of event data fields must not be changed in event classes');
+ }
+ $encoded = json_encode($this->data['other']);
+ if ($encoded === false or $this->data['other'] !== json_decode($encoded, true)) {
+ debugging('other event data must be compatible with json encoding');
+ }
+ if ($this->data['userid'] and !is_number($this->data['userid'])) {
+ debugging('Event property userid must be a number');
+ }
+ if ($this->data['courseid'] and !is_number($this->data['courseid'])) {
+ debugging('Event property courseid must be a number');
+ }
+ if ($this->data['objectid'] and !is_number($this->data['objectid'])) {
+ debugging('Event property objectid must be a number');
+ }
+ if ($this->data['relateduserid'] and !is_number($this->data['relateduserid'])) {
+ debugging('Event property relateduserid must be a number');
+ }
+ if ($this->data['objecttable']) {
+ if (!$DB->get_manager()->table_exists($this->data['objecttable'])) {
+ debugging('Unknown table specified in objecttable field');
+ }
+ }
+ }
+ }
+
+ /**
+ * Trigger event.
+ */
+ public final function trigger() {
+ global $CFG;
+
+ if ($this->restored) {
+ throw new \coding_exception('Can not trigger restored event');
+ }
+ if ($this->triggered or $this->dispatched) {
+ throw new \coding_exception('Can not trigger event twice');
+ }
+
+ $this->validate_before_trigger();
+
+ $this->triggered = true;
+
+ if (isset($CFG->loglifetime) and $CFG->loglifetime != -1) {
+ if ($data = $this->get_legacy_logdata()) {
+ call_user_func_array('add_to_log', $data);
+ }
+ }
+
+ if (PHPUNIT_TEST and \phpunit_util::is_redirecting_events()) {
+ $this->dispatched = true;
+ \phpunit_util::event_triggered($this);
+ return;
+ }
+
+ \core\event\manager::dispatch($this);
+
+ $this->dispatched = true;
+
+ if ($legacyeventname = $this->get_legacy_eventname()) {
+ events_trigger($legacyeventname, $this->get_legacy_eventdata());
+ }
+ }
+
+ /**
+ * Was this event already triggered?
+ *
+ * @return bool
+ */
+ public final function is_triggered() {
+ return $this->triggered;
+ }
+
+ /**
+ * Used from event manager to prevent direct access.
+ *
+ * @return bool
+ */
+ public final function is_dispatched() {
+ return $this->dispatched;
+ }
+
+ /**
+ * Was this event restored?
+ *
+ * @return bool
+ */
+ public final function is_restored() {
+ return $this->restored;
+ }
+
+ /**
+ * Add cached data that will be most probably used in event observers.
+ *
+ * This is used to improve performance, but it is required for data
+ * that was just deleted.
+ *
+ * @param string $tablename
+ * @param \stdClass $record
+ *
+ * @throws \coding_exception if used after ::trigger()
+ */
+ public final function add_record_snapshot($tablename, $record) {
+ global $DB;
+
+ if ($this->triggered) {
+ throw new \coding_exception('It is not possible to add snapshots after triggering of events');
+ }
+
+ // NOTE: this might use some kind of MUC cache,
+ // hopefully we will not run out of memory here...
+ if (debugging('', DEBUG_DEVELOPER)) { // This should be replaced by new $CFG->slowdebug flag if introduced.
+ if (!$DB->get_manager()->table_exists($tablename)) {
+ debugging("Invalid table name '$tablename' specified, database table does not exist.");
+ }
+ }
+ $this->recordsnapshots[$tablename][$record->id] = $record;
+ }
+
+ /**
+ * Returns cached record or fetches data from database if not cached.
+ *
+ * @param string $tablename
+ * @param int $id
+ * @return \stdClass
+ *
+ * @throws \coding_exception if used after ::restore()
+ */
+ public final function get_record_snapshot($tablename, $id) {
+ global $DB;
+
+ if ($this->restored) {
+ throw new \coding_exception('It is not possible to get snapshots from restored events');
+ }
+
+ if (isset($this->recordsnapshots[$tablename][$id])) {
+ return $this->recordsnapshots[$tablename][$id];
+ }
+
+ $record = $DB->get_record($tablename, array('id'=>$id));
+ $this->recordsnapshots[$tablename][$id] = $record;
+
+ return $record;
+ }
+
+ /**
+ * Magic getter for read only access.
+ *
+ * @param string $name
+ * @return mixed
+ */
+ public function __get($name) {
+ if (array_key_exists($name, $this->data)) {
+ return $this->data[$name];
+ }
+
+ debugging("Accessing non-existent event property '$name'");
+ }
+
+ /**
+ * Magic setter.
+ *
+ * Note: we must not allow modification of data from outside,
+ * after trigger() the data MUST NOT CHANGE!!!
+ *
+ * @param string $name
+ * @param mixed $value
+ *
+ * @throws \coding_exception
+ */
+ public function __set($name, $value) {
+ throw new \coding_exception('Event properties must not be modified.');
+ }
+
+ /**
+ * Is data property set?
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function __isset($name) {
+ return isset($this->data[$name]);
+ }
+
+ /**
+ * Create an iterator because magic vars can't be seen by 'foreach'.
+ *
+ * @return \ArrayIterator
+ */
+ public function getIterator() {
+ return new \ArrayIterator($this->data);
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+namespace core\event;
+
+/**
+ * New event manager class.
+ *
+ * @package core
+ * @copyright 2013 Petr Skoda {@link http://skodak.org}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Class used for event dispatching.
+ *
+ * Note: Do NOT use directly in your code, it is intended to be used from
+ * base event class only.
+ */
+class manager {
+ /** @var array buffer of event for dispatching */
+ protected static $buffer = array();
+
+ /** @var array buffer for events that were not sent to external observers when DB transaction in progress */
+ protected static $extbuffer = array();
+
+ /** @var bool evert dispatching already in progress - prevents nesting */
+ protected static $dispatching = false;
+
+ /** @var array cache of all observers */
+ protected static $allobservers = null;
+
+ /** @var bool should we reload observers after the test? */
+ protected static $reloadaftertest = false;
+
+ /**
+ * Trigger new event.
+ *
+ * @internal to be used only from \core\event\base::trigger() method.
+ * @param \core\event\base $event
+ *
+ * @throws \coding_Exception if used directly.
+ */
+ public static function dispatch(\core\event\base $event) {
+ if (during_initial_install()) {
+ return;
+ }
+ if (!$event->is_triggered() or $event->is_dispatched()) {
+ throw new \coding_exception('Illegal event dispatching attempted.');
+ }
+
+ self::$buffer[] = $event;
+
+ if (self::$dispatching) {
+ return;
+ }
+
+ self::$dispatching = true;
+ self::process_buffers();
+ self::$dispatching = false;
+ }
+
+ /**
+ * Notification from DML layer.
+ * @internal to be used from DML layer only.
+ */
+ public static function database_transaction_commited() {
+ if (self::$dispatching or empty(self::$extbuffer)) {
+ return;
+ }
+
+ self::$dispatching = true;
+ self::process_buffers();
+ self::$dispatching = false;
+ }
+
+ /**
+ * Notification from DML layer.
+ * @internal to be used from DML layer only.
+ */
+ public static function database_transaction_rolledback() {
+ self::$extbuffer = array();
+ }
+
+ protected static function process_buffers() {
+ global $DB, $CFG;
+
+ while (self::$buffer or self::$extbuffer) {
+
+ $fromextbuffer = false;
+ $addedtoextbuffer = false;
+
+ if (self::$extbuffer and !$DB->is_transaction_started()) {
+ $fromextbuffer = true;
+ $event = reset(self::$extbuffer);
+ unset(self::$extbuffer[key(self::$extbuffer)]);
+
+ } else if (self::$buffer) {
+ $event = reset(self::$buffer);
+ unset(self::$buffer[key(self::$buffer)]);
+
+ } else {
+ return;
+ }
+
+ $observers = self::get_event_observers('\\'.get_class($event));
+
+ foreach ($observers as $observer) {
+ if ($observer->internal) {
+ if ($fromextbuffer) {
+ // Do not send buffered external events to internal handlers,
+ // they processed them already.
+ continue;
+ }
+ } else {
+ if ($DB->is_transaction_started()) {
+ if ($fromextbuffer) {
+ // Weird!
+ continue;
+ }
+ // Do not notify external observers while in DB transaction.
+ if (!$addedtoextbuffer) {
+ self::$extbuffer[] = $event;
+ $addedtoextbuffer = true;
+ }
+ continue;
+ }
+ }
+
+ if (isset($observer->includefile) and file_exists($observer->includefile)) {
+ include_once($observer->includefile);
+ }
+ if (is_callable($observer->callable)) {
+ try {
+ call_user_func($observer->callable, $event);
+ } catch (\Exception $e) {
+ // Observers are notified before installation and upgrade, this may throw errors.
+ if (empty($CFG->upgraderunning)) {
+ // Ignore errors during upgrade, otherwise warn developers.
+ debugging("Exception encountered in event observer '$observer->callable': ".$e->getMessage(), DEBUG_DEVELOPER, $e->getTrace());
+ }
+ }
+ } else {
+ debugging("Can not execute event observer '$observer->callable'");
+ }
+ }
+
+ // TODO: Invent some infinite loop protection in case events cross-trigger one another.
+ }
+ }
+
+ /**
+ * Returns list of event observers.
+ * @param string $classname
+ * @return array
+ */
+ protected static function get_event_observers($classname) {
+ self::init_all_observers();
+
+ if (isset(self::$allobservers[$classname])) {
+ return self::$allobservers[$classname];
+ }
+
+ if (isset(self::$allobservers['*'])) {
+ return self::$allobservers['*'];
+ }
+
+ return array();
+ }
+
+ /**
+ * Initialise the list of observers.
+ */
+ protected static function init_all_observers() {
+ global $CFG;
+
+ if (is_array(self::$allobservers)) {
+ return;
+ }
+
+ if (!PHPUNIT_TEST and !during_initial_install()) {
+ $cache = \cache::make('core', 'observers');
+ $cached = $cache->get('all');
+ $dirroot = $cache->get('dirroot');
+ if ($dirroot === $CFG->dirroot and is_array($cached)) {
+ self::$allobservers = $cached;
+ return;
+ }
+ }
+
+ self::$allobservers = array();
+
+ $plugintypes = \core_component::get_plugin_types();
+ $systemdone = false;
+ foreach ($plugintypes as $plugintype => $ignored) {
+ $plugins = \core_component::get_plugin_list($plugintype);
+ if (!$systemdone) {
+ $plugins[] = "$CFG->dirroot/lib";
+ $systemdone = true;
+ }
+
+ foreach ($plugins as $fulldir) {
+ if (!file_exists("$fulldir/db/events.php")) {
+ continue;
+ }
+ $observers = null;
+ include("$fulldir/db/events.php");
+ if (!is_array($observers)) {
+ continue;
+ }
+ self::add_observers($observers, "$fulldir/db/events.php");
+ }
+ }
+
+ self::order_all_observers();
+
+ if (!PHPUNIT_TEST and !during_initial_install()) {
+ $cache->set('all', self::$allobservers);
+ $cache->set('dirroot', $CFG->dirroot);
+ }
+ }
+
+ /**
+ * Add observers.
+ * @param array $observers
+ * @param string $file
+ */
+ protected static function add_observers(array $observers, $file) {
+ global $CFG;
+
+ foreach ($observers as $observer) {
+ if (empty($observer['eventname']) or !is_string($observer['eventname'])) {
+ debugging("Invalid 'eventname' detected in $file observer definition", DEBUG_DEVELOPER);
+ continue;
+ }
+ if ($observer['eventname'] !== '*' and strpos($observer['eventname'], '\\') !== 0) {
+ $observer['eventname'] = '\\'.$observer['eventname'];
+ }
+ if (empty($observer['callback'])) {
+ debugging("Invalid 'callback' detected in $file observer definition", DEBUG_DEVELOPER);
+ continue;
+ }
+ $o = new \stdClass();
+ $o->callable = $observer['callback'];
+ if (!isset($observer['priority'])) {
+ $o->priority = 0;
+ } else {
+ $o->priority = (int)$observer['priority'];
+ }
+ if (!isset($observer['internal'])) {
+ $o->internal = true;
+ } else {
+ $o->internal = (bool)$observer['internal'];
+ }
+ if (empty($observer['includefile'])) {
+ $o->includefile = null;
+ } else {
+ if ($CFG->admin !== 'admin' and strpos($observer['includefile'], '/admin/') === 0) {
+ $observer['includefile'] = preg_replace('|^/admin/|', '/'.$CFG->admin.'/', $observer['includefile']);
+ }
+ if (!file_exists($observer['includefile'])) {
+ debugging("Invalid 'includefile' detected in $file observer definition", DEBUG_DEVELOPER);
+ continue;
+ }
+ $o->includefile = $observer['includefile'];
+ }
+ self::$allobservers[$observer['eventname']][] = $o;
+ }
+ }
+
+ /**
+ * Reorder observers to allow quick lookup of observer for each event.
+ */
+ protected static function order_all_observers() {
+ $catchall = array();
+ if (isset(self::$allobservers['*'])) {
+ $catchall = self::$allobservers['*'];
+ unset(self::$allobservers['*']); // Move it to the end.
+ \core_collator::asort_objects_by_property($catchall, 'priority', \core_collator::SORT_NUMERIC);
+ $catchall = array_reverse($catchall);
+ self::$allobservers['*'] = $catchall;
+ }
+ foreach (self::$allobservers as $classname => $observers) {
+ if ($classname === '*') {
+ continue;
+ }
+ if ($catchall) {
+ $observers = array_merge($observers, $catchall);
+ }
+
+ \core_collator::asort_objects_by_property($observers, 'priority', \core_collator::SORT_NUMERIC);
+ self::$allobservers[$classname] = array_reverse($observers);
+ }
+ }
+
+ /**
+ * Replace all standard observers.
+ * @param array $observers
+ * @return array
+ *
+ * @throws \coding_Exception if used outside of unit tests.
+ */
+ public static function phpunit_replace_observers(array $observers) {
+ if (!PHPUNIT_TEST) {
+ throw new \coding_exception('Cannot override event observers outside of phpunit tests!');
+ }
+
+ self::phpunit_reset();
+ self::$allobservers = array();
+ self::$reloadaftertest = true;
+
+ self::add_observers($observers, 'phpunit');
+ self::order_all_observers();
+
+ return self::$allobservers;
+ }
+
+ /**
+ * Reset everything if necessary.
+ * @private
+ *
+ * @throws \coding_Exception if used outside of unit tests.
+ */
+ public static function phpunit_reset() {
+ if (!PHPUNIT_TEST) {
+ throw new \coding_exception('Cannot reset event manager outside of phpunit tests!');
+ }
+ self::$buffer = array();
+ self::$extbuffer = array();
+ self::$dispatching = false;
+ if (!self::$reloadaftertest) {
+ self::$allobservers = null;
+ }
+ self::$reloadaftertest = false;
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+namespace core\event;
+
+/**
+ * Role assigned event.
+ *
+ * @package core
+ * @copyright 2013 Petr Skoda {@link http://skodak.org}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+class role_assigned extends base {
+ protected function init() {
+ $this->data['objecttable'] = 'role';
+ $this->data['crud'] = 'c';
+ // TODO: MDL-37658 set level
+ $this->data['level'] = 50;
+ }
+
+ /**
+ * Returns localised general event name.
+ *
+ * @return string|\lang_string
+ */
+ public static function get_name() {
+ //TODO: MDL-37658 localise
+ return 'Role assigned';
+ }
+
+ /**
+ * Returns localised description of what happened.
+ *
+ * @return string|\lang_string
+ */
+ public function get_description() {
+ //TODO: MDL-37658 localise
+ return 'Role '.$this->objectid.' was assigned to user '.$this->relateduserid.' in context '.$this->contextid;
+ }
+
+ /**
+ * Returns relevant URL.
+ * @return \moodle_url
+ */
+ public function get_url() {
+ return new moodle_url('/admin/roles/assign.php', array('contextid'=>$this->contextid, 'roleid'=>$this->objectid));
+ }
+
+ /**
+ * Does this event replace legacy event?
+ *
+ * @return null|string legacy event name
+ */
+ protected function get_legacy_eventname() {
+ return 'role_assigned';
+ }
+
+ /**
+ * Legacy event data if get_legacy_eventname() is not empty.
+ *
+ * @return mixed
+ */
+ protected function get_legacy_eventdata() {
+ return $this->get_record_snapshot('role_assignments', $this->data['other']['id']);
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+namespace core\event;
+
+/**
+ * Role unassigned event.
+ *
+ * @package core
+ * @copyright 2013 Petr Skoda {@link http://skodak.org}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+class role_unassigned extends base {
+ protected function init() {
+ $this->data['objecttable'] = 'role';
+ $this->data['crud'] = 'd';
+ // TODO: MDL-37658 set level
+ $this->data['level'] = 50;
+ }
+
+ /**
+ * Returns localised general event name.
+ *
+ * @return string|\lang_string
+ */
+ public static function get_name() {
+ //TODO: MDL-37658 localise
+ return 'Role unassigned';
+ }
+
+ /**
+ * Returns localised description of what happened.
+ *
+ * @return string|\lang_string
+ */
+ public function get_description() {
+ //TODO: MDL-37658 localise
+ return 'Role '.$this->objectid.'was unassigned from user '.$this->relateduserid.' in context '.$this->contextid;
+ }
+
+ /**
+ * Returns relevant URL.
+ * @return \moodle_url
+ */
+ public function get_url() {
+ return new moodle_url('/admin/roles/assign.php', array('contextid'=>$this->contextid, 'roleid'=>$this->objectid));
+ }
+
+ /**
+ * Does this event replace legacy event?
+ *
+ * @return null|string legacy event name
+ */
+ protected function get_legacy_eventname() {
+ return 'role_unassigned';
+ }
+
+ /**
+ * Legacy event data if get_legacy_eventname() is not empty.
+ *
+ * @return mixed
+ */
+ protected function get_legacy_eventdata() {
+ return $this->get_record_snapshot('role_assignments', $this->data['other']['id']);
+ }
+}
} else {
$context = context_course::instance($courseid);
- $contextlists = get_related_contexts_string($context);
+
+ // We want to query both the current context and parent contexts.
+ list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
$sql = "SELECT u.id, u.firstname, u.lastname, u.email
FROM {user} u
JOIN {role_assignments} ra ON ra.userid = u.id
- WHERE $select AND ra.contextid $contextlists
+ WHERE $select AND ra.contextid $relatedctxsql
$except
$order";
+ $params = array_merge($params, $relatedctxparams);
return $DB->get_records_sql($sql, $params);
}
}
'mode' => cache_store::MODE_APPLICATION,
),
+ // Cache for the list of event observers.
+ 'observers' => array(
+ 'mode' => cache_store::MODE_APPLICATION,
+ 'simplekeys' => true,
+ 'simpledata' => true,
+ 'persistent' => true,
+ 'persistentmaxsize' => 2,
+ ),
+
// Cache used by the {@link plugininfo_base} class.
'plugininfo_base' => array(
'mode' => cache_store::MODE_APPLICATION,
defined('MOODLE_INTERNAL') || die();
-/* List of handlers */
+/* List of legacy event handlers */
$handlers = array(
-/*
- * portfolio queued event - for non interactive file transfers
- * NOTE: this is a HACK, please do not add any more things like this here
- * (it is just abusing cron to do very time consuming things which is wrong any way)
- *
- * TODO: this has to be moved into separate queueing framework....
- */
- 'portfolio_send' => array (
- 'handlerfile' => '/lib/portfolio.php',
- 'handlerfunction' => 'portfolio_handle_event', // argument to call_user_func(), could be an array
- 'schedule' => 'cron',
- 'internal' => 0,
- ),
'course_completed' => array (
'handlerfile' => '/lib/badgeslib.php',
'handlerfunction' => 'badges_award_handle_course_criteria_review',
'internal' => 1,
),
+/*
+ * portfolio queued event - for non interactive file transfers
+ * NOTE: this is a HACK, please do not add any more things like this here
+ * (it is just abusing cron to do very time consuming things which is wrong any way)
+ *
+ * TODO: this has to be moved into separate queueing framework....
+ */
+ 'portfolio_send' => array (
+ 'handlerfile' => '/lib/portfolio.php',
+ 'handlerfunction' => 'portfolio_handle_event', // argument to call_user_func(), could be an array
+ 'schedule' => 'cron',
+ 'internal' => 0,
+ ),
+
/* no more here please, core should not consume any events!!!!!!! */
);
*
* used to print recent activity
*
- * @todo MDL-36993 this function is still used in block_recent_activity, deprecate properly
- * @global object
- * @uses CONTEXT_COURSE
* @param int $courseid The course in question.
* @param int $timestart The date to check forward of
* @return object|false {@link $USER} records or false if error.
function get_recent_enrolments($courseid, $timestart) {
global $DB;
- $context = context_course::instance($courseid);
+ debugging('get_recent_enrolments() is deprecated as it returned inaccurate results.', DEBUG_DEVELOPER);
+ $context = context_course::instance($courseid);
$sql = "SELECT u.id, u.firstname, u.lastname, MAX(l.time)
FROM {user} u, {role_assignments} ra, {log} l
WHERE l.time > ?
$join = "LEFT JOIN {context} $tablealias ON ($tablealias.instanceid = $joinon AND $tablealias.contextlevel = $contextlevel)";
return array($select, $join);
}
+
+/**
+ * Gets a string for sql calls, searching for stuff in this context or above.
+ *
+ * @deprecated since 2.2
+ * @see context::get_parent_context_ids()
+ * @param context $context
+ * @return string
+ */
+function get_related_contexts_string(context $context) {
+ debugging('get_related_contexts_string() is deprecated, please use $context->get_parent_context_ids(true) instead.', DEBUG_DEVELOPER);
+ if ($parents = $context->get_parent_context_ids()) {
+ return (' IN ('.$context->id.','.implode(',', $parents).')');
+ } else {
+ return (' ='.$context->id);
+ }
+}
$this->commit_transaction();
}
array_pop($this->transactions);
+
+ if (empty($this->transactions)) {
+ \core\event\manager::database_transaction_commited();
+ }
}
/**
if (empty($this->transactions)) {
// finally top most level rolled back
$this->force_rollback = false;
+ \core\event\manager::database_transaction_rolledback();
}
throw $e;
}
* plugins you can set $alwaysadd to false and check the return value
* to see if it succeeded.
*
+ * Note: button will not be added if it is already present in any row
+ * (separator is an exception).
+ *
+ * The following example will add the button 'newbutton' after the
+ * 'existingbutton' if it exists or in the end of the last row otherwise:
+ * <pre>
+ * if ($row = $this->find_button($params, 'existingbutton')) {
+ * $this->add_button_after($params, $row, 'newbutton', 'existingbutton');
+ * } else {
+ * $this->add_button_after($params, $this->count_button_rows($params), 'newbutton');
+ * }
+ * </pre>
+ *
* @param array $params TinyMCE init parameters array
* @param int $row Row to add button to (1 to 3)
* @param string $button Identifier of button/plugin
* @param string $after Adds button directly after the named plugin
* @param bool $alwaysadd If specified $after string not found, add at end
- * @return bool True if added
+ * @return bool True if added or button already exists (in any row)
*/
protected function add_button_after(array &$params, $row, $button,
$after = '', $alwaysadd = true) {
- if ($this->is_button_present($params, $button)) {
+ if ($button !== '|' && $this->find_button($params, $button)) {
return true;
}
* plugins you can set $alwaysadd to false and check the return value
* to see if it succeeded.
*
+ * Note: button will not be added if it is already present in any row
+ * (separator is an exception).
+ *
+ * The following example will add the button 'newbutton' before the
+ * 'existingbutton' if it exists or in the end of the last row otherwise:
+ * <pre>
+ * if ($row = $this->find_button($params, 'existingbutton')) {
+ * $this->add_button_before($params, $row, 'newbutton', 'existingbutton');
+ * } else {
+ * $this->add_button_after($params, $this->count_button_rows($params), 'newbutton');
+ * }
+ * </pre>
+ *
* @param array $params TinyMCE init parameters array
* @param int $row Row to add button to (1 to 10)
* @param string $button Identifier of button/plugin
* @param string $before Adds button directly before the named plugin
* @param bool $alwaysadd If specified $before string not found, add at start
- * @return bool True if added
+ * @return bool True if added or button already exists (in any row)
*/
protected function add_button_before(array &$params, $row, $button,
$before = '', $alwaysadd = true) {
- if ($this->is_button_present($params, $button)) {
+ if ($button !== '|' && $this->find_button($params, $button)) {
return true;
}
$row = $this->fix_row($params, $row);
}
/**
- * Tests if button already present.
- * @param array $params
- * @param string $button
- * @return bool
+ * Tests if button is already present.
+ *
+ * @param array $params TinyMCE init parameters array
+ * @param string $button button name
+ * @return false|int false if button is not found, row number otherwise (row numbers start from 1)
*/
- private function is_button_present(array $params, $button) {
- for($i=1; $i<=10; $i++) {
- $field = 'theme_advanced_buttons' . $i;
- if (!isset($params[$field])) {
- continue;
- }
- $buttons = explode(',', $params[$field]);
- if (in_array($button, $buttons)) {
- return true;
+ protected function find_button(array &$params, $button) {
+ foreach ($params as $key => $value) {
+ if (preg_match('/^theme_advanced_buttons(\d+)$/', $key, $matches) &&
+ strpos(','. $value. ',', ','. $button. ',') !== false) {
+ return (int)$matches[1];
}
}
return false;
* @return int requested row if exists, lower number if does not exist.
*/
private function fix_row(array &$params, $row) {
- $row = ($row < 1) ? 1 : (int)$row;
- $row = ($row > 10) ? 10 : $row;
-
- $field = 'theme_advanced_buttons' . $row;
- if (isset($params[$field])) {
+ if ($row <= 1) {
+ // Row 1 is always present.
+ return 1;
+ } else if (isset($params['theme_advanced_buttons' . $row])) {
return $row;
+ } else {
+ return $this->count_button_rows($params);
}
- for($i=$row; $i>=1; $i--) {
- $field = 'theme_advanced_buttons' . $i;
- if (isset($params[$field])) {
- return $i;
+ }
+
+ /**
+ * Counts the number of rows in TinyMCE editor (row numbering starts with 1)
+ *
+ * @param array $params TinyMCE init parameters array
+ * @return int the maximum existing row number
+ */
+ protected function count_button_rows(array &$params) {
+ $maxrow = 1;
+ foreach ($params as $key => $value) {
+ if (preg_match('/^theme_advanced_buttons(\d+)$/', $key, $matches) &&
+ (int)$matches[1] > $maxrow) {
+ $maxrow = (int)$matches[1];
}
}
- // This should not happen.
- return 1;
+ return $maxrow;
}
/**
}
}
- // Add button before 'nonbreaking' in advancedbuttons3.
- $this->add_button_before($params, 3, 'dragmath', 'nonbreaking');
+ if ($row = $this->find_button($params, 'nonbreaking')) {
+ // Add button before 'nonbreaking'.
+ $this->add_button_before($params, $row, 'dragmath', 'nonbreaking');
+ } else {
+ // If 'nonbreaking' is not found, add button in the end of the last row:
+ $this->add_button_after($params, $this->count_button_rows($params), 'dragmath');
+ }
// Add JS file, which uses default name.
$this->add_js_plugin($params);
}
}
- // Add button after 'image' in advancedbuttons3.
- $this->add_button_after($params, 3, 'moodleemoticon', 'image');
+ if ($row = $this->find_button($params, 'image')) {
+ // Add button after 'image'.
+ $this->add_button_after($params, $row, 'moodleemoticon', 'image');
+ } else {
+ // If 'image' is not found, add button in the end of the last row.
+ $this->add_button_after($params, $this->count_button_rows($params), 'moodleemoticon');
+ }
// Add JS file, which uses default name.
$this->add_js_plugin($params);
}
}
- // Add button after emoticon button in advancedbuttons1.
- $added = $this->add_button_after($params, 1, 'moodlemedia', 'moodleemoticon', false);
-
- // Note: We know that the emoticon button has already been added, if it
- // exists, because I set the sort order higher for this. So, if no
- // emoticon, add after 'image'.
- if (!$added) {
- $this->add_button_after($params, 1, 'moodlemedia', 'image');
+ if ($row = $this->find_button($params, 'moodleemoticon')) {
+ // Add button after 'moodleemoticon' icon.
+ $this->add_button_after($params, $row, 'moodlemedia', 'moodleemoticon');
+ } else if ($row = $this->find_button($params, 'image')) {
+ // Note: We know that the plugin emoticon button has already been added
+ // if it is enabled because this plugin has higher sortorder.
+ // Otherwise add after 'image'.
+ $this->add_button_after($params, $row, 'moodlemedia', 'image');
+ } else {
+ // Add this button in the end of the first row (by default 'image' button should be in the first row).
+ $this->add_button_after($params, 1, 'moodlemedia');
}
// Add JS file, which uses default name.
protected function update_init_params(array &$params, context $context,
array $options = null) {
- // Add button after 'unlink' in advancedbuttons1.
- $this->add_button_after($params, 1, 'moodlenolink', 'unlink');
+ if ($row = $this->find_button($params, 'unlink')) {
+ // Add button after 'unlink'.
+ $this->add_button_after($params, $row, 'moodlenolink', 'unlink');
+ } else {
+ // Add this button in the end of the first row (by default 'unlink' button should be in the first row).
+ $this->add_button_after($params, 1, 'moodlenolink');
+ }
// Add JS file, which uses default name.
$this->add_js_plugin($params);
protected function update_init_params(array &$params, context $context,
array $options = null) {
- $rowsnumber = $this->count_rows($params);
+ $rowsnumber = $this->count_button_rows($params);
if ($rowsnumber > 1) {
// Add button before 'undo' in advancedbuttons1.
- $this->add_button_before($params, 1, ' | ', '');
+ $this->add_button_before($params, 1, '|', '');
$this->add_button_before($params, 1, 'pdw_toggle', '');
$params['pdw_toggle_on'] = 1;
$params['pdw_toggle_toolbars'] = join(',', range(2, $rowsnumber));
}
/**
- * Counts the number of rows in TinyMCE editor
+ * Gets the order in which to run this plugin
*
- * @param array $params TinyMCE init parameters array
- * @return int the maximum existing row number
+ * We need pdw plugin to be added the last, so nothing is added before the button.
*/
- private function count_rows(array &$params) {
- for($i = 10; $i >= 1; $i--) {
- $field = 'theme_advanced_buttons' . $i;
- if (isset($params[$field])) {
- return $i;
- }
- }
- // This should not happen.
- return 1;
+ protected function get_sort_order() {
+ return 100000;
}
}
// 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;
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 !== '') {
// Prevent the built-in spell checker in Firefox, Safari and other sane browsers.
unset($params['gecko_spellcheck']);
- // Add button after code button in advancedbuttons3.
- $added = $this->add_button_after($params, 3, 'spellchecker', 'code', false);
+ if ($row = $this->find_button($params, 'code')) {
+ // Add button after 'code'.
+ $this->add_button_after($params, $row, 'spellchecker', 'code');
+ }
// Add JS file, which uses default name.
$this->add_js_plugin($params);
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,' .
$result = tinymce_texteditor::parse_toolbar_setting("one\ntwo\n\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten");
$this->assertSame(array('one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'), $result);
}
+
+ public function test_add_button() {
+ global $CFG;
+ $plugin = new tinymce_testplugin(__DIR__);
+ $config = get_config('editor_tinymce');
+ $params = array(
+ 'moodle_config' => $config,
+ 'entity_encoding' => "raw",
+ 'plugins' => 'lists,table,style,layer,advhr,advlink,emotions,inlinepopups,' .
+ 'searchreplace,paste,directionality,fullscreen,nonbreaking,contextmenu,' .
+ 'insertdatetime,save,iespell,preview,print,noneditable,visualchars,' .
+ 'xhtmlxtras,template,pagebreak',
+ 'gecko_spellcheck' => true,
+ 'theme_advanced_font_sizes' => "1,2,3,4,5,6,7",
+ 'moodle_plugin_base' => "$CFG->httpswwwroot/lib/editor/tinymce/plugins/",
+ 'theme_advanced_font_sizes' => "1,2,3,4,5,6,7",
+ 'theme_advanced_layout_manager' => "SimpleLayout",
+ 'theme_advanced_buttons1' => 'one,two,|,three,four',
+ 'theme_advanced_buttons2' => 'five,six',
+ 'theme_advanced_buttons3' => 'seven,eight,|',
+ 'theme_advanced_buttons4' => '|,nine',
+ 'theme_advanced_buttons5' => 'ten,eleven,twelve',
+ 'theme_advanced_buttons6' => 'thirteen,fourteen',
+ 'theme_advanced_buttons7' => 'fiveteen',
+ 'theme_advanced_buttons' => 'zero', // this is a fake entry, it is not a button row.
+ 'theme_something' => 123,
+ );
+
+ // Count number of rows.
+ $this->assertSame(7, $plugin->test_count_button_rows($params));
+
+ // Find button - first button in a row.
+ $this->assertSame(1, $plugin->test_find_button($params, 'one'));
+ // Find button - last button in a row.
+ $this->assertSame(4, $plugin->test_find_button($params, 'nine'));
+ // Find button - middle button in a row.
+ $this->assertSame(5, $plugin->test_find_button($params, 'eleven'));
+ // Find button - the only button in a row.
+ $this->assertSame(7, $plugin->test_find_button($params, 'fiveteen'));
+ // Find button - button not present.
+ $this->assertSame(false, $plugin->test_find_button($params, 'sixteen'));
+ // Find button - button not present.
+ $this->assertSame(false, $plugin->test_find_button($params, 'zero'));
+
+ // Adding button in the beginning of the row.
+ $this->assertTrue($plugin->test_add_button_before($params, 1, 'new1', '', true));
+ $this->assertSame('new1,one,two,|,three,four', $params['theme_advanced_buttons1']);
+ // Adding button that already exists (nothing changes).
+ $this->assertTrue($plugin->test_add_button_before($params, 1, 'new1', '', true));
+ $this->assertSame('new1,one,two,|,three,four', $params['theme_advanced_buttons1']);
+ // Adding button before existing button.
+ $this->assertTrue($plugin->test_add_button_before($params, 1, 'new2', 'two', true));
+ $this->assertSame('new1,one,new2,two,|,three,four', $params['theme_advanced_buttons1']);
+ // Adding button before another button that does not exist ($alwaysadd = false).
+ $this->assertTrue($plugin->test_add_button_before($params, 4, 'new3', 'fiveteen', true));
+ $this->assertSame('new3,|,nine', $params['theme_advanced_buttons4']);
+ // Adding button before another button that does not exist ($alwaysadd = false).
+ $this->assertFalse($plugin->test_add_button_before($params, 4, 'new4', 'fiveteen', false));
+ $this->assertSame('new3,|,nine', $params['theme_advanced_buttons4']);
+ // Adding button into non-existing 0 row.
+ $this->assertTrue($plugin->test_add_button_before($params, 0, 'new9'));
+ $this->assertSame('new9,new1,one,new2,two,|,three,four', $params['theme_advanced_buttons1']);
+ $this->assertFalse(isset($params['theme_advanced_buttons0']));
+ // Adding button into non-existing 9 row.
+ $this->assertTrue($plugin->test_add_button_before($params, 9, 'new10'));
+ $this->assertSame('new10,fiveteen', $params['theme_advanced_buttons7']);
+ $this->assertFalse(isset($params['theme_advanced_buttons9']));
+
+ // Adding button in the end of the row.
+ $this->assertTrue($plugin->test_add_button_after($params, 5, 'new5', '', true));
+ $this->assertSame('ten,eleven,twelve,new5', $params['theme_advanced_buttons5']);
+ // Adding button that already exists.
+ $this->assertTrue($plugin->test_add_button_after($params, 5, 'new5', '', true));
+ $this->assertSame('ten,eleven,twelve,new5', $params['theme_advanced_buttons5']);
+ // Adding button after the existing button.
+ $this->assertTrue($plugin->test_add_button_after($params, 6, 'new6', 'thirteen', true));
+ $this->assertSame('thirteen,new6,fourteen', $params['theme_advanced_buttons6']);
+ // Adding button after another button that does not exist ($alwaysadd = true).
+ $this->assertTrue($plugin->test_add_button_after($params, 6, 'new7', 'fiveteen', true));
+ $this->assertSame('thirteen,new6,fourteen,new7', $params['theme_advanced_buttons6']);
+ // Adding button after another button that does not exist ($alwaysadd = false).
+ $this->assertFalse($plugin->test_add_button_after($params, 6, 'new8', 'fiveteen', false));
+ $this->assertSame('thirteen,new6,fourteen,new7', $params['theme_advanced_buttons6']);
+ // Adding button into non-existing 0 row.
+ $this->assertTrue($plugin->test_add_button_after($params, 0, 'new11'));
+ $this->assertSame('new9,new1,one,new2,two,|,three,four,new11', $params['theme_advanced_buttons1']);
+ $this->assertFalse(isset($params['theme_advanced_buttons0']));
+ // Adding button into non-existing 9 row.
+ $this->assertTrue($plugin->test_add_button_after($params, 9, 'new12'));
+ $this->assertSame('new10,fiveteen,new12', $params['theme_advanced_buttons7']);
+ $this->assertFalse(isset($params['theme_advanced_buttons9']));
+ }
}
+
+/**
+ * Pseudo plugin class for testing editor_tinymce_plugin protected methods
+ *
+ * @package editor_tinymce
+ * @category phpunit
+ * @copyright 2013 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tinymce_testplugin extends editor_tinymce_plugin {
+ protected function update_init_params(array &$params, context $context, array $options = null) {
+ // Empty function just to make a class not abstract.
+ }
+
+ public function test_count_button_rows(array &$params) {
+ return parent::count_button_rows($params);
+ }
+
+ public function test_find_button(array &$params, $button) {
+ return parent::find_button($params, $button);
+ }
+
+ public function test_add_button_after(array &$params, $row, $button, $after = '', $alwaysadd = true) {
+ return parent::add_button_after($params, $row, $button, $after, $alwaysadd);
+ }
+
+ public function test_add_button_before(array &$params, $row, $button, $before = '', $alwaysadd = true) {
+ return parent::add_button_before($params, $row, $button, $before, $alwaysadd);
+ }
+}
\ No newline at end of file
* added a new plugin to toggle the 2nd and 3rd toolbars
* added a new plugin to wrap the toolbars on small screens.
This plugin will be automatically added to existing toolbars by replacing the middle occurrence of | with "wrap".
+* New functions to make adding of new buttons easier:
+ - editor_tinymce_plugin::find_button()
+ - editor_tinymce_plugin::count_button_rows()
=== 2.5 ===
};
dependencyManager.prototype = {
_form : null,
- _depElements : [],
- _nameCollections : [],
+ _locks : [],
+ _hides : [],
+ _dirty : [],
+ _nameCollections : null,
+ _fileinputs : null,
initializer : function(config) {
var i = 0, nodeName;
this._form = Y.one('#'+formid);
for (i in dependencies) {
- this._depElements[i] = this.elementsByName(i);
- if (this._depElements[i].size() == 0) {
+ var elements = this.elementsByName(i);
+ if (elements.size() == 0) {
continue;
}
- this._depElements[i].each(function(node){
+ elements.each(function(node){
nodeName = node.get('nodeName').toUpperCase();
if (nodeName == 'INPUT') {
if (node.getAttribute('type').match(/^(button|submit|radio|checkbox)$/)) {
- node.on('click', this.checkDependencies, this);
+ node.on('click', this.updateEventDependencies, this);
} else {
- node.on('blur', this.checkDependencies, this);
+ node.on('blur', this.updateEventDependencies, this);
}
- node.on('change', this.checkDependencies, this);
+ node.on('change', this.updateEventDependencies, this);
} else if (nodeName == 'SELECT') {
- node.on('change', this.checkDependencies, this);
+ node.on('change', this.updateEventDependencies, this);
} else {
- node.on('click', this.checkDependencies, this);
- node.on('blur', this.checkDependencies, this);
- node.on('change', this.checkDependencies, this);
+ node.on('click', this.updateEventDependencies, this);
+ node.on('blur', this.updateEventDependencies, this);
+ node.on('change', this.updateEventDependencies, this);
}
}, this);
}
if (input.getAttribute('type')=='reset') {
input.on('click', function(){
this._form.reset();
- this.checkDependencies();
+ this.updateAllDependencies();
}, this);
}
}, this);
- return this.checkDependencies(null);
+ return this.updateAllDependencies();
+ },
+ /**
+ * Initializes the mapping from element name to YUI NodeList
+ */
+ initElementsByName : function() {
+ var names = [];
+ // Collect element names
+ for (var i in dependencies) {
+ names[i] = new Y.NodeList();
+ for (var condition in dependencies[i]) {
+ for (var value in dependencies[i][condition]) {
+ for (var ei in dependencies[i][condition][value]) {
+ names[dependencies[i][condition][value][ei]] = new Y.NodeList();
+ }
+ }
+ }
+ }
+ // Locate elements for each name
+ this._form.get('elements').each(function(node){
+ var name = node.getAttribute('name');
+ if (names[name]) {
+ names[name].push(node);
+ }
+ });
+ this._nameCollections = names;
},
/**
* Gets all elements in the form by their name and returns
* a YUI NodeList
- * @return Y.NodeList
+ *
+ * @param {string} name The form element name.
+ * @return {Y.NodeList}
*/
elementsByName : function(name) {
+ if (!this._nameCollections) {
+ this.initElementsByName();
+ }
if (!this._nameCollections[name]) {
- var elements = [];
- this._form.get('elements').each(function(){
- if (this.getAttribute('name') == name) {
- elements.push(this);
- }
- });
- this._nameCollections[name] = new Y.NodeList(elements);
+ return new Y.NodeList();
}
return this._nameCollections[name];
},
*
* Changes are made by functions title _dependency_{dependencytype}
* and more can easily be introduced by defining further functions.
+ *
+ * @param {EventFacade | null} e The event, if any.
+ * @param {string} name The form element name to check dependencies against.
*/
- checkDependencies : function(e) {
- var tolock = [],
- tohide = [],
- dependon, condition, value,
- lock, hide, checkfunction, result;
- for (dependon in dependencies) {
- if (this._depElements[dependon].size() == 0) {
- continue;
- }
- for (condition in dependencies[dependon]) {
- for (value in dependencies[dependon][condition]) {
- lock = false;
- hide = false;
- checkfunction = '_dependency_'+condition;
- if (Y.Lang.isFunction(this[checkfunction])) {
- result = this[checkfunction].apply(this, [this._depElements[dependon], value, e]);
- } else {
- result = this._dependency_default(this._depElements[dependon], value, e);
- }
- lock = result.lock || false;
- hide = result.hide || false;
- for (var ei in dependencies[dependon][condition][value]) {
- var eltolock = dependencies[dependon][condition][value][ei];
- if (hide) {
- tohide[eltolock] = true;
- }
- if (tolock[eltolock] != null) {
- tolock[eltolock] = lock || tolock[eltolock];
- } else {
- tolock[eltolock] = lock;
- }
- }
+ checkDependencies : function(e, dependon) {
+ var tohide = [],
+ tolock = [],
+ condition, value, lock, hide,
+ checkfunction, result, elements;
+ if (!dependencies[dependon]) {
+ return true;
+ }
+ elements = this.elementsByName(dependon);
+ for (condition in dependencies[dependon]) {
+ for (value in dependencies[dependon][condition]) {
+ checkfunction = '_dependency_'+condition;
+ if (Y.Lang.isFunction(this[checkfunction])) {
+ result = this[checkfunction].apply(this, [elements, value, e]);
+ } else {
+ result = this._dependency_default(elements, value, e);
+ }
+ lock = result.lock || false;
+ hide = result.hide || false;
+ for (var ei in dependencies[dependon][condition][value]) {
+ var eltolock = dependencies[dependon][condition][value][ei];
+ tohide[eltolock] = tohide[eltolock] || hide;
+ tolock[eltolock] = tolock[eltolock] || lock;
}
}
}
for (var el in tolock) {
- this._disableElement(el, tolock[el]);
- if (tohide.propertyIsEnumerable(el)) {
- this._hideElement(el, tohide[el]);
+ var needsupdate = false;
+ if (tolock[el]) {
+ this._locks[el] = this._locks[el] || [];
+ if (!this._locks[el][dependon]) {
+ this._locks[el][dependon] = true;
+ needsupdate = true;
+ }
+ } else if (this._locks[el] && this._locks[el][dependon]) {
+ delete this._locks[el][dependon];
+ needsupdate = true;
+ }
+ if (tohide[el]) {
+ this._hides[el] = this._hides[el] || [];
+ if (!this._hides[el][dependon]) {
+ this._hides[el][dependon] = true;
+ needsupdate = true;
+ }
+ } else if (this._hides[el] && this._hides[el][dependon]) {
+ delete this._hides[el][dependon];
+ needsupdate = true;
+ }
+ if (needsupdate) {
+ this._dirty[el] = true;
}
}
return true;
},
/**
- * Disabled all form elements with the given name
+ * Update all dependencies in form
+ */
+ updateAllDependencies : function() {
+ for (var el in dependencies) {
+ this.checkDependencies(null, el);
+ }
+ this.updateForm();
+ },
+ /**
+ * Update dependencies associated with event
+ *
+ * @param {Event} e The event.
+ */
+ updateEventDependencies : function(e) {
+ var el = e.target.getAttribute('name');
+ this.checkDependencies(e, el);
+ this.updateForm();
+ },
+ /**
+ * Flush pending changes to the form
+ */
+ updateForm : function() {
+ for (var el in this._dirty) {
+ if (this._locks[el]) {
+ var locked = !this._isObjectEmpty(this._locks[el]);
+ this._disableElement(el, locked);
+ }
+ if (this._hides[el]) {
+ var hidden = !this._isObjectEmpty(this._hides[el]);
+ this._hideElement(el, hidden);
+ }
+ }
+ this._dirty = [];
+ },
+ /**
+ * Disables or enables all form elements with the given name
+ *
+ * @param {string} name The form element name.
+ * @param {boolean} disabled True to disable, false to enable.
*/
_disableElement : function(name, disabled) {
var els = this.elementsByName(name);
- var form = this;
- els.each(function(){
+ var filepicker = this.isFilePicker(name);
+ els.each(function(node){
if (disabled) {
- this.setAttribute('disabled', 'disabled');
+ node.setAttribute('disabled', 'disabled');
} else {
- this.removeAttribute('disabled');
+ node.removeAttribute('disabled');
}
// Extra code to disable filepicker or filemanager form elements
- var fitem = this.ancestor('.fitem');
- if (fitem && (fitem.hasClass('fitem_ffilemanager') || fitem.hasClass('fitem_ffilepicker'))) {
- if (disabled){
- fitem.addClass('disabled');
- } else {
- fitem.removeClass('disabled');
+ if (filepicker) {
+ var fitem = node.ancestor('.fitem');
+ if (fitem) {
+ if (disabled){
+ fitem.addClass('disabled');
+ } else {
+ fitem.removeClass('disabled');
+ }
}
}
})
},
/**
- * Hides all elements with the given name.
+ * Hides or shows all form elements with the given name.
+ *
+ * @param {string} name The form element name.
+ * @param {boolean} disabled True to hide, false to show.
*/
_hideElement : function(name, hidden) {
var els = this.elementsByName(name);
- els.each(function(){
- var e = els.ancestor('.fitem');
+ els.each(function(node){
+ var e = node.ancestor('.fitem');
if (e) {
e.setStyles({
display : (hidden)?'none':''
}
});
},
+ /**
+ * Is the form element inside a filepicker or filemanager?
+ *
+ * @param {string} el The form element name.
+ * @return {boolean}
+ */
+ isFilePicker : function(el) {
+ if (!this._fileinputs) {
+ var fileinputs = [];
+ var els = this._form.all('.fitem.fitem_ffilepicker input,.fitem.fitem_ffilemanager input');
+ els.each(function(node){
+ fileinputs[node.getAttribute('name')] = true;
+ });
+ this._fileinputs = fileinputs;
+ }
+ return this._fileinputs[el] || false;
+ },
+ /**
+ * Check if the object is empty
+ *
+ * @param {object} obj
+ * @return {boolean}
+ */
+ _isObjectEmpty : function(obj) {
+ for(var prop in obj) {
+ if(obj.hasOwnProperty(prop))
+ return false;
+ }
+ return true;
+ },
_dependency_notchecked : function(elements, value) {
var lock = false;
elements.each(function(){
*/
M.form.updateFormState = function(formid) {
if (formid in M.form.dependencyManagers) {
- M.form.dependencyManagers[formid].checkDependencies(null);
+ M.form.dependencyManagers[formid].updateAllDependencies();
}
};