}
public function save_changes() {
- global $DB;
+ global $DB, $CFG;
if (!$this->roleid) {
// Creating role.
// the UI. It would be better to do this only when we know that fields affected are
// updated. But thats getting into the weeds of the coursecat cache and role edits
// should not be that frequent, so here is the ugly brutal approach.
+ require_once($CFG->libdir . '/coursecatlib.php');
coursecat::role_assignment_changed($this->role->id, context_system::instance());
}
$event = \core\event\role_capabilities_updated::create(
array(
'context' => $systemcontext,
- 'objectid' => $roleid
+ 'objectid' => $tableroleid
)
);
$event->set_legacy_logdata(array(SITEID, 'role', $action, 'admin/roles/define.php?action=view&roleid=' . $tableroleid,
if (has_capability('tool/monitor:subscribe', context_system::instance())) {
$options[0] = get_string('site');
}
- if ($courses = get_user_capability_course('tool/monitor:subscribe', null, true, 'fullname', $orderby)) {
+ if ($courses = get_user_capability_course('tool/monitor:subscribe', null, true, 'fullname, visible', $orderby)) {
foreach ($courses as $course) {
- $options[$course->id] = format_string($course->fullname, true,
- array('context' => context_course::instance($course->id)));
+ $coursectx = context_course::instance($course->id);
+ if ($course->visible || has_capability('moodle/course:viewhiddencourses', $coursectx)) {
+ $options[$course->id] = format_string($course->fullname, true, array('context' => $coursectx));
+ }
}
}
// If there are no courses and there is no site permission then return false.
$updateuser = new stdClass();
$updateuser->id = $user->id;
$updateuser->suspended = 1;
+ $updateuser = $this->clean_data($updateuser);
user_update_user($updateuser, false);
$trace->output(get_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)), 1);
}
$updateuser = new stdClass();
$updateuser->id = $olduser->id;
$updateuser->suspended = 0;
+ $updateuser = $this->clean_data($updateuser);
user_update_user($updateuser);
$trace->output(get_string('auth_dbreviveduser', 'auth_db', array('name' => $username,
'id' => $olduser->id)), 1);
$trace->output(get_string('auth_dbinsertuserduplicate', 'auth_db', array('username'=>$user->username, 'auth'=>$collision->auth)), 1);
continue;
}
+ $user = $this->clean_data($user);
try {
$id = user_create_user($user, false); // It is truly a new user.
$trace->output(get_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)), 1);
}
if ($needsupdate) {
require_once($CFG->dirroot . '/user/lib.php');
+ $updateuser = $this->clean_data($updateuser);
user_update_user($updateuser);
}
return $DB->get_record('user', array('id'=>$userid, 'deleted'=>0));
error_reporting($CFG->debug);
ob_end_flush();
}
+
+ /**
+ * Clean the user data that comes from an external database.
+ *
+ * @param array $user the user data to be validated against properties definition.
+ * @return stdClass $user the cleaned user data.
+ */
+ public function clean_data($user) {
+ if (empty($user)) {
+ return $user;
+ }
+
+ foreach ($user as $field => $value) {
+ // Get the property parameter type and do the cleaning.
+ try {
+ $property = core_user::get_property_definition($field);
+ $user->$field = clean_param($value, $property['type']);
+ } catch (coding_exception $e) {
+ debugging("The property '$field' could not be cleaned.", DEBUG_DEVELOPER);
+ }
+ }
+
+ return $user;
+ }
}
$this->assertEquals("select * from table WHERE column=? AND anothercolumn > ?", $sqlout);
$this->assertEquals(array(1, 'b'), $arrout);
}
+
+ /**
+ * Testing the clean_data() method.
+ */
+ public function test_clean_data() {
+ global $DB;
+
+ $this->resetAfterTest(false);
+ $this->preventResetByRollback();
+ $this->init_auth_database();
+ $auth = get_auth_plugin('db');
+ $auth->db_init();
+
+ // Create users on external table.
+ $extdbuser1 = (object)array('name'=>'u1', 'pass'=>'heslo', 'email'=>'u1@example.com');
+ $extdbuser1->id = $DB->insert_record('auth_db_users', $extdbuser1);
+
+ // User with malicious data on the name.
+ $extdbuser2 = (object)array('name'=>'user<script>alert(1);</script>xss', 'pass'=>'heslo', 'email'=>'xssuser@example.com');
+ $extdbuser2->id = $DB->insert_record('auth_db_users', $extdbuser2);
+
+ $trace = new null_progress_trace();
+
+ // Let's test user sync make sure still works as expected..
+ $auth->sync_users($trace, true);
+
+ // Get the user on moodle user table.
+ $user2 = $DB->get_record('user', array('email'=> $extdbuser2->email, 'auth'=>'db'));
+
+ // The malicious code should be sanitized.
+ $this->assertEquals($user2->username, 'userscriptalert1scriptxss');
+ $this->assertNotEquals($user2->username, $extdbuser2->name);
+
+ // User with correct data, should be equal to external db.
+ $user1 = $DB->get_record('user', array('email'=> $extdbuser1->email, 'auth'=>'db'));
+ $this->assertEquals($extdbuser1->name, $user1->username);
+ $this->assertEquals($extdbuser1->email, $user1->email);
+
+ // Now, let's update the name.
+ $extdbuser2->name = 'user no xss anymore';
+ $DB->update_record('auth_db_users', $extdbuser2);
+
+ // Run sync again to update the user data.
+ $auth->sync_users($trace, true);
+
+ // The user information should be updated.
+ $user2 = $DB->get_record('user', array('username' => 'usernoxssanymore', 'auth' => 'db'));
+ // The spaces should be removed, as it's the username.
+ $this->assertEquals($user2->username, 'usernoxssanymore');
+
+ // Now let's test just the clean_data() method isolated.
+ // Testing PARAM_USERNAME, PARAM_NOTAGS, PARAM_RAW_TRIMMED and others.
+ $user3 = new stdClass();
+ $user3->firstname = 'John <script>alert(1)</script> Doe';
+ $user3->username = 'john%#&~%*_doe';
+ $user3->email = ' john@testing.com ';
+ $user3->deleted = 'no';
+ $user3->description = '<b>A description <script>alert(123)</script>about myself.</b>';
+ $user3cleaned = $auth->clean_data($user3);
+
+ // Expected results.
+ $this->assertEquals($user3cleaned->firstname, 'John alert(1) Doe');
+ $this->assertEquals($user3cleaned->email, 'john@testing.com');
+ $this->assertEquals($user3cleaned->deleted, 0);
+ $this->assertEquals($user3->description, '<b>A description about myself.</b>');
+ $this->assertEquals($user3->username, 'john_doe');
+
+ // Try to clean an invalid property (fullname).
+ $user3->fullname = 'John Doe';
+ $auth->clean_data($user3);
+ $this->assertDebuggingCalled("The property 'fullname' could not be cleaned.");
+ $this->cleanup_auth_database();
+ }
}
'completion', 'completiongradeitemnumber', 'completionview', 'completionexpected',
'availability', 'showdescription'));
+ $tags = new backup_nested_element('tags');
+ $tag = new backup_nested_element('tag', array('id'), array('name', 'rawname'));
+
// attach format plugin structure to $module element, only one allowed
$this->add_plugin_structure('format', $module, false);
// attach local plugin structure to $module, multiple allowed
$this->add_plugin_structure('local', $module, true);
+ $module->add_child($tags);
+ $tags->add_child($tag);
+
// Set the sources
$concat = $DB->sql_concat("'mod_'", 'm.name');
$module->set_source_sql("
JOIN {course_sections} s ON s.id = cm.section
WHERE cm.id = ?", array(backup::VAR_MODID));
+ $tag->set_source_sql("SELECT t.id, t.name, t.rawname
+ FROM {tag} t
+ JOIN {tag_instance} ti ON ti.tagid = t.id
+ WHERE ti.itemtype = 'course_modules'
+ AND ti.component = 'core'
+ AND ti.itemid = ?", array(backup::VAR_MODID));
+
// Define annotations
$module->annotate_ids('grouping', 'groupingid');
$paths[] = new restore_path_element('availability_field', '/module/availability_info/availability_field');
}
+ $paths[] = new restore_path_element('tag', '/module/tags/tag');
+
// Apply for 'format' plugins optional paths at module level
$this->add_plugin_structure('format', $module);
}
}
+ /**
+ * Fetch all the existing because tag_set() deletes them
+ * so everything must be reinserted on each call.
+ *
+ * @param stdClass $data Record data
+ */
+ protected function process_tag($data) {
+ global $CFG;
+
+ $data = (object)$data;
+
+ if (core_tag_tag::is_enabled('core', 'course_modules')) {
+ $modcontext = context::instance_by_id($this->task->get_contextid());
+ $instanceid = $this->task->get_moduleid();
+
+ core_tag_tag::add_item_tag('core', 'course_modules', $instanceid, $modcontext, $data->rawname);
+ }
+ }
+
/**
* Process the legacy availability table record. This table does not exist
* in Moodle 2.7+ but we still support restore.
array('id' => $availfield->coursemoduleid));
}
}
+ /**
+ * This method will be executed after the rest of the restore has been processed.
+ *
+ * Update old tag instance itemid(s).
+ */
+ protected function after_restore() {
+ global $DB;
+
+ $contextid = $this->task->get_contextid();
+ $instanceid = $this->task->get_activityid();
+ $olditemid = $this->task->get_old_activityid();
+
+ $DB->set_field('tag_instance', 'itemid', $instanceid, array('contextid' => $contextid, 'itemid' => $olditemid));
+ }
}
/**
require_once($CFG->dirroot . "/webservice/xmlrpc/lib.php");
$xmlrpcclient = new webservice_xmlrpc_client($serverurl, $token);
try {
- $result = $xmlrpcclient->call($function, $params);
+ $result = $xmlrpcclient->call($function, array_values($params));
$courses = $result['courses'];
$coursetotal = $result['coursetotal'];
} catch (Exception $e) {
$params['options']['timeend'] = PHP_INT_MAX;
}
+ // Event list does not check visibility and permissions, we'll check that later.
$eventlist = calendar_get_events($params['options']['timestart'], $params['options']['timeend'], $funcparam['users'], $funcparam['groups'],
$funcparam['courses'], true, $params['options']['ignorehidden']);
+
// WS expects arrays.
$events = array();
- foreach ($eventlist as $id => $event) {
- $events[$id] = (array) $event;
- }
// We need to get events asked for eventids.
- $eventsbyid = calendar_get_events_by_id($params['events']['eventids']);
- foreach ($eventsbyid as $eventid => $eventobj) {
+ if ($eventsbyid = calendar_get_events_by_id($params['events']['eventids'])) {
+ $eventlist += $eventsbyid;
+ }
+
+ foreach ($eventlist as $eventid => $eventobj) {
$event = (array) $eventobj;
- if (isset($events[$eventid])) {
- continue;
- }
+
if ($hassystemcap) {
// User can see everything, no further check is needed.
$events[$eventid] = $event;
$events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
$this->assertEquals(1, count($events['events']));
$this->assertEquals(0, count($events['warnings']));
+
+ // Now, create an activity event.
+ $this->setAdminUser();
+ $nexttime = time() + DAYSECS;
+ $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id, 'duedate' => $nexttime));
+
+ $this->setUser($user);
+ $paramevents = array ('courseids' => array($course->id));
+ $options = array ('siteevents' => true, 'userevents' => true, 'timeend' => time() + WEEKSECS);
+ $events = core_calendar_external::get_calendar_events($paramevents, $options);
+ $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
+
+ $this->assertCount(5, $events['events']);
+
+ // Hide the assignment.
+ set_coursemodule_visible($assign->cmid, 0);
+ // Empty all the caches that may be affected by this change.
+ accesslib_clear_all_caches_for_unit_testing();
+ course_modinfo::clear_instance_cache();
+
+ $events = core_calendar_external::get_calendar_events($paramevents, $options);
+ $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
+ // Expect one less.
+ $this->assertCount(4, $events['events']);
}
/**
if (checkdate($mon, $day, $year)) {
$time = make_timestamp($year, $mon, $day);
} else {
- $time = time();
+ $time = usergetmidnight(time());
}
} else if (empty($time)) {
- $time = time();
+ $time = usergetmidnight(time());
}
$url->param('time', $time);
(search, modulelist (only admins), blocklist (only admins), tagid)'),
'criteriavalue' => new external_value(PARAM_RAW, 'criteria value'),
'page' => new external_value(PARAM_INT, 'page number (0 based)', VALUE_DEFAULT, 0),
- 'perpage' => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0)
+ 'perpage' => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0),
+ 'requiredcapabilities' => new external_multiple_structure(
+ new external_value(PARAM_CAPABILITY, 'Capability string used to filter courses by permission'),
+ VALUE_OPTIONAL
+ )
)
);
}
* @param string $criteriavalue Criteria value
* @param int $page Page number (for pagination)
* @param int $perpage Items per page
+ * @param array $requiredcapabilities Optional list of required capabilities (used to filter the list).
* @return array of course objects and warnings
* @since Moodle 3.0
* @throws moodle_exception
*/
- public static function search_courses($criterianame, $criteriavalue, $page=0, $perpage=0) {
+ public static function search_courses($criterianame,
+ $criteriavalue,
+ $page=0,
+ $perpage=0,
+ $requiredcapabilities=array()) {
global $CFG;
require_once($CFG->libdir . '/coursecatlib.php');
'criterianame' => $criterianame,
'criteriavalue' => $criteriavalue,
'page' => $page,
- 'perpage' => $perpage
+ 'perpage' => $perpage,
+ 'requiredcapabilities' => $requiredcapabilities
);
$params = self::validate_parameters(self::search_courses_parameters(), $parameters);
}
// Search the courses.
- $courses = coursecat::search_courses($searchcriteria, $options);
- $totalcount = coursecat::search_courses_count($searchcriteria);
+ $courses = coursecat::search_courses($searchcriteria, $options, $params['requiredcapabilities']);
+ $totalcount = coursecat::search_courses_count($searchcriteria, $options, $params['requiredcapabilities']);
$finalcourses = array();
$categoriescache = array();
list($summary, $summaryformat) =
external_format_text($course->summary, $course->summaryformat, $coursecontext->id, 'course', 'summary', null);
+ $displayname = get_course_display_name_for_list($course);
$coursereturns = array();
$coursereturns['id'] = $course->id;
- $coursereturns['fullname'] = $course->get_formatted_fullname();
- $coursereturns['shortname'] = $course->get_formatted_shortname();
+ $coursereturns['fullname'] = external_format_string($course->fullname, $coursecontext->id);
+ $coursereturns['displayname'] = external_format_string($displayname, $coursecontext->id);
+ $coursereturns['shortname'] = external_format_string($course->shortname, $coursecontext->id);
$coursereturns['categoryid'] = $course->category;
$coursereturns['categoryname'] = $category->name;
$coursereturns['summary'] = $summary;
array(
'id' => new external_value(PARAM_INT, 'course id'),
'fullname' => new external_value(PARAM_TEXT, 'course full name'),
+ 'displayname' => new external_value(PARAM_TEXT, 'course display name'),
'shortname' => new external_value(PARAM_TEXT, 'course short name'),
'categoryid' => new external_value(PARAM_INT, 'category id'),
'categoryname' => new external_value(PARAM_TEXT, 'category name'),
// Delete all tag instances associated with the instance of this module.
core_tag_tag::delete_instances('mod_' . $modulename, null, $modcontext->id);
+ core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
// Delete the context.
context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
$data->completionexpected = $cm->completionexpected;
$data->completionusegrade = is_null($cm->completiongradeitemnumber) ? 0 : 1;
$data->showdescription = $cm->showdescription;
+ $data->tags = core_tag_tag::get_item_tags_array('core', 'course_modules', $cm->id);
if (!empty($CFG->enableavailability)) {
$data->availabilityconditionsjson = $cm->availability;
}
$DB->set_field($moduleinfo->modulename, 'intro', $moduleinfo->intro, array('id'=>$moduleinfo->instance));
}
+ // Add module tags.
+ if (core_tag_tag::is_enabled('core', 'course_modules') && isset($moduleinfo->tags)) {
+ core_tag_tag::set_item_tags('core', 'course_modules', $moduleinfo->coursemodule, $modcontext, $moduleinfo->tags);
+ }
+
// Course_modules and course_sections each contain a reference to each other.
// So we have to update one of them twice.
$sectionid = course_add_cm_to_section($course, $moduleinfo->coursemodule, $moduleinfo->section);
set_coursemodule_idnumber($moduleinfo->coursemodule, $moduleinfo->cmidnumber);
}
+ // Update module tags.
+ if (core_tag_tag::is_enabled('core', 'course_modules') && isset($moduleinfo->tags)) {
+ core_tag_tag::set_item_tags('core', 'course_modules', $moduleinfo->coursemodule, $modcontext, $moduleinfo->tags);
+ }
+
// Now that module is fully updated, also update completion data if required.
// (this will wipe all user completion data and recalculate it)
if ($completion->is_enabled() && !empty($moduleinfo->completionunlocked)) {
$mform->disabledIf('completionexpected', 'completion', 'eq', COMPLETION_TRACKING_NONE);
}
+ // Populate module tags.
+ if (core_tag_tag::is_enabled('core', 'course_modules')) {
+ $mform->addElement('header', 'tagshdr', get_string('tags', 'tag'));
+ $mform->addElement('tags', 'tags', get_string('tags'), array('itemtype' => 'course_modules', 'component' => 'core'));
+ if ($this->_cm) {
+ $tags = core_tag_tag::get_item_tags_array('core', 'course_modules', $this->_cm->id);
+ $mform->setDefault('tags', $tags);
+ }
+ }
+
$this->standard_hidden_coursemodule_elements();
}
case 'assign':
// Add some tags to this assignment.
core_tag_tag::set_item_tags('mod_assign', 'assign', $module->id, $modcontext, array('Tag 1', 'Tag 2', 'Tag 3'));
+ core_tag_tag::set_item_tags('core', 'course_modules', $module->cmid, $modcontext, array('Tag 3', 'Tag 4', 'Tag 5'));
// Confirm the tag instances were added.
- $criteria = array('component' => 'mod_assign', 'contextid' => $modcontext->id);
+ $criteria = array('component' => 'mod_assign', 'itemtype' => 'assign', 'contextid' => $modcontext->id);
+ $this->assertEquals(3, $DB->count_records('tag_instance', $criteria));
+ $criteria = array('component' => 'core', 'itemtype' => 'course_modules', 'contextid' => $modcontext->id);
$this->assertEquals(3, $DB->count_records('tag_instance', $criteria));
// Verify event assignment event has been generated.
// Verify the tag instances were deleted.
$criteria = array('component' => 'mod_assign', 'contextid' => $modcontext->id);
$this->assertEquals(0, $DB->count_records('tag_instance', $criteria));
+
+ $criteria = array('component' => 'core', 'itemtype' => 'course_modules', 'contextid' => $modcontext->id);
+ $this->assertEquals(0, $DB->count_records('tag_instance', $criteria));
break;
case 'quiz':
// Verify category deleted.
* Add new instance of enrol plugin.
* @param object $course
* @param array $fields instance fields
- * @return int id of new instance, null if can not be created
+ * @return int id of last instance, null if can not be created
*/
public function add_instance($course, array $fields = null) {
global $CFG;
require_once("$CFG->dirroot/enrol/meta/locallib.php");
- if (!empty($fields['customint2']) && $fields['customint2'] == ENROL_META_CREATE_GROUP) {
- $context = context_course::instance($course->id);
- require_capability('moodle/course:managegroups', $context);
- $groupid = enrol_meta_create_new_group($course->id, $fields['customint1']);
- $fields['customint2'] = $groupid;
+ // Support creating multiple at once.
+ if (is_array($fields['customint1'])) {
+ $courses = array_unique($fields['customint1']);
+ } else {
+ $courses = array($fields['customint1']);
}
+ foreach ($courses as $courseid) {
+ if (!empty($fields['customint2']) && $fields['customint2'] == ENROL_META_CREATE_GROUP) {
+ $context = context_course::instance($course->id);
+ require_capability('moodle/course:managegroups', $context);
+ $groupid = enrol_meta_create_new_group($course->id, $courseid);
+ $fields['customint2'] = $groupid;
+ }
- $result = parent::add_instance($course, $fields);
+ $fields['customint1'] = $courseid;
+ $result = parent::add_instance($course, $fields);
+ }
enrol_meta_sync($course->id);
}
// TODO: this has to be done via ajax or else it will fail very badly on large sites!
- $courses = array('' => get_string('choosedots'));
+ $courses = array();
$select = ', ' . context_helper::get_preload_record_columns_sql('ctx');
$join = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
public function edit_instance_form($instance, MoodleQuickForm $mform, $coursecontext) {
global $DB;
- $courses = $this->get_course_options($instance, $coursecontext);
$groups = $this->get_group_options($coursecontext);
+ $existing = $DB->get_records('enrol', array('enrol' => 'meta', 'courseid' => $coursecontext->instanceid), '', 'customint1, id');
- $mform->addElement('select', 'customint1', get_string('linkedcourse', 'enrol_meta'), $courses);
+ $excludelist = array($coursecontext->instanceid);
+ foreach ($existing as $existinginstance) {
+ $excludelist[] = $existinginstance->customint1;
+ }
+
+ $options = array(
+ 'requiredcapabilities' => array('enrol/meta:selectaslinked'),
+ 'multiple' => true,
+ 'exclude' => $excludelist
+ );
+ $mform->addElement('course', 'customint1', get_string('linkedcourse', 'enrol_meta'), $options);
$mform->addRule('customint1', get_string('required'), 'required', null, 'client');
if (!empty($instance->id)) {
$mform->freeze('customint1');
$c = false;
if (!empty($data['customint1'])) {
- $c = $DB->get_record('course', array('id' => $data['customint1']));
- }
-
- if (!$c) {
- $errors['customint1'] = get_string('required');
- } else {
- $coursecontext = context_course::instance($c->id);
- $existing = $DB->get_records('enrol', array('enrol' => 'meta', 'courseid' => $thiscourseid), '', 'customint1, id');
- if (!$c->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
- $errors['customint1'] = get_string('error');
- } else if (!has_capability('enrol/meta:selectaslinked', $coursecontext)) {
- $errors['customint1'] = get_string('error');
- } else if ($c->id == SITEID or $c->id == $thiscourseid or isset($existing[$c->id])) {
- $errors['customint1'] = get_string('error');
+ foreach ($data['customint1'] as $courseid) {
+ $c = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
+ $coursecontext = context_course::instance($c->id);
+ $existing = $DB->get_records('enrol', array('enrol' => 'meta', 'courseid' => $thiscourseid), '', 'customint1, id');
+ if (!$c->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
+ $errors['customint1'] = get_string('error');
+ } else if (!has_capability('enrol/meta:selectaslinked', $coursecontext)) {
+ $errors['customint1'] = get_string('error');
+ } else if ($c->id == SITEID or $c->id == $thiscourseid or isset($existing[$c->id])) {
+ $errors['customint1'] = get_string('error');
+ }
}
+ } else {
+ $errors['customint1'] = get_string('required');
}
- $validcourses = array_keys($this->get_course_options($instance, $context));
$validgroups = array_keys($this->get_group_options($context));
$tovalidate = array(
- 'customint1' => $validcourses,
'customint2' => $validgroups
);
$typeerrors = $this->validate_param_types($data, $tovalidate);
-@enrol @enrol_meta
+@enrol @enrol_meta @javascript
Feature: Enrolments are synchronised with meta courses
In order to simplify enrolments in parent courses
As a teacher
| student4 | Student | 4 | student4@asd.com |
And the following "courses" exist:
| fullname | shortname |
- | Course 1 | C1 |
- | Course 2 | C2 |
- | Course 3 | C3 |
+ | Course 1 | C1C1 |
+ | Course 2 | C2C2 |
+ | Course 3 | C3C3 |
And the following "groups" exist:
| name | course | idnumber |
- | Groupcourse 1 | C3 | G1 |
- | Groupcourse 2 | C3 | G2 |
+ | Groupcourse 1 | C3C3 | G1 |
+ | Groupcourse 2 | C3C3 | G2 |
And the following "course enrolments" exist:
| user | course | role |
- | student1 | C1 | student |
- | student2 | C1 | student |
- | student3 | C1 | student |
- | student4 | C1 | student |
- | student1 | C2 | student |
- | student2 | C2 | student |
+ | student1 | C1C1 | student |
+ | student2 | C1C1 | student |
+ | student3 | C1C1 | student |
+ | student4 | C1C1 | student |
+ | student1 | C2C2 | student |
+ | student2 | C2C2 | student |
And I log in as "admin"
And I navigate to "Manage enrol plugins" node in "Site administration > Plugins > Enrolments"
And I click on "Enable" "link" in the "Course meta link" "table_row"
Scenario: Add meta enrolment instance without groups
When I follow "Course 3"
And I add "Course meta link" enrolment method with:
- | Link course | Course 1 |
+ | Link course | C1C1 |
And I navigate to "Enrolled users" node in "Course administration > Users"
Then I should see "Student 1"
And I should see "Student 4"
Scenario: Add meta enrolment instance with groups
When I follow "Course 3"
- And I navigate to "Enrolment methods" node in "Course administration > Users"
- And I select "Course meta link" from the "Add method" singleselect
- And I set the following fields to these values:
- | Link course | Course 1 |
+ And I add "Course meta link" enrolment method with:
+ | Link course | C1C1 |
| Add to group | Groupcourse 1 |
- And I press "Add method"
- And I set the field "Add method" to "Course meta link"
- And I press "Go"
- And I set the following fields to these values:
- | Link course | Course 2 |
+ And I follow "Course 3"
+ And I add "Course meta link" enrolment method with:
+ | Link course | C2C2 |
| Add to group | Groupcourse 2 |
- And I press "Add method"
And I navigate to "Enrolled users" node in "Course administration > Users"
Then I should see "Groupcourse 1" in the "Student 1" "table_row"
And I should see "Groupcourse 1" in the "Student 2" "table_row"
Scenario: Add meta enrolment instance with auto-created groups
When I follow "Course 3"
- And I navigate to "Enrolment methods" node in "Course administration > Users"
- And I set the field "Add method" to "Course meta link"
- And I press "Go"
- And I set the following fields to these values:
- | Link course | Course 1 |
+ And I add "Course meta link" enrolment method with:
+ | Link course | C1C1 |
| Add to group | Create new group |
- And I press "Add method"
And I navigate to "Enrolled users" node in "Course administration > Users"
Then I should see "Course 1 course" in the "Student 1" "table_row"
And I should see "Course 1 course" in the "Student 2" "table_row"
Scenario: Backup and restore of meta enrolment instance
When I follow "Course 3"
- And I navigate to "Enrolment methods" node in "Course administration > Users"
- And I set the field "Add method" to "Course meta link"
- And I press "Go"
- And I set the following fields to these values:
- | Link course | Course 1 |
+ And I add "Course meta link" enrolment method with:
+ | Link course | C1C1 |
| Add to group | Groupcourse 1 |
- And I press "Add method"
- And I select "Course meta link" from the "Add method" singleselect
- And I set the following fields to these values:
- | Link course | Course 2 |
- And I press "Add method"
+ And I follow "Course 3"
+ And I add "Course meta link" enrolment method with:
+ | Link course | C2C2 |
When I backup "Course 3" course using this options:
| Confirmation | Filename | test_backup.mbz |
And I click on "Restore" "link" in the "test_backup.mbz" "table_row"
$grade_item->outcomeid = null;
+ if (!empty($data->grade_item_rescalegrades) && $data->grade_item_rescalegrades == 'yes') {
+ $grade_item->rescale_grades_keep_percentage($grade_item_copy->grademin, $grade_item_copy->grademax, $grade_item->grademin,
+ $grade_item->grademax, 'gradebook');
+ }
+
// update hiding flag
if ($hiddenuntil) {
$grade_item->set_hidden($hiddenuntil, false);
private $aggregation_options = array();
function definition() {
- global $CFG, $COURSE, $DB;
+ global $CFG, $COURSE, $DB, $OUTPUT;
$mform =& $this->_form;
$category = $this->_customdata['current'];
$mform->addHelpButton('grade_item_idnumber', 'idnumbermod');
$mform->setType('grade_item_idnumber', PARAM_RAW);
+ if (!empty($category->id)) {
+ $gradecategory = grade_category::fetch(array('id' => $category->id));
+ $gradeitem = $gradecategory->load_grade_item();
+
+ // If grades exist set a message so the user knows why they can not alter the grade type or scale.
+ // We could never change the grade type for external items, so only need to show this for manual grade items.
+ if ($gradeitem->has_overridden_grades()) {
+ // Set a message so the user knows why the can not alter the grade type or scale.
+ if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
+ $gradesexistmsg = get_string('modgradecategorycantchangegradetyporscalemsg', 'grades');
+ } else {
+ $gradesexistmsg = get_string('modgradecategorycantchangegradetypemsg', 'grades');
+ }
+ $notification = new \core\output\notification($gradesexistmsg, \core\output\notification::NOTIFY_INFO);
+ $notification->set_show_closebutton(false);
+ $mform->addElement('static', 'gradesexistmsg', '', $OUTPUT->render($notification));
+ }
+ }
+
$options = array(GRADE_TYPE_NONE=>get_string('typenone', 'grades'),
GRADE_TYPE_VALUE=>get_string('typevalue', 'grades'),
GRADE_TYPE_SCALE=>get_string('typescale', 'grades'),
$mform->disabledIf('grade_item_scaleid', 'grade_item_gradetype', 'noteq', GRADE_TYPE_SCALE);
$mform->disabledIf('grade_item_scaleid', 'aggregation', 'eq', GRADE_AGGREGATE_SUM);
+ $choices = array();
+ $choices[''] = get_string('choose');
+ $choices['no'] = get_string('no');
+ $choices['yes'] = get_string('yes');
+ $mform->addElement('select', 'grade_item_rescalegrades', get_string('modgradecategoryrescalegrades', 'grades'), $choices);
+ $mform->addHelpButton('grade_item_rescalegrades', 'modgradecategoryrescalegrades', 'grades');
+ $mform->disabledIf('grade_item_rescalegrades', 'grade_item_gradetype', 'noteq', GRADE_TYPE_VALUE);
+
$mform->addElement('text', 'grade_item_grademax', get_string('grademax', 'grades'));
$mform->setType('grade_item_grademax', PARAM_RAW);
$mform->addHelpButton('grade_item_grademax', 'grademax', 'grades');
}
}
}
+
+ $mform->removeElement('grade_item_rescalegrades');
}
$mform->removeElement('grade_item_display');
$mform->removeElement('grade_item_decimals');
$mform->hardFreeze('grade_item_scaleid');
+ // Only show the option to rescale grades on a category if its corresponding grade_item has overridden grade_grades.
+ } else if ($grade_item->has_overridden_grades()) {
+ // Can't change the grade type or the scale if there are grades.
+ $mform->hardFreeze('grade_item_gradetype, grade_item_scaleid');
+
+ // If we are using scles then remove the unnecessary rescale and grade fields.
+ if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
+ $mform->removeElement('grade_item_rescalegrades');
+ $mform->removeElement('grade_item_grademax');
+ if ($mform->elementExists('grade_item_grademin')) {
+ $mform->removeElement('grade_item_grademin');
+ }
+ } else { // Not using scale, so remove it.
+ $mform->removeElement('grade_item_scaleid');
+ $mform->disabledIf('grade_item_grademax', 'grade_item_rescalegrades', 'eq', '');
+ $mform->disabledIf('grade_item_grademin', 'grade_item_rescalegrades', 'eq', '');
+ }
+ } else { // Remove the rescale element if there are no grades.
+ $mform->removeElement('grade_item_rescalegrades');
}
//remove the aggregation coef element if not needed
/// perform extra validation before submission
function validation($data, $files) {
global $COURSE;
+ $gradeitem = false;
+ if ($data['id']) {
+ $gradecategory = grade_category::fetch(array('id' => $data['id']));
+ $gradeitem = $gradecategory->load_grade_item();
+ }
$errors = parent::validation($data, $files);
}
}
+ if ($data['id'] && $gradeitem->has_overridden_grades()) {
+ if ($gradeitem->gradetype == GRADE_TYPE_VALUE) {
+ if (grade_floats_different($data['grade_item_grademin'], $gradeitem->grademin) ||
+ grade_floats_different($data['grade_item_grademax'], $gradeitem->grademax)) {
+ if (empty($data['grade_item_rescalegrades'])) {
+ $errors['grade_item_rescalegrades'] = get_string('mustchooserescaleyesorno', 'grades');
+ }
+ }
+ }
+ }
return $errors;
}
}
$html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']);
}
if ($this->validationerrors) {
- $html .= html_writer::div($renderer->notification($this->validationerrors, 'error'), '', array('role' => 'alert'));
+ $html .= html_writer::div($renderer->notification($this->validationerrors));
}
$html .= $renderer->display_guide($data['criteria'], $data['comments'], $data['options'], $mode, $this->getName());
return $html;
$html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']);
}
if ($this->validationerrors) {
- $html .= html_writer::div($renderer->notification($this->validationerrors, 'error'), '', array('role' => 'alert'));
+ $html .= html_writer::div($renderer->notification($this->validationerrors));
}
$html .= $renderer->display_rubric($data['criteria'], $data['options'], $mode, $this->getName());
return $html;
$value = $this->prepare_data($this->_findValue($submitValues));
return $this->_prepareValue($value, $assoc);
}
-}
\ No newline at end of file
+}
continue;
}
+ // If the user submits Exclude grade elements without the proper.
+ // permissions then we should refuse to update.
+ if ($matches[1] === 'exclude' && !has_capability('moodle/grade:manage', $this->context)){
+ $warnings[] = get_string('nopermissions', 'error', get_string('grade:manage', 'role'));
+ continue;
+ }
+
$msg = $element->set($posted);
// Optional type.
$html .= $this->structure->get_grade_analysis_icon($grade);
}
+ // Singleview users without proper permissions should be presented
+ // disabled checkboxes for the Exclude grade attribute.
+ if ($field == 'exclude' && !has_capability('moodle/grade:manage', $this->context)){
+ $html->disabled = true;
+ }
+
$line[] = $html;
}
return $line;
* @copyright 2014 Moodle Pty Ltd (http://moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class exclude extends grade_attribute_format implements be_checked {
+class exclude extends grade_attribute_format implements be_checked, be_disabled {
/** @var string $name The name of the input */
public $name = 'exclude';
+ /** @var bool $disabled Is the checkbox disabled? */
+ public $disabled = false;
+
/**
* Is it checked?
*
return $this->grade->is_excluded();
}
+ /**
+ * Is it disabled?
+ *
+ * @return bool
+ */
+ public function is_disabled() {
+ return $this->disabled;
+ }
+
/**
* Generate the element used to render the UI
*
return new checkbox_attribute(
$this->get_name(),
$this->get_label(),
- $this->is_checked()
+ $this->is_checked(),
+ $this->is_disabled()
);
}
And the following "users" exist:
| username | firstname | lastname | email | idnumber | alternatename |
| teacher1 | Teacher | 1 | teacher1@example.com | t1 | fred |
+ | teacher2 | No edit | 1 | teacher2@example.com | t2 | nick |
| student1 | Student | 1 | student1@example.com | s1 | james |
| student2 | Student | 2 | student1@example.com | s2 | holly |
| student3 | Student | 3 | student1@example.com | s3 | anna |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
+ | teacher2 | C1 | teacher |
| student1 | C1 | student |
| student2 | C1 | student |
| student3 | C1 | student |
And the following "grade items" exist:
| itemname | course | gradetype |
| Test grade item | C1 | Scale |
+ And the following "permission overrides" exist:
+ | capability | permission | role | contextlevel | reference |
+ | moodle/grade:edit | Allow | teacher | Course | C1 |
+ | gradereport/singleview:view | Allow | teacher | Course | C1 |
And I log in as "teacher1"
And I follow "Course 1"
And I navigate to "Grades" node in "Course administration"
And the following should exist in the "generaltable" table:
| First name (Alternate name) Surname | Grade |
| james (Student) 1 | Very good |
+ And I log out
+ And I log in as "teacher2"
+ And I follow "Course 1"
+ And I navigate to "Grades" node in "Course administration"
+ And I click on "Single view" "option"
+ And I click on "Student 4" "option"
+ And the "Exclude for Test assignment one" "checkbox" should be disabled
+ And the "Override for Test assignment one" "checkbox" should be enabled
Scenario: Single view links work on grade report.
Given I follow "Single view for Test assignment one"
--- /dev/null
+@core_grades
+Feature: Editing a grade item
+ In order to ensure validation is provided to the teacher
+ As a teacher
+ I need to know why I can not add/edit values on the grade category form
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | student1 | Student | 1 | student1@example.com |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ And the following "courses" exist:
+ | fullname | shortname | category | groupmode |
+ | Course 1 | C1 | 0 | 1 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+ And I log in as "admin"
+ And I navigate to "Scales" node in "Site administration > Grades"
+ And I press "Add a new scale"
+ And I set the following fields to these values:
+ | Name | ABCDEF |
+ | Scale | F,E,D,C,B,A |
+ And I press "Save changes"
+ And I press "Add a new scale"
+ And I set the following fields to these values:
+ | Name | Letter scale |
+ | Scale | Disappointing, Good, Very good, Excellent |
+ And I press "Save changes"
+ 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 log out
+ And I log in as "teacher1"
+ And I am on site homepage
+ And I follow "Course 1"
+ And I navigate to "Gradebook setup" node in "Course administration"
+ And I press "Add category"
+ And I set the following fields to these values:
+ | Category name | Cat 1 |
+ | Aggregation | Highest grade |
+ And I press "Save changes"
+ And I press "Add grade item"
+ And I set the following fields to these values:
+ | Item name | Item 1 |
+ | Grade category | Cat 1 |
+ And I press "Save changes"
+ And I press "Add grade item"
+ And I set the following fields to these values:
+ | Item name | Item 2 |
+ | Grade category | Cat 1 |
+ And I press "Save changes"
+
+ Scenario: Being able to change the grade type, scale and maximum grade for a grade category when there are no overridden grades
+ Given I click on "Edit" "link" in the "Cat 1" "table_row"
+ When I click on "Edit settings" "link" in the "Cat 1" "table_row"
+ Then I should not see "This category has associated grade items which have been overridden. Therefore some grades have already been awarded"
+ And I set the field "Grade type" to "Scale"
+ And I press "Save changes"
+ And I should see "Scale must be selected"
+ And I set the field "Scale" to "ABCDEF"
+ And I press "Save changes"
+ And I should not see "You cannot change the type, as grades already exist for this item"
+ And I click on "Edit" "link" in the "Cat 1" "table_row"
+ And I click on "Edit settings" "link" in the "Cat 1" "table_row"
+ And I should not see "This category has associated grade items which have been overridden. Therefore some grades have already been awarded"
+ And I set the field "Scale" to "Letter scale"
+ And I press "Save changes"
+ And I should not see "You cannot change the scale, as grades already exist for this item"
+
+ Scenario: Attempting to change a category item's grade type when overridden grades already exist
+ Given I navigate to "Grader report" node in "Grade administration"
+ And I turn editing mode on
+ And I give the grade "20.00" to the user "Student 1" for the grade item "Cat 1 total"
+ And I press "Save changes"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
+ And I click on "Edit" "link" in the "Cat 1" "table_row"
+ When I click on "Edit settings" "link" in the "Cat 1" "table_row"
+ Then I should see "This category has associated grade items which have been overridden. Therefore some grades have already been awarded, so the grade type cannot be changed. If you wish to change the maximum grade, you must first choose whether or not to rescale existing grades."
+ And "//div[contains(concat(' ', normalize-space(@class), ' '), 'fstatic') and contains(text(), 'Value')]" "xpath_element" should exist
+
+ Scenario: Attempting to change a category item's scale when overridden grades already exist
+ Given I click on "Edit" "link" in the "Cat 1" "table_row"
+ And I click on "Edit settings" "link" in the "Cat 1" "table_row"
+ And I set the field "Grade type" to "Scale"
+ And I set the field "Scale" to "ABCDEF"
+ And I press "Save changes"
+ And I navigate to "Grader report" node in "Grade administration"
+ And I turn editing mode on
+ And I give the grade "C" to the user "Student 1" for the grade item "Cat 1 total"
+ And I press "Save changes"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
+ And I click on "Edit" "link" in the "Cat 1" "table_row"
+ When I click on "Edit settings" "link" in the "Cat 1" "table_row"
+ Then I should see "This category has associated grade items which have been overridden. Therefore some grades have already been awarded, so the grade type and scale cannot be changed."
+ And "//div[contains(concat(' ', normalize-space(@class), ' '), 'fstatic') and contains(text(), 'ABCDEF')]" "xpath_element" should exist
+
+ Scenario: Attempting to change a category item's maximum grade when no rescaling option has been chosen
+ Given I navigate to "Grader report" node in "Grade administration"
+ And I turn editing mode on
+ And I give the grade "20.00" to the user "Student 1" for the grade item "Cat 1 total"
+ And I press "Save changes"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
+ And I click on "Edit" "link" in the "Cat 1" "table_row"
+ And I click on "Edit settings" "link" in the "Cat 1" "table_row"
+ And I set the field "Maximum grade" to "50"
+ When I press "Save changes"
+ Then I should see "You must choose whether to rescale existing grades or not."
$string['taskdeleteunconfirmedusers'] = 'Delete unconfirmed users';
$string['taskeventscron'] = 'Background processing for events';
$string['taskfiletrashcleanup'] = 'Cleanup files in trash';
-$string['taskglobalsearch'] = 'Global search indexing';
+$string['taskglobalsearchindex'] = 'Global search indexing';
+$string['taskglobalsearchoptimize'] = 'Global search index optimization';
$string['taskgradecron'] = 'Background processing for gradebook';
$string['tasklegacycron'] = 'Legacy cron processing for plugins';
$string['taskmessagingcleanup'] = 'Background processing for messaging';
$string['modgradecantchangegradetype'] = 'You cannot change the type, as grades already exist for this item.';
$string['modgradecantchangegradetypemsg'] = 'Some grades have already been awarded, so the grade type cannot be changed. If you wish to change the maximum grade, you must first choose whether or not to rescale existing grades.';
$string['modgradecantchangegradetyporscalemsg'] = 'Some grades have already been awarded, so the grade type and scale cannot be changed.';
+$string['modgradecategorycantchangegradetypemsg'] = 'This category has associated grade items which have been overridden. Therefore some grades have already been awarded, so the grade type cannot be changed. If you wish to change the maximum grade, you must first choose whether or not to rescale existing grades.';
+$string['modgradecategorycantchangegradetyporscalemsg'] = 'This category has associated grade items which have been overridden. Therefore some grades have already been awarded, so the grade type and scale cannot be changed.';
$string['modgradecantchangescale'] = 'You cannot change the scale, as grades already exist for this item.';
$string['modgradecantchangeratingmaxgrade'] = 'You cannot change the maximum grade when grades already exist for an activity with ratings.';
$string['modgradedonotmodify'] = 'Do not modify existing grades';
If this is set to \'Yes\', any existing grades will be rescaled so that the percentage grade remains the same.
For example, if this option is set to \'Yes\', changing the maximum grade on an item from 10 to 20 would cause a grade of 6/10 (60%) to be rescaled to 12/20 (60%). With this option set to \'No\', the grade would change from 6/10 (60%) to 6/20 (30%), requiring manual adjustment of the grade items to ensure correct scores.';
+$string['modgradecategoryrescalegrades'] = 'Rescale overridden grades';
+$string['modgradecategoryrescalegrades_help'] = 'When changing the maximum grades on a gradebook item you need to specify whether or not this will cause existing percentage grades to change as well.
+
+If this is set to \'Yes\', any existing overridden grades will be rescaled so that the percentage grade remains the same.
+
+For example, if this option is set to \'Yes\', changing the maximum grade on an item from 10 to 20 would cause a grade of 6/10 (60%) to be rescaled to 12/20 (60%). With this option set to \'No\', the grade will remain unchanged, requiring manual adjustment of the grade items to ensure correct scores.';
$string['modgradetype'] = 'Type';
$string['modgradetypenone'] = 'None';
$string['modgradetypepoint'] = 'Point';
$string['type_report_plural'] = 'Reports';
$string['type_repository'] = 'Repository';
$string['type_repository_plural'] = 'Repositories';
+$string['type_search'] = 'Search engine';
+$string['type_search_plural'] = 'Search engines';
$string['type_theme'] = 'Theme';
$string['type_theme_plural'] = 'Themes';
$string['type_tool'] = 'Admin tool';
$string['tagarea_post'] = 'Blog posts';
$string['tagarea_user'] = 'User interests';
$string['tagarea_course'] = 'Courses';
+$string['tagarea_course_modules'] = 'Course modules';
$string['tagareaenabled'] = 'Enabled';
$string['tagareaname'] = 'Name';
$string['tagareas'] = 'Tag areas';
$return .= $brtag . get_string('searchsetupdescription', 'search') . $brtag . $brtag;
- // Enable global search.
- $row = array();
- $url = new moodle_url("/admin/search.php?query=enableglobalsearch");
- $row[0] = '1. ' . html_writer::tag('a', get_string('enableglobalsearch', 'admin'),
- array('href' => $url));
- $status = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
- if (\core_search\manager::is_global_search_enabled()) {
- $status = html_writer::tag('span', get_string('yes'), array('class' => 'statusok'));
- }
- $row[1] = $status;
- $table->data[] = $row;
-
// Select a search engine.
$row = array();
$url = new moodle_url('/admin/settings.php?section=manageglobalsearch#admin-searchengine');
- $row[0] = '2. ' . html_writer::tag('a', get_string('selectsearchengine', 'admin'),
+ $row[0] = '1. ' . html_writer::tag('a', get_string('selectsearchengine', 'admin'),
array('href' => $url));
$status = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
// Available areas.
$row = array();
$url = new moodle_url('/admin/settings.php?section=manageglobalsearch#admin-searchengine');
- $row[0] = '3. ' . html_writer::tag('a', get_string('enablesearchareas', 'admin'),
+ $row[0] = '2. ' . html_writer::tag('a', get_string('enablesearchareas', 'admin'),
array('href' => $url));
$status = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
// Setup search engine.
$row = array();
if (empty($CFG->searchengine)) {
- $row[0] = '4. ' . get_string('setupsearchengine', 'admin');
+ $row[0] = '3. ' . get_string('setupsearchengine', 'admin');
$row[1] = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
} else {
$url = new moodle_url('/admin/settings.php?section=search' . $CFG->searchengine);
- $row[0] = '4. ' . html_writer::tag('a', get_string('setupsearchengine', 'admin'),
+ $row[0] = '3. ' . html_writer::tag('a', get_string('setupsearchengine', 'admin'),
array('href' => $url));
// Check the engine status.
$searchengine = \core_search\manager::search_engine_instance();
// Indexed data.
$row = array();
$url = new moodle_url('/report/search/index.php#searchindexform');
- $row[0] = '5. ' . html_writer::tag('a', get_string('indexdata', 'admin'), array('href' => $url));
+ $row[0] = '4. ' . html_writer::tag('a', get_string('indexdata', 'admin'), array('href' => $url));
if ($anyindexed) {
$status = html_writer::tag('span', get_string('yes'), array('class' => 'statusok'));
} else {
$row[1] = $status;
$table->data[] = $row;
+ // Enable global search.
+ $row = array();
+ $url = new moodle_url("/admin/search.php?query=enableglobalsearch");
+ $row[0] = '5. ' . html_writer::tag('a', get_string('enableglobalsearch', 'admin'),
+ array('href' => $url));
+ $status = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
+ if (\core_search\manager::is_global_search_enabled()) {
+ $status = html_writer::tag('span', get_string('yes'), array('class' => 'statusok'));
+ }
+ $row[1] = $status;
+ $table->data[] = $row;
+
$return .= html_writer::table($table);
return highlight($query, $return);
/** Include course lib for its functions */
require_once($CFG->dirroot.'/course/lib.php');
+if (!empty($CFG->forcelogin)) {
+ require_login();
+}
+
try {
// Start buffer capture so that we can `remove` any errors
ob_start();
context: requests,
dataType: 'json',
processData: false,
- async: async
+ async: async,
+ contentType: "application/json"
};
var script = config.wwwroot + '/lib/ajax/service.php?sesskey=' + config.sesskey;
if (!option.prop('selected')) {
option.remove();
} else {
- existingValues.push(option.attr('value'));
+ existingValues.push(String(option.attr('value')));
}
});
+
+ if (!options.multiple && originalSelect.children('option').length === 0) {
+ // If this is a single select - and there are no current options
+ // the first option added will be selected by the browser. This causes a bug!
+ // We need to insert an empty option so that none of the real options are selected.
+ var option = $('<option>');
+ originalSelect.append(option);
+ }
// And add all the new ones returned from ajax.
$.each(processedResults, function(resultIndex, result) {
- if (existingValues.indexOf(result.value) === -1) {
+ if (existingValues.indexOf(String(result.value)) === -1) {
var option = $('<option>');
option.append(result.label);
option.attr('value', result.value);
});
// Handler used to force set the value from behat.
inputElement.on('behat:set-value', function() {
- if (options.tags) {
+ var suggestionsElement = $(document.getElementById(state.suggestionsId));
+ if ((inputElement.attr('aria-expanded') === "true") &&
+ (suggestionsElement.children('[aria-selected=true]').length > 0)) {
+ // If the suggestion list has an active item, select it.
+ selectCurrentItem(options, state, originalSelect);
+ } else if (options.tags) {
+ // If tags are enabled, create a tag.
createItem(options, state, originalSelect);
}
});
// If this field uses ajax, set it up.
if (options.ajax) {
require([options.ajax], function(ajaxHandler) {
+ var throttleTimeout = null;
var handler = function(e) {
updateAjax(e, options, state, originalSelect, ajaxHandler);
};
+
+ // For input events, we do not want to trigger many, many updates.
+ var throttledHandler = function(e) {
+ if (throttleTimeout !== null) {
+ window.clearTimeout(throttleTimeout);
+ throttleTimeout = null;
+ }
+ throttleTimeout = window.setTimeout(handler.bind(this, e), 300);
+ };
// Trigger an ajax update after the text field value changes.
- inputElement.on("input keypress", handler);
+ inputElement.on("input keypress", throttledHandler);
+
var arrowElement = $(document.getElementById(state.downArrowId));
arrowElement.on("click", handler);
});
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Course selector adaptor for auto-complete form element.
+ *
+ * @module core/form-course-selector
+ * @class form-course-selector
+ * @package core
+ * @copyright 2016 Damyon Wiese <damyon@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since 3.1
+ */
+define(['core/ajax', 'jquery'], function(ajax, $) {
+
+ return /** @alias module:core/form-course-selector */ {
+ // Public variables and functions.
+ processResults: function(selector, data) {
+ // Mangle the results into an array of objects.
+ var results = [], i = 0;
+ var excludelist = String($(selector).data('exclude')).split(',');
+
+ for (i = 0; i < data.courses.length; i++) {
+ if (excludelist.indexOf(String(data.courses[i].id)) === -1) {
+ results.push({ value: data.courses[i].id, label: data.courses[i].displayname });
+ }
+ }
+ return results;
+ },
+
+ transport: function(selector, query, success, failure) {
+ // Parse some data-attributes from the form element.
+ var requiredcapabilities = $(selector).data('requiredcapabilities');
+ if (requiredcapabilities.trim() !== "") {
+ requiredcapabilities = requiredcapabilities.split(',');
+ } else {
+ requiredcapabilities = [];
+ }
+ // Build the query.
+ var promise = null;
+
+ if (typeof query === "undefined") {
+ query = '';
+ }
+
+ var searchargs = {
+ criterianame: 'search',
+ criteriavalue: query,
+ page: 0,
+ perpage: 100,
+ requiredcapabilities: requiredcapabilities
+ };
+ // Go go go!
+ promise = ajax.call([{
+ methodname: 'core_course_search_courses', args: searchargs
+ }]);
+
+ promise[0].done(success);
+ promise[0].fail(failure);
+
+ return promise;
+ }
+ };
+});
throw new coding_exception('Setting the valid of an autocomplete field requires javascript.');
}
$this->field->setValue($value);
+ // After the value is set, there is a 400ms throttle and then search. So adding 2 sec. delay to ensure both
+ // throttle + search finishes.
+ sleep(2);
$id = $this->field->getAttribute('id');
$js = ' require(["jquery"], function($) { $(document.getElementById("'.$id.'")).trigger("behat:set-value"); }); ';
$this->session->executeScript($js);
* @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class search_task extends scheduled_task {
+class search_index_task extends scheduled_task {
/**
* Get a descriptive name for this task (shown to admins).
* @return string
*/
public function get_name() {
- return get_string('taskglobalsearch', 'admin');
+ return get_string('taskglobalsearchindex', 'admin');
}
/**
// Indexing database records for modules + rich documents of forum.
$globalsearch->index();
-
- // Optimize index at last.
- $globalsearch->optimize_index();
}
}
--- /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/>.
+
+/**
+ * A scheduled task for global search.
+ *
+ * @package core
+ * @copyright 2016 Eric Merrill {@link https://www.merrilldigital.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Runs search index optimization.
+ *
+ * @package core
+ * @copyright 2016 Eric Merrill {@link https://www.merrilldigital.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class search_optimize_task extends scheduled_task {
+
+ /**
+ * Get a descriptive name for this task (shown to admins).
+ *
+ * @return string
+ */
+ public function get_name() {
+ return get_string('taskglobalsearchoptimize', 'admin');
+ }
+
+ /**
+ * Do the job.
+ * Throw exceptions on errors (the job will be retried).
+ */
+ public function execute() {
+ if (!\core_search\manager::is_global_search_enabled()) {
+ return;
+ }
+
+ $globalsearch = \core_search\manager::instance();
+
+ // Optimize index at last.
+ $globalsearch->optimize_index();
+ }
+}
return $result;
}
+ /**
+ * Truncates a string to no more than a certain number of bytes in a multi-byte safe manner.
+ * UTF-8 only!
+ *
+ * Many of the other charsets we test for (like ISO-2022-JP and EUC-JP) are not supported
+ * by typo3, and will give invalid results, so we are supporting UTF-8 only.
+ *
+ * @param string $string String to truncate
+ * @param int $bytes Maximum length of bytes in the result
+ * @return string Portion of string specified by $bytes
+ * @since Moodle 3.1
+ */
+ public static function str_max_bytes($string, $bytes) {
+ if (function_exists('mb_strcut')) {
+ return mb_strcut($string, 0, $bytes, 'UTF-8');
+ }
+
+ $oldlevel = error_reporting(E_PARSE);
+ $result = self::typo3()->strtrunc('utf-8', $string, $bytes);
+ error_reporting($oldlevel);
+
+ return $result;
+ }
+
/**
* Finds the last occurrence of a character in a string within another.
* UTF-8 ONLY safe mb_strrchr().
}
return implode(' ', $words);
}
-}
\ No newline at end of file
+}
/** @var stdClass keep record of support user */
public static $supportuser = false;
+ /** @var array store user fields properties cache. */
+ protected static $propertiescache = null;
+
/**
* Return user object from db or create noreply or support user,
* if userid matches corse_user::NOREPLY_USER or corse_user::SUPPORT_USER
throw new moodle_exception('suspended', 'auth');
}
}
+
+ /**
+ * Definition of user profile fields and the expected parameter type for data validation.
+ *
+ * @return void
+ */
+ protected static function fill_properties_cache() {
+
+ if (self::$propertiescache !== null) {
+ return;
+ }
+
+ // Array of user fields properties and expected parameters.
+ // Every new field on the user table should be added here otherwise it won't be validated.
+ $fields = array();
+ $fields['id'] = array('type' => PARAM_INT);
+ $fields['auth'] = array('type' => PARAM_NOTAGS);
+ $fields['confirmed'] = array('type' => PARAM_BOOL);
+ $fields['policyagreed'] = array('type' => PARAM_BOOL);
+ $fields['deleted'] = array('type' => PARAM_BOOL);
+ $fields['suspended'] = array('type' => PARAM_BOOL);
+ $fields['mnethostid'] = array('type' => PARAM_BOOL);
+ $fields['username'] = array('type' => PARAM_USERNAME);
+ $fields['password'] = array('type' => PARAM_NOTAGS);
+ $fields['idnumber'] = array('type' => PARAM_NOTAGS);
+ $fields['firstname'] = array('type' => PARAM_NOTAGS);
+ $fields['lastname'] = array('type' => PARAM_NOTAGS);
+ $fields['surname'] = array('type' => PARAM_NOTAGS);
+ $fields['email'] = array('type' => PARAM_RAW_TRIMMED);
+ $fields['emailstop'] = array('type' => PARAM_INT);
+ $fields['icq'] = array('type' => PARAM_NOTAGS);
+ $fields['skype'] = array('type' => PARAM_NOTAGS);
+ $fields['aim'] = array('type' => PARAM_NOTAGS);
+ $fields['yahoo'] = array('type' => PARAM_NOTAGS);
+ $fields['msn'] = array('type' => PARAM_NOTAGS);
+ $fields['phone1'] = array('type' => PARAM_NOTAGS);
+ $fields['phone2'] = array('type' => PARAM_NOTAGS);
+ $fields['institution'] = array('type' => PARAM_TEXT);
+ $fields['department'] = array('type' => PARAM_TEXT);
+ $fields['address'] = array('type' => PARAM_TEXT);
+ $fields['city'] = array('type' => PARAM_TEXT);
+ $fields['country'] = array('type' => PARAM_TEXT);
+ $fields['lang'] = array('type' => PARAM_TEXT);
+ $fields['calendartype'] = array('type' => PARAM_NOTAGS);
+ $fields['theme'] = array('type' => PARAM_NOTAGS);
+ $fields['timezones'] = array('type' => PARAM_TEXT);
+ $fields['firstaccess'] = array('type' => PARAM_INT);
+ $fields['lastaccess'] = array('type' => PARAM_INT);
+ $fields['lastlogin'] = array('type' => PARAM_INT);
+ $fields['currentlogin'] = array('type' => PARAM_INT);
+ $fields['lastip'] = array('type' => PARAM_NOTAGS);
+ $fields['secret'] = array('type' => PARAM_TEXT);
+ $fields['picture'] = array('type' => PARAM_INT);
+ $fields['url'] = array('type' => PARAM_URL);
+ $fields['description'] = array('type' => PARAM_CLEANHTML);
+ $fields['descriptionformat'] = array('type' => PARAM_INT);
+ $fields['mailformat'] = array('type' => PARAM_INT);
+ $fields['maildigest'] = array('type' => PARAM_INT);
+ $fields['maildisplay'] = array('type' => PARAM_INT);
+ $fields['autosubscribe'] = array('type' => PARAM_INT);
+ $fields['trackforums'] = array('type' => PARAM_INT);
+ $fields['timecreated'] = array('type' => PARAM_INT);
+ $fields['timemodified'] = array('type' => PARAM_INT);
+ $fields['trustbitmask'] = array('type' => PARAM_INT);
+ $fields['imagealt'] = array('type' => PARAM_TEXT);
+ $fields['lastnamephonetic'] = array('type' => PARAM_NOTAGS);
+ $fields['firstnamephonetic'] = array('type' => PARAM_NOTAGS);
+ $fields['middlename'] = array('type' => PARAM_NOTAGS);
+ $fields['alternatename'] = array('type' => PARAM_NOTAGS);
+
+ self::$propertiescache = $fields;
+ }
+
+ /**
+ * Get properties of a user field.
+ *
+ * @param string $property property name to be retrieved.
+ * @throws coding_exception if the requested property name is invalid.
+ * @return array the property definition.
+ */
+ public static function get_property_definition($property) {
+
+ self::fill_properties_cache();
+
+ if (!array_key_exists($property, self::$propertiescache)) {
+ throw new coding_exception('Invalid property requested.');
+ }
+
+ return self::$propertiescache[$property];
+ }
+
+ /**
+ * Clean the properties cache.
+ *
+ * During unit tests we need to be able to reset all caches so that each new test starts in a known state.
+ * Intended for use only for testing, phpunit calls this before every test.
+ */
+ public static function reset_caches() {
+ self::$propertiescache = null;
+ }
}
* - tagid - id of tag
* @param array $options display options, same as in get_courses() except 'recursive' is ignored -
* search is always category-independent
+ * @param array $requiredcapabilites List of capabilities required to see return course.
* @return course_in_list[]
*/
- public static function search_courses($search, $options = array()) {
+ public static function search_courses($search, $options = array(), $requiredcapabilities = array()) {
global $DB;
$offset = !empty($options['offset']) ? $options['offset'] : 0;
$limit = !empty($options['limit']) ? $options['limit'] : null;
$sortfields = !empty($options['sort']) ? $options['sort'] : array('sortorder' => 1);
$coursecatcache = cache::make('core', 'coursecat');
- $cachekey = 's-'. serialize($search + array('sort' => $sortfields));
+ $cachekey = 's-'. serialize(
+ $search + array('sort' => $sortfields) + array('requiredcapabilities' => $requiredcapabilities)
+ );
$cntcachekey = 'scnt-'. serialize($search);
$ids = $coursecatcache->get($cachekey);
$preloadcoursecontacts = !empty($options['coursecontacts']);
unset($options['coursecontacts']);
- if (!empty($search['search'])) {
+ // Empty search string will return all results.
+ if (!isset($search['search'])) {
+ $search['search'] = '';
+ }
+
+ if (empty($search['blocklist']) && empty($search['modulelist']) && empty($search['tagid'])) {
// Search courses that have specified words in their names/summaries.
$searchterms = preg_split('|\s+|', trim($search['search']), 0, PREG_SPLIT_NO_EMPTY);
- $searchterms = array_filter($searchterms, create_function('$v', 'return strlen($v) > 1;'));
- $courselist = get_courses_search($searchterms, 'c.sortorder ASC', 0, 9999999, $totalcount);
+
+ $courselist = get_courses_search($searchterms, 'c.sortorder ASC', 0, 9999999, $totalcount, $requiredcapabilities);
self::sort_records($courselist, $sortfields);
$coursecatcache->set($cachekey, array_keys($courselist));
$coursecatcache->set($cntcachekey, $totalcount);
return array();
}
$courselist = self::get_course_records($where, $params, $options, true);
+ if (!empty($requiredcapabilities)) {
+ foreach ($courselist as $key => $course) {
+ context_helper::preload_from_record($course);
+ $coursecontext = context_course::instance($course->id);
+ if (!has_all_capabilities($requiredcapabilities, $coursecontext)) {
+ unset($courselist[$key]);
+ }
+ }
+ }
self::sort_records($courselist, $sortfields);
$coursecatcache->set($cachekey, array_keys($courselist));
$coursecatcache->set($cntcachekey, count($courselist));
* @param array $search search criteria, see method search_courses() for more details
* @param array $options display options. They do not affect the result but
* the 'sort' property is used in cache key for storing list of course ids
+ * @param array $requiredcapabilites List of capabilities required to see return course.
* @return int
*/
- public static function search_courses_count($search, $options = array()) {
+ public static function search_courses_count($search, $options = array(), $requiredcapabilities = array()) {
$coursecatcache = cache::make('core', 'coursecat');
- $cntcachekey = 'scnt-'. serialize($search);
+ $cntcachekey = 'scnt-'. serialize($search) . serialize($requiredcapabilities);
if (($cnt = $coursecatcache->get($cntcachekey)) === false) {
// Cached value not found. Retrieve ALL courses and return their count.
unset($options['offset']);
unset($options['summary']);
unset($options['coursecontacts']);
$options['idonly'] = true;
- $courses = self::search_courses($search, $options);
+ $courses = self::search_courses($search, $options, $requiredcapabilities);
$cnt = count($courses);
}
return $cnt;
* @param int $page The page number to get
* @param int $recordsperpage The number of records per page
* @param int $totalcount Passed in by reference.
+ * @param array $requiredcapabilities Extra list of capabilities used to filter courses
* @return object {@link $COURSE} records
*/
-function get_courses_search($searchterms, $sort, $page, $recordsperpage, &$totalcount) {
+function get_courses_search($searchterms, $sort, $page, $recordsperpage, &$totalcount,
+ $requiredcapabilities = array()) {
global $CFG, $DB;
if ($DB->sql_regex_supported()) {
}
if (empty($searchcond)) {
- $totalcount = 0;
- return array();
+ $searchcond = array('1 = 1');
}
$searchcond = implode(" AND ", $searchcond);
$rs = $DB->get_recordset_sql($sql, $params);
foreach($rs as $course) {
- if (!$course->visible) {
- // preload contexts only for hidden courses or courses we need to return
- context_helper::preload_from_record($course);
- $coursecontext = context_course::instance($course->id);
- if (!has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
+ // Preload contexts only for hidden courses or courses we need to return.
+ context_helper::preload_from_record($course);
+ $coursecontext = context_course::instance($course->id);
+ if (!$course->visible && !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
+ continue;
+ }
+ if (!empty($requiredcapabilities)) {
+ if (!has_all_capabilities($requiredcapabilities, $coursecontext)) {
continue;
}
}
<FIELD NAME="classpath" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="capabilities" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="all capabilities that are required to be run by the function (separated by comma)"/>
+ <FIELD NAME="services" TYPE="char" LENGTH="1333" NOTNULL="false" SEQUENCE="false" COMMENT="all the services (by shortname) where this function must be included"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
'description' => 'Return course details',
'type' => 'read',
'capabilities'=> 'moodle/course:view,moodle/course:update,moodle/course:viewhiddencourses',
+ 'ajax' => true,
),
'core_course_search_courses' => array(
'description' => 'Search courses by (name, module, block, tag)',
'type' => 'read',
'capabilities' => '',
+ 'ajax' => true,
),
'moodle_course_create_courses' => array(
'core_notes_delete_notes',
'core_user_get_course_user_profiles',
'core_message_send_instant_messages',
- 'mod_assign_get_grades',
- 'mod_assign_get_assignments',
- 'mod_assign_get_submissions',
- 'mod_assign_get_user_flags',
- 'mod_assign_set_user_flags',
- 'mod_assign_get_user_mappings',
- 'mod_assign_revert_submissions_to_draft',
- 'mod_assign_lock_submissions',
- 'mod_assign_unlock_submissions',
- 'mod_assign_save_submission',
- 'mod_assign_submit_for_grading',
- 'mod_assign_save_grade',
- 'mod_assign_save_user_extensions',
- 'mod_assign_reveal_identities',
'message_airnotifier_is_system_configured',
'message_airnotifier_are_notification_preferences_configured',
'core_grades_update_grades',
- 'mod_forum_get_forums_by_courses',
- 'mod_forum_get_forum_discussions_paginated',
- 'mod_forum_get_forum_discussion_posts',
- 'mod_forum_add_discussion_post',
- 'mod_forum_add_discussion',
- 'mod_forum_can_add_discussion',
'core_files_get_files',
'core_message_get_messages',
'core_message_create_contacts',
'core_course_get_courses',
'core_completion_update_activity_completion_status_manually',
'core_completion_mark_course_self_completed',
- 'mod_data_get_databases_by_courses',
'core_comment_get_comments',
- 'mod_forum_view_forum',
'core_course_view_course',
'core_course_search_courses',
'core_course_get_course_module',
'core_message_mark_message_read',
'core_message_delete_message',
'core_notes_view_notes',
- 'mod_forum_view_forum_discussion',
'core_user_view_user_profile',
'gradereport_user_view_grade_report',
'core_rating_get_item_ratings',
- 'mod_url_view_url',
'core_user_get_users_by_field',
'core_user_add_user_private_files',
- 'mod_assign_view_grading_table',
- 'mod_assign_view_submission_status',
- 'mod_scorm_view_scorm',
- 'mod_scorm_get_scorm_scoes',
- 'mod_scorm_get_scorm_user_data',
- 'mod_scorm_insert_scorm_tracks',
- 'mod_scorm_get_scorm_sco_tracks',
- 'mod_scorm_get_scorm_attempt_count',
- 'mod_scorm_get_scorms_by_courses',
- 'mod_scorm_launch_sco',
- 'mod_survey_get_surveys_by_courses',
- 'mod_survey_view_survey',
- 'mod_survey_get_questions',
- 'mod_survey_submit_answers',
- 'mod_page_view_page',
- 'mod_resource_view_resource',
- 'mod_folder_view_folder',
- 'mod_chat_login_user',
- 'mod_chat_get_chat_users',
- 'mod_chat_send_chat_message',
- 'mod_chat_get_chat_latest_messages',
- 'mod_chat_view_chat',
- 'mod_chat_get_chats_by_courses',
- 'mod_book_view_book',
- 'mod_book_get_books_by_courses',
- 'mod_choice_get_choice_results',
- 'mod_choice_get_choice_options',
- 'mod_choice_submit_choice_response',
- 'mod_choice_view_choice',
- 'mod_choice_get_choices_by_courses',
- 'mod_choice_delete_choice_responses',
- 'mod_lti_get_tool_launch_data',
- 'mod_lti_get_ltis_by_courses',
- 'mod_lti_view_lti',
- 'mod_imscp_view_imscp',
- 'mod_imscp_get_imscps_by_courses',
- 'mod_quiz_get_quizzes_by_courses',
- 'mod_quiz_view_quiz',
- 'mod_glossary_get_glossaries_by_courses',
- 'mod_wiki_get_wikis_by_courses',
- 'mod_wiki_view_wiki',
- 'mod_wiki_view_page',
- 'mod_wiki_get_subwikis',
- 'mod_glossary_view_glossary',
- 'mod_glossary_view_entry',
- 'mod_glossary_get_entries_by_letter',
- 'mod_glossary_get_entries_by_date',
- 'mod_glossary_get_categories',
- 'mod_glossary_get_entries_by_category',
- 'mod_glossary_get_authors',
- 'mod_glossary_get_entries_by_author',
- 'mod_glossary_get_entries_by_author_id',
- 'mod_glossary_get_entries_by_search',
- 'mod_glossary_get_entries_by_term',
- 'mod_glossary_get_entries_to_approve',
- 'mod_glossary_get_entry_by_id',
),
'enabled' => 0,
'restrictedusers' => 0,
'itemtype' => 'blog_external', // External blogs.
'component' => 'core',
),
+ array(
+ 'itemtype' => 'course_modules', // Course modules.
+ 'component' => 'core',
+ ),
);
'month' => '*'
),
array(
- 'classname' => 'core\task\search_task',
+ 'classname' => 'core\task\search_index_task',
'blocking' => 0,
'minute' => '*/30',
'hour' => '*',
'dayofweek' => '*',
'month' => '*'
),
+ array(
+ 'classname' => 'core\task\search_optimize_task',
+ 'blocking' => 0,
+ 'minute' => '15',
+ 'hour' => '*/12',
+ 'day' => '*',
+ 'dayofweek' => '*',
+ 'month' => '*'
+ ),
array(
'classname' => 'core\task\stats_cron_task',
'blocking' => 0,
upgrade_main_savepoint(true, 2016030103.00);
}
+ if ($oldversion < 2016030400.01) {
+ // Add the new services field.
+ $table = new xmldb_table('external_functions');
+ $field = new xmldb_field('services', XMLDB_TYPE_CHAR, '1333', null, null, null, null, 'capabilities');
+
+ // Conditionally launch add field services.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+ // Main savepoint reached.
+ upgrade_main_savepoint(true, 2016030400.01);
+ }
+
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/>.
+
+
+/**
+ * Course selector field.
+ *
+ * Allows auto-complete ajax searching for courses and can restrict by enrolment, permissions, viewhidden...
+ *
+ * @package core_form
+ * @copyright 2015 Damyon Wiese <damyon@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+global $CFG;
+require_once($CFG->libdir . '/form/autocomplete.php');
+
+/**
+ * Form field type for choosing a course.
+ *
+ * Allows auto-complete ajax searching for courses and can restrict by enrolment, permissions, viewhidden...
+ *
+ * @package core_form
+ * @copyright 2015 Damyon Wiese <damyon@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class MoodleQuickForm_course extends MoodleQuickForm_autocomplete {
+
+ /**
+ * @var array $exclude Exclude a list of courses from the list (e.g. the current course).
+ */
+ protected $exclude = array();
+
+ /**
+ * @var boolean $allowmultiple Allow selecting more than one course.
+ */
+ protected $multiple = false;
+
+ /**
+ * @var array $requiredcapabilities Array of extra capabilities to check at the course context.
+ */
+ protected $requiredcapabilities = array();
+
+ /**
+ * Constructor
+ *
+ * @param string $elementname Element name
+ * @param mixed $elementlabel Label(s) for an element
+ * @param array $options Options to control the element's display
+ * Valid options are:
+ * 'multiple' - boolean multi select
+ * 'exclude' - array or int, list of course ids to never show
+ * 'requiredcapabilities' - array of capabilities. Uses ANY to combine them.
+ */
+ public function __construct($elementname = null, $elementlabel = null, $options = array()) {
+ if (isset($options['multiple'])) {
+ $this->multiple = $options['multiple'];
+ }
+ if (isset($options['exclude'])) {
+ $this->exclude = $options['exclude'];
+ if (!is_array($this->exclude)) {
+ $this->exclude = array($this->exclude);
+ }
+ }
+ if (isset($options['requiredcapabilities'])) {
+ $this->requiredcapabilities = $options['requiredcapabilities'];
+ }
+
+ $validattributes = array(
+ 'ajax' => 'core/form-course-selector',
+ 'data-requiredcapabilities' => implode(',', $this->requiredcapabilities),
+ 'data-exclude' => implode(',', $this->exclude)
+ );
+ if ($this->multiple) {
+ $validattributes['multiple'] = 'multiple';
+ }
+
+ parent::__construct($elementname, $elementlabel, array(), $validattributes);
+ }
+
+ /**
+ * Set the value of this element. If values can be added or are unknown, we will
+ * make sure they exist in the options array.
+ * @param string|array $value The value to set.
+ * @return boolean
+ */
+ public function setValue($value) {
+ global $DB;
+ $values = (array) $value;
+ $coursestofetch = array();
+
+ foreach ($values as $onevalue) {
+ if ((!$this->optionExists($onevalue)) &&
+ ($onevalue !== '_qf__force_multiselect_submission')) {
+ array_push($coursestofetch, $onevalue);
+ }
+ }
+
+ if (empty($coursestofetch)) {
+ return $this->setSelected(array());
+ }
+
+ // There is no API function to load a list of course from a list of ids.
+ $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
+ $fields = array('c.id', 'c.category', 'c.sortorder',
+ 'c.shortname', 'c.fullname', 'c.idnumber',
+ 'c.startdate', 'c.visible', 'c.cacherev');
+ list($whereclause, $params) = $DB->get_in_or_equal($coursestofetch, SQL_PARAMS_NAMED, 'id');
+
+ $sql = "SELECT ". join(',', $fields). ", $ctxselect
+ FROM {course} c
+ JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextcourse
+ WHERE c.id ". $whereclause." ORDER BY c.sortorder";
+ $list = $DB->get_records_sql($sql, array('contextcourse' => CONTEXT_COURSE) + $params);
+
+ $coursestoselect = array();
+ foreach ($list as $course) {
+ context_helper::preload_from_record($course);
+ // Make sure we can see the course.
+ if (!$course->visible && !has_capability('moodle/course:viewhiddencourses', context_course::instance($course->id))) {
+ continue;
+ }
+ $label = get_course_display_name_for_list($course);
+ $this->addOption($label, $course->id);
+ array_push($coursestoselect, $course->id);
+ }
+ return $this->setSelected($coursestoselect);
+ }
+}
return true;
}
// Check if we are changing the scale type when grades are present.
- if (isset($val['modgrade_type']) && $val['modgrade_type'] === 'scale') {
+ // If modgrade_type is empty then use currentgradetype.
+ $gradetype = isset($val['modgrade_type']) ? $val['modgrade_type'] : $this->currentgradetype;
+ if ($gradetype === 'scale') {
if (isset($val['modgrade_scale']) && ($val['modgrade_scale'] !== $this->currentscaleid)) {
return false;
}
return true;
}
// Check if we are changing the max grade if we are using ratings and there is a grade.
- if (isset($val['modgrade_type']) && $val['modgrade_type'] === 'point') {
+ // If modgrade_type is empty then use currentgradetype.
+ $gradetype = isset($val['modgrade_type']) ? $val['modgrade_type'] : $this->currentgradetype;
+ if ($gradetype === 'point') {
if (isset($val['modgrade_point']) &&
grade_floats_different($this->currentgrade, $val['modgrade_point'])) {
return false;
};
$checkmaxgrade = function($val) {
// Closure to validate a max points value. See the note above about scope if this confuses you.
- if (isset($val['modgrade_type']) && $val['modgrade_type'] === 'point') {
- if (!isset($val['modgrade_point'])) {
- return false;
+ // If modgrade_type is empty then use currentgradetype.
+ $gradetype = isset($val['modgrade_type']) ? $val['modgrade_type'] : $this->currentgradetype;
+ if ($gradetype === 'point') {
+ if (isset($val['modgrade_point'])) {
+ return $this->validate_point($val['modgrade_point']);
}
- return $this->validate_point($val['modgrade_point']);
}
return true;
};
$checkvalidscale = function($val) {
// Closure to validate a scale value. See the note above about scope if this confuses you.
- if (isset($val['modgrade_type']) && $val['modgrade_type'] === 'scale') {
- if (!isset($val['modgrade_scale'])) {
- return false;
+ // If modgrade_type is empty then use currentgradetype.
+ $gradetype = isset($val['modgrade_type']) ? $val['modgrade_type'] : $this->currentgradetype;
+ if ($gradetype === 'scale') {
+ if (isset($val['modgrade_scale'])) {
+ return $this->validate_scale($val['modgrade_scale']);
}
- return $this->validate_scale($val['modgrade_scale']);
}
return true;
};
return true;
}
// Closure to validate a scale value. See the note above about scope if this confuses you.
- if (isset($val['modgrade_type']) && $val['modgrade_type'] === 'point') {
+ // If modgrade_type is empty then use currentgradetype.
+ $gradetype = isset($val['modgrade_type']) ? $val['modgrade_type'] : $this->currentgradetype;
+ if ($gradetype === 'point' && isset($val['modgrade_point'])) {
// Work out if the value was actually changed in the form.
if (grade_floats_different($this->currentgrade, $val['modgrade_point'])) {
if (empty($val['modgrade_rescalegrades'])) {
MoodleQuickForm::registerElementType('autocomplete', "$CFG->libdir/form/autocomplete.php", 'MoodleQuickForm_autocomplete');
MoodleQuickForm::registerElementType('button', "$CFG->libdir/form/button.php", 'MoodleQuickForm_button');
MoodleQuickForm::registerElementType('cancel', "$CFG->libdir/form/cancel.php", 'MoodleQuickForm_cancel');
+MoodleQuickForm::registerElementType('course', "$CFG->libdir/form/course.php", 'MoodleQuickForm_course');
MoodleQuickForm::registerElementType('searchableselector', "$CFG->libdir/form/searchableselector.php", 'MoodleQuickForm_searchableselector');
MoodleQuickForm::registerElementType('checkbox', "$CFG->libdir/form/checkbox.php", 'MoodleQuickForm_checkbox');
MoodleQuickForm::registerElementType('date_selector', "$CFG->libdir/form/dateselector.php", 'MoodleQuickForm_date_selector');
return $count > 0;
}
+ /**
+ * Check to see if there are existing overridden grades for this grade_item.
+ *
+ * @return boolean - true if there are overridden grades for this grade_item.
+ */
+ public function has_overridden_grades() {
+ global $DB;
+
+ $count = $DB->count_records_select('grade_grades',
+ 'itemid = :gradeitemid AND finalgrade IS NOT NULL AND overridden > 0',
+ array('gradeitemid' => $this->id));
+ return $count > 0;
+ }
+
/**
* Finds and returns all grade_item instances based on params.
*
// Set this object in the item so it doesn't re-fetch it.
$grade->grade_item = $this;
- // Updating the raw grade automatically updates the min/max.
- if ($this->is_raw_used()) {
- $rawgrade = (($grade->rawgrade - $oldgrademin) * $scale) + $newgrademin;
- $this->update_raw_grade(false, $rawgrade, $source, false, FORMAT_MOODLE, null, null, null, $grade);
- } else {
- $finalgrade = (($grade->finalgrade - $oldgrademin) * $scale) + $newgrademin;
- $this->update_final_grade($grade->userid, $finalgrade, $source);
+ if (!$this->is_category_item() || ($this->is_category_item() && $grade->is_overridden())) {
+ // Updating the raw grade automatically updates the min/max.
+ if ($this->is_raw_used()) {
+ $rawgrade = (($grade->rawgrade - $oldgrademin) * $scale) + $newgrademin;
+ $this->update_raw_grade(false, $rawgrade, $source, false, FORMAT_MOODLE, null, null, null, $grade);
+ } else {
+ $finalgrade = (($grade->finalgrade - $oldgrademin) * $scale) + $newgrademin;
+ $this->update_final_grade($grade->userid, $finalgrade, $source);
+ }
}
}
$rs->close();
}
}
+
+/**
+ * A custom HTMLPurifier transformation. Adds rel="noreferrer" to all links with target="_blank".
+ *
+ * @package core
+ * @copyright Cameron Ball
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class HTMLPurifier_AttrTransform_Noreferrer extends HTMLPurifier_AttrTransform {
+ /** @var HTMLPurifier_URIParser $parser */
+ private $parser;
+
+ /**
+ * Constructor.
+ */
+ public function __construct() {
+ $this->parser = new HTMLPurifier_URIParser();
+ }
+
+ /**
+ * Transforms a tags such that when a target attribute is present, rel="noreferrer" is added.
+ *
+ * Note that this will not respect Attr.AllowedRel
+ *
+ * @param array $attr Assoc array of attributes, usually from
+ * HTMLPurifier_Token_Tag::$attr
+ * @param HTMLPurifier_Config $config Mandatory HTMLPurifier_Config object.
+ * @param HTMLPurifier_Context $context Mandatory HTMLPurifier_Context object
+ * @return array Processed attribute array.
+ */
+ public function transform($attr, $config, $context) {
+ // Nothing to do If we already have noreferrer in the rel attribute
+ if (!empty($attr['rel']) && substr($attr['rel'], 'noreferrer') !== false) {
+ return $attr;
+ }
+
+ // If _blank target attribute exists, add rel=noreferrer
+ if (!empty($attr['target']) && $attr['target'] == '_blank') {
+ $attr['rel'] = !empty($attr['rel']) ? $attr['rel'] . ' noreferrer' : 'noreferrer';
+ }
+
+ return $attr;
+ }
+}
+
+/**
+ * A custom HTMLPurifier module to add rel="noreferrer" attributes a tags.
+ *
+ * @package core
+ * @copyright Cameron Ball
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class HTMLPurifier_HTMLModule_Noreferrer extends HTMLPurifier_HTMLModule {
+ /** @var string $name */
+ public $name = 'Noreferrer';
+
+ /**
+ * Module setup
+ *
+ * @param HTMLPurifier_Config $config
+ */
+ public function setup($config) {
+ $a = $this->addBlankElement('a');
+ $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_Noreferrer();
+ }
+}
filter_manager::reset_caches();
core_filetypes::reset_caches();
\core_search\manager::clear_static();
+ core_user::reset_caches();
// Reset static unit test options.
if (class_exists('\availability_date\condition', false)) {
$record->introformat = FORMAT_MOODLE;
}
+ if (isset($record->tags) && !is_array($record->tags)) {
+ $record->tags = preg_split('/\s*,\s*/', trim($record->tags), -1, PREG_SPLIT_NO_EMPTY);
+ }
+
// Before Moodle 2.6 it was possible to create a module with completion tracking when
// it is not setup for course and/or site-wide. Display debugging message so it is
// easier to trace an error in unittests.
$cm = get_coursemodule_from_instance('page', $page->id, $SITE->id, true);
$this->assertEquals(3, $cm->sectionnum);
+ $page = $generator->create_module('page', array('course' => $SITE->id, 'tags' => 'Cat, Dog'));
+ $this->assertEquals(array('Cat', 'Dog'),
+ array_values(core_tag_tag::get_item_tags_array('core', 'course_modules', $page->cmid)));
+
// Prepare environment to generate modules with all possible options.
// Enable advanced functionality.
$res = coursecat::search_courses(array('search' => 'Математика'));
$this->assertEquals(array($c3->id, $c6->id), array_keys($res));
$this->assertEquals(2, coursecat::search_courses_count(array('search' => 'Математика'), array()));
+
+ $this->setUser($this->getDataGenerator()->create_user());
+
+ // Add necessary capabilities.
+ $this->assign_capability('moodle/course:create', CAP_ALLOW, context_coursecat::instance($cat2->id));
+ // Do another search with restricted capabilities.
+ $reqcaps = array('moodle/course:create');
+ $res = coursecat::search_courses(array('search' => 'test'), array(), $reqcaps);
+ $this->assertEquals(array($c8->id, $c5->id), array_keys($res));
+ $this->assertEquals(2, coursecat::search_courses_count(array('search' => 'test'), array(), $reqcaps));
}
public function test_course_contacts() {
* Verify _blank target is allowed.
*/
public function test_allow_blank_target() {
+ // See MDL-52651 for an explanation as to why the rel="noreferrer" attribute is expected here.
+ // Also note we do not need to test links with an existing rel attribute as the HTML Purifier is configured to remove
+ // the rel attribute.
$text = '<a href="http://moodle.org" target="_blank">Some link</a>';
+ $expected = '<a href="http://moodle.org" target="_blank" rel="noreferrer">Some link</a>';
$result = format_text($text, FORMAT_HTML);
- $this->assertSame($text, $result);
+ $this->assertSame($expected, $result);
$result = format_text('<a href="http://moodle.org" target="some">Some link</a>', FORMAT_HTML);
$this->assertSame('<a href="http://moodle.org">Some link</a>', $result);
$this->assertSame(4, core_text::strlen($str, 'GB18030'));
}
+ /**
+ * Test unicode safe string truncation.
+ */
+ public function test_str_max_bytes() {
+ // These are all 3 byte characters, so this is a 12-byte string.
+ $str = '言語è¨å®š';
+
+ $this->assertEquals(12, strlen($str));
+
+ // Step back, shortening the string 1 byte at a time. Should remove in 1 char chunks.
+ $conv = core_text::str_max_bytes($str, 12);
+ $this->assertEquals(12, strlen($conv));
+ $this->assertSame('言語è¨å®š', $conv);
+ $conv = core_text::str_max_bytes($str, 11);
+ $this->assertEquals(9, strlen($conv));
+ $this->assertSame('言語è¨', $conv);
+ $conv = core_text::str_max_bytes($str, 10);
+ $this->assertEquals(9, strlen($conv));
+ $this->assertSame('言語è¨', $conv);
+ $conv = core_text::str_max_bytes($str, 9);
+ $this->assertEquals(9, strlen($conv));
+ $this->assertSame('言語è¨', $conv);
+ $conv = core_text::str_max_bytes($str, 8);
+ $this->assertEquals(6, strlen($conv));
+ $this->assertSame('言語', $conv);
+
+ // Now try a mixed byte string.
+ $str = '言語è¨a定';
+
+ $this->assertEquals(13, strlen($str));
+
+ $conv = core_text::str_max_bytes($str, 11);
+ $this->assertEquals(10, strlen($conv));
+ $this->assertSame('言語è¨a', $conv);
+ $conv = core_text::str_max_bytes($str, 10);
+ $this->assertEquals(10, strlen($conv));
+ $this->assertSame('言語è¨a', $conv);
+ $conv = core_text::str_max_bytes($str, 9);
+ $this->assertEquals(9, strlen($conv));
+ $this->assertSame('言語è¨', $conv);
+ $conv = core_text::str_max_bytes($str, 8);
+ $this->assertEquals(6, strlen($conv));
+ $this->assertSame('言語', $conv);
+
+ // Test 0 byte case.
+ $conv = core_text::str_max_bytes($str, 0);
+ $this->assertEquals(0, strlen($conv));
+ $this->assertSame('', $conv);
+ }
+
/**
* Tests the static strtolower method.
*/
}
}
+
+ /**
+ * Test get_property_definition() method.
+ */
+ public function test_get_property_definition() {
+ // Try to get a existing property.
+ $properties = core_user::get_property_definition('id');
+ $this->assertEquals($properties['type'], PARAM_INT);
+ $properties = core_user::get_property_definition('username');
+ $this->assertEquals($properties['type'], PARAM_USERNAME);
+
+ // Invalid property.
+ try {
+ core_user::get_property_definition('fullname');
+ } catch (coding_exception $e) {
+ $this->assertRegExp('/Invalid property requested./', $e->getMessage());
+ }
+
+ // Empty parameter.
+ try {
+ core_user::get_property_definition('');
+ } catch (coding_exception $e) {
+ $this->assertRegExp('/Invalid property requested./', $e->getMessage());
+ }
+ }
}
=== 3.1 ===
+* The core_user::fill_properties_cache() static method has been introduced to be a reference
+ and allow standard user fields data validation. Right now only type validation is supported
+ checking it against the parameter (PARAM_*) type of the target user field. MDL-52781 is
+ going to add support to null/not null and choices validation, replacing the existing code to
+ validate the user fields in different places in a common way.
+* Webservice function core_course_search_courses now returns results when the search string
+ is less than 2 chars long.
+* Webservice function core_course_search_courses accepts a new parameter 'requiredcapabilities' to filter the results
+ by the capabilities of the current user.
+* New mform element 'course' handles thousands of courses with good performance and usability.
* The redirect() function will now redirect immediately if output has not
already started. Messages will be displayed on the subsequent page using
session notifications. The type of message output can be configured using the
is now a part of \antivirus_clamav\scanner class methods.
* \repository::antivir_scan_file() has been deprecated, \core\antivirus\manager::scan_file() that
applies antivirus plugins is replacing its functionality.
+* Added core_text::str_max_bytes() which safely truncates multi-byte strings to a maximum number of bytes.
=== 3.0 ===
$dbfunction->capabilities = $functioncapabilities;
$update = true;
}
+
+ if (isset($function['services']) and is_array($function['services'])) {
+ sort($function['services']);
+ $functionservices = implode(',', $function['services']);
+ } else {
+ // Force null values in the DB.
+ $functionservices = null;
+ }
+
+ if ($dbfunction->services != $functionservices) {
+ // Now, we need to check if services were removed, in that case we need to remove the function from them.
+ $servicesremoved = array_diff(explode(",", $dbfunction->services), explode(",", $functionservices));
+ foreach ($servicesremoved as $removedshortname) {
+ if ($externalserviceid = $DB->get_field('external_services', 'id', array("shortname" => $removedshortname))) {
+ $DB->delete_records('external_services_functions', array('functionname' => $dbfunction->name,
+ 'externalserviceid' => $externalserviceid));
+ }
+ }
+
+ $dbfunction->services = $functionservices;
+ $update = true;
+ }
if ($update) {
$DB->update_record('external_functions', $dbfunction);
}
$dbfunction->classpath = empty($function['classpath']) ? null : $function['classpath'];
$dbfunction->component = $component;
$dbfunction->capabilities = array_key_exists('capabilities', $function)?$function['capabilities']:'';
+
+ if (isset($function['services']) and is_array($function['services'])) {
+ sort($function['services']);
+ $dbfunction->services = implode(',', $function['services']);
+ } else {
+ // Force null values in the DB.
+ $dbfunction->services = null;
+ }
+
$dbfunction->id = $DB->insert_record('external_functions', $dbfunction);
}
unset($functions);
}
}
+/**
+ * Allow plugins to add external functions to other plugins or core services.
+ * This function is executed just after all the plugins have been updated.
+ */
+function external_update_services() {
+ global $DB;
+
+ // Look for external functions that want to be added in existing services.
+ $functions = $DB->get_records_select('external_functions', 'services IS NOT NULL');
+
+ $servicescache = array();
+ foreach ($functions as $function) {
+ // Prevent edge cases.
+ if (empty($function->services)) {
+ continue;
+ }
+ $services = explode(',', $function->services);
+
+ foreach ($services as $serviceshortname) {
+ // Get the service id by shortname.
+ if (!empty($servicescache[$serviceshortname])) {
+ $serviceid = $servicescache[$serviceshortname];
+ } else if ($service = $DB->get_record('external_services', array('shortname' => $serviceshortname))) {
+ // If the component is empty, it means that is not a built-in service.
+ // We don't allow functions to inject themselves in services created by an user in Moodle.
+ if (empty($service->component)) {
+ continue;
+ }
+ $serviceid = $service->id;
+ $servicescache[$serviceshortname] = $serviceid;
+ } else {
+ // Service not found.
+ continue;
+ }
+ // Finally add the function to the service.
+ $newf = new stdClass();
+ $newf->externalserviceid = $serviceid;
+ $newf->functionname = $function->name;
+
+ if (!$DB->record_exists('external_services_functions', (array)$newf)) {
+ $DB->insert_record('external_services_functions', $newf);
+ }
+ }
+ }
+}
+
/**
* upgrade logging functions
*/
foreach ($plugintypes as $type=>$location) {
upgrade_plugins($type, 'print_upgrade_part_start', 'print_upgrade_part_end', $verbose);
}
+ // Upgrade services. This function gives plugins a chance to add functions to existing core or non-core services.
+ external_update_services();
+
// Update cache definitions. Involves scanning each plugin for any changes.
cache_helper::update_definitions();
// Mark the site as upgraded.
$config = HTMLPurifier_Config::createDefault();
$config->set('HTML.DefinitionID', 'moodlehtml');
- $config->set('HTML.DefinitionRev', 3);
+ $config->set('HTML.DefinitionRev', 4);
$config->set('Cache.SerializerPath', $cachedir);
$config->set('Cache.SerializerPermissions', $CFG->directorypermissions);
$config->set('Core.NormalizeNewlines', false);
// Use the built-in Ruby module to add annotation support.
$def->manager->addModule(new HTMLPurifier_HTMLModule_Ruby());
+
+ // Use the custom Noreferrer module.
+ $def->manager->addModule(new HTMLPurifier_HTMLModule_Noreferrer());
}
$purifier = new HTMLPurifier($config);
$testsession = optional_param('testsession', 0, PARAM_INT); // test session works properly
$cancel = optional_param('cancel', 0, PARAM_BOOL); // redirect to frontpage, needed for loginhttps
+$anchor = optional_param('anchor', '', PARAM_RAW); // Used to restore hash anchor to wantsurl.
if ($cancel) {
redirect(new moodle_url('/'));
$frm = data_submitted();
}
+// Restore the #anchor to the original wantsurl. Note that this
+// will only work for internal auth plugins, SSO plugins such as
+// SAML / CAS / OIDC will have to handle this correctly directly.
+if ($anchor && isset($SESSION->wantsurl) && strpos($SESSION->wantsurl, '#') === false) {
+ $wantsurl = new moodle_url($SESSION->wantsurl);
+ $wantsurl->set_anchor(substr($anchor, 1));
+ $SESSION->wantsurl = $wantsurl->out();
+}
+
/// Check if the user has actually submitted login data to us
if ($frm and isset($frm->username)) { // Login WITH cookies
</div>
<?php } ?>
<div class="clearer"><!-- --></div>
+ <input id="anchor" type="hidden" name="anchor" value="" />
+ <script>document.getElementById('anchor').value = location.hash</script>
<input type="submit" id="loginbtn" value="<?php print_string("login") ?>" />
<div class="forgetpass"><a href="forgot_password.php"><?php print_string("forgotten") ?></a></div>
</form>
require_once(dirname(__FILE__) . '/../../config.php');
require_once($CFG->dirroot.'/mod/assign/adminlib.php');
+$subtype = required_param('subtype', PARAM_PLUGIN);
+$action = optional_param('action', null, PARAM_PLUGIN);
+$plugin = optional_param('plugin', null, PARAM_PLUGIN);
+
+if (!empty($plugin)) {
+ require_sesskey();
+}
+
// Create the class for this controller.
-$pluginmanager = new assign_plugin_manager(required_param('subtype', PARAM_PLUGIN));
+$pluginmanager = new assign_plugin_manager($subtype);
$PAGE->set_context(context_system::instance());
// Execute the controller.
-$pluginmanager->execute(optional_param('action', null, PARAM_PLUGIN),
- optional_param('plugin', null, PARAM_PLUGIN));
+$pluginmanager->execute($action, $plugin);
'methodname' => 'get_grades',
'classpath' => 'mod/assign/externallib.php',
'description' => 'Returns grades from the assignment',
- 'type' => 'read'
+ 'type' => 'read',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_assign_get_assignments' => array(
'methodname' => 'get_assignments',
'classpath' => 'mod/assign/externallib.php',
'description' => 'Returns the courses and assignments for the users capability',
- 'type' => 'read'
+ 'type' => 'read',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_assign_get_submissions' => array(
'methodname' => 'get_submissions',
'classpath' => 'mod/assign/externallib.php',
'description' => 'Returns the submissions for assignments',
- 'type' => 'read'
+ 'type' => 'read',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_assign_get_user_flags' => array(
'methodname' => 'get_user_flags',
'classpath' => 'mod/assign/externallib.php',
'description' => 'Returns the user flags for assignments',
- 'type' => 'read'
+ 'type' => 'read',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_assign_set_user_flags' => array(
'classpath' => 'mod/assign/externallib.php',
'description' => 'Creates or updates user flags',
'type' => 'write',
- 'capabilities'=> 'mod/assign:grade'
+ 'capabilities'=> 'mod/assign:grade',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_assign_get_user_mappings' => array(
'methodname' => 'get_user_mappings',
'classpath' => 'mod/assign/externallib.php',
'description' => 'Returns the blind marking mappings for assignments',
- 'type' => 'read'
+ 'type' => 'read',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_assign_revert_submissions_to_draft' => array(
'methodname' => 'revert_submissions_to_draft',
'classpath' => 'mod/assign/externallib.php',
'description' => 'Reverts the list of submissions to draft status',
- 'type' => 'write'
+ 'type' => 'write',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_assign_lock_submissions' => array(
'methodname' => 'lock_submissions',
'classpath' => 'mod/assign/externallib.php',
'description' => 'Prevent students from making changes to a list of submissions',
- 'type' => 'write'
+ 'type' => 'write',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_assign_unlock_submissions' => array(
'methodname' => 'unlock_submissions',
'classpath' => 'mod/assign/externallib.php',
'description' => 'Allow students to make changes to a list of submissions',
- 'type' => 'write'
+ 'type' => 'write',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_assign_save_submission' => array(
'methodname' => 'save_submission',
'classpath' => 'mod/assign/externallib.php',
'description' => 'Update the current students submission',
- 'type' => 'write'
+ 'type' => 'write',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_assign_submit_for_grading' => array(
'methodname' => 'submit_for_grading',
'classpath' => 'mod/assign/externallib.php',
'description' => 'Submit the current students assignment for grading',
- 'type' => 'write'
+ 'type' => 'write',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_assign_save_grade' => array(
'methodname' => 'save_grade',
'classpath' => 'mod/assign/externallib.php',
'description' => 'Save a grade update for a single student.',
- 'type' => 'write'
+ 'type' => 'write',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_assign_save_grades' => array(
'methodname' => 'save_grades',
'classpath' => 'mod/assign/externallib.php',
'description' => 'Save multiple grade updates for an assignment.',
- 'type' => 'write'
+ 'type' => 'write',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_assign_save_user_extensions' => array(
'methodname' => 'save_user_extensions',
'classpath' => 'mod/assign/externallib.php',
'description' => 'Save a list of assignment extensions',
- 'type' => 'write'
+ 'type' => 'write',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_assign_reveal_identities' => array(
'methodname' => 'reveal_identities',
'classpath' => 'mod/assign/externallib.php',
'description' => 'Reveal the identities for a blind marking assignment',
- 'type' => 'write'
+ 'type' => 'write',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_assign_view_grading_table' => array(
'classpath' => 'mod/assign/externallib.php',
'description' => 'Trigger the grading_table_viewed event.',
'type' => 'write',
- 'capabilities' => 'mod/assign:view, mod/assign:viewgrades'
+ 'capabilities' => 'mod/assign:view, mod/assign:viewgrades',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_assign_view_submission_status' => array(
'classpath' => 'mod/assign/externallib.php',
'description' => 'Trigger the submission status viewed event.',
'type' => 'write',
- 'capabilities' => 'mod/assign:view'
+ 'capabilities' => 'mod/assign:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
);
$notices = array();
- $submissiondata = (object)$params['plugindata'];
-
- $assignment->save_submission($submissiondata, $notices);
+ if (!$assignment->submissions_open($USER->id)) {
+ $notices[] = get_string('duedatereached', 'assign');
+ } else {
+ $submissiondata = (object)$params['plugindata'];
+ $assignment->save_submission($submissiondata, $notices);
+ }
$warnings = array();
foreach ($notices as $notice) {
$this->assertEquals(0, count($result));
+ // Set up a due and cutoff passed date.
+ $instance->duedate = time() - WEEKSECS;
+ $instance->cutoffdate = time() - WEEKSECS;
+ $DB->update_record('assign', $instance);
+
+ $result = mod_assign_external::save_submission($instance->id, $submissionpluginparams);
+ $result = external_api::clean_returnvalue(mod_assign_external::save_submission_returns(), $result);
+
+ $this->assertCount(1, $result);
+ $this->assertEquals(get_string('duedatereached', 'assign'), $result[0]['item']);
}
/**
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics).
-$plugin->version = 2015111601; // The current module version (Date: YYYYMMDDXX).
+$plugin->version = 2015111602; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2015111000; // Requires this Moodle version.
$plugin->cron = 60;
'methodname' => 'view_book',
'description' => 'Simulate the view.php web interface book: trigger events, completion, etc...',
'type' => 'write',
- 'capabilities' => 'mod/book:read'
+ 'capabilities' => 'mod/book:read',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_book_get_books_by_courses' => array(
'description' => 'Returns a list of book instances in a provided set of courses,
if no courses are provided then all the book instances the user has access to will be returned.',
'type' => 'read',
- 'capabilities' => ''
+ 'capabilities' => '',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
)
);
defined('MOODLE_INTERNAL') || die;
$plugin->component = 'mod_book'; // Full name of the plugin (used for diagnostics)
-$plugin->version = 2015111600; // The current module version (Date: YYYYMMDDXX)
+$plugin->version = 2015111601; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2015111000; // Requires this Moodle version
$plugin->cron = 0; // Period for cron to check this module (secs)
'methodname' => 'login_user',
'description' => 'Log a user into a chat room in the given chat.',
'type' => 'write',
- 'capabilities' => 'mod/chat:chat'
+ 'capabilities' => 'mod/chat:chat',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_chat_get_chat_users' => array(
'methodname' => 'get_chat_users',
'description' => 'Get the list of users in the given chat session.',
'type' => 'read',
- 'capabilities' => 'mod/chat:chat'
+ 'capabilities' => 'mod/chat:chat',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_chat_send_chat_message' => array(
'methodname' => 'send_chat_message',
'description' => 'Send a message on the given chat session.',
'type' => 'write',
- 'capabilities' => 'mod/chat:chat'
+ 'capabilities' => 'mod/chat:chat',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_chat_get_chat_latest_messages' => array(
'methodname' => 'get_chat_latest_messages',
'description' => 'Get the latest messages from the given chat session.',
'type' => 'read',
- 'capabilities' => 'mod/chat:chat'
+ 'capabilities' => 'mod/chat:chat',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_chat_view_chat' => array(
'methodname' => 'view_chat',
'description' => 'Trigger the course module viewed event and update the module completion status.',
'type' => 'write',
- 'capabilities' => 'mod/chat:chat'
+ 'capabilities' => 'mod/chat:chat',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_chat_get_chats_by_courses' => array(
'description' => 'Returns a list of chat instances in a provided set of courses,
if no courses are provided then all the chat instances the user has access to will be returned.',
'type' => 'read',
- 'capabilities' => ''
+ 'capabilities' => '',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
)
);
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015111600; // The current module version (Date: YYYYMMDDXX).
+$plugin->version = 2015111601; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2015111000; // Requires this Moodle version.
$plugin->component = 'mod_chat'; // Full name of the plugin (used for diagnostics).
$plugin->cron = 300;
'methodname' => 'get_choice_results',
'description' => 'Retrieve users results for a given choice.',
'type' => 'read',
- 'capabilities' => ''
+ 'capabilities' => '',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_choice_get_choice_options' => array(
'methodname' => 'get_choice_options',
'description' => 'Retrieve options for a specific choice.',
'type' => 'read',
- 'capabilities' => 'mod/choice:choose'
+ 'capabilities' => 'mod/choice:choose',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_choice_submit_choice_response' => array(
'methodname' => 'submit_choice_response',
'description' => 'Submit responses to a specific choice item.',
'type' => 'write',
- 'capabilities' => 'mod/choice:choose'
+ 'capabilities' => 'mod/choice:choose',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_choice_view_choice' => array(
'methodname' => 'view_choice',
'description' => 'Trigger the course module viewed event and update the module completion status.',
'type' => 'write',
- 'capabilities' => ''
+ 'capabilities' => '',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_choice_get_choices_by_courses' => array(
'description' => 'Returns a list of choice instances in a provided set of courses,
if no courses are provided then all the choice instances the user has access to will be returned.',
'type' => 'read',
- 'capabilities' => ''
+ 'capabilities' => '',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_choice_delete_choice_responses' => array(
'methodname' => 'delete_choice_responses',
'description' => 'Delete the given submitted responses in a choice',
'type' => 'write',
- 'capabilities' => 'mod/choice:choose'
+ 'capabilities' => 'mod/choice:choose',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
);
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2016020100; // The current module version (Date: YYYYMMDDXX)
+$plugin->version = 2016020101; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2015111000; // Requires this Moodle version
$plugin->component = 'mod_choice'; // Full name of the plugin (used for diagnostics)
$plugin->cron = 0;
'description' => 'Returns a list of database instances in a provided set of courses, if
no courses are provided then all the database instances the user has access to will be returned.',
'type' => 'read',
- 'capabilities' => 'mod/data:viewentry'
+ 'capabilities' => 'mod/data:viewentry',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
)
);
// editing an existing database entry
if ($formdata) {
$fieldname = 'field_' . $this->field->id . '_file';
- $itemid = $formdata->$fieldname;
+ $itemid = clean_param($formdata->$fieldname, PARAM_INT);
} else if ($recordid) {
if ($content = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))) {
}
// itemid element
- $html .= '<input type="hidden" name="field_'.$this->field->id.'_file" value="'.$itemid.'" />';
+ $html .= '<input type="hidden" name="field_'.$this->field->id.'_file" value="'.s($itemid).'" />';
$options = new stdClass();
$options->maxbytes = $this->field->param3;
function display_search_field($value = '') {
return '<label class="accesshide" for="f_' . $this->field->id . '">' . $this->field->name . '</label>' .
- '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.$value.'" />';
+ '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.s($value).'" />';
}
function generate_sql($tablealias, $value) {
function display_search_field($value = '') {
return '<label class="accesshide" for="f_'.$this->field->id.'">' . get_string('fieldname', 'data') . '</label>' .
- '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.$value.'" />';
+ '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.s($value).'" />';
}
function parse_search_field() {
if ($formdata) {
$fieldname = 'field_' . $this->field->id . '_file';
- $itemid = $formdata->$fieldname;
+ $itemid = clean_param($formdata->$fieldname, PARAM_INT);
$fieldname = 'field_' . $this->field->id . '_alttext';
if (isset($formdata->$fieldname)) {
$alttext = $formdata->$fieldname;
$str .= $output->render($fm);
$str .= '<div class="mdl-left">';
- $str .= '<input type="hidden" name="field_'.$this->field->id.'_file" value="'.$itemid.'" />';
+ $str .= '<input type="hidden" name="field_'.$this->field->id.'_file" value="'.s($itemid).'" />';
$str .= '<label for="field_'.$this->field->id.'_alttext">'.get_string('alttext','data') .'</label> <input type="text" name="field_'
.$this->field->id.'_alttext" id="field_'.$this->field->id.'_alttext" value="'.s($alttext).'" />';
$str .= '</div>';
function display_search_field($value = '') {
return '<label class="accesshide" for="f_'.$this->field->id.'">' . get_string('fieldname', 'data') . '</label>' .
- '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.$value.'" />';
+ '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.s($value).'" />';
}
function parse_search_field() {
var $type = 'text';
function display_search_field($value = '') {
- return '<label class="accesshide" for="f_' . $this->field->id . '">'. $this->field->name.'</label>' . '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.$value.'" />';
+ return '<label class="accesshide" for="f_' . $this->field->id . '">'. $this->field->name.'</label>' . '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.s($value).'" />';
}
function parse_search_field() {
}
$fieldname = 'field_' . $this->field->id . '_itemid';
if (isset($formdata->$fieldname)) {
- $draftitemid = $formdata->$fieldname;
+ $draftitemid = clean_param($formdata->$fieldname, PARAM_INT);
} else {
$draftitemid = file_get_unused_draft_itemid();
}
}
$editor->set_text($text);
$editor->use_editor($field, $options, $fpoptions);
- $str .= '<input type="hidden" name="'.$field.'_itemid" value="'.$draftitemid.'" />';
+ $str .= '<input type="hidden" name="'.$field.'_itemid" value="'.s($draftitemid).'" />';
$str .= '<div class="mod-data-input">';
$str .= '<div><textarea id="'.$field.'" name="'.$field.'" rows="'.$this->field->param3.'" cols="'.$this->field->param2.'" spellcheck="true">'.s($text).'</textarea></div>';
$str .= '<div><label class="accesshide" for="' . $field . '_content1">' . get_string('format') . '</label>';
function display_search_field($value = '') {
return '<label class="accesshide" for="f_' . $this->field->id . '">' . $this->field->name . '</label>' .
- '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.$value.'" />';
+ '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.s($value).'" />';
}
function parse_search_field() {
}
$str .= '</td><td>';
$str .= $label;
- $str .= '<input type="text" name="field_'.$this->field->id.'_0" id="'.$fieldid.'" value="'.$url.'" size="60" />';
+ $str .= '<input type="text" name="field_'.$this->field->id.'_0" id="'.$fieldid.'" value="'.s($url).'" size="60" />';
$str .= '<button id="filepicker-button-'.$options->client_id.'" style="display:none">'.$straddlink.'</button></td></tr>';
$str .= '<tr><td align="right"><span class="mod-data-input">'.get_string('text', 'data').':</span></td><td>';
$str .= '<input type="text" name="field_'.$this->field->id.'_1" id="field_'.$this->field->id.'_1" value="'.s($text).'"';
function display_search_field($value = '') {
return '<label class="accesshide" for="f_'.$this->field->id.'">' . get_string('fieldname', 'data') . '</label>' .
- '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.$value.'" />';
+ '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.s($value).'" />';
}
function parse_search_field() {
if ($this->field->param3) {
// param3 defines whether this URL should open in a new window.
$attributes['target'] = '_blank';
+ $attributes['rel'] = 'noreferrer';
}
if (empty($text)) {
$fn = !empty($search_array[DATA_FIRSTNAME]->data) ? $search_array[DATA_FIRSTNAME]->data : '';
$ln = !empty($search_array[DATA_LASTNAME]->data) ? $search_array[DATA_LASTNAME]->data : '';
$patterns[] = '/##firstname##/';
- $replacement[] = '<label class="accesshide" for="u_fn">'.get_string('authorfirstname', 'data').'</label><input type="text" size="16" id="u_fn" name="u_fn" value="'.$fn.'" />';
+ $replacement[] = '<label class="accesshide" for="u_fn">'.get_string('authorfirstname', 'data').'</label><input type="text" size="16" id="u_fn" name="u_fn" value="'.s($fn).'" />';
$patterns[] = '/##lastname##/';
- $replacement[] = '<label class="accesshide" for="u_ln">'.get_string('authorlastname', 'data').'</label><input type="text" size="16" id="u_ln" name="u_ln" value="'.$ln.'" />';
+ $replacement[] = '<label class="accesshide" for="u_ln">'.get_string('authorlastname', 'data').'</label><input type="text" size="16" id="u_ln" name="u_ln" value="'.s($ln).'" />';
// actual replacement of the tags
$newtext = preg_replace($patterns, $replacement, $data->asearchtemplate);
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015111600; // The current module version (Date: YYYYMMDDXX)
+$plugin->version = 2015111601; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2015111000; // Requires this Moodle version
$plugin->component = 'mod_data'; // Full name of the plugin (used for diagnostics)
$plugin->cron = 0;
$string['average'] = 'Average';
$string['bold'] = 'Bold';
$string['cancel_moving'] = 'Cancel moving';
+$string['cannotaccess'] = 'You can only access this feedback from a course';
$string['cannotmapfeedback'] = 'Database problem, unable to map feedback to course';
$string['cannotsavetempl'] = 'saving templates is not allowed';
$string['cannotunmap'] = 'Database problem, unable to unmap';
}
}
- return array_merge($feedbacks1, $feedbacks2);
+ $feedbacks = array_merge($feedbacks1, $feedbacks2);
+ $modinfo = get_fast_modinfo(SITEID);
+ return array_filter($feedbacks, function($f) use ($modinfo) {
+ return ($cm = $modinfo->get_cm($f->cmid)) && $cm->uservisible;
+ });
}
$feedback_complete_cap = true;
}
-if (isset($CFG->feedback_allowfullanonymous)
- AND $CFG->feedback_allowfullanonymous
+if (!empty($CFG->feedback_allowfullanonymous)
AND $course->id == SITEID
- AND (!$courseid OR $courseid == SITEID)
- AND $feedback->anonymous == FEEDBACK_ANONYMOUS_YES ) {
+ AND $feedback->anonymous == FEEDBACK_ANONYMOUS_YES
+ AND (!isloggedin() OR isguestuser())) {
+ // Guests are allowed to complete fully anonymous feedback without having 'mod/feedback:complete' capability.
$feedback_complete_cap = true;
}
$courseid = SITEID;
}
-//check whether the feedback is mapped to the given courseid
-if ($course->id == SITEID AND !has_capability('mod/feedback:edititems', $context)) {
- if ($DB->get_records('feedback_sitecourse_map', array('feedbackid'=>$feedback->id))) {
- $params = array('feedbackid'=>$feedback->id, 'courseid'=>$courseid);
- if (!$DB->get_record('feedback_sitecourse_map', $params)) {
- print_error('invalidcoursemodule');
- }
- }
-}
-
if ($feedback->anonymous != FEEDBACK_ANONYMOUS_YES) {
if ($course->id == SITEID) {
require_login($course, true);
}
}
+if ($course->id == SITEID) {
+ $PAGE->set_context($context);
+ $PAGE->set_cm($cm, $course);
+ $PAGE->set_pagelayout('incourse');
+}
+$PAGE->set_url('/mod/feedback/view.php', array('id'=>$cm->id, 'do_show'=>'view'));
+$PAGE->set_title($feedback->name);
+$PAGE->set_heading($course->fullname);
+
+// Check whether the feedback is mapped to the given courseid.
+if ($course->id == SITEID AND !has_capability('mod/feedback:edititems', $context)) {
+ if ($DB->get_records('feedback_sitecourse_map', array('feedbackid' => $feedback->id))) {
+ $params = array('feedbackid' => $feedback->id, 'courseid' => $courseid);
+ if (!$DB->get_record('feedback_sitecourse_map', $params)) {
+ if ($courseid == SITEID) {
+ echo $OUTPUT->header();
+ echo $OUTPUT->notification(get_string('cannotaccess', 'mod_feedback'));
+ echo $OUTPUT->footer();
+ exit;
+ } else {
+ print_error('invalidcoursemodule');
+ }
+ }
+ }
+}
+
//check whether the given courseid exists
if ($courseid AND $courseid != SITEID) {
if ($course2 = $DB->get_record('course', array('id'=>$courseid))) {
/// Print the page header
$strfeedbacks = get_string("modulenameplural", "feedback");
$strfeedback = get_string("modulename", "feedback");
-
-if ($course->id == SITEID) {
- $PAGE->set_context($context);
- $PAGE->set_cm($cm, $course); // set's up global $COURSE
- $PAGE->set_pagelayout('incourse');
-}
-$PAGE->set_url('/mod/feedback/view.php', array('id'=>$cm->id, 'do_show'=>'view'));
-$PAGE->set_title($feedback->name);
-$PAGE->set_heading($course->fullname);
echo $OUTPUT->header();
//ishidden check.
'methodname' => 'view_folder',
'description' => 'Simulate the view.php web interface folder: trigger events, completion, etc...',
'type' => 'write',
- 'capabilities' => 'mod/folder:view'
+ 'capabilities' => 'mod/folder:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
);
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2016020201; // The current module version (Date: YYYYMMDDXX)
+$plugin->version = 2016020202; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2015111000; // Requires this Moodle version
$plugin->component = 'mod_folder'; // Full name of the plugin (used for diagnostics)
$plugin->cron = 0;
no courses are provided then all the forum instances the user has access to will be
returned.',
'type' => 'read',
- 'capabilities' => 'mod/forum:viewdiscussion'
+ 'capabilities' => 'mod/forum:viewdiscussion',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_forum_get_forum_discussions' => array(
'classpath' => 'mod/forum/externallib.php',
'description' => 'Returns a list of forum posts for a discussion.',
'type' => 'read',
- 'capabilities' => 'mod/forum:viewdiscussion, mod/forum:viewqandawithoutposting'
+ 'capabilities' => 'mod/forum:viewdiscussion, mod/forum:viewqandawithoutposting',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_forum_get_forum_discussions_paginated' => array(
'classpath' => 'mod/forum/externallib.php',
'description' => 'Returns a list of forum discussions optionally sorted and paginated.',
'type' => 'read',
- 'capabilities' => 'mod/forum:viewdiscussion, mod/forum:viewqandawithoutposting'
+ 'capabilities' => 'mod/forum:viewdiscussion, mod/forum:viewqandawithoutposting',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_forum_view_forum' => array(
'classpath' => 'mod/forum/externallib.php',
'description' => 'Trigger the course module viewed event and update the module completion status.',
'type' => 'write',
- 'capabilities' => 'mod/forum:viewdiscussion'
+ 'capabilities' => 'mod/forum:viewdiscussion',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_forum_view_forum_discussion' => array(
'classpath' => 'mod/forum/externallib.php',
'description' => 'Trigger the forum discussion viewed event.',
'type' => 'write',
- 'capabilities' => 'mod/forum:viewdiscussion'
+ 'capabilities' => 'mod/forum:viewdiscussion',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_forum_add_discussion_post' => array(
'classpath' => 'mod/forum/externallib.php',
'description' => 'Create new posts into an existing discussion.',
'type' => 'write',
- 'capabilities' => 'mod/forum:replypost'
+ 'capabilities' => 'mod/forum:replypost',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_forum_add_discussion' => array(
'classpath' => 'mod/forum/externallib.php',
'description' => 'Add a new discussion into an existing forum.',
'type' => 'write',
- 'capabilities' => 'mod/forum:startdiscussion'
+ 'capabilities' => 'mod/forum:startdiscussion',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_forum_can_add_discussion' => array(
'methodname' => 'can_add_discussion',
'classpath' => 'mod/forum/externallib.php',
'description' => 'Check if the current user can add discussions in the given forum (and optionally for the given group).',
- 'type' => 'read'
+ 'type' => 'read',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
);
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015120801; // The current module version (Date: YYYYMMDDXX)
+$plugin->version = 2015120802; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2015111000; // Requires this Moodle version
$plugin->component = 'mod_forum'; // Full name of the plugin (used for diagnostics)
'methodname' => 'get_glossaries_by_courses',
'description' => 'Retrieve a list of glossaries from several courses.',
'type' => 'read',
- 'capabilities' => 'mod/glossary:view'
+ 'capabilities' => 'mod/glossary:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_glossary_view_glossary' => array(
'methodname' => 'view_glossary',
'description' => 'Notify the glossary as being viewed.',
'type' => 'write',
- 'capabilities' => 'mod/glossary:view'
+ 'capabilities' => 'mod/glossary:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_glossary_view_entry' => array(
'methodname' => 'view_entry',
'description' => 'Notify a glossary entry as being viewed.',
'type' => 'write',
- 'capabilities' => 'mod/glossary:view'
+ 'capabilities' => 'mod/glossary:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_glossary_get_entries_by_letter' => array(
'methodname' => 'get_entries_by_letter',
'description' => 'Browse entries by letter.',
'type' => 'read',
- 'capabilities' => 'mod/glossary:view'
+ 'capabilities' => 'mod/glossary:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_glossary_get_entries_by_date' => array(
'methodname' => 'get_entries_by_date',
'description' => 'Browse entries by date.',
'type' => 'read',
- 'capabilities' => 'mod/glossary:view'
+ 'capabilities' => 'mod/glossary:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_glossary_get_categories' => array(
'methodname' => 'get_categories',
'description' => 'Get the categories.',
'type' => 'read',
- 'capabilities' => 'mod/glossary:view'
+ 'capabilities' => 'mod/glossary:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_glossary_get_entries_by_category' => array(
'methodname' => 'get_entries_by_category',
'description' => 'Browse entries by category.',
'type' => 'read',
- 'capabilities' => 'mod/glossary:view'
+ 'capabilities' => 'mod/glossary:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_glossary_get_authors' => array(
'methodname' => 'get_authors',
'description' => 'Get the authors.',
'type' => 'read',
- 'capabilities' => 'mod/glossary:view'
+ 'capabilities' => 'mod/glossary:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_glossary_get_entries_by_author' => array(
'methodname' => 'get_entries_by_author',
'description' => 'Browse entries by author.',
'type' => 'read',
- 'capabilities' => 'mod/glossary:view'
+ 'capabilities' => 'mod/glossary:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_glossary_get_entries_by_author_id' => array(
'methodname' => 'get_entries_by_author_id',
'description' => 'Browse entries by author ID.',
'type' => 'read',
- 'capabilities' => 'mod/glossary:view'
+ 'capabilities' => 'mod/glossary:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_glossary_get_entries_by_search' => array(
'methodname' => 'get_entries_by_search',
'description' => 'Browse entries by search query.',
'type' => 'read',
- 'capabilities' => 'mod/glossary:view'
+ 'capabilities' => 'mod/glossary:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_glossary_get_entries_by_term' => array(
'methodname' => 'get_entries_by_term',
'description' => 'Browse entries by term (concept or alias).',
'type' => 'read',
- 'capabilities' => 'mod/glossary:view'
+ 'capabilities' => 'mod/glossary:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_glossary_get_entries_to_approve' => array(
'methodname' => 'get_entries_to_approve',
'description' => 'Browse entries to be approved.',
'type' => 'read',
- 'capabilities' => 'mod/glossary:approve'
+ 'capabilities' => 'mod/glossary:approve',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_glossary_get_entry_by_id' => array(
'methodname' => 'get_entry_by_id',
'description' => 'Get an entry by ID',
'type' => 'read',
- 'capabilities' => 'mod/glossary:view'
+ 'capabilities' => 'mod/glossary:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
);
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015111605; // The current module version (Date: YYYYMMDDXX)
+$plugin->version = 2015111606; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2015111000; // Requires this Moodle version
$plugin->component = 'mod_glossary'; // Full name of the plugin (used for diagnostics)
$plugin->cron = 0;
'methodname' => 'view_imscp',
'description' => 'Simulate the view.php web interface imscp: trigger events, completion, etc...',
'type' => 'write',
- 'capabilities' => 'mod/imscp:view'
+ 'capabilities' => 'mod/imscp:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_imscp_get_imscps_by_courses' => array(
'description' => 'Returns a list of IMSCP instances in a provided set of courses,
if no courses are provided then all the IMSCP instances the user has access to will be returned.',
'type' => 'read',
- 'capabilities' => 'moodle/imscp:view'
+ 'capabilities' => 'mod/imscp:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
);
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015111600; // The current module version (Date: YYYYMMDDXX).
+$plugin->version = 2015111601; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2015111000; // Requires this Moodle version.
$plugin->component = 'mod_imscp'; // Full name of the plugin (used for diagnostics).
$plugin->cron = 0;
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Exception handler for LTI services
+ *
+ * @package mod_lti
+ * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_lti;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__.'/../locallib.php');
+require_once(__DIR__.'/../servicelib.php');
+
+/**
+ * Handles exceptions when handling incoming LTI messages.
+ *
+ * Ensures that LTI always returns a XML message that can be consumed by the caller.
+ *
+ * @package mod_lti
+ * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class service_exception_handler {
+ /**
+ * Enable error response logging.
+ *
+ * @var bool
+ */
+ protected $log = false;
+
+ /**
+ * The LTI service message ID, if known.
+ *
+ * @var string
+ */
+ protected $id = '';
+
+ /**
+ * The LTI service message type, if known.
+ *
+ * @var string
+ */
+ protected $type = 'unknownRequest';
+
+ /**
+ * Constructor.
+ *
+ * @param boolean $log Enable error response logging.
+ */
+ public function __construct($log) {
+ $this->log = $log;
+ }
+
+ /**
+ * Set the LTI message ID being handled.
+ *
+ * @param string $id
+ */
+ public function set_message_id($id) {
+ if (!empty($id)) {
+ $this->id = $id;
+ }
+ }
+
+ /**
+ * Set the LTI message type being handled.
+ *
+ * @param string $type
+ */
+ public function set_message_type($type) {
+ if (!empty($type)) {
+ $this->type = $type;
+ }
+ }
+
+ /**
+ * Echo an exception message encapsulated in XML.
+ *
+ * @param \Exception $exception The exception that was thrown
+ */
+ public function handle(\Exception $exception) {
+ $message = $exception->getMessage();
+
+ // Add the exception backtrace for developers.
+ if (debugging('', DEBUG_DEVELOPER)) {
+ $message .= "\n".format_backtrace(get_exception_info($exception)->backtrace, true);
+ }
+
+ // Switch to response.
+ $type = str_replace('Request', 'Response', $this->type);
+
+ // Build the appropriate xml.
+ $response = lti_get_response_xml('failure', $message, $this->id, $type);
+
+ $xml = $response->asXML();
+
+ // Log the request if necessary.
+ if ($this->log) {
+ lti_log_response($xml, $exception);
+ }
+
+ echo $xml;
+ }
+}
'methodname' => 'get_tool_launch_data',
'description' => 'Return the launch data for a given external tool.',
'type' => 'read',
- 'capabilities' => 'mod/lti:view'
+ 'capabilities' => 'mod/lti:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_lti_get_ltis_by_courses' => array(
'description' => 'Returns a list of external tool instances in a provided set of courses, if
no courses are provided then all the external tool instances the user has access to will be returned.',
'type' => 'read',
- 'capabilities' => 'mod/lti:view'
+ 'capabilities' => 'mod/lti:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_lti_view_lti' => array(
'methodname' => 'view_lti',
'description' => 'Trigger the course module viewed event and update the module completion status.',
'type' => 'write',
- 'capabilities' => 'mod/lti:view'
+ 'capabilities' => 'mod/lti:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
);
}
/**
- * Logs the request to a file in temp dir
+ * Logs the request to a file in temp dir.
*
* @param string $rawbody
*/
function lti_log_request($rawbody) {
if ($tempdir = make_temp_directory('mod_lti', false)) {
if ($tempfile = tempnam($tempdir, 'mod_lti_request'.date('YmdHis'))) {
- file_put_contents($tempfile, $rawbody);
+ $content = "Request Headers:\n";
+ foreach (moodle\mod\lti\OAuthUtil::get_headers() as $header => $value) {
+ $content .= "$header: $value\n";
+ }
+ $content .= "Request Body:\n";
+ $content .= $rawbody;
+
+ file_put_contents($tempfile, $content);
+ chmod($tempfile, 0644);
+ }
+ }
+}
+
+/**
+ * Log an LTI response.
+ *
+ * @param string $responsexml The response XML
+ * @param Exception $e If there was an exception, pass that too
+ */
+function lti_log_response($responsexml, $e = null) {
+ if ($tempdir = make_temp_directory('mod_lti', false)) {
+ if ($tempfile = tempnam($tempdir, 'mod_lti_response'.date('YmdHis'))) {
+ $content = '';
+ if ($e instanceof Exception) {
+ $info = get_exception_info($e);
+
+ $content .= "Exception:\n";
+ $content .= "Message: $info->message\n";
+ $content .= "Debug info: $info->debuginfo\n";
+ $content .= "Backtrace:\n";
+ $content .= format_backtrace($info->backtrace, true);
+ $content .= "\n";
+ }
+ $content .= "Response XML:\n";
+ $content .= $responsexml;
+
+ file_put_contents($tempfile, $content);
chmod($tempfile, 0644);
}
}
require_once($CFG->dirroot.'/mod/lti/servicelib.php');
// TODO: Switch to core oauthlib once implemented - MDL-30149.
+use mod_lti\service_exception_handler;
use moodle\mod\lti as lti;
$rawbody = file_get_contents("php://input");
-if (lti_should_log_request($rawbody)) {
+$logrequests = lti_should_log_request($rawbody);
+$errorhandler = new service_exception_handler($logrequests);
+
+// Register our own error handler so we can always send valid XML response.
+set_exception_handler(array($errorhandler, 'handle'));
+
+if ($logrequests) {
lti_log_request($rawbody);
}
$messagetype = $child->getName();
}
+// We know more about the message, update error handler to send better errors.
+$errorhandler->set_message_id(lti_parse_message_id($xml));
+$errorhandler->set_message_type($messagetype);
+
switch ($messagetype) {
case 'replaceResultRequest':
- try {
- $parsed = lti_parse_grade_replace_message($xml);
- } catch (Exception $e) {
- $responsexml = lti_get_response_xml(
- 'failure',
- $e->getMessage(),
- uniqid(),
- 'replaceResultResponse');
-
- echo $responsexml->asXML();
- break;
- }
+ $parsed = lti_parse_grade_replace_message($xml);
$ltiinstance = $DB->get_record('lti', array('id' => $parsed->instanceid));
$gradestatus = lti_update_grade($ltiinstance, $parsed->userid, $parsed->launchid, $parsed->gradeval);
+ if (!$gradestatus) {
+ throw new Exception('Grade replace response');
+ }
+
$responsexml = lti_get_response_xml(
- $gradestatus ? 'success' : 'failure',
+ 'success',
'Grade replace response',
$parsed->messageid,
'replaceResultResponse'
$gradestatus = lti_delete_grade($ltiinstance, $parsed->userid);
+ if (!$gradestatus) {
+ throw new Exception('Grade delete request');
+ }
+
$responsexml = lti_get_response_xml(
- $gradestatus ? 'success' : 'failure',
+ 'success',
'Grade delete request',
$parsed->messageid,
'deleteResultResponse'
}
function lti_parse_message_id($xml) {
+ if (empty($xml->imsx_POXHeader)) {
+ return '';
+ }
+
$node = $xml->imsx_POXHeader->imsx_POXRequestHeaderInfo->imsx_messageIdentifier;
$messageid = (string)$node;
if (!empty($ltigrade) && isset($grades) && isset($grades->items[0]) && is_array($grades->items[0]->grades)) {
foreach ($grades->items[0]->grades as $agrade) {
$grade = $agrade->grade;
- $grade = $grade / $ltigrade;
- break;
+ if (isset($grade)) {
+ return $grade / $ltigrade;
+ }
}
}
-
- if (isset($grade)) {
- return $grade;
- }
}
function lti_delete_grade($ltiinstance, $userid) {
function lti_extend_lti_services($data) {
$plugins = get_plugin_list_with_function('ltisource', $data->messagetype);
if (!empty($plugins)) {
- try {
- // There can only be one.
- if (count($plugins) > 1) {
- throw new coding_exception('More than one ltisource plugin handler found');
- }
- $data->xml = new SimpleXMLElement($data->body);
- $callback = current($plugins);
- call_user_func($callback, $data);
- } catch (moodle_exception $e) {
- $error = $e->getMessage();
- if (debugging('', DEBUG_DEVELOPER)) {
- $error .= ' '.format_backtrace(get_exception_info($e)->backtrace);
- }
- $responsexml = lti_get_response_xml(
- 'failure',
- $error,
- $data->messageid,
- $data->messagetype
- );
-
- header('HTTP/1.0 400 bad request');
- echo $responsexml->asXML();
+ // There can only be one.
+ if (count($plugins) > 1) {
+ throw new coding_exception('More than one ltisource plugin handler found');
}
+ $data->xml = new SimpleXMLElement($data->body);
+ $callback = current($plugins);
+ call_user_func($callback, $data);
+
return true;
}
return false;
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tests Exception handler for LTI services
+ *
+ * @package mod_lti
+ * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use mod_lti\service_exception_handler;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tests Exception handler for LTI services
+ *
+ * @package mod_lti
+ * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_lti_service_exception_handler_testcase extends advanced_testcase {
+ /**
+ * Testing service error handling.
+ */
+ public function test_handle() {
+ $handler = new service_exception_handler(false);
+ $handler->set_message_id('123');
+ $handler->set_message_type('testRequest');
+ $handler->handle(new Exception('Error happened'));
+
+ $this->expectOutputRegex('/imsx_codeMajor>failure/');
+ $this->expectOutputRegex('/imsx_description>Error happened/');
+ $this->expectOutputRegex('/imsx_messageRefIdentifier>123/');
+ $this->expectOutputRegex('/imsx_operationRefIdentifier>testRequest/');
+ $this->expectOutputRegex('/imsx_POXBody><testResponse/');
+ }
+
+ /**
+ * Testing service error handling when message ID and type are not known yet.
+ */
+ public function test_handle_early_error() {
+ $handler = new service_exception_handler(false);
+ $handler->handle(new Exception('Error happened'));
+
+ $this->expectOutputRegex('/imsx_codeMajor>failure/');
+ $this->expectOutputRegex('/imsx_description>Error happened/');
+ $this->expectOutputRegex('/imsx_messageRefIdentifier\/>/');
+ $this->expectOutputRegex('/imsx_operationRefIdentifier>unknownRequest/');
+ $this->expectOutputRegex('/imsx_POXBody><unknownResponse/');
+ }
+
+ /**
+ * Testing that a log file is generated when logging is turned on.
+ */
+ public function test_handle_log() {
+ global $CFG;
+
+ $this->resetAfterTest();
+
+ $handler = new service_exception_handler(true);
+
+ ob_start();
+ $handler->handle(new Exception('Error happened'));
+ ob_end_clean();
+
+ $this->assertTrue(is_dir($CFG->dataroot.'/temp/mod_lti'));
+ $files = glob($CFG->dataroot.'/temp/mod_lti/mod_lti_response*');
+ $this->assertEquals(1, count($files));
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tests for servicelib.php
+ *
+ * @package mod_lti
+ * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot.'/mod/lti/servicelib.php');
+
+/**
+ * Tests for servicelib.php
+ *
+ * @package mod_lti
+ * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_lti_servicelib_testcase extends basic_testcase {
+ /**
+ * Test that lti_parse_message_id never fails with good and bad XML.
+ *
+ * @dataProvider message_id_provider
+ * @param mixed $expected Expected message ID.
+ * @param string $xml XML to parse.
+ */
+ public function test_lti_parse_message_id($expected, $xml) {
+ $xml = simplexml_load_string($xml);
+ $this->assertEquals($expected, lti_parse_message_id($xml));
+ }
+
+ /**
+ * Test data provider for testing lti_parse_message_id
+ *
+ * @return array
+ */
+ public function message_id_provider() {
+ $valid = <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<imsx_POXEnvelopeRequest xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
+ <imsx_POXHeader>
+ <imsx_POXRequestHeaderInfo>
+ <imsx_version>V1.0</imsx_version>
+ <imsx_messageIdentifier>9999</imsx_messageIdentifier>
+ </imsx_POXRequestHeaderInfo>
+ </imsx_POXHeader>
+ <imsx_POXBody/>
+</imsx_POXEnvelopeRequest>
+XML;
+
+ $noheader = <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<imsx_POXEnvelopeRequest xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
+ <badXmlHere>
+ <imsx_POXRequestHeaderInfo>
+ <imsx_version>V1.0</imsx_version>
+ <imsx_messageIdentifier>9999</imsx_messageIdentifier>
+ </imsx_POXRequestHeaderInfo>
+ </badXmlHere>
+ <imsx_POXBody/>
+</imsx_POXEnvelopeRequest>
+XML;
+
+ $noinfo = <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<imsx_POXEnvelopeRequest xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
+ <imsx_POXHeader>
+ <badXmlHere>
+ <imsx_version>V1.0</imsx_version>
+ <imsx_messageIdentifier>9999</imsx_messageIdentifier>
+ </badXmlHere>
+ </imsx_POXHeader>
+ <imsx_POXBody/>
+</imsx_POXEnvelopeRequest>
+XML;
+
+ $noidentifier = <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<imsx_POXEnvelopeRequest xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
+ <imsx_POXHeader>
+ <imsx_POXRequestHeaderInfo>
+ <imsx_version>V1.0</imsx_version>
+ </imsx_POXRequestHeaderInfo>
+ </imsx_POXHeader>
+ <imsx_POXBody/>
+</imsx_POXEnvelopeRequest>
+XML;
+
+ return array(
+ array(9999, $valid),
+ array('', $noheader),
+ array('', $noinfo),
+ array('', $noidentifier),
+ );
+ }
+}
\ No newline at end of file
This files describes API changes in the lti code.
+=== 3.1 ===
+
+* When requesting a grade from the outcome service, and empty grade will now be returned as
+ an empty grade in accordance with the spec. Previously a grade of 0 would be returned.
+
=== 3.0 ===
* Function lti_view (locallib.php) was renamed to lti_launch_tool.
defined('MOODLE_INTERNAL') || die;
-$plugin->version = 2015111600; // The current module version (Date: YYYYMMDDXX).
+$plugin->version = 2015111601; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2015111000; // Requires this Moodle version.
$plugin->component = 'mod_lti'; // Full name of the plugin (used for diagnostics).
$plugin->cron = 0;
'methodname' => 'view_page',
'description' => 'Simulate the view.php web interface page: trigger events, completion, etc...',
'type' => 'write',
- 'capabilities' => 'mod/page:view'
+ 'capabilities' => 'mod/page:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
);
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015111600; // The current module version (Date: YYYYMMDDXX)
+$plugin->version = 2015111601; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2015111000; // Requires this Moodle version
$plugin->component = 'mod_page'; // Full name of the plugin (used for diagnostics)
$plugin->cron = 0;
);
}
+ /**
+ * Describes the parameters for get_user_attempts.
+ *
+ * @return external_external_function_parameters
+ * @since Moodle 3.1
+ */
+ public static function get_user_attempts_parameters() {
+ return new external_function_parameters (
+ array(
+ 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
+ 'userid' => new external_value(PARAM_INT, 'user id, empty for current user', VALUE_DEFAULT, 0),
+ 'status' => new external_value(PARAM_ALPHA, 'quiz status: all, finished or unfinished', VALUE_DEFAULT, 'finished'),
+ 'includepreviews' => new external_value(PARAM_BOOL, 'whether to include previews or not', VALUE_DEFAULT, false),
+
+ )
+ );
+ }
+
+ /**
+ * Return a list of attempts for the given quiz and user.
+ *
+ * @param int $quizid quiz instance id
+ * @param int $userid user id
+ * @param string $status quiz status: all, finished or unfinished
+ * @param bool $includepreviews whether to include previews or not
+ * @return array of warnings and the list of attempts
+ * @since Moodle 3.1
+ * @throws invalid_parameter_exception
+ */
+ public static function get_user_attempts($quizid, $userid = 0, $status = 'finished', $includepreviews = false) {
+ global $DB, $USER;
+
+ $warnings = array();
+
+ $params = array(
+ 'quizid' => $quizid,
+ 'userid' => $userid,
+ 'status' => $status,
+ 'includepreviews' => $includepreviews,
+ );
+ $params = self::validate_parameters(self::get_user_attempts_parameters(), $params);
+
+ // Request and permission validation.
+ $quiz = $DB->get_record('quiz', array('id' => $params['quizid']), '*', MUST_EXIST);
+ list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
+
+ $context = context_module::instance($cm->id);
+ self::validate_context($context);
+
+ if (!in_array($params['status'], array('all', 'finished', 'unfinished'))) {
+ throw new invalid_parameter_exception('Invalid status value');
+ }
+
+ // Default value for userid.
+ if (empty($params['userid'])) {
+ $params['userid'] = $USER->id;
+ }
+
+ $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
+ core_user::require_active_user($user);
+
+ // Extra checks so only users with permissions can view other users attempts.
+ if ($USER->id != $user->id) {
+ require_capability('mod/quiz:viewreports', $context);
+ }
+
+ $attempts = quiz_get_user_attempts($quiz->id, $user->id, $params['status'], $params['includepreviews']);
+
+ $result = array();
+ $result['attempts'] = $attempts;
+ $result['warnings'] = $warnings;
+ return $result;
+ }
+
+ /**
+ * Describes the get_user_attempts return value.
+ *
+ * @return external_single_structure
+ * @since Moodle 3.1
+ */
+ public static function get_user_attempts_returns() {
+ return new external_single_structure(
+ array(
+ 'attempts' => new external_multiple_structure(
+ new external_single_structure(
+ array(
+ 'id' => new external_value(PARAM_INT, 'Attempt id.', VALUE_OPTIONAL),
+ 'quiz' => new external_value(PARAM_INT, 'Foreign key reference to the quiz that was attempted.',
+ VALUE_OPTIONAL),
+ 'userid' => new external_value(PARAM_INT, 'Foreign key reference to the user whose attempt this is.',
+ VALUE_OPTIONAL),
+ 'attempt' => new external_value(PARAM_INT, 'Sequentially numbers this students attempts at this quiz.',
+ VALUE_OPTIONAL),
+ 'uniqueid' => new external_value(PARAM_INT, 'Foreign key reference to the question_usage that holds the
+ details of the the question_attempts that make up this quiz
+ attempt.', VALUE_OPTIONAL),
+ 'layout' => new external_value(PARAM_RAW, 'Attempt layout.', VALUE_OPTIONAL),
+ 'currentpage' => new external_value(PARAM_INT, 'Attempt current page.', VALUE_OPTIONAL),
+ 'preview' => new external_value(PARAM_INT, 'Whether is a preview attempt or not.', VALUE_OPTIONAL),
+ 'state' => new external_value(PARAM_ALPHA, 'The current state of the attempts. \'inprogress\',
+ \'overdue\', \'finished\' or \'abandoned\'.', VALUE_OPTIONAL),
+ 'timestart' => new external_value(PARAM_INT, 'Time when the attempt was started.', VALUE_OPTIONAL),
+ 'timefinish' => new external_value(PARAM_INT, 'Time when the attempt was submitted.
+ 0 if the attempt has not been submitted yet.', VALUE_OPTIONAL),
+ 'timemodified' => new external_value(PARAM_INT, 'Last modified time.', VALUE_OPTIONAL),
+ 'timecheckstate' => new external_value(PARAM_INT, 'Next time quiz cron should check attempt for
+ state changes. NULL means never check.', VALUE_OPTIONAL),
+ 'sumgrades' => new external_value(PARAM_FLOAT, 'Total marks for this attempt.', VALUE_OPTIONAL),
+ )
+ )
+ ),
+ 'warnings' => new external_warnings(),
+ )
+ );
+ }
+
+ /**
+ * Describes the parameters for get_user_best_grade.
+ *
+ * @return external_external_function_parameters
+ * @since Moodle 3.1
+ */
+ public static function get_user_best_grade_parameters() {
+ return new external_function_parameters (
+ array(
+ 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
+ 'userid' => new external_value(PARAM_INT, 'user id', VALUE_DEFAULT, 0),
+ )
+ );
+ }
+
+ /**
+ * Get the best current grade for the given user on a quiz.
+ *
+ * @param int $quizid quiz instance id
+ * @param int $userid user id
+ * @return array of warnings and the grade information
+ * @since Moodle 3.1
+ */
+ public static function get_user_best_grade($quizid, $userid = 0) {
+ global $DB, $USER;
+
+ $warnings = array();
+
+ $params = array(
+ 'quizid' => $quizid,
+ 'userid' => $userid,
+ );
+ $params = self::validate_parameters(self::get_user_best_grade_parameters(), $params);
+
+ // Request and permission validation.
+ $quiz = $DB->get_record('quiz', array('id' => $params['quizid']), '*', MUST_EXIST);
+ list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
+
+ $context = context_module::instance($cm->id);
+ self::validate_context($context);
+
+ // Default value for userid.
+ if (empty($params['userid'])) {
+ $params['userid'] = $USER->id;
+ }
+
+ $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
+ core_user::require_active_user($user);
+
+ // Extra checks so only users with permissions can view other users attempts.
+ if ($USER->id != $user->id) {
+ require_capability('mod/quiz:viewreports', $context);
+ }
+
+ $result = array();
+ $grade = quiz_get_best_grade($quiz, $user->id);
+
+ if ($grade === null) {
+ $result['hasgrade'] = false;
+ } else {
+ $result['hasgrade'] = true;
+ $result['grade'] = $grade;
+ }
+ $result['warnings'] = $warnings;
+ return $result;
+ }
+
+ /**
+ * Describes the get_user_best_grade return value.
+ *
+ * @return external_single_structure
+ * @since Moodle 3.1
+ */
+ public static function get_user_best_grade_returns() {
+ return new external_single_structure(
+ array(
+ 'hasgrade' => new external_value(PARAM_BOOL, 'Whether the user has a grade on the given quiz.'),
+ 'grade' => new external_value(PARAM_FLOAT, 'The grade (only if the user has a grade).', VALUE_OPTIONAL),
+ 'warnings' => new external_warnings(),
+ )
+ );
+ }
+
+ /**
+ * Describes the parameters for get_combined_review_options.
+ *
+ * @return external_external_function_parameters
+ * @since Moodle 3.1
+ */
+ public static function get_combined_review_options_parameters() {
+ return new external_function_parameters (
+ array(
+ 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
+ 'userid' => new external_value(PARAM_INT, 'user id (empty for current user)', VALUE_DEFAULT, 0),
+
+ )
+ );
+ }
+
+ /**
+ * Combines the review options from a number of different quiz attempts.
+ *
+ * @param int $quizid quiz instance id
+ * @param int $userid user id (empty for current user)
+ * @return array of warnings and the review options
+ * @since Moodle 3.1
+ */
+ public static function get_combined_review_options($quizid, $userid = 0) {
+ global $DB, $USER;
+
+ $warnings = array();
+
+ $params = array(
+ 'quizid' => $quizid,
+ 'userid' => $userid,
+ );
+ $params = self::validate_parameters(self::get_combined_review_options_parameters(), $params);
+
+ // Request and permission validation.
+ $quiz = $DB->get_record('quiz', array('id' => $params['quizid']), '*', MUST_EXIST);
+ list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
+
+ $context = context_module::instance($cm->id);
+ self::validate_context($context);
+
+ // Default value for userid.
+ if (empty($params['userid'])) {
+ $params['userid'] = $USER->id;
+ }
+
+ $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
+ core_user::require_active_user($user);
+
+ // Extra checks so only users with permissions can view other users attempts.
+ if ($USER->id != $user->id) {
+ require_capability('mod/quiz:viewreports', $context);
+ }
+
+ $attempts = quiz_get_user_attempts($quiz->id, $user->id, 'all', true);
+
+ $result = array();
+ $result['someoptions'] = [];
+ $result['alloptions'] = [];
+
+ list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts);
+
+ foreach (array('someoptions', 'alloptions') as $typeofoption) {
+ foreach ($$typeofoption as $key => $value) {
+ $result[$typeofoption][] = array(
+ "name" => $key,
+ "value" => (!empty($value)) ? $value : 0
+ );
+ }
+ }
+
+ $result['warnings'] = $warnings;
+ return $result;
+ }
+
+ /**
+ * Describes the get_combined_review_options return value.
+ *
+ * @return external_single_structure
+ * @since Moodle 3.1
+ */
+ public static function get_combined_review_options_returns() {
+ return new external_single_structure(
+ array(
+ 'someoptions' => new external_multiple_structure(
+ new external_single_structure(
+ array(
+ 'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'),
+ 'value' => new external_value(PARAM_INT, 'option value'),
+ )
+ )
+ ),
+ 'alloptions' => new external_multiple_structure(
+ new external_single_structure(
+ array(
+ 'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'),
+ 'value' => new external_value(PARAM_INT, 'option value'),
+ )
+ )
+ ),
+ 'warnings' => new external_warnings(),
+ )
+ );
+ }
+
}
'description' => 'Returns a list of quizzes in a provided list of courses,
if no list is provided all quizzes that the user can view will be returned.',
'type' => 'read',
- 'capabilities' => 'mod/quiz:view'
+ 'capabilities' => 'mod/quiz:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_quiz_view_quiz' => array(
'methodname' => 'view_quiz',
'description' => 'Trigger the course module viewed event and update the module completion status.',
'type' => 'write',
- 'capabilities' => 'mod/quiz:view'
+ 'capabilities' => 'mod/quiz:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+ ),
+
+ 'mod_quiz_get_user_attempts' => array(
+ 'classname' => 'mod_quiz_external',
+ 'methodname' => 'get_user_attempts',
+ 'description' => 'Return a list of attempts for the given quiz and user.',
+ 'type' => 'read',
+ 'capabilities' => 'mod/quiz:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+ ),
+
+ 'mod_quiz_get_user_best_grade' => array(
+ 'classname' => 'mod_quiz_external',
+ 'methodname' => 'get_user_best_grade',
+ 'description' => 'Get the best current grade for the given user on a quiz.',
+ 'type' => 'read',
+ 'capabilities' => 'mod/quiz:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+ ),
+
+ 'mod_quiz_get_combined_review_options' => array(
+ 'classname' => 'mod_quiz_external',
+ 'methodname' => 'get_combined_review_options',
+ 'description' => 'Combines the review options from a number of different quiz attempts.',
+ 'type' => 'read',
+ 'capabilities' => 'mod/quiz:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
);
$someoptions->marks = question_display_options::HIDDEN;
$alloptions->marks = question_display_options::MARK_AND_MAX;
+ // This shouldn't happen, but we need to prevent reveal information.
+ if (empty($attempts)) {
+ return array($someoptions, $someoptions);
+ }
+
foreach ($attempts as $attempt) {
$attemptoptions = mod_quiz_display_options::make_from_quiz($quiz,
quiz_attempt_state($quiz, $attempt));
}
+ /**
+ * Test get_user_attempts
+ */
+ public function test_get_user_attempts() {
+
+ // Create a new quiz with attempts.
+ $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
+ $data = array('course' => $this->course->id,
+ 'sumgrades' => 1);
+ $quiz = $quizgenerator->create_instance($data);
+
+ // Create a couple of questions.
+ $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
+
+ $cat = $questiongenerator->create_question_category();
+ $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
+ quiz_add_quiz_question($question->id, $quiz);
+
+ $quizobj = quiz::create($quiz->id, $this->student->id);
+
+ // Set grade to pass.
+ $item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod',
+ 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null));
+ $item->gradepass = 80;
+ $item->update();
+
+ // Start the passing attempt.
+ $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
+ $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
+
+ $timenow = time();
+ $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id);
+ quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
+ quiz_attempt_save_started($quizobj, $quba, $attempt);
+
+ // Process some responses from the student.
+ $attemptobj = quiz_attempt::create($attempt->id);
+ $tosubmit = array(1 => array('answer' => '3.14'));
+ $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
+
+ // Finish the attempt.
+ $attemptobj = quiz_attempt::create($attempt->id);
+ $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
+ $attemptobj->process_finish($timenow, false);
+
+ $this->setUser($this->student);
+ $result = mod_quiz_external::get_user_attempts($quiz->id);
+ $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
+
+ $this->assertCount(1, $result['attempts']);
+ $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
+ $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
+ $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
+ $this->assertEquals(1, $result['attempts'][0]['attempt']);
+
+ // Test filters. Only finished.
+ $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'finished', false);
+ $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
+
+ $this->assertCount(1, $result['attempts']);
+ $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
+
+ // Test filters. All attempts.
+ $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false);
+ $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
+
+ $this->assertCount(1, $result['attempts']);
+ $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
+
+ // Test filters. Unfinished.
+ $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false);
+ $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
+
+ $this->assertCount(0, $result['attempts']);
+
+ // Start a new attempt, but not finish it.
+ $timenow = time();
+ $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
+ $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
+ $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
+
+ quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
+ quiz_attempt_save_started($quizobj, $quba, $attempt);
+
+ // Test filters. All attempts.
+ $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false);
+ $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
+
+ $this->assertCount(2, $result['attempts']);
+
+ // Test filters. Unfinished.
+ $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false);
+ $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
+
+ $this->assertCount(1, $result['attempts']);
+
+ // Test manager can see user attempts.
+ $this->setUser($this->teacher);
+ $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id);
+ $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
+
+ $this->assertCount(1, $result['attempts']);
+ $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
+
+ $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'all');
+ $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
+
+ $this->assertCount(2, $result['attempts']);
+ $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
+
+ // Invalid parameters.
+ try {
+ mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'INVALID_PARAMETER');
+ $this->fail('Exception expected due to missing capability.');
+ } catch (invalid_parameter_exception $e) {
+ $this->assertEquals('invalidparameter', $e->errorcode);
+ }
+ }
+
+ /**
+ * Test get_user_best_grade
+ */
+ public function test_get_user_best_grade() {
+ global $DB;
+
+ $this->setUser($this->student);
+
+ $result = mod_quiz_external::get_user_best_grade($this->quiz->id);
+ $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
+
+ // No grades yet.
+ $this->assertFalse($result['hasgrade']);
+ $this->assertTrue(!isset($result['grade']));
+
+ $grade = new stdClass();
+ $grade->quiz = $this->quiz->id;
+ $grade->userid = $this->student->id;
+ $grade->grade = 8.9;
+ $grade->timemodified = time();
+ $grade->id = $DB->insert_record('quiz_grades', $grade);
+
+ $result = mod_quiz_external::get_user_best_grade($this->quiz->id);
+ $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
+
+ // Now I have grades.
+ $this->assertTrue($result['hasgrade']);
+ $this->assertEquals(8.9, $result['grade']);
+
+ // We should not see other users grades.
+ $anotherstudent = self::getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual');
+
+ try {
+ mod_quiz_external::get_user_best_grade($this->quiz->id, $anotherstudent->id);
+ $this->fail('Exception expected due to missing capability.');
+ } catch (required_capability_exception $e) {
+ $this->assertEquals('nopermissions', $e->errorcode);
+ }
+
+ // Teacher must be able to see student grades.
+ $this->setUser($this->teacher);
+
+ $result = mod_quiz_external::get_user_best_grade($this->quiz->id, $this->student->id);
+ $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
+
+ $this->assertTrue($result['hasgrade']);
+ $this->assertEquals(8.9, $result['grade']);
+
+ // Invalid user.
+ try {
+ mod_quiz_external::get_user_best_grade($this->quiz->id, -1);
+ $this->fail('Exception expected due to missing capability.');
+ } catch (dml_missing_record_exception $e) {
+ $this->assertEquals('invaliduser', $e->errorcode);
+ }
+
+ // Remove the created data.
+ $DB->delete_records('quiz_grades', array('id' => $grade->id));
+
+ }
+ /**
+ * Test get_combined_review_options.
+ * This is a basic test, this is already tested in mod_quiz_display_options_testcase.
+ */
+ public function test_get_combined_review_options() {
+ global $DB;
+
+ // Create a new quiz with attempts.
+ $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
+ $data = array('course' => $this->course->id,
+ 'sumgrades' => 1);
+ $quiz = $quizgenerator->create_instance($data);
+
+ // Create a couple of questions.
+ $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
+
+ $cat = $questiongenerator->create_question_category();
+ $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
+ quiz_add_quiz_question($question->id, $quiz);
+
+ $quizobj = quiz::create($quiz->id, $this->student->id);
+
+ // Set grade to pass.
+ $item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod',
+ 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null));
+ $item->gradepass = 80;
+ $item->update();
+
+ // Start the passing attempt.
+ $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
+ $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
+
+ $timenow = time();
+ $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id);
+ quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
+ quiz_attempt_save_started($quizobj, $quba, $attempt);
+
+ $this->setUser($this->student);
+
+ $result = mod_quiz_external::get_combined_review_options($quiz->id);
+ $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
+
+ // Expected values.
+ $expected = array(
+ "someoptions" => array(
+ array("name" => "feedback", "value" => 1),
+ array("name" => "generalfeedback", "value" => 1),
+ array("name" => "rightanswer", "value" => 1),
+ array("name" => "overallfeedback", "value" => 0),
+ array("name" => "marks", "value" => 2),
+ ),
+ "alloptions" => array(
+ array("name" => "feedback", "value" => 1),
+ array("name" => "generalfeedback", "value" => 1),
+ array("name" => "rightanswer", "value" => 1),
+ array("name" => "overallfeedback", "value" => 0),
+ array("name" => "marks", "value" => 2),
+ ),
+ "warnings" => [],
+ );
+
+ $this->assertEquals($expected, $result);
+
+ // Now, finish the attempt.
+ $attemptobj = quiz_attempt::create($attempt->id);
+ $attemptobj->process_finish($timenow, false);
+
+ $expected = array(
+ "someoptions" => array(
+ array("name" => "feedback", "value" => 1),
+ array("name" => "generalfeedback", "value" => 1),
+ array("name" => "rightanswer", "value" => 1),
+ array("name" => "overallfeedback", "value" => 1),
+ array("name" => "marks", "value" => 2),
+ ),
+ "alloptions" => array(
+ array("name" => "feedback", "value" => 1),
+ array("name" => "generalfeedback", "value" => 1),
+ array("name" => "rightanswer", "value" => 1),
+ array("name" => "overallfeedback", "value" => 1),
+ array("name" => "marks", "value" => 2),
+ ),
+ "warnings" => [],
+ );
+
+ // We should see now the overall feedback.
+ $result = mod_quiz_external::get_combined_review_options($quiz->id);
+ $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
+ $this->assertEquals($expected, $result);
+
+ // Start a new attempt, but not finish it.
+ $timenow = time();
+ $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
+ $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
+ $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
+ quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
+ quiz_attempt_save_started($quizobj, $quba, $attempt);
+
+ $expected = array(
+ "someoptions" => array(
+ array("name" => "feedback", "value" => 1),
+ array("name" => "generalfeedback", "value" => 1),
+ array("name" => "rightanswer", "value" => 1),
+ array("name" => "overallfeedback", "value" => 1),
+ array("name" => "marks", "value" => 2),
+ ),
+ "alloptions" => array(
+ array("name" => "feedback", "value" => 1),
+ array("name" => "generalfeedback", "value" => 1),
+ array("name" => "rightanswer", "value" => 1),
+ array("name" => "overallfeedback", "value" => 0),
+ array("name" => "marks", "value" => 2),
+ ),
+ "warnings" => [],
+ );
+
+ $result = mod_quiz_external::get_combined_review_options($quiz->id);
+ $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
+ $this->assertEquals($expected, $result);
+
+ // Teacher, for see student options.
+ $this->setUser($this->teacher);
+
+ $result = mod_quiz_external::get_combined_review_options($quiz->id, $this->student->id);
+ $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
+
+ $this->assertEquals($expected, $result);
+
+ // Invalid user.
+ try {
+ mod_quiz_external::get_combined_review_options($quiz->id, -1);
+ $this->fail('Exception expected due to missing capability.');
+ } catch (dml_missing_record_exception $e) {
+ $this->assertEquals('invaliduser', $e->errorcode);
+ }
+ }
+
}
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015111602;
+$plugin->version = 2015111606;
$plugin->requires = 2015111000;
$plugin->component = 'mod_quiz';
$plugin->cron = 60;
'methodname' => 'view_resource',
'description' => 'Simulate the view.php web interface resource: trigger events, completion, etc...',
'type' => 'write',
- 'capabilities' => 'mod/resource:view'
+ 'capabilities' => 'mod/resource:view',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
);
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015111600; // The current module version (Date: YYYYMMDDXX)
+$plugin->version = 2015111601; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2015111000; // Requires this Moodle version
$plugin->component = 'mod_resource'; // Full name of the plugin (used for diagnostics)
$plugin->cron = 0;
'methodname' => 'view_scorm',
'description' => 'Trigger the course module viewed event.',
'type' => 'write',
- 'capabilities' => ''
+ 'capabilities' => '',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_scorm_get_scorm_attempt_count' => array(
'methodname' => 'get_scorm_attempt_count',
'description' => 'Return the number of attempts done by a user in the given SCORM.',
'type' => 'read',
- 'capabilities' => ''
+ 'capabilities' => '',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_scorm_get_scorm_scoes' => array(
'methodname' => 'get_scorm_scoes',
'description' => 'Returns a list containing all the scoes data related to the given scorm id',
'type' => 'read',
- 'capabilities' => ''
+ 'capabilities' => '',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_scorm_get_scorm_user_data' => array(
'methodname' => 'get_scorm_user_data',
'description' => 'Retrieves user tracking and SCO data and default SCORM values',
'type' => 'read',
- 'capabilities' => ''
+ 'capabilities' => '',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_scorm_insert_scorm_tracks' => array(
Validation should be performed before running the function to ensure the user will not lose any existing
attempt data.',
'type' => 'write',
- 'capabilities' => 'mod/scorm:savetrack'
+ 'capabilities' => 'mod/scorm:savetrack',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_scorm_get_scorm_sco_tracks' => array(
'methodname' => 'get_scorm_sco_tracks',
'description' => 'Retrieves SCO tracking data for the given user id and attempt number',
'type' => 'read',
- 'capabilities' => ''
+ 'capabilities' => '',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),