required in non-interactive mode.
--allow-unstable Install even if the version is not marked as stable yet,
required in non-interactive mode.
+--skip-database Stop the installation before installing the database.
-h, --help Print out this help
Example:
'non-interactive' => false,
'agree-license' => false,
'allow-unstable' => false,
+ 'skip-database' => false,
'help' => false
),
array(
cli_error(get_string('pluginschecktodo', 'admin'));
}
-install_cli_database($options, $interactive);
+if (!$options['skip-database']) {
+ install_cli_database($options, $interactive);
+} else {
+ echo get_string('cliskipdatabase', 'install')."\n";
+}
echo get_string('cliinstallfinished', 'install')."\n";
exit(0); // 0 means success
<VENDOR name="oracle" version="10.2" />
</DATABASE>
<PHP version="5.4.4" level="required">
+ <RESTRICT function="restrict_php_version_7" message="unsupportedphpversion7" />
</PHP>
<PCREUNICODE level="optional">
<FEEDBACK>
<ON_ERROR message="quizattemptsupgradedmessage" />
</FEEDBACK>
</CUSTOM_CHECK>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="slashargumentswarning" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="unsupporteddbtablerowformat" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
</CUSTOM_CHECKS>
</MOODLE>
<MOODLE version="2.8" requires="2.2">
<VENDOR name="oracle" version="10.2" />
</DATABASE>
<PHP version="5.4.4" level="required">
+ <RESTRICT function="restrict_php_version_7" message="unsupportedphpversion7" />
</PHP>
<PCREUNICODE level="optional">
<FEEDBACK>
<ON_ERROR message="quizattemptsupgradedmessage" />
</FEEDBACK>
</CUSTOM_CHECK>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="slashargumentswarning" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="unsupporteddbtablerowformat" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
</CUSTOM_CHECKS>
</MOODLE>
<MOODLE version="2.9" requires="2.2">
<VENDOR name="oracle" version="10.2" />
</DATABASE>
<PHP version="5.4.4" level="required">
+ <RESTRICT function="restrict_php_version_7" message="unsupportedphpversion7" />
</PHP>
<PCREUNICODE level="optional">
<FEEDBACK>
echo '<form action="' . $PAGE->url->out(true) . '" method="post" id="adminsettings">';
echo '<div>';
echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
+// HACK to prevent browsers from automatically inserting the user's password into the wrong fields.
+echo prevent_form_autofill_password();
echo '</div>';
echo '<fieldset>';
echo '<div class="clearer"><!-- --></div>';
echo html_writer::input_hidden_params($PAGE->url);
echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
echo '<input type="hidden" name="return" value="'.$return.'" />';
+ // HACK to prevent browsers from automatically inserting the user's password into the wrong fields.
+ echo prevent_form_autofill_password();
echo $settingspage->output_html();
echo html_writer::input_hidden_params($PAGE->url);
echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
echo '<input type="hidden" name="return" value="'.$return.'" />';
+ // HACK to prevent browsers from automatically inserting the user's password into the wrong fields.
+ echo prevent_form_autofill_password();
echo $OUTPUT->heading($settingspage->visiblename);
echo $settingspage->output_html();
$temp->add(new admin_setting_heading('manageauthscommonheading', new lang_string('commonsettings', 'admin'), ''));
$temp->add(new admin_setting_special_registerauth());
$temp->add(new admin_setting_configcheckbox('authloginviaemail', new lang_string('authloginviaemail', 'core_auth'), new lang_string('authloginviaemail_desc', 'core_auth'), 0));
+ $temp->add(new admin_setting_configcheckbox('allowaccountssameemail',
+ new lang_string('allowaccountssameemail', 'core_auth'),
+ new lang_string('allowaccountssameemail_desc', 'core_auth'), 0));
$temp->add(new admin_setting_configcheckbox('authpreventaccountcreation', new lang_string('authpreventaccountcreation', 'admin'), new lang_string('authpreventaccountcreation_help', 'admin'), 0));
$temp->add(new admin_setting_configcheckbox('loginpageautofocus', new lang_string('loginpageautofocus', 'admin'), new lang_string('loginpageautofocus_help', 'admin'), 0));
$temp->add(new admin_setting_configselect('guestloginbutton', new lang_string('guestloginbutton', 'auth'),
| user | course | role |
| teacher1 | C1 | editingteacher |
- @javascript
Scenario: Default system capabilities modification
Given I log in as "admin"
And I set the following system permissions of "Teacher" role:
And "moodle/grade:managesharedforms" capability has "Prevent" permission
And "moodle/course:request" capability has "Prohibit" permission
- @javascript
Scenario: Course capabilities overrides
Given I log in as "teacher1"
And I follow "Course 1"
| mod/forum:editanypost | Prevent |
| mod/forum:addquestion | Allow |
When I set the field "Advanced role override" to "Student (3)"
+ And I press "Go"
Then "mod/forum:deleteanypost" capability has "Prohibit" permission
And "mod/forum:editanypost" capability has "Prevent" permission
And "mod/forum:addquestion" capability has "Allow" permission
- @javascript
Scenario: Module capabilities overrides
Given I log in as "teacher1"
And I follow "Course 1"
| mod/forum:editanypost | Prevent |
| mod/forum:addquestion | Allow |
When I set the field "Advanced role override" to "Student (3)"
+ And I press "Go"
Then "mod/forum:deleteanypost" capability has "Prohibit" permission
And "mod/forum:editanypost" capability has "Prevent" permission
And "mod/forum:addquestion" capability has "Allow" permission
And I follow "Preferences" in the user menu
And I follow "Edit profile"
- @javascript
Scenario: Use nasty strings on steps arguments
When I set the field "Surname" to "$NASTYSTRING1"
And I set the field "Description" to "$NASTYSTRING2"
And the field "Surname" matches value "$NASTYSTRING1"
And the field "City/town" matches value "$NASTYSTRING3"
- @javascript
Scenario: Use nasty strings on table nodes
When I set the following fields to these values:
| Surname | $NASTYSTRING1 |
And the field "Surname" matches value "$NASTYSTRING1"
And the field "City/town" matches value "$NASTYSTRING3"
- @javascript
Scenario: Use double quotes
When I set the following fields to these values:
| First name | va"lue1 |
And the field "Description" matches value "va\\"lue2"
And the field "City/town" matches value "va\"lue3"
- @javascript
Scenario: Nasty strings with other contents
When I set the field "First name" to "My Firstname $NASTYSTRING1"
And I set the following fields to these values:
$allowdeletes = (!empty($formdata->uuallowdeletes) and $optype != UU_USER_ADDNEW and $optype != UU_USER_ADDINC);
$allowsuspends = (!empty($formdata->uuallowsuspends));
$bulk = $formdata->uubulk;
- $noemailduplicates = $formdata->uunoemailduplicates;
+ $noemailduplicates = empty($CFG->allowaccountssameemail) ? 1 : $formdata->uunoemailduplicates;
$standardusernames = $formdata->uustandardusernames;
$resetpasswords = isset($formdata->uuforcepasswordchange) ? $formdata->uuforcepasswordchange : UU_PWRESET_NONE;
$mform->disabledIf('uuallowsuspends', 'uutype', 'eq', UU_USER_ADDNEW);
$mform->disabledIf('uuallowsuspends', 'uutype', 'eq', UU_USER_ADDINC);
- $mform->addElement('selectyesno', 'uunoemailduplicates', get_string('uunoemailduplicates', 'tool_uploaduser'));
- $mform->setDefault('uunoemailduplicates', 1);
+ if (!empty($CFG->allowaccountssameemail)) {
+ $mform->addElement('selectyesno', 'uunoemailduplicates', get_string('uunoemailduplicates', 'tool_uploaduser'));
+ $mform->setDefault('uunoemailduplicates', 1);
+ } else {
+ $mform->addElement('hidden', 'uunoemailduplicates', 1);
+ }
+ $mform->setType('uunoemailduplicates', PARAM_BOOL);
$mform->addElement('selectyesno', 'uustandardusernames', get_string('uustandardusernames', 'tool_uploaduser'));
$mform->setDefault('uustandardusernames', 1);
echo '<div>';
echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
echo '<input type="hidden" name="return" value="'.$return.'" />';
+// HACK to prevent browsers from automatically inserting the user's password into the wrong fields.
+echo prevent_form_autofill_password();
echo '<fieldset>';
echo '<div class="clearer"><!-- --></div>';
echo $newsettingshtml;
}
// If Moodle is configured to use a proxy, phpCAS needs some curl options set.
- if (!empty($CFG->proxyhost) && !is_proxybypass($this->config->hostname)) {
+ if (!empty($CFG->proxyhost) && !is_proxybypass(phpCAS::getServerLoginURL())) {
phpCAS::setExtraCurlOption(CURLOPT_PROXY, $CFG->proxyhost);
if (!empty($CFG->proxyport)) {
phpCAS::setExtraCurlOption(CURLOPT_PROXYPORT, $CFG->proxyport);
function user_login($username, $password) {
global $CFG, $DB;
+ if ($this->is_configured() === false) {
+ debugging(get_string('auth_notconfigured', 'auth', $this->authtype));
+ return false;
+ }
+
$extusername = core_text::convert($username, 'utf-8', $this->config->extencoding);
$extpassword = core_text::convert($password, 'utf-8', $this->config->extencoding);
$authdb = $this->db_init();
- $rs = $authdb->Execute("SELECT {$this->config->fieldpass} AS userpass
+ $rs = $authdb->Execute("SELECT {$this->config->fieldpass}
FROM {$this->config->table}
WHERE {$this->config->fielduser} = '".$this->ext_addslashes($extusername)."'");
if (!$rs) {
}
$fields = array_change_key_case($rs->fields, CASE_LOWER);
- $fromdb = $fields['userpass'];
+ $fromdb = $fields[strtolower($this->config->fieldpass)];
$rs->Close();
$authdb->Close();
* Connect to external database.
*
* @return ADOConnection
+ * @throws moodle_exception
*/
function db_init() {
+ if ($this->is_configured() === false) {
+ throw new moodle_exception('auth_dbcantconnect', 'auth_db');
+ }
+
// Connect to the external database (forcing new connection).
$authdb = ADONewConnection($this->config->type);
if (!empty($this->config->debugauthdb)) {
if ($selectfields) {
$select = array();
foreach ($selectfields as $localname=>$externalname) {
- $select[] = "$externalname AS $localname";
+ $select[] = "$externalname";
}
$select = implode(', ', $select);
$sql = "SELECT $select
FROM {$this->config->table}
WHERE {$this->config->fielduser} = '".$this->ext_addslashes($extusername)."'";
+
if ($rs = $authdb->Execute($sql)) {
if (!$rs->EOF) {
- $fields_obj = $rs->FetchObj();
- $fields_obj = (object)array_change_key_case((array)$fields_obj , CASE_LOWER);
- foreach ($selectfields as $localname=>$externalname) {
- $result[$localname] = core_text::convert($fields_obj->{strtolower($localname)}, $this->config->extencoding, 'utf-8');
+ $fields = $rs->FetchRow();
+ // Convert the associative array to an array of its values so we don't have to worry about the case of its keys.
+ $fields = array_values($fields);
+ foreach (array_keys($selectfields) as $index => $localname) {
+ $value = $fields[$index];
+ $result[$localname] = core_text::convert($value, $this->config->extencoding, 'utf-8');
}
}
$rs->Close();
$authdb = $this->db_init();
// Fetch userlist.
- $rs = $authdb->Execute("SELECT {$this->config->fielduser} AS username
+ $rs = $authdb->Execute("SELECT {$this->config->fielduser}
FROM {$this->config->table} ");
if (!$rs) {
print_error('auth_dbcantconnect','auth_db');
} else if (!$rs->EOF) {
while ($rec = $rs->FetchRow()) {
- $rec = (object)array_change_key_case((array)$rec , CASE_LOWER);
- array_push($result, $rec->username);
+ $rec = array_change_key_case((array)$rec, CASE_LOWER);
+ array_push($result, $rec[strtolower($this->config->fielduser)]);
}
}
return ($this->config->passtype === 'internal');
}
+ /**
+ * Returns false if this plugin is enabled but not configured.
+ *
+ * @return bool
+ */
+ public function is_configured() {
+ if (!empty($this->config->type)) {
+ return true;
+ }
+ return false;
+ }
+
/**
* Indicates if moodle should automatically update internal user
* records with data from external sources using the information
WHERE
gi.courseid = ?', array($userid, $courseid));
foreach ($rs as $record) {
- if (is_null($record->finalgrade)) {
+ // This function produces division by zero error warnings when rawgrademax and rawgrademin
+ // are equal. Below change does not affect function behavior, just avoids the warning.
+ if (is_null($record->finalgrade) || $record->rawgrademax == $record->rawgrademin) {
// No grade = false.
$cachedgrades[$record->id] = false;
} else {
// Just get current grade.
$record = $DB->get_record('grade_grades', array(
'userid' => $userid, 'itemid' => $gradeitemid));
- if ($record && !is_null($record->finalgrade)) {
+ // This function produces division by zero error warnings when rawgrademax and rawgrademin
+ // are equal. Below change does not affect function behavior, just avoids the warning.
+ if ($record && !is_null($record->finalgrade) && $record->rawgrademax != $record->rawgrademin) {
$score = (($record->finalgrade - $record->rawgrademin) * 100) /
($record->rawgrademax - $record->rawgrademin);
} else {
// fail, but it's obvious when running test at least.
$pluginmanager = core_plugin_manager::instance();
$list = $pluginmanager->get_enabled_plugins('availability');
- $this->assertEquals(array('completion', 'date', 'grade', 'group', 'grouping', 'profile'),
- array_keys($list));
+ $this->assertArrayHasKey('completion', $list);
+ $this->assertArrayHasKey('date', $list);
+ $this->assertArrayHasKey('grade', $list);
+ $this->assertArrayHasKey('group', $list);
+ $this->assertArrayHasKey('grouping', $list);
+ $this->assertArrayHasKey('profile', $list);
}
}
* as needed, rebuilding course cache and other friends
*/
class restore_section_structure_step extends restore_structure_step {
+ /** @var array Cache: Array of id => course format */
+ private static $courseformats = array();
+
+ /**
+ * Resets a static cache of course formats. Required for unit testing.
+ */
+ public static function reset_caches() {
+ self::$courseformats = array();
+ }
protected function define_structure() {
global $CFG;
public function process_course_format_options($data) {
global $DB;
- static $courseformats = array();
$courseid = $this->get_courseid();
- if (!array_key_exists($courseid, $courseformats)) {
+ if (!array_key_exists($courseid, self::$courseformats)) {
// It is safe to have a static cache of course formats because format can not be changed after this point.
- $courseformats[$courseid] = $DB->get_field('course', 'format', array('id' => $courseid));
+ self::$courseformats[$courseid] = $DB->get_field('course', 'format', array('id' => $courseid));
}
$data = (array)$data;
- if ($courseformats[$courseid] === $data['format']) {
+ if (self::$courseformats[$courseid] === $data['format']) {
// Import section format options only if both courses (the one that was backed up
// and the one we are restoring into) have same formats.
$params = array(
'numdaystocomplete' => 2);
$courseobject->update_section_format_options($data);
- $this->backup_and_restore($course, $course);
+ $this->backup_and_restore($course, $course, backup::TARGET_EXISTING_ADDING);
$sectionoptions = $courseobject->get_format_options(1);
$this->assertArrayHasKey('numdaystocomplete', $sectionoptions);
$this->resetAfterTest(true);
$this->setAdminUser();
- $CFG->enableavailability = true;
- $CFG->enablecompletion = true;
- // Create a course with some availability data set.
+ // Create a source course using the test_cs2_options format.
$generator = $this->getDataGenerator();
$course = $generator->create_course(
array('format' => 'test_cs2_options', 'numsections' => 3,
'enablecompletion' => COMPLETION_ENABLED),
array('createsections' => true));
+
+ // Create a target course using test_cs_options format.
$newcourse = $generator->create_course(
array('format' => 'test_cs_options', 'numsections' => 3,
'enablecompletion' => COMPLETION_ENABLED),
array('createsections' => true));
+ // Set section 2 to have both options, and a name.
$courseobject = format_base::instance($course->id);
$section = $DB->get_record('course_sections',
- array('course' => $course->id, 'section' => 1), '*', MUST_EXIST);
-
+ array('course' => $course->id, 'section' => 2), '*', MUST_EXIST);
$data = array('id' => $section->id,
'numdaystocomplete' => 2,
- 'secondparameter' => 8);
+ 'secondparameter' => 8
+ );
$courseobject->update_section_format_options($data);
- // Backup and restore it.
- $this->backup_and_restore($course, $newcourse);
+ $DB->set_field('course_sections', 'name', 'Frogs', array('id' => $section->id));
+
+ // Backup and restore to the new course using 'add to existing' so it
+ // keeps the current (test_cs_options) format.
+ $this->backup_and_restore($course, $newcourse, backup::TARGET_EXISTING_ADDING);
+ // Check that the section contains the options suitable for the new
+ // format and that even the one with the same name as from the old format
+ // has NOT been set.
$newcourseobject = format_base::instance($newcourse->id);
- $sectionoptions = $newcourseobject->get_format_options(1);
+ $sectionoptions = $newcourseobject->get_format_options(2);
$this->assertArrayHasKey('numdaystocomplete', $sectionoptions);
- $this->assertArrayHasKey('secondparameter', $sectionoptions);
+ $this->assertArrayNotHasKey('secondparameter', $sectionoptions);
$this->assertEquals(0, $sectionoptions['numdaystocomplete']);
- $this->assertEquals(0, $sectionoptions['secondparameter']);
+
+ // However, the name should have been changed, as this does not depend
+ // on the format.
+ $modinfo = get_fast_modinfo($newcourse->id);
+ $section = $modinfo->get_section_info(2);
+ $this->assertEquals('Frogs', $section->name);
}
/**
*
* @param stdClass $srccourse Course object to backup
* @param stdClass $dstcourse Course object to restore into
+ * @param int $target Target course mode (backup::TARGET_xx)
* @return int ID of newly restored course
*/
- protected function backup_and_restore($srccourse, $dstcourse = null) {
+ protected function backup_and_restore($srccourse, $dstcourse = null,
+ $target = backup::TARGET_NEW_COURSE) {
global $USER, $CFG;
// Turn off file logging, otherwise it can't delete the file (Windows).
}
$rc = new restore_controller($backupid, $newcourseid,
backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
- backup::TARGET_NEW_COURSE);
+ $target);
$this->assertTrue($rc->execute_precheck());
$rc->execute_plan();
class format_test_cs2_options extends format_test_cs_options {
public function section_format_options($foreditform = false) {
return array(
+ 'numdaystocomplete' => array(
+ 'type' => PARAM_INT,
+ 'label' => 'Test days',
+ 'element_type' => 'text',
+ 'default' => 0,
+ ),
'secondparameter' => array(
'type' => PARAM_INT,
'label' => 'Test Parmater',
global $USER, $CFG, $DB, $OUTPUT;
if (!$CFG->messaging) {
+ $this->content = new stdClass;
$this->content->text = '';
+ $this->content->footer = '';
if ($this->page->user_is_editing()) {
$this->content->text = get_string('disabled', 'message');
}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+use block_online_users\fetcher;
+
/**
* This block needs to be reworked.
* The new roles system does away with the concepts of rigid student and
$timetoshowusers = $CFG->block_online_users_timetosee * 60;
}
$now = time();
- $timefrom = 100 * floor(($now - $timetoshowusers) / 100); // Round to nearest 100 seconds for better query cache
//Calculate if we are in separate groups
$isseparategroups = ($this->page->course->groupmode == SEPARATEGROUPS
//Get the user current group
$currentgroup = $isseparategroups ? groups_get_course_group($this->page->course) : NULL;
- $groupmembers = "";
- $groupselect = "";
- $params = array();
-
- //Add this to the SQL to show only group users
- if ($currentgroup !== NULL) {
- $groupmembers = ", {groups_members} gm";
- $groupselect = "AND u.id = gm.userid AND gm.groupid = :currentgroup";
- $params['currentgroup'] = $currentgroup;
- }
+ $sitelevel = $this->page->course->id == SITEID || $this->page->context->contextlevel < CONTEXT_COURSE;
- $userfields = user_picture::fields('u', array('username'));
- $params['now'] = $now;
- $params['timefrom'] = $timefrom;
- if ($this->page->course->id == SITEID or $this->page->context->contextlevel < CONTEXT_COURSE) { // Site-level
- $sql = "SELECT $userfields, MAX(u.lastaccess) AS lastaccess
- FROM {user} u $groupmembers
- WHERE u.lastaccess > :timefrom
- AND u.lastaccess <= :now
- AND u.deleted = 0
- $groupselect
- GROUP BY $userfields
- ORDER BY lastaccess DESC ";
-
- $csql = "SELECT COUNT(u.id)
- FROM {user} u $groupmembers
- WHERE u.lastaccess > :timefrom
- AND u.lastaccess <= :now
- AND u.deleted = 0
- $groupselect";
-
- } else {
- // Course level - show only enrolled users for now
- // TODO: add a new capability for viewing of all users (guests+enrolled+viewing)
-
- list($esqljoin, $eparams) = get_enrolled_sql($this->page->context);
- $params = array_merge($params, $eparams);
-
- $sql = "SELECT $userfields, MAX(ul.timeaccess) AS lastaccess
- FROM {user_lastaccess} ul $groupmembers, {user} u
- JOIN ($esqljoin) euj ON euj.id = u.id
- WHERE ul.timeaccess > :timefrom
- AND u.id = ul.userid
- AND ul.courseid = :courseid
- AND ul.timeaccess <= :now
- AND u.deleted = 0
- $groupselect
- GROUP BY $userfields
- ORDER BY lastaccess DESC";
-
- $csql = "SELECT COUNT(u.id)
- FROM {user_lastaccess} ul $groupmembers, {user} u
- JOIN ($esqljoin) euj ON euj.id = u.id
- WHERE ul.timeaccess > :timefrom
- AND u.id = ul.userid
- AND ul.courseid = :courseid
- AND ul.timeaccess <= :now
- AND u.deleted = 0
- $groupselect";
-
- $params['courseid'] = $this->page->course->id;
- }
+ $onlineusers = new fetcher($currentgroup, $now, $timetoshowusers, $sitelevel,
+ $this->page->context, $this->page->course->id);
//Calculate minutes
$minutes = floor($timetoshowusers/60);
// Verify if we can see the list of users, if not just print number of users
if (!has_capability('block/online_users:viewlist', $this->page->context)) {
- if (!$usercount = $DB->count_records_sql($csql, $params)) {
+ if (!$usercount = $onlineusers->count_users()) {
$usercount = get_string("none");
}
$this->content->text = "<div class=\"info\">".get_string("periodnminutes","block_online_users",$minutes).": $usercount</div>";
return $this->content;
}
-
- if ($users = $DB->get_records_sql($sql, $params, 0, 50)) { // We'll just take the most recent 50 maximum
+ $userlimit = 50; // We'll just take the most recent 50 maximum.
+ if ($users = $onlineusers->get_users($userlimit)) {
foreach ($users as $user) {
$users[$user->id]->fullname = fullname($user);
}
$users = array();
}
- if (count($users) < 50) {
+ if (count($users) < $userlimit) {
$usercount = "";
} else {
- $usercount = $DB->count_records_sql($csql, $params);
+ $usercount = $onlineusers->count_users();
$usercount = ": $usercount";
}
--- /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/>.
+
+/**
+ * File containing onlineusers class.
+ *
+ * @package block_online_users
+ * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_online_users;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class used to list and count online users
+ *
+ * @package block_online_users
+ * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class fetcher {
+
+ /** @var string The SQL query for retrieving a list of online users */
+ public $sql;
+ /** @var string The SQL query for counting the number of online users */
+ public $csql;
+ /** @var string The params for the SQL queries */
+ public $params;
+
+ /**
+ * Class constructor
+ *
+ * @param int $currentgroup The group (if any) to filter on
+ * @param int $now Time now
+ * @param int $timetoshowusers Number of seconds to show online users
+ * @param context $context Context object used to generate the sql for users enrolled in a specific course
+ * @param bool $sitelevel Whether to check online users at site level.
+ * @param int $courseid The course id to check
+ */
+ public function __construct($currentgroup, $now, $timetoshowusers, $context, $sitelevel = true, $courseid = null) {
+ $this->set_sql($currentgroup, $now, $timetoshowusers, $context, $sitelevel, $courseid);
+ }
+
+ /**
+ * Store the SQL queries & params for listing online users
+ *
+ * @param int $currentgroup The group (if any) to filter on
+ * @param int $now Time now
+ * @param int $timetoshowusers Number of seconds to show online users
+ * @param context $context Context object used to generate the sql for users enrolled in a specific course
+ * @param bool $sitelevel Whether to check online users at site level.
+ * @param int $courseid The course id to check
+ */
+ protected function set_sql($currentgroup, $now, $timetoshowusers, $context, $sitelevel, $courseid) {
+ $timefrom = 100 * floor(($now - $timetoshowusers) / 100); // Round to nearest 100 seconds for better query cache.
+
+ $groupmembers = "";
+ $groupselect = "";
+ $groupby = "";
+ $lastaccess = ", lastaccess";
+ $timeaccess = ", ul.timeaccess AS lastaccess";
+ $params = array();
+
+ $userfields = \user_picture::fields('u', array('username'));
+
+ // Add this to the SQL to show only group users.
+ if ($currentgroup !== null) {
+ $groupmembers = ", {groups_members} gm";
+ $groupselect = "AND u.id = gm.userid AND gm.groupid = :currentgroup";
+ $groupby = "GROUP BY $userfields";
+ $lastaccess = ", MAX(u.lastaccess) AS lastaccess";
+ $timeaccess = ", MAX(ul.timeaccess) AS lastaccess";
+ $params['currentgroup'] = $currentgroup;
+ }
+
+ $params['now'] = $now;
+ $params['timefrom'] = $timefrom;
+ if ($sitelevel) {
+ $sql = "SELECT $userfields $lastaccess
+ FROM {user} u $groupmembers
+ WHERE u.lastaccess > :timefrom
+ AND u.lastaccess <= :now
+ AND u.deleted = 0
+ $groupselect $groupby
+ ORDER BY lastaccess DESC ";
+
+ $csql = "SELECT COUNT(u.id)
+ FROM {user} u $groupmembers
+ WHERE u.lastaccess > :timefrom
+ AND u.lastaccess <= :now
+ AND u.deleted = 0
+ $groupselect";
+
+ } else {
+ // Course level - show only enrolled users for now.
+ // TODO: add a new capability for viewing of all users (guests+enrolled+viewing).
+ list($esqljoin, $eparams) = get_enrolled_sql($context);
+ $params = array_merge($params, $eparams);
+
+ $sql = "SELECT $userfields $timeaccess
+ FROM {user_lastaccess} ul $groupmembers, {user} u
+ JOIN ($esqljoin) euj ON euj.id = u.id
+ WHERE ul.timeaccess > :timefrom
+ AND u.id = ul.userid
+ AND ul.courseid = :courseid
+ AND ul.timeaccess <= :now
+ AND u.deleted = 0
+ $groupselect $groupby
+ ORDER BY lastaccess DESC";
+
+ $csql = "SELECT COUNT(u.id)
+ FROM {user_lastaccess} ul $groupmembers, {user} u
+ JOIN ($esqljoin) euj ON euj.id = u.id
+ WHERE ul.timeaccess > :timefrom
+ AND u.id = ul.userid
+ AND ul.courseid = :courseid
+ AND ul.timeaccess <= :now
+ AND u.deleted = 0
+ $groupselect";
+
+ $params['courseid'] = $courseid;
+ }
+ $this->sql = $sql;
+ $this->csql = $csql;
+ $this->params = $params;
+ }
+
+ /**
+ * Get a list of the most recent online users
+ *
+ * @param int $userlimit The maximum number of users that will be returned (optional, unlimited if not set)
+ * @return array
+ */
+ public function get_users($userlimit = 0) {
+ global $DB;
+ $users = $DB->get_records_sql($this->sql, $this->params, 0, $userlimit);
+ return $users;
+ }
+
+ /**
+ * Count the number of online users
+ *
+ * @return int
+ */
+ public function count_users() {
+ global $DB;
+ return $DB->count_records_sql($this->csql, $this->params);
+ }
+
+}
return $instance;
}
+
+ /**
+ * Create (simulated) logged in users and add some of them to groups in a course
+ */
+ public function create_logged_in_users() {
+ global $DB;
+
+ $generator = advanced_testcase::getDataGenerator();
+ $data = array();
+
+ // Create 2 courses.
+ $course1 = $generator->create_course();
+ $data['course1'] = $course1;
+ $course2 = $generator->create_course();
+ $data['course2'] = $course2;
+
+ // Create 9 (simulated) logged in users enroled into $course1.
+ for ($i = 1; $i <= 9; $i++) {
+ $user = $generator->create_user();
+ $DB->set_field('user', 'lastaccess', time(), array('id' => $user->id));
+ $generator->enrol_user($user->id, $course1->id);
+ $DB->insert_record('user_lastaccess', array('userid' => $user->id, 'courseid' => $course1->id, 'timeaccess' => time()));
+ $data['user' . $i] = $user;
+ }
+ // Create 3 (simulated) logged in users who are not enroled into $course1.
+ for ($i = 10; $i <= 12; $i++) {
+ $user = $generator->create_user();
+ $DB->set_field('user', 'lastaccess', time(), array('id' => $user->id));
+ $data['user' . $i] = $user;
+ }
+
+ // Create 3 groups in course 1.
+ $group1 = $generator->create_group(array('courseid' => $course1->id));
+ $data['group1'] = $group1;
+ $group2 = $generator->create_group(array('courseid' => $course1->id));
+ $data['group2'] = $group2;
+ $group3 = $generator->create_group(array('courseid' => $course1->id));
+ $data['group3'] = $group3;
+
+ // Add 3 users to course group 1.
+ $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $data['user1']->id));
+ $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $data['user2']->id));
+ $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $data['user3']->id));
+
+ // Add 4 users to course group 2.
+ $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $data['user3']->id));
+ $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $data['user4']->id));
+ $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $data['user5']->id));
+ $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $data['user6']->id));
+
+ return $data; // Return the user, course and group objects.
+ }
}
--- /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/>.
+
+/**
+ * Online users tests
+ *
+ * @package block_online_users
+ * @category test
+ * @copyright 2015 University of Nottingham <www.nottingham.ac.uk>
+ * @author Barry Oosthuizen <barry.oosthuizen@nottingham.ac.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use block_online_users\fetcher;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Online users testcase
+ *
+ * @package block_online_users
+ * @category test
+ * @copyright 2015 University of Nottingham <www.nottingham.ac.uk>
+ * @author Barry Oosthuizen <barry.oosthuizen@nottingham.ac.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_online_users_testcase extends advanced_testcase {
+
+ protected $data;
+
+ /**
+ * Tests initial setup.
+ *
+ * Prepare the site with some courses, groups, users and
+ * simulate various recent accesses.
+ */
+ protected function setUp() {
+
+ // Generate (simulated) recently logged-in users.
+ $generator = $this->getDataGenerator()->get_plugin_generator('block_online_users');
+ $this->data = $generator->create_logged_in_users();
+
+ // Confirm we have modified the site and requires reset.
+ $this->resetAfterTest(true);
+ }
+
+ /**
+ * Check logged in group 1, 2 & 3 members in course 1 (should be 3, 4 and 0).
+ *
+ * @param array $data Array of user, course and group objects
+ * @param int $now Current Unix timestamp
+ * @param int $timetoshowusers The time window (in seconds) to check for the latest logged in users
+ */
+ public function test_fetcher_course1_group_members() {
+ global $CFG;
+
+ $groupid = $this->data['group1']->id;
+ $now = time();
+ $timetoshowusers = $CFG->block_online_users_timetosee * 60;
+ $context = context_course::instance($this->data['course1']->id);
+ $courseid = $this->data['course1']->id;
+ $onlineusers = new fetcher($groupid, $now, $timetoshowusers, $context, false, $courseid);
+
+ $usercount = $onlineusers->count_users();
+ $users = $onlineusers->get_users();
+ $this->assertEquals(3, $usercount, 'There was a problem counting the number of online users in group 1');
+ $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in group 1');
+
+ $groupid = $this->data['group2']->id;
+ $onlineusers = new fetcher($groupid, $now, $timetoshowusers, $context, false, $courseid);
+
+ $usercount = $onlineusers->count_users();
+ $users = $onlineusers->get_users();
+ $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in group 2');
+ $this->assertEquals(4, $usercount, 'There was a problem counting the number of online users in group 2');
+
+ $groupid = $this->data['group3']->id;
+ $onlineusers = new fetcher($groupid, $now, $timetoshowusers, $context, false, $courseid);
+
+ $usercount = $onlineusers->count_users();
+ $users = $onlineusers->get_users();
+ $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in group 3');
+ $this->assertEquals(0, $usercount, 'There was a problem counting the number of online users in group 3');
+ }
+
+ /**
+ * Check logged in users in courses 1 & 2 (should be 9 and 0).
+ *
+ * @param array $data Array of user, course and group objects
+ * @param int $now Current Unix timestamp
+ * @param int $timetoshowusers The time window (in seconds) to check for the latest logged in users
+ */
+ public function test_fetcher_courses() {
+
+ global $CFG;
+
+ $currentgroup = null;
+ $now = time();
+ $timetoshowusers = $CFG->block_online_users_timetosee * 60;
+ $context = context_course::instance($this->data['course1']->id);
+ $courseid = $this->data['course1']->id;
+ $onlineusers = new fetcher($currentgroup, $now, $timetoshowusers, $context, false, $courseid);
+
+ $usercount = $onlineusers->count_users();
+ $users = $onlineusers->get_users();
+ $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in course 1');
+ $this->assertEquals(9, $usercount, 'There was a problem counting the number of online users in course 1');
+
+ $courseid = $this->data['course2']->id;
+ $onlineusers = new fetcher($currentgroup, $now, $timetoshowusers, $context, false, $courseid);
+
+ $usercount = $onlineusers->count_users();
+ $users = $onlineusers->get_users();
+ $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in course 2');
+ $this->assertEquals(0, $usercount, 'There was a problem counting the number of online users in course 2');
+ }
+
+ /**
+ * Check logged in at the site level (should be 12).
+ *
+ * @param int $now Current Unix timestamp
+ * @param int $timetoshowusers The time window (in seconds) to check for the latest logged in users
+ */
+ public function test_fetcher_sitelevel() {
+ global $CFG;
+
+ $currentgroup = null;
+ $now = time();
+ $timetoshowusers = $CFG->block_online_users_timetosee * 60;
+ $context = context_system::instance();
+ $onlineusers = new fetcher($currentgroup, $now, $timetoshowusers, $context, true);
+
+ $usercount = $onlineusers->count_users();
+ $users = $onlineusers->get_users();
+ $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users at site level');
+ $this->assertEquals(12, $usercount, 'There was a problem counting the number of online users at site level');
+ }
+}
*/
class block_rss_client extends block_base {
+ /** The maximum time in seconds that cron will wait between attempts to retry failing RSS feeds. */
+ const CLIENT_MAX_SKIPTIME = 43200; // 60 * 60 * 12 seconds.
function init() {
$this->title = get_string('pluginname', 'block_rss_client');
}
/**
- * cron - goes through all feeds and retrieves them with the cache
- * duration set to 0 in order to force the retrieval of the item and
- * refresh the cache
+ * cron - goes through all the feeds. If the feed has a skipuntil value
+ * that is less than the current time cron will attempt to retrieve it
+ * with the cache duration set to 0 in order to force the retrieval of
+ * the item and refresh the cache.
*
- * @return boolean true if all feeds were retrieved succesfully
+ * If a feed fails then the skipuntil time of that feed is set to be
+ * later than the next expected cron time. The amount of time will
+ * increase each time the fetch fails until the maximum is reached.
+ *
+ * If a feed that has been failing is successfully retrieved it will
+ * go back to being handled as though it had never failed.
+ *
+ * CRON should therefor process requests for permanently broken RSS
+ * feeds infrequently, and temporarily unavailable feeds will be tried
+ * less often until they become available again.
+ *
+ * @return boolean Always returns true
*/
function cron() {
global $CFG, $DB;
require_once($CFG->libdir.'/simplepie/moodle_simplepie.php');
+ // Get the legacy cron time, strangely the cron property of block_base
+ // does not seem to get set. This means we must retrive it here.
+ $this->cron = $DB->get_field('block', 'cron', array('name' => 'rss_client'));
+
// We are going to measure execution times
$starttime = microtime();
-
- // And we have one initial $status
- $status = true;
+ $starttimesec = time();
// Fetch all site feeds.
$rs = $DB->get_recordset('block_rss_client');
mtrace('');
foreach ($rs as $rec) {
mtrace(' ' . $rec->url . ' ', '');
+
+ // Skip feed if it failed recently.
+ if ($starttimesec < $rec->skipuntil) {
+ mtrace('skipping until ' . userdate($rec->skipuntil));
+ continue;
+ }
+
// Fetch the rss feed, using standard simplepie caching
// so feeds will be renewed only if cache has expired
core_php_time_limit::raise(60);
$feed->init();
if ($feed->error()) {
- mtrace('Error: could not load/find the RSS feed');
- $status = false;
+ // Skip this feed (for an ever-increasing time if it keeps failing).
+ $rec->skiptime = $this->calculate_skiptime($rec->skiptime);
+ $rec->skipuntil = time() + $rec->skiptime;
+ $DB->update_record('block_rss_client', $rec);
+ mtrace("Error: could not load/find the RSS feed - skipping for {$rec->skiptime} seconds.");
} else {
mtrace ('ok');
+ // It worked this time, so reset the skiptime.
+ if ($rec->skiptime > 0) {
+ $rec->skiptime = 0;
+ $rec->skipuntil = 0;
+ $DB->update_record('block_rss_client', $rec);
+ }
+ // Only increase the counter when a feed is sucesfully refreshed.
+ $counter ++;
}
- $counter ++;
}
$rs->close();
// Show times
mtrace($counter . ' feeds refreshed (took ' . microtime_diff($starttime, microtime()) . ' seconds)');
- // And return $status
- return $status;
+ return true;
+ }
+
+ /**
+ * Calculates a new skip time for a record based on the current skip time.
+ *
+ * @param int $currentskip The curreent skip time of a record.
+ * @return int A new skip time that should be set.
+ */
+ protected function calculate_skiptime($currentskip) {
+ // The default time to skiptime.
+ $newskiptime = $this->cron * 1.1;
+ if ($currentskip > 0) {
+ // Double the last time.
+ $newskiptime = $currentskip * 2;
+ }
+ if ($newskiptime > self::CLIENT_MAX_SKIPTIME) {
+ // Do not allow the skip time to increase indefinatly.
+ $newskiptime = self::CLIENT_MAX_SKIPTIME;
+ }
+ return $newskiptime;
}
}
<?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="blocks/rss_client/db" VERSION="20120122" COMMENT="XMLDB file for Moodle rss_client block"
+<XMLDB PATH="blocks/rss_client/db" VERSION="20150717" COMMENT="XMLDB file for Moodle rss_client block"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
<FIELD NAME="description" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="shared" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="url" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
+ <FIELD NAME="skiptime" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="How many seconds skip this feed for (increases every time it fails, resets to 0 when it succeeds)"/>
+ <FIELD NAME="skipuntil" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Do not query this RSS feed again until this time"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="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/>.
+
+/**
+ * Database upgrades for the RSS block.
+ *
+ * @package block_rss_client
+ * @copyright 2014 Davo Smith
+ * @author Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL
+ */
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Upgrade the block_rss_client database.
+ *
+ * @param int $oldversion The version number of the plugin that was installed.
+ * @return boolean
+ */
+function xmldb_block_rss_client_upgrade($oldversion) {
+ global $DB;
+ $dbman = $DB->get_manager();
+
+ if ($oldversion < 2015071700) {
+ // Support for skipping RSS feeds for a while when they fail.
+ $table = new xmldb_table('block_rss_client');
+ // How many seconds we are currently ignoring this RSS feed for (due to an error).
+ $field = new xmldb_field('skiptime', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'url');
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+ // When to next update this RSS feed.
+ $field = new xmldb_field('skipuntil', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'skiptime');
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+ upgrade_block_savepoint(true, 2015071700, 'rss_client');
+ }
+
+ return true;
+}
--- /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/>.
+
+/**
+ * PHPunit tests for rss client cron.
+ *
+ * @package block_rss_client
+ * @copyright 2015 University of Nottingham
+ * @author Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die();
+require_once(dirname(dirname(__DIR__)) . '/moodleblock.class.php');
+require_once(dirname(__DIR__) . '/block_rss_client.php');
+
+/**
+ * Class for the PHPunit tests for rss client cron.
+ *
+ * @package block_rss_client
+ * @copyright 2015 Universit of Nottingham
+ * @author Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_rss_client_cron_testcase extends advanced_testcase {
+ /**
+ * Test that when a record has a skipuntil time that is greater
+ * than the current time the attempt is skipped.
+ */
+ public function test_skip() {
+ global $DB;
+ $this->resetAfterTest();
+ // Create a RSS feed record with a skip until time set to the future.
+ $record = (object) array(
+ 'userid' => 1,
+ 'title' => 'Skip test feed',
+ 'preferredtitle' => '',
+ 'description' => 'A feed to test the skip time.',
+ 'shared' => 0,
+ 'url' => 'http://example.com/rss',
+ 'skiptime' => 330,
+ 'skipuntil' => time() + 300,
+ );
+ $DB->insert_record('block_rss_client', $record);
+
+ $block = new block_rss_client();
+ ob_start();
+ // Silence SimplePie php notices.
+ @$block->cron();
+ $cronoutput = ob_get_clean();
+ $this->assertContains('skipping until ' . userdate($record->skipuntil), $cronoutput);
+ $this->assertContains('0 feeds refreshed (took ', $cronoutput);
+ }
+
+ /**
+ * Test that when a feed has an error the skip time is increaed correctly.
+ */
+ public function test_error() {
+ global $DB;
+ $this->resetAfterTest();
+ $time = time();
+ // A record that has failed before.
+ $record = (object) array(
+ 'userid' => 1,
+ 'title' => 'Skip test feed',
+ 'preferredtitle' => '',
+ 'description' => 'A feed to test the skip time.',
+ 'shared' => 0,
+ 'url' => 'http://example.com/rss',
+ 'skiptime' => 330,
+ 'skipuntil' => $time - 300,
+ );
+ $record->id = $DB->insert_record('block_rss_client', $record);
+
+ // A record that has not failed before.
+ $record2 = (object) array(
+ 'userid' => 1,
+ 'title' => 'Skip test feed',
+ 'preferredtitle' => '',
+ 'description' => 'A feed to test the skip time.',
+ 'shared' => 0,
+ 'url' => 'http://example.com/rss2',
+ 'skiptime' => 0,
+ 'skipuntil' => 0,
+ );
+ $record2->id = $DB->insert_record('block_rss_client', $record2);
+
+ // A record that is near the maximum wait time.
+ $record3 = (object) array(
+ 'userid' => 1,
+ 'title' => 'Skip test feed',
+ 'preferredtitle' => '',
+ 'description' => 'A feed to test the skip time.',
+ 'shared' => 0,
+ 'url' => 'http://example.com/rss3',
+ 'skiptime' => block_rss_client::CLIENT_MAX_SKIPTIME - 5,
+ 'skipuntil' => $time - 1,
+ );
+ $record3->id = $DB->insert_record('block_rss_client', $record3);
+
+ // Run the cron.
+ $block = new block_rss_client();
+ ob_start();
+ // Silence SimplePie php notices.
+ @$block->cron();
+ $cronoutput = ob_get_clean();
+ $skiptime1 = $record->skiptime * 2;
+ $message1 = 'http://example.com/rss Error: could not load/find the RSS feed - skipping for ' . $skiptime1 . ' seconds.';
+ $this->assertContains($message1, $cronoutput);
+ $skiptime2 = 330; // Assumes that the cron time in the version file is 300.
+ $message2 = 'http://example.com/rss2 Error: could not load/find the RSS feed - skipping for ' . $skiptime2 . ' seconds.';
+ $this->assertContains($message2, $cronoutput);
+ $skiptime3 = block_rss_client::CLIENT_MAX_SKIPTIME;
+ $message3 = 'http://example.com/rss3 Error: could not load/find the RSS feed - skipping for ' . $skiptime3 . ' seconds.';
+ $this->assertContains($message3, $cronoutput);
+ $this->assertContains('0 feeds refreshed (took ', $cronoutput);
+
+ // Test that the records have been correctly updated.
+ $newrecord = $DB->get_record('block_rss_client', array('id' => $record->id));
+ $this->assertAttributeEquals($skiptime1, 'skiptime', $newrecord);
+ $this->assertAttributeGreaterThanOrEqual($time + $skiptime1, 'skipuntil', $newrecord);
+ $newrecord2 = $DB->get_record('block_rss_client', array('id' => $record2->id));
+ $this->assertAttributeEquals($skiptime2, 'skiptime', $newrecord2);
+ $this->assertAttributeGreaterThanOrEqual($time + $skiptime2, 'skipuntil', $newrecord2);
+ $newrecord3 = $DB->get_record('block_rss_client', array('id' => $record3->id));
+ $this->assertAttributeEquals($skiptime3, 'skiptime', $newrecord3);
+ $this->assertAttributeGreaterThanOrEqual($time + $skiptime3, 'skipuntil', $newrecord3);
+ }
+}
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015051100; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version = 2015071700; // The current plugin version (Date: YYYYMMDDXX)
$plugin->requires = 2015050500; // Requires this Moodle version
$plugin->component = 'block_rss_client'; // Full name of the plugin (used for diagnostics)
$plugin->cron = 300; // Set min time between cron executions to 300 secs (5 mins)
return $this->content;
}
- $course = $this->page->course;
+ $course = get_site();
require_once($CFG->dirroot.'/course/lib.php');
$context = context_course::instance($course->id);
$isediting = $this->page->user_is_editing() && has_capability('moodle/course:manageactivities', $context);
$attrs['title'] = $cm->modfullname;
$attrs['class'] = $cm->extraclasses . ' activity-action';
if ($cm->onclick) {
- $attrs['id'] = html_writer::random_id('onclick');
- $OUTPUT->add_action_handler(new component_action('click', $cm->onclick), $attrs['id']);
+ // Get on-click attribute value if specified and decode the onclick - it
+ // has already been encoded for display.
+ $attrs['onclick'] = htmlspecialchars_decode($cm->onclick);
}
if (!$cm->visible) {
$attrs['class'] .= ' dimmed';
$attrs['title'] = $mod->modfullname;
$attrs['class'] = $mod->extraclasses . ' activity-action';
if ($mod->onclick) {
- $attrs['id'] = html_writer::random_id('onclick');
- $OUTPUT->add_action_handler(new component_action('click', $mod->onclick), $attrs['id']);
+ // Get on-click attribute value if specified and decode the onclick - it
+ // has already been encoded for display.
+ $attrs['onclick'] = htmlspecialchars_decode($mod->onclick);
}
if (!$mod->visible) {
$attrs['class'] .= ' dimmed';
--- /dev/null
+@block @block_main_menu
+Feature: Add URL to main menu block
+ In order to add helpful resources for students
+ As a admin
+ I need to add URLs to the main menu block and check it works.
+
+ @javascript
+ Scenario: Add a URL in menu block and ensure it appears
+ Given I log in as "admin"
+ And I am on site homepage
+ And I navigate to "Turn editing on" node in "Front page settings"
+ When I add a "URL" to section "0" and I fill the form with:
+ | Name | google |
+ | Description | gooooooooogle |
+ | External URL | http://www.google.com |
+ | id_display | In pop-up |
+ Then "google" "link" should exist in the "Main menu" "block"
+ And "Add an activity or resource" "link" should exist in the "Main menu" "block"
class block_tag_youtube extends block_base {
+ /**
+ * @var Google_Service_Youtube
+ */
+ protected $service = null;
+
function init() {
$this->title = get_string('pluginname','block_tag_youtube');
+ $this->config = new stdClass();
}
function applicable_formats() {
return array('tag' => true);
}
+ /**
+ * It can be configured.
+ *
+ * @return bool
+ */
+ public function has_config() {
+ return true;
+ }
+
function specialization() {
$this->title = !empty($this->config->title) ? $this->config->title : get_string('pluginname', 'block_tag_youtube');
// Convert numeric categories (old YouTube API) to
return $this->content;
}
+ $this->content = new stdClass();
+ $this->content->footer = '';
+
+ if (!$this->get_service()) {
+ $this->content->text = $this->get_error_message();
+ return $this->content;
+ }
+
$text = '';
if(!empty($this->config->playlist)){
//videos from a playlist
}
}
- $this->content = new stdClass;
$this->content->text = $text;
- $this->content->footer = '';
return $this->content;
}
function get_videos_by_playlist(){
+ if (!$service = $this->get_service()) {
+ return $this->get_error_message();
+ }
+
$numberofvideos = DEFAULT_NUMBER_OF_VIDEOS;
if( !empty($this->config->numberofvideos)) {
$numberofvideos = $this->config->numberofvideos;
}
- $request = 'http://gdata.youtube.com/feeds/api/playlists/' .
- $this->config->playlist .
- '?start-index=1&max-results=' .
- $numberofvideos .
- '&format=5';
+ try {
+ $response = $service->playlistItems->listPlaylistItems('id,snippet', array(
+ 'playlistId' => $this->config->playlist,
+ 'maxResults' => $numberofvideos
+ ));
+ } catch (Google_Service_Exception $e) {
+ debugging('Google service exception: ' . $e->getMessage(), DEBUG_DEVELOPER);
+ return $this->get_error_message(get_string('requesterror', 'block_tag_youtube'));
+ }
- return $this->fetch_request($request);
+ return $this->render_items($response);
}
function get_videos_by_tag(){
+ if (!$service = $this->get_service()) {
+ return $this->get_error_message();
+ }
+
$tagid = optional_param('id', 0, PARAM_INT); // tag id - for backware compatibility
$tag = optional_param('tag', '', PARAM_TAG); // tag
$numberofvideos = $this->config->numberofvideos;
}
- $request = 'http://gdata.youtube.com/feeds/api/videos?vq=' .
- $querytag .
- '&start-index=1&max-results=' .
- $numberofvideos .
- '&format=5';
+ try {
+ $response = $service->search->listSearch('id,snippet', array(
+ 'q' => $querytag,
+ 'type' => 'video',
+ 'maxResults' => $numberofvideos
+ ));
+ } catch (Google_Service_Exception $e) {
+ debugging('Google service exception: ' . $e->getMessage(), DEBUG_DEVELOPER);
+ return $this->get_error_message(get_string('requesterror', 'block_tag_youtube'));
+ }
- return $this->fetch_request($request);
+ return $this->render_items($response);
}
function get_videos_by_tag_and_category(){
+ if (!$service = $this->get_service()) {
+ return $this->get_error_message();
+ }
+
$tagid = optional_param('id', 0, PARAM_INT); // tag id - for backware compatibility
$tag = optional_param('tag', '', PARAM_TAG); // tag
$numberofvideos = $this->config->numberofvideos;
}
- $request = 'http://gdata.youtube.com/feeds/api/videos?category=' .
- $this->config->category .
- '&vq=' .
- $querytag .
- '&start-index=1&max-results=' .
- $numberofvideos .
- '&format=5';
-
+ try {
+ $response = $service->search->listSearch('id,snippet', array(
+ 'q' => $querytag,
+ 'type' => 'video',
+ 'maxResults' => $numberofvideos,
+ 'videoCategoryId' => $this->config->category
+ ));
+ } catch (Google_Service_Exception $e) {
+ debugging('Google service exception: ' . $e->getMessage(), DEBUG_DEVELOPER);
+ return $this->get_error_message(get_string('requesterror', 'block_tag_youtube'));
+ }
- return $this->fetch_request($request);
+ return $this->render_items($response);
}
- function fetch_request($request){
+ /**
+ * Sends a request to fetch data.
+ *
+ * @see block_tag_youtube::service
+ * @deprecated since Moodle 2.8.8, 2.9.2 and 3.0 MDL-49085 - please do not use this function any more.
+ * @param string $request
+ * @throws coding_exception
+ */
+ public function fetch_request($request) {
+ throw new coding_exception('Sorry, this function has been deprecated in Moodle 2.8.8, 2.9.2 and 3.0. Use block_tag_youtube::get_service instead.');
+
$c = new curl(array('cache' => true, 'module_cache'=>'tag_youtube'));
$c->setopt(array('CURLOPT_TIMEOUT' => 3, 'CURLOPT_CONNECTTIMEOUT' => 3));
return $this->render_video_list($xml);
}
+ /**
+ * Renders the video list.
+ *
+ * @see block_tag_youtube::render_items
+ * @deprecated since Moodle 2.8.8, 2.9.2 and 3.0 MDL-49085 - please do not use this function any more.
+ * @param SimpleXMLElement $xml
+ * @throws coding_exception
+ */
function render_video_list(SimpleXMLElement $xml){
+ throw new coding_exception('Sorry, this function has been deprecated in Moodle 2.8.8, 2.9.2 and 3.0. Use block_tag_youtube::render_items instead.');
+ }
- $text = '';
- $text .= '<ul class="yt-video-entry unlist img-text">';
-
- foreach($xml->entry as $entry){
- $media = $entry->children('http://search.yahoo.com/mrss/');
- $playerattrs = $media->group->player->attributes();
- $url = s($playerattrs['url']);
- $thumbattrs = $media->group->thumbnail[0]->attributes();
- $thumbnail = s($thumbattrs['url']);
- $title = s($media->group->title);
- $yt = $media->children('http://gdata.youtube.com/schemas/2007');
- $secattrs = $yt->duration->attributes();
- $seconds = $secattrs['seconds'];
-
- $text .= '<li>';
- $text .= '<div class="clearfix">';
- $text .= '<a href="'. $url . '">';
- $text .= '<img alt="" class="youtube-thumb" src="'. $thumbnail .'" /></a>';
- $text .= '</div><span><a href="'. $url . '">'. $title .'</a></span>';
- $text .= '<div>';
- $text .= format_time($seconds);
- $text .= "</div></li>\n";
+ /**
+ * Returns an error message.
+ *
+ * Useful when the block is not properly set or something goes wrong.
+ *
+ * @param string $message The message to display.
+ * @return string HTML
+ */
+ protected function get_error_message($message = null) {
+ global $OUTPUT;
+
+ if (empty($message)) {
+ $message = get_string('apierror', 'block_tag_youtube');
+ }
+ return $OUTPUT->notification($message);
+ }
+
+ /**
+ * Gets the youtube service object.
+ *
+ * @return Google_Service_YouTube
+ */
+ protected function get_service() {
+ global $CFG;
+
+ if (!$apikey = get_config('block_tag_youtube', 'apikey')) {
+ return false;
}
- $text .= "</ul><div class=\"clearer\"></div>\n";
+
+ // Wrapped in an if in case we call different get_videos_* multiple times.
+ if (!isset($this->service)) {
+ require_once($CFG->libdir . '/google/lib.php');
+ $client = get_google_client();
+ $client->setDeveloperKey($apikey);
+ $client->setScopes(array(Google_Service_YouTube::YOUTUBE_READONLY));
+ $this->service = new Google_Service_YouTube($client);
+ }
+
+ return $this->service;
+ }
+
+ /**
+ * Renders the list of items.
+ *
+ * @param array $videosdata
+ * @return string HTML
+ */
+ protected function render_items($videosdata) {
+
+ if (!$videosdata || empty($videosdata->items)) {
+ if (!empty($videosdata->error)) {
+ debugging('Error fetching data from youtube: ' . $videosdata->error->message, DEBUG_DEVELOPER);
+ }
+ return '';
+ }
+
+ // If we reach that point we already know that the API key is set.
+ $service = $this->get_service();
+
+ $text = html_writer::start_tag('ul', array('class' => 'yt-video-entry unlist img-text'));
+ foreach ($videosdata->items as $video) {
+
+ // Link to the video included in the playlist if listing a playlist.
+ if (!empty($video->snippet->resourceId)) {
+ $id = $video->snippet->resourceId->videoId;
+ $playlist = '&list=' . $video->snippet->playlistId;
+ } else {
+ $id = $video->id->videoId;
+ $playlist = '';
+ }
+
+ $thumbnail = $video->snippet->getThumbnails()->getDefault();
+ $url = 'http://www.youtube.com/watch?v=' . $id . $playlist;
+
+ $videodetails = $service->videos->listVideos('id,contentDetails', array('id' => $id));
+ if ($videodetails && !empty($videodetails->items)) {
+
+ // We fetch by id so we just use the first one.
+ $details = $videodetails->items[0];
+ $start = new DateTime('@0');
+ $start->add(new DateInterval($details->contentDetails->duration));
+ $seconds = $start->format('U');
+ }
+
+ $text .= html_writer::start_tag('li');
+
+ $imgattrs = array('class' => 'youtube-thumb', 'src' => $thumbnail->url, 'alt' => $video->snippet->title);
+ $thumbhtml = html_writer::empty_tag('img', $imgattrs);
+ $link = html_writer::tag('a', $thumbhtml, array('href' => $url));
+ $text .= html_writer::tag('div', $link, array('class' => 'clearfix'));
+
+ $text .= html_writer::tag('span', html_writer::tag('a', $video->snippet->title, array('href' => $url)));
+
+ if (!empty($seconds)) {
+ $text .= html_writer::tag('div', format_time($seconds));
+ }
+ $text .= html_writer::end_tag('li');
+ }
+ $text .= html_writer::end_tag('ul');
+
return $text;
}
--- /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/>.
+
+/**
+ * Tag Youtube block installation.
+ *
+ * @package block_tag_youtube
+ * @copyright 2015 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Sets the install values for the tag_youtube entry in the block table.
+ *
+ * @return void
+ */
+function xmldb_block_tag_youtube_install() {
+ global $DB;
+
+ // Disable this block by default.
+ $DB->set_field('block', 'visible', 0, array('name' => 'tag_youtube'));
+}
+
*/
$string['anycategory'] = 'Any category';
+$string['apierror'] = 'The Youtube API key is not set. Contact your administrator.';
+$string['apikey'] = 'API key';
+$string['apikeyinfo'] = 'Get a <a href="https://developers.google.com/youtube/v3/getting-started">Google API key</a> for your Moodle site.';
$string['autosvehicles'] = 'Autos & Vehicles';
$string['category'] = 'Category';
$string['comedy'] = 'Comedy';
$string['peopleblogs'] = 'People & Blogs';
$string['petsanimals'] = 'Pets & Animals';
$string['pluginname'] = 'Youtube';
+$string['requesterror'] = 'Data could not be obtained from the server. Contact your administrator if the problem persist.';
$string['scienceandtech'] = 'Science & Tech';
$string['sports'] = 'Sports';
$string['tag_youtube:addinstance'] = 'Add a new youtube block';
--- /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 the RSS client block.
+ *
+ * @package block_tag_youtube
+ * @copyright 2015 David Monllao
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+if ($ADMIN->fulltree) {
+ $settings->add(new admin_setting_configtext('block_tag_youtube/apikey', get_string('apikey', 'block_tag_youtube'),
+ get_string('apikeyinfo', 'block_tag_youtube'), '', PARAM_RAW_TRIMMED, 40));
+}
--- /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/>.
+
+/**
+ * Block Tag Youtube tests
+ *
+ * @package block_tag_youtube
+ * @category test
+ * @copyright 2015 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Block Tag Youtube test class.
+ *
+ * @package block_tag_youtube
+ * @category test
+ * @copyright 2015 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_block_tag_youtube_testcase extends advanced_testcase {
+
+ /**
+ * Testing the tag youtube block's initial state after a new installation.
+ *
+ * @return void
+ */
+ public function test_after_install() {
+ global $DB;
+
+ $this->resetAfterTest(true);
+
+ // Assert that tag_youtube entry exists and that its visible attribute is set to 0 (disabled).
+ $this->assertTrue($DB->record_exists('block', array('name' => 'tag_youtube', 'visible' => 0)));
+ }
+}
--- /dev/null
+This files describes API changes in the block tag_youtube code.
+
+=== 3.0 ===
+
+* Due to the final YouTube API v2.0 deprecation we needed to adapt the current
+ code to YouTube Data API v3. block_tag_youtube::fetch_request and
+ block_tag_youtube::render_video_list have been deprecated as they can not be
+ used any more.
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015051100; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version = 2015051101; // The current plugin version (Date: YYYYMMDDXX)
$plugin->requires = 2015050500; // Requires this Moodle version
$plugin->component = 'block_tag_youtube'; // Full name of the plugin (used for diagnostics)
And I press "Update profile"
And I log out
- @javascript
Scenario: Add Tags block on a front page
When I log in as "admin"
And I am on site homepage
And I click on "Dogs" "link" in the "Tags" "block"
And I should see "Log in to the site" in the ".breadcrumb" "css_element"
- @javascript
Scenario: Add Tags block in a course
When I log in as "teacher1"
And I follow "Course 1"
Then I should see "Activities" in the "Activities" "block"
And I should see "Course completion status" in the "Course completion status" "block"
- @javascript
Scenario: Blocks can not be added when the admin restricts the permissions
Given I log in as "admin"
And I set the following system permissions of "Teacher" role:
And I press "Save changes"
And I log out
- @javascript
Scenario: A student can not see another student's blog entries.
Given I log in as "testuser"
And I follow "Course 1"
protected static function ensure_ready_for_stats($store, $definition, $mode = cache_store::MODE_APPLICATION) {
// This function is performance-sensitive, so exit as quickly as possible
// if we do not need to do anything.
- if (isset(self::$stats[$definition][$store])) {
+ if (isset(self::$stats[$definition]['stores'][$store])) {
return;
}
if (!array_key_exists($definition, self::$stats)) {
)
)
);
- } else if (!array_key_exists($store, self::$stats[$definition])) {
+ } else if (!array_key_exists($store, self::$stats[$definition]['stores'])) {
self::$stats[$definition]['stores'][$store] = array(
'hits' => 0,
'misses' => 0,
$maxtime = cache::now() - $ttl;
}
$readfile = false;
- if ($this->prescan && array_key_exists($key, $this->keys)) {
- if (!$ttl || $this->keys[$filename] >= $maxtime && file_exists($file)) {
+ if ($this->prescan && array_key_exists($filename, $this->keys)) {
+ if ((!$ttl || $this->keys[$filename] >= $maxtime) && file_exists($file)) {
$readfile = true;
} else {
$this->delete($key);
protected function get_class_name() {
return 'cachestore_file';
}
+
+ /**
+ * Testing cachestore_file::get with prescan enabled and with
+ * deleting the cache between the prescan and the call to get.
+ *
+ * The deleting of cache simulates some other process purging
+ * the cache.
+ */
+ public function test_cache_get_with_prescan_and_purge() {
+ global $CFG;
+
+ $definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, 'cachestore_file', 'phpunit_test');
+ $name = 'File test';
+
+ $path = make_cache_directory('cachestore_file_test');
+ $cache = new cachestore_file($name, array('path' => $path, 'prescan' => true));
+ $cache->initialise($definition);
+
+ $cache->set('testing', 'value');
+
+ $path = make_cache_directory('cachestore_file_test');
+ $cache = new cachestore_file($name, array('path' => $path, 'prescan' => true));
+ $cache->initialise($definition);
+
+ // Let's pretend that some other process purged caches.
+ remove_dir($CFG->cachedir.'/cachestore_file_test', true);
+ make_cache_directory('cachestore_file_test');
+
+ $cache->get('testing');
+ }
}
\ No newline at end of file
$returnedinstance1->name = 'b';
$this->assertEquals('b', $returnedinstance2->name);
}
+
+ public function test_performance_debug() {
+ global $CFG;
+ $this->resetAfterTest(true);
+ $CFG->perfdebug = 15;
+
+ $instance = cache_config_testing::instance();
+ $applicationid = 'phpunit/applicationperf';
+ $instance->phpunit_add_definition($applicationid, array(
+ 'mode' => cache_store::MODE_APPLICATION,
+ 'component' => 'phpunit',
+ 'area' => 'applicationperf'
+ ));
+ $sessionid = 'phpunit/sessionperf';
+ $instance->phpunit_add_definition($sessionid, array(
+ 'mode' => cache_store::MODE_SESSION,
+ 'component' => 'phpunit',
+ 'area' => 'sessionperf'
+ ));
+ $requestid = 'phpunit/requestperf';
+ $instance->phpunit_add_definition($requestid, array(
+ 'mode' => cache_store::MODE_REQUEST,
+ 'component' => 'phpunit',
+ 'area' => 'requestperf'
+ ));
+
+ $application = cache::make('phpunit', 'applicationperf');
+ $session = cache::make('phpunit', 'sessionperf');
+ $request = cache::make('phpunit', 'requestperf');
+
+ // Check that no stats are recorded for these definitions yet.
+ $stats = cache_helper::get_stats();
+ $this->assertArrayNotHasKey($applicationid, $stats);
+ $this->assertArrayHasKey($sessionid, $stats); // Session cache sets a key on construct.
+ $this->assertArrayNotHasKey($requestid, $stats);
+
+ // Check that stores register misses.
+ $this->assertFalse($application->get('missMe'));
+ $this->assertFalse($application->get('missMe'));
+ $this->assertFalse($session->get('missMe'));
+ $this->assertFalse($session->get('missMe'));
+ $this->assertFalse($session->get('missMe'));
+ $this->assertFalse($request->get('missMe'));
+ $this->assertFalse($request->get('missMe'));
+ $this->assertFalse($request->get('missMe'));
+ $this->assertFalse($request->get('missMe'));
+
+ $endstats = cache_helper::get_stats();
+ $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['misses']);
+ $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['hits']);
+ $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['sets']);
+ $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['misses']);
+ $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['hits']);
+ $this->assertEquals(1, $endstats[$sessionid]['stores']['cachestore_session']['sets']);
+ $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['misses']);
+ $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['hits']);
+ $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets']);
+
+ $startstats = cache_helper::get_stats();
+
+ // Check that stores register sets.
+ $this->assertTrue($application->set('setMe1', 1));
+ $this->assertTrue($application->set('setMe2', 2));
+ $this->assertTrue($session->set('setMe1', 1));
+ $this->assertTrue($session->set('setMe2', 2));
+ $this->assertTrue($session->set('setMe3', 3));
+ $this->assertTrue($request->set('setMe1', 1));
+ $this->assertTrue($request->set('setMe2', 2));
+ $this->assertTrue($request->set('setMe3', 3));
+ $this->assertTrue($request->set('setMe4', 4));
+
+ $endstats = cache_helper::get_stats();
+ $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['misses'] -
+ $startstats[$applicationid]['stores']['cachestore_file']['misses']);
+ $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['hits'] -
+ $startstats[$applicationid]['stores']['cachestore_file']['hits']);
+ $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['sets'] -
+ $startstats[$applicationid]['stores']['cachestore_file']['sets']);
+ $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['misses'] -
+ $startstats[$sessionid]['stores']['cachestore_session']['misses']);
+ $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['hits'] -
+ $startstats[$sessionid]['stores']['cachestore_session']['hits']);
+ $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['sets'] -
+ $startstats[$sessionid]['stores']['cachestore_session']['sets']);
+ $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['misses'] -
+ $startstats[$requestid]['stores']['cachestore_static']['misses']);
+ $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['hits'] -
+ $startstats[$requestid]['stores']['cachestore_static']['hits']);
+ $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
+ $startstats[$requestid]['stores']['cachestore_static']['sets']);
+
+ $startstats = cache_helper::get_stats();
+
+ // Check that stores register hits.
+ $this->assertEquals($application->get('setMe1'), 1);
+ $this->assertEquals($application->get('setMe2'), 2);
+ $this->assertEquals($session->get('setMe1'), 1);
+ $this->assertEquals($session->get('setMe2'), 2);
+ $this->assertEquals($session->get('setMe3'), 3);
+ $this->assertEquals($request->get('setMe1'), 1);
+ $this->assertEquals($request->get('setMe2'), 2);
+ $this->assertEquals($request->get('setMe3'), 3);
+ $this->assertEquals($request->get('setMe4'), 4);
+
+ $endstats = cache_helper::get_stats();
+ $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['misses'] -
+ $startstats[$applicationid]['stores']['cachestore_file']['misses']);
+ $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['hits'] -
+ $startstats[$applicationid]['stores']['cachestore_file']['hits']);
+ $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['sets'] -
+ $startstats[$applicationid]['stores']['cachestore_file']['sets']);
+ $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['misses'] -
+ $startstats[$sessionid]['stores']['cachestore_session']['misses']);
+ $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['hits'] -
+ $startstats[$sessionid]['stores']['cachestore_session']['hits']);
+ $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['sets'] -
+ $startstats[$sessionid]['stores']['cachestore_session']['sets']);
+ $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['misses'] -
+ $startstats[$requestid]['stores']['cachestore_static']['misses']);
+ $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['hits'] -
+ $startstats[$requestid]['stores']['cachestore_static']['hits']);
+ $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
+ $startstats[$requestid]['stores']['cachestore_static']['sets']);
+
+ $startstats = cache_helper::get_stats();
+
+ // Check that stores register through get_many.
+ $application->get_many(array('setMe1', 'setMe2'));
+ $session->get_many(array('setMe1', 'setMe2', 'setMe3'));
+ $request->get_many(array('setMe1', 'setMe2', 'setMe3', 'setMe4'));
+
+ $endstats = cache_helper::get_stats();
+ $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['misses'] -
+ $startstats[$applicationid]['stores']['cachestore_file']['misses']);
+ $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['hits'] -
+ $startstats[$applicationid]['stores']['cachestore_file']['hits']);
+ $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['sets'] -
+ $startstats[$applicationid]['stores']['cachestore_file']['sets']);
+ $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['misses'] -
+ $startstats[$sessionid]['stores']['cachestore_session']['misses']);
+ $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['hits'] -
+ $startstats[$sessionid]['stores']['cachestore_session']['hits']);
+ $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['sets'] -
+ $startstats[$sessionid]['stores']['cachestore_session']['sets']);
+ $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['misses'] -
+ $startstats[$requestid]['stores']['cachestore_static']['misses']);
+ $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['hits'] -
+ $startstats[$requestid]['stores']['cachestore_static']['hits']);
+ $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
+ $startstats[$requestid]['stores']['cachestore_static']['sets']);
+ }
}
if (empty($subscriptions)) {
$cell = new html_table_cell(get_string('nocalendarsubscriptions', 'calendar'));
- $cell->colspan = 4;
+ $cell->colspan = 5;
$table->data[] = new html_table_row(array($cell));
}
$strnever = new lang_string('never', 'calendar');
require_once('../config.php');
require_once($CFG->dirroot.'/calendar/lib.php');
-require_sesskey();
-
$var = required_param('var', PARAM_ALPHA);
$return = clean_param(base64_decode(required_param('return', PARAM_RAW)), PARAM_LOCALURL);
$courseid = optional_param('id', -1, PARAM_INT);
} else {
$return = new moodle_url($return);
}
+
+if (!confirm_sesskey()) {
+ // Do not call require_sesskey() since this page may be accessed without session (for example by bots).
+ redirect($return);
+}
+
$url = new moodle_url('/calendar/set.php', array('return'=>base64_encode($return->out_as_local_url(false)), 'course' => $courseid, 'var'=>$var, 'sesskey'=>sesskey()));
$PAGE->set_url($url);
$PAGE->set_context(context_system::instance());
require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
-use Behat\Behat\Context\Step\Given as Given,
+use Behat\Behat\Context\Step\Given,
+ Behat\Behat\Context\Step\Then,
Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
/**
return array(
new Given('I go to the current course activity completion report'),
- new Given('I hover "' . $this->escape($xpath) . '" "xpath_element"')
+ new Then('"' . $this->escape($xpath) . '" "xpath_element" should exist')
);
}
"/descendant::img[contains(@title, $titleliteral)]";
return array(
new Given('I go to the current course activity completion report'),
- new Given('I hover "' . $this->escape($xpath) . '" "xpath_element"')
+ new Then('"' . $this->escape($xpath) . '" "xpath_element" should exist')
);
return $steps;
{
"require-dev": {
- "phpunit/phpunit": "3.7.*",
- "phpunit/dbUnit": "1.2.*",
+ "phpunit/phpunit": "4.7.*",
+ "phpunit/dbUnit": "1.4.*",
"moodlehq/behat-extension": "1.30.0"
}
}
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
- "hash": "3ddf5ab21f539f6f64c7e80174be48bb",
+ "hash": "a85d8c9e61ccba5e235093157021f7b5",
"packages": [],
"packages-dev": [
{
],
"time": "2014-12-20 21:24:13"
},
+ {
+ "name": "doctrine/instantiator",
+ "version": "1.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
+ "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3,<8.0-DEV"
+ },
+ "require-dev": {
+ "athletic/athletic": "~0.1.8",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpunit/phpunit": "~4.0",
+ "squizlabs/php_codesniffer": "~2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "http://ocramius.github.com/"
+ }
+ ],
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://github.com/doctrine/instantiator",
+ "keywords": [
+ "constructor",
+ "instantiate"
+ ],
+ "time": "2015-06-14 21:17:01"
+ },
{
"name": "doctrine/lexer",
"version": "v1.0.1",
],
"time": "2015-05-15 02:00:06"
},
+ {
+ "name": "phpdocumentor/reflection-docblock",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
+ "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8",
+ "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.0"
+ },
+ "suggest": {
+ "dflydev/markdown": "~1.0",
+ "erusev/parsedown": "~1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "phpDocumentor": [
+ "src/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "mike.vanriel@naenius.com"
+ }
+ ],
+ "time": "2015-02-03 12:10:50"
+ },
+ {
+ "name": "phpspec/prophecy",
+ "version": "v1.4.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpspec/prophecy.git",
+ "reference": "3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373",
+ "reference": "3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.0.2",
+ "phpdocumentor/reflection-docblock": "~2.0",
+ "sebastian/comparator": "~1.1"
+ },
+ "require-dev": {
+ "phpspec/phpspec": "~2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "Prophecy\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com",
+ "homepage": "http://everzet.com"
+ },
+ {
+ "name": "Marcello Duarte",
+ "email": "marcello.duarte@gmail.com"
+ }
+ ],
+ "description": "Highly opinionated mocking framework for PHP 5.3+",
+ "homepage": "https://github.com/phpspec/prophecy",
+ "keywords": [
+ "Double",
+ "Dummy",
+ "fake",
+ "mock",
+ "spy",
+ "stub"
+ ],
+ "time": "2015-04-27 22:15:08"
+ },
{
"name": "phpunit/dbunit",
- "version": "1.2.3",
+ "version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/dbunit.git",
- "reference": "8386782a2d55153e44a06eb1a9d13d6ed35d9c2d"
+ "reference": "1afe25c90834ec499f007f48dd73767fdec3bf4f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/8386782a2d55153e44a06eb1a9d13d6ed35d9c2d",
- "reference": "8386782a2d55153e44a06eb1a9d13d6ed35d9c2d",
+ "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/1afe25c90834ec499f007f48dd73767fdec3bf4f",
+ "reference": "1afe25c90834ec499f007f48dd73767fdec3bf4f",
"shasum": ""
},
"require": {
"ext-pdo": "*",
"ext-simplexml": "*",
"php": ">=5.3.3",
- "phpunit/phpunit": ">=3.7.0@stable"
+ "phpunit/phpunit": "~4.0",
+ "symfony/yaml": "~2.1"
},
"bin": [
- "dbunit.php"
+ "composer/bin/dbunit"
],
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.2.x-dev"
+ "dev-master": "1.3.x-dev"
}
},
"autoload": {
"testing",
"xunit"
],
- "time": "2013-03-01 11:50:46"
+ "time": "2015-05-21 21:11:02"
},
{
"name": "phpunit/php-code-coverage",
- "version": "1.2.18",
+ "version": "2.2.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "fe2466802556d3fe4e4d1d58ffd3ccfd0a19be0b"
+ "reference": "2d7c03c0e4e080901b8f33b2897b0577be18a13c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/fe2466802556d3fe4e4d1d58ffd3ccfd0a19be0b",
- "reference": "fe2466802556d3fe4e4d1d58ffd3ccfd0a19be0b",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2d7c03c0e4e080901b8f33b2897b0577be18a13c",
+ "reference": "2d7c03c0e4e080901b8f33b2897b0577be18a13c",
"shasum": ""
},
"require": {
"php": ">=5.3.3",
- "phpunit/php-file-iterator": ">=1.3.0@stable",
- "phpunit/php-text-template": ">=1.2.0@stable",
- "phpunit/php-token-stream": ">=1.1.3,<1.3.0"
+ "phpunit/php-file-iterator": "~1.3",
+ "phpunit/php-text-template": "~1.2",
+ "phpunit/php-token-stream": "~1.3",
+ "sebastian/environment": "^1.3.2",
+ "sebastian/version": "~1.0"
},
"require-dev": {
- "phpunit/phpunit": "3.7.*@dev"
+ "ext-xdebug": ">=2.1.4",
+ "phpunit/phpunit": "~4"
},
"suggest": {
"ext-dom": "*",
- "ext-xdebug": ">=2.0.5"
+ "ext-xdebug": ">=2.2.1",
+ "ext-xmlwriter": "*"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.2.x-dev"
+ "dev-master": "2.2.x-dev"
}
},
"autoload": {
"classmap": [
- "PHP/"
+ "src/"
]
},
"notification-url": "https://packagist.org/downloads/",
- "include-path": [
- ""
- ],
"license": [
"BSD-3-Clause"
],
"testing",
"xunit"
],
- "time": "2014-09-02 10:13:14"
+ "time": "2015-08-04 03:42:39"
},
{
"name": "phpunit/php-file-iterator",
- "version": "1.4.0",
+ "version": "1.4.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
- "reference": "a923bb15680d0089e2316f7a4af8f437046e96bb"
+ "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a923bb15680d0089e2316f7a4af8f437046e96bb",
- "reference": "a923bb15680d0089e2316f7a4af8f437046e96bb",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0",
+ "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0",
"shasum": ""
},
"require": {
"filesystem",
"iterator"
],
- "time": "2015-04-02 05:19:05"
+ "time": "2015-06-21 13:08:43"
},
{
"name": "phpunit/php-text-template",
},
{
"name": "phpunit/php-timer",
- "version": "1.0.6",
+ "version": "1.0.7",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-timer.git",
- "reference": "83fe1bdc5d47658b727595c14da140da92b3d66d"
+ "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/83fe1bdc5d47658b727595c14da140da92b3d66d",
- "reference": "83fe1bdc5d47658b727595c14da140da92b3d66d",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b",
+ "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b",
"shasum": ""
},
"require": {
"keywords": [
"timer"
],
- "time": "2015-06-13 07:35:30"
+ "time": "2015-06-21 08:01:12"
},
{
"name": "phpunit/php-token-stream",
- "version": "1.2.2",
+ "version": "1.4.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-token-stream.git",
- "reference": "ad4e1e23ae01b483c16f600ff1bebec184588e32"
+ "reference": "7a9b0969488c3c54fd62b4d504b3ec758fd005d9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/ad4e1e23ae01b483c16f600ff1bebec184588e32",
- "reference": "ad4e1e23ae01b483c16f600ff1bebec184588e32",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/7a9b0969488c3c54fd62b4d504b3ec758fd005d9",
+ "reference": "7a9b0969488c3c54fd62b4d504b3ec758fd005d9",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
"php": ">=5.3.3"
},
+ "require-dev": {
+ "phpunit/phpunit": "~4.2"
+ },
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.2-dev"
+ "dev-master": "1.4-dev"
}
},
"autoload": {
"classmap": [
- "PHP/"
+ "src/"
]
},
"notification-url": "https://packagist.org/downloads/",
- "include-path": [
- ""
- ],
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
- "role": "lead"
+ "email": "sebastian@phpunit.de"
}
],
"description": "Wrapper around PHP's tokenizer extension.",
"keywords": [
"tokenizer"
],
- "time": "2014-03-03 05:10:30"
+ "time": "2015-06-19 03:43:16"
},
{
"name": "phpunit/phpunit",
- "version": "3.7.38",
+ "version": "4.7.7",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "38709dc22d519a3d1be46849868aa2ddf822bcf6"
+ "reference": "9b97f9d807b862c2de2a36e86690000801c85724"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/38709dc22d519a3d1be46849868aa2ddf822bcf6",
- "reference": "38709dc22d519a3d1be46849868aa2ddf822bcf6",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9b97f9d807b862c2de2a36e86690000801c85724",
+ "reference": "9b97f9d807b862c2de2a36e86690000801c85724",
"shasum": ""
},
"require": {
- "ext-ctype": "*",
"ext-dom": "*",
"ext-json": "*",
"ext-pcre": "*",
"ext-reflection": "*",
"ext-spl": "*",
"php": ">=5.3.3",
- "phpunit/php-code-coverage": "~1.2",
- "phpunit/php-file-iterator": "~1.3",
- "phpunit/php-text-template": "~1.1",
- "phpunit/php-timer": "~1.0",
- "phpunit/phpunit-mock-objects": "~1.2",
- "symfony/yaml": "~2.0"
- },
- "require-dev": {
- "pear-pear.php.net/pear": "1.9.4"
+ "phpspec/prophecy": "~1.3,>=1.3.1",
+ "phpunit/php-code-coverage": "~2.1",
+ "phpunit/php-file-iterator": "~1.4",
+ "phpunit/php-text-template": "~1.2",
+ "phpunit/php-timer": ">=1.0.6",
+ "phpunit/phpunit-mock-objects": "~2.3",
+ "sebastian/comparator": "~1.1",
+ "sebastian/diff": "~1.2",
+ "sebastian/environment": "~1.2",
+ "sebastian/exporter": "~1.2",
+ "sebastian/global-state": "~1.0",
+ "sebastian/version": "~1.0",
+ "symfony/yaml": "~2.1|~3.0"
},
"suggest": {
"phpunit/php-invoker": "~1.1"
},
"bin": [
- "composer/bin/phpunit"
+ "phpunit"
],
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.7.x-dev"
+ "dev-master": "4.7.x-dev"
}
},
"autoload": {
"classmap": [
- "PHPUnit/"
+ "src/"
]
},
"notification-url": "https://packagist.org/downloads/",
- "include-path": [
- "",
- "../../symfony/yaml/"
- ],
"license": [
"BSD-3-Clause"
],
}
],
"description": "The PHP Unit Testing framework.",
- "homepage": "http://www.phpunit.de/",
+ "homepage": "https://phpunit.de/",
"keywords": [
"phpunit",
"testing",
"xunit"
],
- "time": "2014-10-17 09:04:17"
+ "time": "2015-07-13 11:28:34"
},
{
"name": "phpunit/phpunit-mock-objects",
- "version": "1.2.3",
+ "version": "2.3.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
- "reference": "5794e3c5c5ba0fb037b11d8151add2a07fa82875"
+ "reference": "18dfbcb81d05e2296c0bcddd4db96cade75e6f42"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/5794e3c5c5ba0fb037b11d8151add2a07fa82875",
- "reference": "5794e3c5c5ba0fb037b11d8151add2a07fa82875",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/18dfbcb81d05e2296c0bcddd4db96cade75e6f42",
+ "reference": "18dfbcb81d05e2296c0bcddd4db96cade75e6f42",
"shasum": ""
},
"require": {
+ "doctrine/instantiator": "~1.0,>=1.0.2",
"php": ">=5.3.3",
- "phpunit/php-text-template": ">=1.1.1@stable"
+ "phpunit/php-text-template": "~1.2",
+ "sebastian/exporter": "~1.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.4"
},
"suggest": {
"ext-soap": "*"
},
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3.x-dev"
+ }
+ },
"autoload": {
"classmap": [
- "PHPUnit/"
+ "src/"
]
},
"notification-url": "https://packagist.org/downloads/",
- "include-path": [
- ""
- ],
"license": [
"BSD-3-Clause"
],
"mock",
"xunit"
],
- "time": "2013-01-13 10:24:48"
+ "time": "2015-07-10 06:54:24"
},
{
"name": "psr/log",
],
"time": "2012-12-21 11:40:51"
},
+ {
+ "name": "sebastian/comparator",
+ "version": "1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "937efb279bd37a375bcadf584dec0726f84dbf22"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22",
+ "reference": "937efb279bd37a375bcadf584dec0726f84dbf22",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "sebastian/diff": "~1.2",
+ "sebastian/exporter": "~1.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.2.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "http://www.github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "time": "2015-07-26 15:48:44"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/863df9687835c62aa423a22412d26fa2ebde3fd3",
+ "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "http://www.github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff"
+ ],
+ "time": "2015-02-22 15:13:53"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "1.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "6324c907ce7a52478eeeaede764f48733ef5ae44"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6324c907ce7a52478eeeaede764f48733ef5ae44",
+ "reference": "6324c907ce7a52478eeeaede764f48733ef5ae44",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "http://www.github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "time": "2015-08-03 06:14:51"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "1.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "7ae5513327cb536431847bcc0c10edba2701064e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/7ae5513327cb536431847bcc0c10edba2701064e",
+ "reference": "7ae5513327cb536431847bcc0c10edba2701064e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "sebastian/recursion-context": "~1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.2.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "http://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "time": "2015-06-21 07:55:53"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/c7428acdb62ece0a45e6306f1ae85e1c05b09c01",
+ "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.2"
+ },
+ "suggest": {
+ "ext-uopz": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "http://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "time": "2014-10-06 09:23:50"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "994d4a811bafe801fb06dccbee797863ba2792ba"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/994d4a811bafe801fb06dccbee797863ba2792ba",
+ "reference": "994d4a811bafe801fb06dccbee797863ba2792ba",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
+ "time": "2015-06-21 08:04:50"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "1.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
+ "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
+ "shasum": ""
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "time": "2015-06-21 13:59:46"
+ },
{
"name": "symfony/icu",
"version": "v1.2.2",
},
{
"name": "twig/twig",
- "version": "v1.18.2",
+ "version": "v1.19.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
- "reference": "e8e6575abf6102af53ec283f7f14b89e304fa602"
+ "reference": "edbeaf43b0a606cdaadc32a11d2673614a377b90"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/twigphp/Twig/zipball/e8e6575abf6102af53ec283f7f14b89e304fa602",
- "reference": "e8e6575abf6102af53ec283f7f14b89e304fa602",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/edbeaf43b0a606cdaadc32a11d2673614a377b90",
+ "reference": "edbeaf43b0a606cdaadc32a11d2673614a377b90",
"shasum": ""
},
"require": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.18-dev"
+ "dev-master": "1.19-dev"
}
},
"autoload": {
"keywords": [
"templating"
],
- "time": "2015-06-06 23:31:24"
+ "time": "2015-07-31 13:45:26"
}
],
"aliases": [],
* @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');
+ return component_callback('mod_' . $modname, 'get_types', array(), MOD_SUBTYPE_NO_CHILDREN) !== MOD_SUBTYPE_NO_CHILDREN;
}
/**
// Use updated grade_item.
$grade_item = $items[$itemid];
}
- $gradecategory = $grade_item->get_parent_category();
- if (!empty($moduleinfo->add)) {
- if (grade_category::aggregation_uses_aggregationcoef($gradecategory->aggregation)) {
- if ($gradecategory->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) {
- $grade_item->aggregationcoef = 1;
- } else {
- $grade_item->aggregationcoef = 0;
- }
- $grade_item->update();
- }
- }
}
}
} else if (isset($moduleinfo->gradecat)) {
$outcome_item->set_parent($moduleinfo->gradecat);
}
- $gradecategory = $outcome_item->get_parent_category();
- if ($outcomeexists == false) {
- if (grade_category::aggregation_uses_aggregationcoef($gradecategory->aggregation)) {
- if ($gradecategory->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) {
- $outcome_item->aggregationcoef = 1;
- } else {
- $outcome_item->aggregationcoef = 0;
- }
- $outcome_item->update();
- }
- }
}
}
}
| Sample course | C1 | 0 |
| Course 1 | COURSE1 | CAT1 |
- @javascript
Scenario: A user can return to the category page from enrolment page
When I log in as "user2"
And I click on "Courses" "link" in the "Navigation" "block"
And I press "Continue"
Then I should see "Edit profile" in the ".breadcrumb-nav" "css_element"
- @javascript
Scenario: User can return to the choice activity from enrolment page
Given the following "roles" exist:
| name | shortname | description | archetype |
Then I should see "Test glossary name"
And I should see "Test chat name"
- @javascript
Scenario: Activities can not be added when the admin restricts the permissions
Given I log in as "admin"
And I set the following system permissions of "Teacher" role:
$extrafields = get_extra_user_fields($this->get_context());
$extrafields[] = 'lastaccess';
$ufields = user_picture::fields('u', $extrafields);
- $sql = "SELECT DISTINCT $ufields, ul.timeaccess AS lastseen
+ $sql = "SELECT DISTINCT $ufields, COALESCE(ul.timeaccess, 0) AS lastcourseaccess
FROM {user} u
JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid $instancessql)
JOIN {enrol} e ON (e.id = ue.enrolid)
LEFT JOIN {user_lastaccess} ul ON (ul.courseid = e.courseid AND ul.userid = u.id)
LEFT JOIN {groups_members} gm ON u.id = gm.userid
WHERE $filtersql
- ORDER BY u.$sort $direction";
+ ORDER BY $sort $direction";
$this->users[$key] = $DB->get_records_sql($sql, $params, $page*$perpage, $perpage);
}
return $this->users[$key];
list($ctxcondition, $params) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'ctx');
$params['courseid'] = $this->course->id;
$params['cid'] = $this->course->id;
- $sql = "SELECT ra.id as raid, ra.contextid, ra.component, ctx.contextlevel, ra.roleid, u.*, ue.lastseen
+ $extrafields = get_extra_user_fields($this->get_context());
+ $ufields = user_picture::fields('u', $extrafields);
+ $sql = "SELECT ra.id as raid, ra.contextid, ra.component, ctx.contextlevel, ra.roleid, $ufields,
+ coalesce(u.lastaccess,0) AS lastaccess
FROM {role_assignments} ra
JOIN {user} u ON u.id = ra.userid
JOIN {context} ctx ON ra.contextid = ctx.id
LEFT JOIN (
- SELECT ue.id, ue.userid, ul.timeaccess AS lastseen
+ SELECT ue.id, ue.userid
FROM {user_enrolments} ue
- LEFT JOIN {enrol} e ON e.id=ue.enrolid
- LEFT JOIN {user_lastaccess} ul ON (ul.courseid = e.courseid AND ul.userid = ue.userid)
+ JOIN {enrol} e ON e.id = ue.enrolid
WHERE e.courseid = :courseid
) ue ON ue.userid=u.id
WHERE ctx.id $ctxcondition AND
ue.id IS NULL
- ORDER BY u.$sort $direction, ctx.depth DESC";
+ ORDER BY $sort $direction, ctx.depth DESC";
$this->otherusers[$key] = $DB->get_records_sql($sql, $params, $page*$perpage, $perpage);
}
return $this->otherusers[$key];
* @param array $extrafields The list of fields as returned from get_extra_user_fields used to determine which
* additional fields may be displayed
* @param int $now The time used for lastaccess calculation
- * @return array The fields to be displayed including userid, courseid, picture, firstname, lastseen and any
+ * @return array The fields to be displayed including userid, courseid, picture, firstname, lastcourseaccess, lastaccess and any
* additional fields from $extrafields
*/
private function prepare_user_for_display($user, $extrafields, $now) {
'courseid' => $this->get_course()->id,
'picture' => new user_picture($user),
'firstname' => fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context())),
- 'lastseen' => get_string('never'),
+ 'lastaccess' => get_string('never'),
'lastcourseaccess' => get_string('never'),
);
foreach ($extrafields as $field) {
}
// Last time user has accessed the site.
- if ($user->lastaccess) {
- $details['lastseen'] = format_time($now - $user->lastaccess);
+ if (!empty($user->lastaccess)) {
+ $details['lastaccess'] = format_time($now - $user->lastaccess);
}
// Last time user has accessed the course.
- if ($user->lastseen) {
- $details['lastcourseaccess'] = format_time($now - $user->lastseen);
+ if (!empty($user->lastcourseaccess)) {
+ $details['lastcourseaccess'] = format_time($now - $user->lastcourseaccess);
}
return $details;
}
*/
protected static function sync_with_parent_course(stdClass $instance, $userid) {
global $DB, $CFG;
+ require_once($CFG->dirroot . '/group/lib.php');
$plugin = enrol_get_plugin('meta');
$fields = array(
'userdetails' => $userdetails,
- 'lastseen' => get_string('lastaccess'),
+ 'lastaccess' => get_string('lastaccess'),
'role' => get_string('roles', 'role')
);
if (!has_capability('moodle/course:viewhiddenuserfields', $context)) {
$hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
if (isset($hiddenfields['lastaccess'])) {
- unset($fields['lastseen']);
+ unset($fields['lastaccess']);
}
}
* @var array
*/
protected static $sortablefields = array('firstname', 'lastname', 'firstnamephonetic', 'lastnamephonetic', 'middlename',
- 'alternatename', 'idnumber', 'email', 'phone1', 'phone2', 'institution', 'department' );
+ 'alternatename', 'idnumber', 'email', 'phone1', 'phone2', 'institution', 'department', 'lastaccess', 'lastcourseaccess' );
/**
* Constructs the table
if (!in_array($n, self::$sortablefields)) {
$bits[] = $l;
} else {
- $link = html_writer::link(new moodle_url($url, array(self::SORTVAR=>$n)), $fields[$name][$n]);
+ $sorturl = new moodle_url($url, array(self::SORTVAR => $n, self::SORTDIRECTIONVAR => $this->get_field_sort_direction($n)));
+ $link = html_writer::link($sorturl, $fields[$name][$n]);
if ($this->sort == $n) {
- $link .= html_writer::link(new moodle_url($url, array(self::SORTVAR=>$n, self::SORTDIRECTIONVAR=>$this->get_field_sort_direction($n))), $this->get_direction_icon($output, $n));
+ $link .= $this->get_direction_icon($output, $n);
}
$bits[] = html_writer::tag('span', $link, array('class'=>'subheading_'.$n));
if (!in_array($name, self::$sortablefields)) {
$newlabel = $label;
} else {
- $newlabel = html_writer::link(new moodle_url($url, array(self::SORTVAR=>$name)), $fields[$name]);
+ $sorturl = new moodle_url($url, array(self::SORTVAR => $name, self::SORTDIRECTIONVAR => $this->get_field_sort_direction($name)));
+ $newlabel = html_writer::link($sorturl, $fields[$name]);
if ($this->sort == $name) {
- $newlabel .= html_writer::link(new moodle_url($url, array(self::SORTVAR=>$name, self::SORTDIRECTIONVAR=>$this->get_field_sort_direction($name))), $this->get_direction_icon($output, $name));
+ $newlabel .= $this->get_direction_icon($output, $name);
}
}
}
*/
class course_enrolment_users_table extends course_enrolment_table {
- /**
- * An array of sortable fields
- * @static
- * @var array
- */
- protected static $sortablefields = array('firstname', 'lastname', 'firstnamephonetic', 'lastnamephonetic', 'middlename',
- 'alternatename', 'email', 'lastaccess');
}
/**
redirect($returnurl);
} else if ($data = $mform->get_data(false)) {
- // If unset, give the aggregationcoef a default based on parent aggregation method
+
+ // This is a new item, and the category chosen is different than the default category.
+ if (empty($grade_item->id) && isset($data->parentcategory) && $parent_category->id != $data->parentcategory) {
+ $parent_category = grade_category::fetch(array('id' => $data->parentcategory));
+ }
+
+ // If unset, give the aggregation values a default based on parent aggregation method.
+ $defaults = grade_category::get_default_aggregation_coefficient_values($parent_category->aggregation);
if (!isset($data->aggregationcoef) || $data->aggregationcoef == '') {
- if ($parent_category->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) {
- $data->aggregationcoef = 1;
- } else {
- $data->aggregationcoef = 0;
- }
+ $data->aggregationcoef = $defaults['aggregationcoef'];
+ }
+ if (!isset($data->weightoverride)) {
+ $data->weightoverride = $defaults['weightoverride'];
}
if (!isset($data->gradepass) || $data->gradepass == '') {
}
if (isset($data->aggregationcoef2) && $parent_category->aggregation == GRADE_AGGREGATE_SUM) {
$data->aggregationcoef2 = $data->aggregationcoef2 / 100.0;
+ } else {
+ $data->aggregationcoef2 = $defaults['aggregationcoef2'];
}
$grade_item = new grade_item(array('id'=>$id, 'courseid'=>$courseid));
// set parent if needed
if (isset($data->parentcategory)) {
- $grade_item->set_parent($data->parentcategory, 'gradebook');
+ $grade_item->set_parent($data->parentcategory, false);
}
} else {
if ($data = $mform->get_data()) {
- if (!isset($data->aggregationcoef)) {
- $data->aggregationcoef = 0;
+ // This is a new item, and the category chosen is different than the default category.
+ if (empty($grade_item->id) && isset($data->parentcategory) && $parent_category->id != $data->parentcategory) {
+ $parent_category = grade_category::fetch(array('id' => $data->parentcategory));
+ }
+
+ // If unset, give the aggregation values a default based on parent aggregation method.
+ $defaults = grade_category::get_default_aggregation_coefficient_values($parent_category->aggregation);
+ if (!isset($data->aggregationcoef) || $data->aggregationcoef == '') {
+ $data->aggregationcoef = $defaults['aggregationcoef'];
+ }
+ if (!isset($data->weightoverride)) {
+ $data->weightoverride = $defaults['weightoverride'];
}
if (property_exists($data, 'calculation')) {
}
if (isset($data->aggregationcoef2) && $parent_category->aggregation == GRADE_AGGREGATE_SUM) {
$data->aggregationcoef2 = $data->aggregationcoef2 / 100.0;
+ } else {
+ $data->aggregationcoef2 = $defaults['aggregationcoef2'];
}
$grade_item = new grade_item(array('id'=>$id, 'courseid'=>$courseid));
} else {
// set parent if needed
if (isset($data->parentcategory)) {
- $grade_item->set_parent($data->parentcategory, 'gradebook');
+ $grade_item->set_parent($data->parentcategory, false);
}
}
} else {
// The grade item for this is not updated.
+ $newfeedback->importonlyfeedback = true;
$insertid = self::insert_grade_record($newfeedback, $this->studentid);
// Check to see if the insert was successful.
if (empty($insertid)) {
$testarray[$key]->feedback = $record->feedback;
$testarray[$key]->importcode = $testobject->get_importcode();
$testarray[$key]->importer = $USER->id;
+ $testarray[$key]->importonlyfeedback = 0;
// Check that the record was inserted into the database.
$this->assertEquals($gradeimportvalues, $testarray);
* (grade_import_value and grade_import_newitem)
* If this function is called, we assume that all data collected
* up to this point is fine and we can go ahead and commit
- * @param int courseid - id of the course
- * @param string importcode - import batch identifier
- * @param feedback print feedback and continue button
+ * @param int $courseid - ID of the course.
+ * @param int $importcode - Import batch identifier.
+ * @param bool $importfeedback - Whether to import feedback as well.
+ * @param bool $verbose - Print feedback and continue button.
* @return bool success
*/
function grade_import_commit($courseid, $importcode, $importfeedback=true, $verbose=true) {
if (!$importfeedback) {
$grade->feedback = false; // ignore it
}
+ if ($grade->importonlyfeedback) {
+ // False means do not change. See grade_itme::update_final_grade().
+ $grade->finalgrade = false;
+ }
if (!$gradeitem->update_final_grade($grade->userid, $grade->finalgrade, 'import', $grade->feedback)) {
$errordata = new stdClass();
$errordata->itemname = $gradeitem->itemname;
* @return array List of warnings
*/
public function process_data($data) {
- if (has_capability('moodle/grade:manage', $this->context)) {
+ if (has_capability('moodle/grade:edit', $this->context)) {
return $this->screen->process($data);
}
}
And I set the field "Grade display type" to "Real (percentage)"
And I press "Save changes"
- @javascript
Scenario: Mean of grades aggregation
And I set the following settings for grade item "Course 1":
| Aggregation | Mean of grades |
And I follow "Grades" in the user menu
And I should see "30.42 (30.42 %)" in the "overview-grade" "table"
- @javascript
Scenario: Weighted mean of grades aggregation
And I set the following settings for grade item "Course 1":
| Aggregation | Weighted mean of grades |
And I follow "Grades" in the user menu
And I should see "26.94 (26.94 %)" in the "overview-grade" "table"
- @javascript
Scenario: Simple weighted mean of grades aggregation
And I set the following settings for grade item "Course 1":
| Aggregation | Simple weighted mean of grades |
And I follow "Grades" in the user menu
And I should see "48.57 (48.57 %)" in the "overview-grade" "table"
- @javascript
Scenario: Mean of grades (with extra credits) aggregation
And I set the following settings for grade item "Course 1":
| Aggregation | Mean of grades (with extra credits) |
And I follow "Grades" in the user menu
And I should see "47.22 (47.22 %)" in the "overview-grade" "table"
- @javascript
Scenario: Median of grades aggregation
And I set the following settings for grade item "Course 1":
| Aggregation | Median of grades |
And I follow "Grades" in the user menu
And I should see "25.83 (25.83 %)" in the "overview-grade" "table"
- @javascript
Scenario: Lowest grade aggregation
And I set the following settings for grade item "Course 1":
| Aggregation | Lowest grade |
And I follow "Grades" in the user menu
And I should see "0.00 (0.00 %)" in the "overview-grade" "table"
- @javascript
Scenario: Highest grade aggregation
And I set the following settings for grade item "Course 1":
| Aggregation | Highest grade |
And I follow "Grades" in the user menu
And I should see "50.00 (50.00 %)" in the "overview-grade" "table"
- @javascript
Scenario: Mode of grades aggregation
And I set the following settings for grade item "Course 1":
| Aggregation | Mode of grades |
And I follow "Grades" in the user menu
And I should see "50.00 (50.00 %)" in the "overview-grade" "table"
- @javascript
Scenario: Natural aggregation on outcome items with natural weights
And the following config values are set as admin:
| enableoutcomes | 1 |
And I follow "Course 1"
And "Test outcome item one" row "Grade" column of "user-grade" table should contain "Excellent (100.00 %)"
- @javascript
Scenario: Natural aggregation on outcome items with modified weights
And the following config values are set as admin:
| enableoutcomes | 1 |
And I follow "Course 1"
And "Test outcome item one" row "Grade" column of "user-grade" table should contain "Excellent (100.00 %)"
- @javascript
Scenario: Natural aggregation
And I set the following settings for grade item "Sub category 1":
| Aggregation | Natural |
And I set the field "Show contribution to course total" to "Show"
And I set the field "Show weightings" to "Show"
And I press "Save changes"
- And I set the field "Grade report" to "User report"
- And I set the field "Select all or one user" to "Student 1"
+ And I select "User report" from the "Grade report" singleselect
+ And I select "Student 1" from the "Select all or one user" singleselect
And the following should exist in the "user-grade" table:
| Grade item | Calculated weight | Grade | Range | Contribution to course total |
| Test assignment five | 28.57 % | 10.00 (50.00 %) | 0–20 | 1.03 % |
| Test assignment three | 30.93 %( Extra credit ) | 40.00 (26.67 %) | 0–150 | 8.25 % |
| Test assignment four | 30.93 % | - | 0–150 | 0.00 % |
- @javascript
Scenario: Natural aggregation with drop lowest
When I log out
And I log in as "admin"
| Exclude empty grades | 0 |
And I navigate to "Categories and items" node in "Grade administration > Setup"
And I press "Add category"
- And I click on "Show more" "link"
And I set the following fields to these values:
| Category name | Sub category 3 |
| Aggregation | Natural |
@javascript
Scenario: Natural aggregation from the setup screen
- And I set the field "Grade report" to "Categories and items"
+ And I select "Categories and items" from the "Grade report" singleselect
And I set the following settings for grade item "Course 1":
| Aggregation | Natural |
And I set the following settings for grade item "Sub category 1":
| Aggregation | Natural |
| Exclude empty grades | 0 |
And I turn editing mode off
- And I set the field "Grade report" to "Categories and items"
+ And I select "Categories and items" from the "Grade report" singleselect
And I set the field "Override weight of Test assignment one" to "1"
And I set the field "Weight of Test assignment one" to "0"
And I set the field "Override weight of Test assignment six" to "1"
And I set the field "Show weightings" to "Show"
And I press "Save changes"
Then I should see "75.00 (16.85 %)" in the ".course" "css_element"
- And I set the field "Grade report" to "User report"
- And I set the field "Select all or one user" to "Student 1"
+ And I select "User report" from the "Grade report" singleselect
+ And I select "Student 1" from the "Select all or one user" singleselect
And the following should exist in the "user-grade" table:
| Grade item | Calculated weight | Grade | Contribution to course total |
| Test assignment five | 57.14 % | 10.00 (50.00 %) | 2.25 % |
--- /dev/null
+@core @core_grades
+Feature: Changing the aggregation of an item affects its weight and extra credit definition
+ In order to switch to another aggregation method
+ As an teacher
+ I need to be able to edit the grade category settings
+
+ Background:
+ Given the following "courses" exist:
+ | fullname | shortname |
+ | Course 1 | C1 |
+ And the following "grade categories" exist:
+ | fullname | course | aggregation |
+ | Cat mean | C1 | 0 |
+ | Cat median | C1 | 2 |
+ | Cat min | C1 | 4 |
+ | Cat max | C1 | 6 |
+ | Cat mode | C1 | 8 |
+ | Cat weighted | C1 | 10 |
+ | Cat weighted2 | C1 | 10 |
+ | Cat simple | C1 | 11 |
+ | Cat ec | C1 | 12 |
+ | Cat natural | C1 | 13 |
+ And the following "grade items" exist:
+ | itemname | course | category | aggregationcoef | aggregationcoef2 | weightoverride |
+ | Item a1 | C1 | ? | 0 | 0 | 0 |
+ | Item a2 | C1 | ? | 0 | 0.40 | 1 |
+ | Item a3 | C1 | ? | 1 | 0.10 | 1 |
+ | Item a4 | C1 | ? | 1 | 0 | 0 |
+ | Item b1 | C1 | Cat natural | 0 | 0 | 0 |
+ | Item b2 | C1 | Cat natural | 0 | 0.40 | 1 |
+ | Item b3 | C1 | Cat natural | 1 | 0.10 | 1 |
+ | Item b4 | C1 | Cat natural | 1 | 0 | 0 |
+ And I log in as "admin"
+ And I set the following administration settings values:
+ | grade_aggregations_visible | Mean of grades,Weighted mean of grades,Simple weighted mean of grades,Mean of grades (with extra credits),Median of grades,Lowest grade,Highest grade,Mode of grades,Natural |
+ And I am on site homepage
+ And I follow "Course 1"
+ And I navigate to "Grades" node in "Course administration"
+ And I navigate to "Grader report" node in "Grade administration"
+ And I turn editing mode on
+ And I follow "Edit Cat mean"
+ And I set the following fields to these values:
+ | Weight adjusted | 1 |
+ | Weight | 20 |
+ | Extra credit | 0 |
+ And I press "Save changes"
+ And I follow "Edit Cat median"
+ And I set the following fields to these values:
+ | Weight adjusted | 1 |
+ | Weight | 5 |
+ | Extra credit | 0 |
+ And I press "Save changes"
+ And I follow "Edit Cat min"
+ And I set the following fields to these values:
+ | Weight adjusted | 0 |
+ | Weight | 0 |
+ | Extra credit | 1 |
+ And I press "Save changes"
+ And I follow "Edit Item a1"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item a2"
+ And the field "Weight adjusted" matches value "1"
+ And the field "id_aggregationcoef2" matches value "40.0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item a3"
+ And the field "Weight adjusted" matches value "1"
+ And the field "id_aggregationcoef2" matches value "10.0"
+ And the field "Extra credit" matches value "1"
+ And I press "Cancel"
+ And I follow "Edit Item a4"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "1"
+ And I press "Cancel"
+ And I follow "Edit Item b1"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item b2"
+ And the field "Weight adjusted" matches value "1"
+ And the field "id_aggregationcoef2" matches value "40.0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item b3"
+ And the field "Weight adjusted" matches value "1"
+ And the field "id_aggregationcoef2" matches value "10.0"
+ And the field "Extra credit" matches value "1"
+ And I press "Cancel"
+ And I follow "Edit Item b4"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "1"
+ And I press "Cancel"
+
+ Scenario: Switching a category from Natural aggregation to Mean of grades and back
+ Given I follow "Edit Course 1"
+ And I set the field "Aggregation" to "Mean of grades"
+ When I press "Save changes"
+ And I follow "Edit Item a1"
+ Then I should not see "Weight adjusted"
+ And I should not see "Weight"
+ And I should not see "Extra credit"
+ And I press "Cancel"
+ And I follow "Edit Item a2"
+ And I should not see "Weight adjusted"
+ And I should not see "Weight"
+ And I should not see "Extra credit"
+ And I press "Cancel"
+ And I follow "Edit Item a3"
+ And I should not see "Weight adjusted"
+ And I should not see "Weight"
+ And I should not see "Extra credit"
+ And I press "Cancel"
+ And I follow "Edit Item a4"
+ And I should not see "Weight adjusted"
+ And I should not see "Weight"
+ And I should not see "Extra credit"
+ And I press "Cancel"
+ And I follow "Edit Cat mean"
+ And I expand all fieldsets
+ And I should not see "Weight adjusted"
+ And I should not see "Weight" in the "#id_headerparent" "css_element"
+ And I should not see "Extra credit"
+ And I press "Cancel"
+ And I follow "Edit Cat median"
+ And I expand all fieldsets
+ And I should not see "Weight adjusted"
+ And I should not see "Weight" in the "#id_headerparent" "css_element"
+ And I should not see "Extra credit"
+ And I press "Cancel"
+ And I follow "Edit Cat min"
+ And I expand all fieldsets
+ And I should not see "Weight adjusted"
+ And I should not see "Weight" in the "#id_headerparent" "css_element"
+ And I should not see "Extra credit"
+ And I press "Cancel"
+ And I follow "Edit Cat natural"
+ And I set the field "Aggregation" to "Mean of grades"
+ And I press "Save changes"
+ And I follow "Edit Item b1"
+ And I should not see "Weight adjusted"
+ And I should not see "Weight"
+ And I should not see "Extra credit"
+ And I press "Cancel"
+ And I follow "Edit Item b2"
+ And I should not see "Weight adjusted"
+ And I should not see "Weight"
+ And I should not see "Extra credit"
+ And I press "Cancel"
+ And I follow "Edit Item b3"
+ And I should not see "Weight adjusted"
+ And I should not see "Weight"
+ And I should not see "Extra credit"
+ And I press "Cancel"
+ And I follow "Edit Item b4"
+ And I should not see "Weight adjusted"
+ And I should not see "Weight"
+ And I should not see "Extra credit"
+ And I press "Cancel"
+ # Switching back.
+ And I follow "Edit Course 1"
+ And I set the field "Aggregation" to "Natural"
+ And I press "Save changes"
+ And I follow "Edit Item a1"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item a2"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item a3"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item a4"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Cat mean"
+ And I expand all fieldsets
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Cat median"
+ And I expand all fieldsets
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Cat min"
+ And I expand all fieldsets
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Cat natural"
+ And I set the field "Aggregation" to "Natural"
+ And I press "Save changes"
+ And I follow "Edit Item b1"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item b2"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item b3"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item b4"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+
+ Scenario: Switching a category from Natural aggregation to Weighted mean of grades and back
+ Given I follow "Edit Course 1"
+ And I set the field "Aggregation" to "Weighted mean of grades"
+ When I press "Save changes"
+ And I follow "Edit Item a1"
+ Then I should not see "Weight adjusted"
+ And I should not see "Extra credit"
+ And the field "Item weight" matches value "1"
+ And I press "Cancel"
+ And I follow "Edit Item a2"
+ And I should not see "Weight adjusted"
+ And I should not see "Extra credit"
+ And the field "Item weight" matches value "1"
+ And I press "Cancel"
+ And I follow "Edit Item a3"
+ And I should not see "Weight adjusted"
+ And I should not see "Extra credit"
+ And the field "Item weight" matches value "1"
+ And I press "Cancel"
+ And I follow "Edit Item a4"
+ And I should not see "Weight adjusted"
+ And I should not see "Extra credit"
+ And the field "Item weight" matches value "1"
+ And I press "Cancel"
+ And I follow "Edit Cat mean"
+ And I expand all fieldsets
+ And I should not see "Weight adjusted"
+ And I should not see "Extra credit"
+ And the field "Item weight" matches value "1"
+ And I press "Cancel"
+ And I follow "Edit Cat median"
+ And I expand all fieldsets
+ And I should not see "Weight adjusted"
+ And I should not see "Extra credit"
+ And the field "Item weight" matches value "1"
+ And I press "Cancel"
+ And I follow "Edit Cat min"
+ And I expand all fieldsets
+ And I should not see "Weight adjusted"
+ And I should not see "Extra credit"
+ And the field "Item weight" matches value "1"
+ And I press "Cancel"
+ And I follow "Edit Cat natural"
+ And I set the field "Aggregation" to "Weighted mean of grades"
+ And I press "Save changes"
+ And I follow "Edit Item b1"
+ And I should not see "Weight adjusted"
+ And I should not see "Extra credit"
+ And the field "Item weight" matches value "1"
+ And I press "Cancel"
+ And I follow "Edit Item b2"
+ And I should not see "Weight adjusted"
+ And I should not see "Extra credit"
+ And the field "Item weight" matches value "1"
+ And I press "Cancel"
+ And I follow "Edit Item b3"
+ And I should not see "Weight adjusted"
+ And I should not see "Extra credit"
+ And the field "Item weight" matches value "1"
+ And I press "Cancel"
+ And I follow "Edit Item b4"
+ And I should not see "Weight adjusted"
+ And I should not see "Extra credit"
+ And the field "Item weight" matches value "1"
+ And I press "Cancel"
+ # Switching back.
+ And I follow "Edit Course 1"
+ And I set the field "Aggregation" to "Natural"
+ And I press "Save changes"
+ And I follow "Edit Item a1"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item a2"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item a3"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item a4"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Cat mean"
+ And I expand all fieldsets
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Cat median"
+ And I expand all fieldsets
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Cat min"
+ And I expand all fieldsets
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Cat natural"
+ And I set the field "Aggregation" to "Natural"
+ And I press "Save changes"
+ And I follow "Edit Item b1"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item b2"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item b3"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item b4"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+
+ @javascript
+ Scenario: Switching grade items between categories
+ # Move to same aggregation (Natural).
+ Given I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I set the field "Select Item a1" to "1"
+ And I set the field "Select Item a2" to "1"
+ And I set the field "Select Item a3" to "1"
+ And I set the field "Select Item a4" to "1"
+ When I select "Cat natural" from the "Move selected items to" singleselect
+ And I navigate to "Grader report" node in "Grade administration"
+ And I follow "Edit Item a1"
+ Then the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item a2"
+ And the field "Weight adjusted" matches value "1"
+ And the field "id_aggregationcoef2" matches value "40.0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item a3"
+ And the field "Weight adjusted" matches value "1"
+ And the field "id_aggregationcoef2" matches value "10.0"
+ And the field "Extra credit" matches value "1"
+ And I press "Cancel"
+ And I follow "Edit Item a4"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "1"
+ And I press "Cancel"
+ # Move to Mean of grades (with extra credit).
+ And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I set the field "Select Item a1" to "1"
+ And I set the field "Select Item a2" to "1"
+ And I set the field "Select Item a3" to "1"
+ And I set the field "Select Item a4" to "1"
+ And I select "Cat ec" from the "Move selected items to" singleselect
+ And I navigate to "Grader report" node in "Grade administration"
+ And I follow "Edit Item a1"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item a2"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item a3"
+ And the field "Extra credit" matches value "1"
+ And I press "Cancel"
+ And I follow "Edit Item a4"
+ And the field "Extra credit" matches value "1"
+ And I press "Cancel"
+ # Move to Simple weight mean of grades.
+ And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I set the field "Select Item a1" to "1"
+ And I set the field "Select Item a2" to "1"
+ And I set the field "Select Item a3" to "1"
+ And I set the field "Select Item a4" to "1"
+ And I select "Cat simple" from the "Move selected items to" singleselect
+ And I navigate to "Grader report" node in "Grade administration"
+ And I follow "Edit Item a1"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item a2"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item a3"
+ And the field "Extra credit" matches value "1"
+ And I press "Cancel"
+ And I follow "Edit Item a4"
+ And the field "Extra credit" matches value "1"
+ And I press "Cancel"
+ # Move to Weighted mean of grades.
+ And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I set the field "Select Item a1" to "1"
+ And I set the field "Select Item a2" to "1"
+ And I set the field "Select Item a3" to "1"
+ And I set the field "Select Item a4" to "1"
+ And I select "Cat weighted" from the "Move selected items to" singleselect
+ And I navigate to "Grader report" node in "Grade administration"
+ And I follow "Edit Item a1"
+ And the field "Item weight" matches value "1"
+ And I set the field "Item weight" to "2"
+ And I press "Save changes"
+ And I follow "Edit Item a2"
+ And the field "Item weight" matches value "1"
+ And I set the field "Item weight" to "5"
+ And I press "Save changes"
+ And I follow "Edit Item a3"
+ And the field "Item weight" matches value "1"
+ And I set the field "Item weight" to "8"
+ And I press "Save changes"
+ And I follow "Edit Item a4"
+ And the field "Item weight" matches value "1"
+ And I set the field "Item weight" to "11"
+ And I press "Save changes"
+ # Move to same (Weighted mean of grades).
+ And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I set the field "Select Item a1" to "1"
+ And I set the field "Select Item a2" to "1"
+ And I set the field "Select Item a3" to "1"
+ And I set the field "Select Item a4" to "1"
+ And I select "Cat weighted2" from the "Move selected items to" singleselect
+ And I wait "2" seconds
+ And I navigate to "Grader report" node in "Grade administration"
+ And I follow "Edit Item a1"
+ And the field "Item weight" matches value "2"
+ And I press "Save changes"
+ And I follow "Edit Item a2"
+ And the field "Item weight" matches value "5"
+ And I press "Save changes"
+ And I follow "Edit Item a3"
+ And the field "Item weight" matches value "8"
+ And I press "Save changes"
+ And I follow "Edit Item a4"
+ And the field "Item weight" matches value "11"
+ And I press "Save changes"
+ # Move back to Natural.
+ And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I set the field "Select Item a1" to "1"
+ And I set the field "Select Item a2" to "1"
+ And I set the field "Select Item a3" to "1"
+ And I set the field "Select Item a4" to "1"
+ And I select "Course 1" from the "Move selected items to" singleselect
+ And I navigate to "Grader report" node in "Grade administration"
+ And I follow "Edit Item a1"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item a2"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item a3"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
+ And I follow "Edit Item a4"
+ And the field "Weight adjusted" matches value "0"
+ And the field "Extra credit" matches value "0"
+ And I press "Cancel"
And I follow "Grader report"
And I turn editing mode on
- @javascript
Scenario: Test displaying scales in gradebook in aggregation method Natural
When I turn editing mode off
Then the following should exist in the "user-grades" table:
| Range | F–A | 0.00–5.00 | 0.00–5.00 |
| Overall average | C | 3.00 | 3.00 |
And I follow "User report"
- And I set the field "Select all or one user" to "Student 3"
- And I click on "Select all or one user" "select"
+ And I select "Student 3" from the "Select all or one user" singleselect
And the following should exist in the "user-grade" table:
| Grade item | Grade | Range | Percentage | Contribution to course total |
| Test assignment one | C | F–A | 50.00 % | 60.00 % |
| Sub category 1 total | 3.00 | 0–5 | 60.00 % | - |
| Course total | 3.00 | 0–5 | 60.00 % | - |
- And I set the field "jump" to "Categories and items"
+ And I select "Categories and items" from the "Grade report" singleselect
And the following should exist in the "grade_edit_tree_table" table:
| Name | Max grade |
| Test assignment one | 5.00 |
| Sub category 1 total | 4.00 | 0–5 | 80.00 % | - |
| Course total | 4.00 | 0–5 | 80.00 % | - |
- @javascript
Scenario Outline: Test displaying scales in gradebook in all other aggregation methods
When I follow "Edit Course 1"
And I set the field "Aggregation" to "<aggregation>"
| Range | F–A | 1.00–5.00 | 0.00–100.00 |
| Overall average | C | 3.00 | <overallavg> |
And I follow "User report"
- And I set the field "Select all or one user" to "Student 3"
- And I click on "Select all or one user" "select"
+ And I select "Student 3" from the "Select all or one user" singleselect
And the following should exist in the "user-grade" table:
| Grade item | Grade | Range | Percentage | Contribution to course total |
| Test assignment one | C | F–A | 50.00 % | <contrib3> |
| Sub category (<aggregation>) total<aggregation>. | 3.00 | 1–5 | 50.00 % | - |
| Course total<aggregation>. | <coursetotal3> | 0–100 | <courseperc3> | - |
- And I set the field "jump" to "Categories and items"
+ And I select "Categories and items" from the "Grade report" singleselect
And the following should exist in the "grade_edit_tree_table" table:
| Name | Max grade |
| Test assignment one | A (5) |
Examples:
| aggregation | coursetotal1 | coursetotal2 | coursetotal3 | coursetotal4 | coursetotal5 |overallavg | courseperc2 | courseperc3 | contrib2 | contrib3 |
| Mean of grades | 100.00 | 75.00 | 50.00 | 25.00 | 0.00 | 50.00 | 75.00 % | 50.00 % | 75.00 % | 50.00 % |
- | Weighted mean of grades | - | - | - | - | - | - | - | - | 0.00 % | 0.00 % |
+ | Weighted mean of grades | 100.00 | 75.00 | 50.00 | 25.00 | 0.00 | 50.00 | 75.00 % | 50.00 % | 75.00 % | 50.00 % |
| Simple weighted mean of grades | 100.00 | 75.00 | 50.00 | 25.00 | 0.00 | 50.00 | 75.00 % | 50.00 % | 75.00 % | 50.00 % |
| Mean of grades (with extra credits) | 100.00 | 75.00 | 50.00 | 25.00 | 0.00 | 50.00 | 75.00 % | 50.00 % | 75.00 % | 50.00 % |
| Median of grades | 100.00 | 75.00 | 50.00 | 25.00 | 0.00 | 50.00 | 75.00 % | 50.00 % | 75.00 % | 50.00 % |
Examples:
| aggregation | contrib1 | cattotal1 | coursetotal1 | catavg | overallavg |
| Mean of grades | 100.00 % | 100.00 | 100.00 | 100.00 | 100.00 |
- | Weighted mean of grades | 0.00 % | 100.00 | - | 100.00 | - |
+ | Weighted mean of grades | 0.00 % | 100.00 | 100.00 | 100.00 | 100.00 |
| Simple weighted mean of grades | 0.00 % | - | - | - | - |
| Mean of grades (with extra credits) | 100.00 % | 100.00 | 100.00 | 100.00 | 100.00 |
| Median of grades | 100.00 % | 100.00 | 100.00 | 100.00 | 100.00 |
And I give the grade "90.00" to the user "Student 1" for the grade item "Test assignment name 2"
And I press "Save changes"
- @javascript
Scenario: Grade a grade item and ensure the results display correctly in the gradebook
- When I set the field "Grade report" to "User report"
+ When I select "User report" from the "Grade report" singleselect
And the "Grade report" select box should contain "Grader report"
And the "Grade report" select box should contain "Outcomes report"
And the "Grade report" select box should contain "User report"
And "Course 1" row "Grade" column of "overview-grade" table should contain "170.00"
And "Course 1" row "Grade" column of "overview-grade" table should not contain "90.00"
- @javascript
Scenario: We can add a weighting to a grade item and it is displayed properly in the user report
- When I set the field "Grade report" to "Categories and items"
+ When I select "Categories and items" from the "Grade report" singleselect
And I set the following settings for grade item "Course 1":
| Aggregation | Weighted mean of grades |
And I set the field "Extra credit value for Test assignment name" to "0.72"
And I press "Save changes"
- And I set the field "Grade report" to "User report"
+ And I select "User report" from the "Grade report" singleselect
And I navigate to "Course grade settings" node in "Grade administration > Setup"
And I set the following fields to these values:
| Show weightings | Show |
--- /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/>.
+
+/**
+ * Unit tests for grade/import/lib.php.
+ *
+ * @package core_grade
+ * @category phpunit
+ * @copyright 2015 Adrian Greeve <adrian@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/grade/import/lib.php');
+
+/**
+ * Tests grade_import_lib functions.
+ */
+class core_grade_import_lib_test extends advanced_testcase {
+
+ /**
+ * Import grades into 'grade_import_values' table. This is done differently in the various import plugins,
+ * so there is no direct API to call.
+ *
+ * @param array $data Information to be inserted into the table.
+ * @return int The insert ID of the sql statement.
+ */
+ private function import_grades($data) {
+ global $DB, $USER;
+ $graderecord = new stdClass();
+ $graderecord->importcode = $data['importcode'];
+ if (isset($data['itemid'])) {
+ $graderecord->itemid = $data['itemid'];
+ }
+ $graderecord->userid = $data['userid'];
+ if (isset($data['importer'])) {
+ $graderecord->importer = $data['importer'];
+ } else {
+ $graderecord->importer = $USER->id;
+ }
+ if (isset($data['finalgrade'])) {
+ $graderecord->finalgrade = $data['finalgrade'];
+ } else {
+ $graderecord->finalgrade = rand(0, 100);
+ }
+ if (isset($data['feedback'])) {
+ $graderecord->feedback = $data['feedback'];
+ }
+ if (isset($data['importonlyfeedback'])) {
+ $graderecord->importonlyfeedback = $data['importonlyfeedback'];
+ } else {
+ $graderecord->importonlyfeedback = false;
+ }
+ if (isset($data['newgradeitem'])) {
+ $graderecord->newgradeitem = $data['newgradeitem'];
+ }
+ return $DB->insert_record('grade_import_values', $graderecord);
+ }
+
+ /**
+ * Tests for importing grades from an external source.
+ */
+ public function test_grade_import_commit() {
+ global $USER, $DB, $CFG;
+ $this->resetAfterTest();
+
+ $importcode = get_new_importcode();
+ $user1 = $this->getDataGenerator()->create_user();
+ $user2 = $this->getDataGenerator()->create_user();
+
+ $course = $this->getDataGenerator()->create_course();
+ $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id));
+ $itemname = $assign->name;
+ $modulecontext = context_module::instance($assign->cmid);
+ // The generator returns a dummy object, lets get the real assign object.
+ $assign = new assign($modulecontext, false, false);
+ $cm = $assign->get_course_module();
+
+ // Enrol users in the course.
+ $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+ $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+
+ // Enter a new grade into an existing grade item.
+ $gradeitem = grade_item::fetch(array('courseid' => $course->id, 'itemtype' => 'mod'));
+
+ // Keep this value around for a test further down.
+ $originalgrade = 55;
+ $this->import_grades(array(
+ 'importcode' => $importcode,
+ 'itemid' => $gradeitem->id,
+ 'userid' => $user1->id,
+ 'finalgrade' => $originalgrade
+ ));
+
+ $status = grade_import_commit($course->id, $importcode, false, false);
+ $this->assertTrue($status);
+
+ // Get imported grade_grade.
+