diff --git a/course/externallib.php b/course/externallib.php index 8b5025695e3..59ea4f0f158 100644 --- a/course/externallib.php +++ b/course/externallib.php @@ -2167,6 +2167,78 @@ class core_course_external extends external_api { ); } + /** + * Return the course information that is public (visible by every one) + * + * @param course_in_list $course course in list object + * @param stdClass $coursecontext course context object + * @return array the course information + * @since Moodle 3.2 + */ + protected static function get_course_public_information(course_in_list $course, $coursecontext) { + + static $categoriescache = array(); + + // Category information. + if (!array_key_exists($course->category, $categoriescache)) { + $categoriescache[$course->category] = coursecat::get($course->category, IGNORE_MISSING); + } + $category = $categoriescache[$course->category]; + + // Retrieve course overview used files. + $files = array(); + foreach ($course->get_course_overviewfiles() as $file) { + $fileurl = moodle_url::make_webservice_pluginfile_url($file->get_contextid(), $file->get_component(), + $file->get_filearea(), null, $file->get_filepath(), + $file->get_filename())->out(false); + $files[] = array( + 'filename' => $file->get_filename(), + 'fileurl' => $fileurl, + 'filesize' => $file->get_filesize(), + 'filepath' => $file->get_filepath(), + 'mimetype' => $file->get_mimetype(), + 'timemodified' => $file->get_timemodified(), + ); + } + + // Retrieve the course contacts, + // we need here the users fullname since if we are not enrolled can be difficult to obtain them via other Web Services. + $coursecontacts = array(); + foreach ($course->get_course_contacts() as $contact) { + $coursecontacts[] = array( + 'id' => $contact['user']->id, + 'fullname' => $contact['username'] + ); + } + + // Allowed enrolment methods (maybe we can self-enrol). + $enroltypes = array(); + $instances = enrol_get_instances($course->id, true); + foreach ($instances as $instance) { + $enroltypes[] = $instance->enrol; + } + + // Format summary. + 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'] = 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 == null ? '' : $category->name; + $coursereturns['summary'] = $summary; + $coursereturns['summaryformat'] = $summaryformat; + $coursereturns['summaryfiles'] = external_util::get_area_files($coursecontext->id, 'course', 'summary', false, false); + $coursereturns['overviewfiles'] = $files; + $coursereturns['contacts'] = $coursecontacts; + $coursereturns['enrollmentmethods'] = $enroltypes; + return $coursereturns; + } + /** * Search courses following the specified criteria. * @@ -2252,63 +2324,7 @@ class core_course_external extends external_api { $coursecontext = context_course::instance($course->id); - // Category information. - if (!array_key_exists($course->category, $categoriescache)) { - $categoriescache[$course->category] = coursecat::get($course->category, IGNORE_MISSING); - } - $category = $categoriescache[$course->category]; - - // Retrieve course overfiew used files. - $files = array(); - foreach ($course->get_course_overviewfiles() as $file) { - $fileurl = moodle_url::make_webservice_pluginfile_url($file->get_contextid(), $file->get_component(), - $file->get_filearea(), null, $file->get_filepath(), - $file->get_filename())->out(false); - $files[] = array( - 'filename' => $file->get_filename(), - 'fileurl' => $fileurl, - 'filesize' => $file->get_filesize(), - 'filepath' => $file->get_filepath(), - 'mimetype' => $file->get_mimetype(), - 'timemodified' => $file->get_timemodified(), - ); - } - - // Retrieve the course contacts, - // we need here the users fullname since if we are not enrolled can be difficult to obtain them via other Web Services. - $coursecontacts = array(); - foreach ($course->get_course_contacts() as $contact) { - $coursecontacts[] = array( - 'id' => $contact['user']->id, - 'fullname' => $contact['username'] - ); - } - - // Allowed enrolment methods (maybe we can self-enrol). - $enroltypes = array(); - $instances = enrol_get_instances($course->id, true); - foreach ($instances as $instance) { - $enroltypes[] = $instance->enrol; - } - - // Format summary. - 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'] = 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 == null ? '' : $category->name; - $coursereturns['summary'] = $summary; - $coursereturns['summaryformat'] = $summaryformat; - $coursereturns['overviewfiles'] = $files; - $coursereturns['contacts'] = $coursecontacts; - $coursereturns['enrollmentmethods'] = $enroltypes; - $finalcourses[] = $coursereturns; + $finalcourses[] = self::get_course_public_information($course, $coursecontext); } return array( @@ -2318,6 +2334,71 @@ class core_course_external extends external_api { ); } + /** + * Returns a course structure definition + * + * @param boolean $onlypublicdata set to true, to retrieve only fields viewable by anyone when the course is visible + * @return array the course structure + * @since Moodle 3.2 + */ + protected static function get_course_structure($onlypublicdata = true) { + $coursestructure = 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'), + 'summary' => new external_value(PARAM_RAW, 'summary'), + 'summaryformat' => new external_format_value('summary'), + 'summaryfiles' => new external_files('summary files in the summary field', VALUE_OPTIONAL), + 'overviewfiles' => new external_files('additional overview files attached to this course'), + 'contacts' => new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'contact user id'), + 'fullname' => new external_value(PARAM_NOTAGS, 'contact user fullname'), + ) + ), + 'contact users' + ), + 'enrollmentmethods' => new external_multiple_structure( + new external_value(PARAM_PLUGIN, 'enrollment method'), + 'enrollment methods list' + ), + ); + + if (!$onlypublicdata) { + $extra = array( + 'idnumber' => new external_value(PARAM_RAW, 'Id number', VALUE_OPTIONAL), + 'format' => new external_value(PARAM_PLUGIN, 'Course format: weeks, topics, social, site,..', VALUE_OPTIONAL), + 'showgrades' => new external_value(PARAM_INT, '1 if grades are shown, otherwise 0', VALUE_OPTIONAL), + 'newsitems' => new external_value(PARAM_INT, 'Number of recent items appearing on the course page', VALUE_OPTIONAL), + 'startdate' => new external_value(PARAM_INT, 'Timestamp when the course start', VALUE_OPTIONAL), + 'maxbytes' => new external_value(PARAM_INT, 'Largest size of file that can be uploaded into', VALUE_OPTIONAL), + 'showreports' => new external_value(PARAM_INT, 'Are activity report shown (yes = 1, no =0)', VALUE_OPTIONAL), + 'visible' => new external_value(PARAM_INT, '1: available to student, 0:not available', VALUE_OPTIONAL), + 'groupmode' => new external_value(PARAM_INT, 'no group, separate, visible', VALUE_OPTIONAL), + 'groupmodeforce' => new external_value(PARAM_INT, '1: yes, 0: no', VALUE_OPTIONAL), + 'defaultgroupingid' => new external_value(PARAM_INT, 'default grouping id', VALUE_OPTIONAL), + 'enablecompletion' => new external_value(PARAM_INT, 'Completion enabled? 1: yes 0: no', VALUE_OPTIONAL), + 'completionnotify' => new external_value(PARAM_INT, '1: yes 0: no', VALUE_OPTIONAL), + 'lang' => new external_value(PARAM_SAFEDIR, 'Forced course language', VALUE_OPTIONAL), + 'theme' => new external_value(PARAM_PLUGIN, 'Fame of the forced theme', VALUE_OPTIONAL), + 'sortorder' => new external_value(PARAM_INT, 'Sort order in the category', VALUE_OPTIONAL), + 'marker' => new external_value(PARAM_INT, 'Current course marker', VALUE_OPTIONAL), + 'legacyfiles' => new external_value(PARAM_INT, 'If legacy files are enabled', VALUE_OPTIONAL), + 'calendartype' => new external_value(PARAM_PLUGIN, 'Calendar type', VALUE_OPTIONAL), + 'timecreated' => new external_value(PARAM_INT, 'Time when the course was created', VALUE_OPTIONAL), + 'timemodified' => new external_value(PARAM_INT, 'Last time the course was updated', VALUE_OPTIONAL), + 'requested' => new external_value(PARAM_INT, 'If is a requested course', VALUE_OPTIONAL), + 'cacherev' => new external_value(PARAM_INT, 'Cache revision number', VALUE_OPTIONAL), + ); + $coursestructure = array_merge($coursestructure, $extra); + } + return new external_single_structure($coursestructure); + } + /** * Returns description of method result value * @@ -2325,38 +2406,10 @@ class core_course_external extends external_api { * @since Moodle 3.0 */ public static function search_courses_returns() { - return new external_single_structure( array( 'total' => new external_value(PARAM_INT, 'total course count'), - 'courses' => new external_multiple_structure( - new external_single_structure( - 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'), - 'summary' => new external_value(PARAM_RAW, 'summary'), - 'summaryformat' => new external_format_value('summary'), - 'overviewfiles' => new external_files('additional overview files attached to this course'), - 'contacts' => new external_multiple_structure( - new external_single_structure( - array( - 'id' => new external_value(PARAM_INT, 'contact user id'), - 'fullname' => new external_value(PARAM_NOTAGS, 'contact user fullname'), - ) - ), - 'contact users' - ), - 'enrollmentmethods' => new external_multiple_structure( - new external_value(PARAM_PLUGIN, 'enrollment method'), - 'enrollment methods list' - ), - ) - ), 'course' - ), + 'courses' => new external_multiple_structure(self::get_course_structure(), 'course'), 'warnings' => new external_warnings() ) ); @@ -2767,4 +2820,135 @@ class core_course_external extends external_api { public static function get_user_administration_options_returns() { return self::get_user_navigation_options_returns(); } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 3.2 + */ + public static function get_courses_by_field_parameters() { + return new external_function_parameters( + array( + 'field' => new external_value(PARAM_ALPHA, 'The field to search can be left empty for all courses or: + id: course id + ids: comma separated course ids + shortname: course short name + idnumber: course id number + category: category id the course belongs to + ', VALUE_DEFAULT, ''), + 'value' => new external_value(PARAM_RAW, 'The value to match', VALUE_DEFAULT, '') + ) + ); + } + + + /** + * Get courses matching a specific field (id/s, shortname, idnumber, category) + * + * @param string $field field name to search, or empty for all courses + * @param string $value value to search + * @return array list of courses and warnings + * @throws invalid_parameter_exception + * @since Moodle 3.2 + */ + public static function get_courses_by_field($field = '', $value = '') { + global $DB, $CFG; + require_once($CFG->libdir . '/coursecatlib.php'); + + $params = self::validate_parameters(self::get_courses_by_field_parameters(), + array( + 'field' => $field, + 'value' => $value, + ) + ); + $warnings = array(); + + if (empty($params['field'])) { + $courses = $DB->get_records('course', null, 'id ASC'); + } else { + switch ($params['field']) { + case 'id': + case 'category': + $value = clean_param($params['value'], PARAM_INT); + break; + case 'ids': + $value = clean_param($params['value'], PARAM_SEQUENCE); + break; + case 'shortname': + $value = clean_param($params['value'], PARAM_TEXT); + break; + case 'idnumber': + $value = clean_param($params['value'], PARAM_RAW); + break; + default: + throw new invalid_parameter_exception('Invalid field name'); + } + + if ($params['field'] === 'ids') { + $courses = $DB->get_records_list('course', 'id', explode(',', $value), 'id ASC'); + } else { + $courses = $DB->get_records('course', array($params['field'] => $value), 'id ASC'); + } + } + + $coursesdata = array(); + foreach ($courses as $course) { + $context = context_course::instance($course->id); + $canupdatecourse = has_capability('moodle/course:update', $context); + $canviewhiddencourses = has_capability('moodle/course:viewhiddencourses', $context); + + // Check if the course is visible in the site for the user. + if (!$course->visible and !$canviewhiddencourses and !$canupdatecourse) { + continue; + } + // Get the public course information, even if we are not enrolled. + $courseinlist = new course_in_list($course); + $coursesdata[$course->id] = self::get_course_public_information($courseinlist, $context); + + // Now, check if we have access to the course. + try { + self::validate_context($context); + } catch (Exception $e) { + continue; + } + // Return information for any user that can access the course. + $coursefields = array('format', 'showgrades', 'newsitems', 'startdate', 'maxbytes', 'showreports', 'visible', + 'groupmode', 'groupmodeforce', 'defaultgroupingid', 'enablecompletion', 'completionnotify', 'lang', 'theme', + 'sortorder', 'marker'); + + // Information for managers only. + if ($canupdatecourse) { + $managerfields = array('idnumber', 'legacyfiles', 'calendartype', 'timecreated', 'timemodified', 'requested', + 'cacherev'); + $coursefields = array_merge($coursefields, $managerfields); + } + + // Populate fields. + foreach ($coursefields as $field) { + $coursesdata[$course->id][$field] = $course->{$field}; + } + } + + return array( + 'courses' => $coursesdata, + 'warnings' => $warnings + ); + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 3.2 + */ + public static function get_courses_by_field_returns() { + // Course structure, including not only public viewable fields. + return new external_single_structure( + array( + 'courses' => new external_multiple_structure(self::get_course_structure(false), 'Course'), + 'warnings' => new external_warnings() + ) + ); + } } diff --git a/course/tests/externallib_test.php b/course/tests/externallib_test.php index 2a49a475785..53689682794 100644 --- a/course/tests/externallib_test.php +++ b/course/tests/externallib_test.php @@ -1870,4 +1870,163 @@ class core_course_externallib_testcase extends externallib_advanced_testcase { } } } + + /** + * Test get_courses_by_fields + */ + public function test_get_courses_by_field() { + global $DB; + $this->resetAfterTest(true); + + $category1 = self::getDataGenerator()->create_category(); + $category2 = self::getDataGenerator()->create_category(array('parent' => $category1->id)); + $course1 = self::getDataGenerator()->create_course(array('category' => $category1->id, 'shortname' => 'c1')); + $course2 = self::getDataGenerator()->create_course(array('visible' => 0, 'category' => $category2->id, 'idnumber' => 'i2')); + + $student1 = self::getDataGenerator()->create_user(); + $user1 = self::getDataGenerator()->create_user(); + $studentrole = $DB->get_record('role', array('shortname' => 'student')); + self::getDataGenerator()->enrol_user($student1->id, $course1->id, $studentrole->id); + self::getDataGenerator()->enrol_user($student1->id, $course2->id, $studentrole->id); + + self::setAdminUser(); + // As admins, we should be able to retrieve everything. + $result = core_course_external::get_courses_by_field(); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(3, $result['courses']); + // Expect to receive all the fields. + $this->assertCount(35, $result['courses'][0]); + $this->assertCount(35, $result['courses'][1]); + $this->assertCount(35, $result['courses'][2]); + + $result = core_course_external::get_courses_by_field('id', $course1->id); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(1, $result['courses']); + $this->assertEquals($course1->id, $result['courses'][0]['id']); + // Expect to receive all the fields. + $this->assertCount(35, $result['courses'][0]); + + $result = core_course_external::get_courses_by_field('id', $course2->id); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(1, $result['courses']); + $this->assertEquals($course2->id, $result['courses'][0]['id']); + + $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id"); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(2, $result['courses']); + + $result = core_course_external::get_courses_by_field('category', $category1->id); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(1, $result['courses']); + $this->assertEquals($course1->id, $result['courses'][0]['id']); + + $result = core_course_external::get_courses_by_field('shortname', 'c1'); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(1, $result['courses']); + $this->assertEquals($course1->id, $result['courses'][0]['id']); + + $result = core_course_external::get_courses_by_field('idnumber', 'i2'); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(1, $result['courses']); + $this->assertEquals($course2->id, $result['courses'][0]['id']); + + $result = core_course_external::get_courses_by_field('idnumber', 'x'); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(0, $result['courses']); + + self::setUser($student1); + // All visible courses (including front page) for normal student. + $result = core_course_external::get_courses_by_field(); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(2, $result['courses']); + $this->assertCount(28, $result['courses'][0]); + $this->assertCount(28, $result['courses'][1]); + + $result = core_course_external::get_courses_by_field('id', $course1->id); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(1, $result['courses']); + $this->assertEquals($course1->id, $result['courses'][0]['id']); + // Expect to receive all the files that a student can see. + $this->assertCount(28, $result['courses'][0]); + + // Course 2 is not visible. + $result = core_course_external::get_courses_by_field('id', $course2->id); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(0, $result['courses']); + + $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id"); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(1, $result['courses']); + + $result = core_course_external::get_courses_by_field('category', $category1->id); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(1, $result['courses']); + $this->assertEquals($course1->id, $result['courses'][0]['id']); + + $result = core_course_external::get_courses_by_field('shortname', 'c1'); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(1, $result['courses']); + $this->assertEquals($course1->id, $result['courses'][0]['id']); + + $result = core_course_external::get_courses_by_field('idnumber', 'i2'); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(0, $result['courses']); + + $result = core_course_external::get_courses_by_field('idnumber', 'x'); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(0, $result['courses']); + + self::setUser($user1); + // All visible courses (including front page) for authenticated user. + $result = core_course_external::get_courses_by_field(); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(2, $result['courses']); + $this->assertCount(28, $result['courses'][0]); // Site course. + $this->assertCount(12, $result['courses'][1]); // Only public information, not enrolled. + + $result = core_course_external::get_courses_by_field('id', $course1->id); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(1, $result['courses']); + $this->assertEquals($course1->id, $result['courses'][0]['id']); + // Expect to receive all the files that a authenticated can see. + $this->assertCount(12, $result['courses'][0]); + + // Course 2 is not visible. + $result = core_course_external::get_courses_by_field('id', $course2->id); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(0, $result['courses']); + + $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id"); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(1, $result['courses']); + + $result = core_course_external::get_courses_by_field('category', $category1->id); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(1, $result['courses']); + $this->assertEquals($course1->id, $result['courses'][0]['id']); + + $result = core_course_external::get_courses_by_field('shortname', 'c1'); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(1, $result['courses']); + $this->assertEquals($course1->id, $result['courses'][0]['id']); + + $result = core_course_external::get_courses_by_field('idnumber', 'i2'); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(0, $result['courses']); + + $result = core_course_external::get_courses_by_field('idnumber', 'x'); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(0, $result['courses']); + } + + public function test_get_courses_by_field_invalid_field() { + $this->expectException('invalid_parameter_exception'); + $result = core_course_external::get_courses_by_field('zyx', 'x'); + } + + public function test_get_courses_by_field_invalid_courses() { + $result = core_course_external::get_courses_by_field('id', '-1'); + $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); + $this->assertCount(0, $result['courses']); + } } diff --git a/lib/db/services.php b/lib/db/services.php index 8fc5595d5e4..cbcaf80c09f 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -319,6 +319,14 @@ $functions = array( 'type' => 'read', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), + 'core_course_get_courses_by_field' => array( + 'classname' => 'core_course_external', + 'methodname' => 'get_courses_by_field', + 'classpath' => 'course/externallib.php', + 'description' => 'Get courses matching a specific field (id/s, shortname, idnumber, category)', + 'type' => 'read', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + ), 'core_enrol_get_course_enrolment_methods' => array( 'classname' => 'core_enrol_external', 'methodname' => 'get_course_enrolment_methods', diff --git a/version.php b/version.php index a965a61000a..dbe9e01274c 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2016092300.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2016092300.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.