diff --git a/completion/classes/cm_completion_details.php b/completion/classes/cm_completion_details.php index 11b9fb3888cfd..f05c077eef103 100644 --- a/completion/classes/cm_completion_details.php +++ b/completion/classes/cm_completion_details.php @@ -40,6 +40,9 @@ class cm_completion_details { /** @var completion_info The completion info instance for this cm's course. */ protected $completioninfo = null; + /** @var object The completion data. */ + protected $completiondata = null; + /** @var cm_info The course module information. */ protected $cminfo = null; @@ -62,6 +65,7 @@ class cm_completion_details { */ public function __construct(completion_info $completioninfo, cm_info $cminfo, int $userid, bool $returndetails = true) { $this->completioninfo = $completioninfo; + $this->completiondata = $completioninfo->get_data($cminfo, false, $userid); $this->cminfo = $cminfo; $this->userid = $userid; $this->returndetails = $returndetails; @@ -87,7 +91,7 @@ public function get_details(): array { return []; } - $completiondata = $this->completioninfo->get_data($this->cminfo, false, $this->userid); + $completiondata = $this->completiondata; $hasoverride = !empty($this->overridden_by()); $details = []; @@ -155,8 +159,7 @@ public function get_details(): array { * @return int The overall completion state for this course module. */ public function get_overall_completion(): int { - $completiondata = $this->completioninfo->get_data($this->cminfo, false, $this->userid); - return (int)$completiondata->completionstate; + return (int)$this->completiondata->completionstate; } /** @@ -183,8 +186,7 @@ public function is_automatic(): bool { * @return int|null */ public function overridden_by(): ?int { - $completiondata = $this->completioninfo->get_data($this->cminfo); - return isset($completiondata->overrideby) ? (int)$completiondata->overrideby : null; + return isset($this->completiondata->overrideby) ? (int)$this->completiondata->overrideby : null; } /** @@ -219,6 +221,15 @@ public function show_manual_completion(): bool { return false; } + /** + * Completion state timemodified + * + * @return int timestamp + */ + public function get_timemodified(): int { + return (int)$this->completiondata->timemodified; + } + /** * Generates an instance of this class. * @@ -229,7 +240,7 @@ public function show_manual_completion(): bool { */ public static function get_instance(cm_info $cminfo, int $userid, bool $returndetails = true): cm_completion_details { $course = $cminfo->get_course(); - $completioninfo = new completion_info($course); + $completioninfo = new \completion_info($course); return new self($completioninfo, $cminfo, $userid, $returndetails); } } diff --git a/completion/classes/external.php b/completion/classes/external.php index 1cb3fa3a8f567..6bb4ece1a3aff 100644 --- a/completion/classes/external.php +++ b/completion/classes/external.php @@ -221,7 +221,7 @@ public static function get_activities_completion_status_parameters() { * @throws moodle_exception */ public static function get_activities_completion_status($courseid, $userid) { - global $CFG, $USER; + global $CFG, $USER, $PAGE; require_once($CFG->libdir . '/grouplib.php'); $warnings = array(); @@ -253,25 +253,24 @@ public static function get_activities_completion_status($courseid, $userid) { $results = array(); foreach ($activities as $activity) { - // Check if current user has visibility on this activity. if (!$activity->uservisible) { continue; } - // Get progress information and state (we must use get_data because it works for all user roles in course). - $activitycompletiondata = $completion->get_data($activity, true, $user->id); - - $results[] = array( - 'cmid' => $activity->id, - 'modname' => $activity->modname, - 'instance' => $activity->instance, - 'state' => $activitycompletiondata->completionstate, - 'timecompleted' => $activitycompletiondata->timemodified, - 'tracking' => $activity->completion, - 'overrideby' => $activitycompletiondata->overrideby, - 'valueused' => core_availability\info::completion_value_used($course, $activity->id) + $exporter = new \core_completion\external\completion_info_exporter( + $course, + $activity, + $userid, ); + $renderer = $PAGE->get_renderer('core'); + $data = (array)$exporter->export($renderer); + $results[] = array_merge([ + 'cmid' => $activity->id, + 'modname' => $activity->modname, + 'instance' => $activity->instance, + 'tracking' => $activity->completion, + ], $data); } $results = array( @@ -292,21 +291,59 @@ public static function get_activities_completion_status_returns() { array( 'statuses' => new external_multiple_structure( new external_single_structure( - array( - 'cmid' => new external_value(PARAM_INT, 'comment ID'), + [ + 'cmid' => new external_value(PARAM_INT, 'course module ID'), 'modname' => new external_value(PARAM_PLUGIN, 'activity module name'), 'instance' => new external_value(PARAM_INT, 'instance ID'), - 'state' => new external_value(PARAM_INT, 'completion state value: - 0 means incomplete, 1 complete, - 2 complete pass, 3 complete fail'), - 'timecompleted' => new external_value(PARAM_INT, 'timestamp for completed activity'), - 'tracking' => new external_value(PARAM_INT, 'type of tracking: - 0 means none, 1 manual, 2 automatic'), - 'overrideby' => new external_value(PARAM_INT, 'The user id who has overriden the status, or null', + 'state' => new external_value(PARAM_INT, + "Completion state value: + 0 means incomplete, + 1 complete, + 2 complete pass, + 3 complete fail" + ), + 'timecompleted' => new external_value(PARAM_INT, + 'timestamp for completed activity'), + 'tracking' => new external_value(PARAM_INT, + "type of tracking: + 0 means none, + 1 manual, + 2 automatic" + ), + 'overrideby' => new external_value(PARAM_INT, + 'The user id who has overriden the status, or null', VALUE_OPTIONAL), + 'valueused' => new external_value(PARAM_BOOL, + 'Whether the completion status affects the availability of another activity.', + VALUE_OPTIONAL), + 'hascompletion' => new external_value(PARAM_BOOL, + 'Whether this activity module has completion enabled', + VALUE_OPTIONAL), + 'isautomatic' => new external_value(PARAM_BOOL, + 'Whether this activity module instance tracks completion automatically.', + VALUE_OPTIONAL), + 'istrackeduser' => new external_value(PARAM_BOOL, + 'Whether completion is being tracked for this user.', + VALUE_OPTIONAL), + 'uservisible' => new external_value(PARAM_BOOL, + 'Whether this activity is visible to the user.', VALUE_OPTIONAL), - 'valueused' => new external_value(PARAM_BOOL, 'Whether the completion status affects the availability - of another activity.', VALUE_OPTIONAL), - ), 'Activity' + 'details' => new external_multiple_structure( + new external_single_structure( + [ + 'rulename' => new external_value(PARAM_TEXT, 'Rule name'), + 'rulevalue' => new external_single_structure( + [ + 'status' => new external_value(PARAM_INT, 'Completion status'), + 'description' => new external_value(PARAM_TEXT, 'Completion description'), + ] + ) + ] + ), + VALUE_DEFAULT, + [] + ), + + ], 'Activity' ), 'List of activities status' ), 'warnings' => new external_warnings() diff --git a/completion/classes/external/completion_info_exporter.php b/completion/classes/external/completion_info_exporter.php new file mode 100644 index 0000000000000..de710d276a94b --- /dev/null +++ b/completion/classes/external/completion_info_exporter.php @@ -0,0 +1,153 @@ +. + +declare(strict_types=1); + +namespace core_completion\external; + +defined('MOODLE_INTERNAL') || die(); + +use renderer_base; + +/** + * Completion info exporter + * + * @package core_completion + * @copyright 2021 Dongsheng Cai + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class completion_info_exporter extends \core\external\exporter { + /** + * @var object $course moodle course object + */ + private $course; + /** + * @var object|cm_info $cm course module info + */ + private $cminfo; + /** + * @var int $userid user id + */ + private $userid; + + /** + * Constructor for the completion info exporter. + * + * @param object $course course object + * @param object|cm_info $cm course module info + * @param int $userid user id + * @param array $related related values + */ + public function __construct(object $course, object $cm, int $userid, array $related = []) { + $this->course = $course; + $this->cminfo = \cm_info::create($cm); + $this->userid = $userid; + parent::__construct([], $related); + } + + /** + * Get the additional values to inject while exporting. + * + * @param renderer_base $output The renderer. + * @return array Keys are the property names, values are their values. + */ + protected function get_other_values(renderer_base $output): array { + $cmcompletion = \core_completion\cm_completion_details::get_instance($this->cminfo, $this->userid); + $cmcompletiondetails = $cmcompletion->get_details(); + + $details = []; + foreach ($cmcompletiondetails as $rulename => $rulevalue) { + $details[] = [ + 'rulename' => $rulename, + 'rulevalue' => (array)$rulevalue, + ]; + } + return [ + 'state' => $cmcompletion->get_overall_completion(), + 'timecompleted' => $cmcompletion->get_timemodified(), + 'overrideby' => $cmcompletion->overridden_by(), + 'valueused' => \core_availability\info::completion_value_used($this->course, $this->cminfo->id), + 'hascompletion' => $cmcompletion->has_completion(), + 'isautomatic' => $cmcompletion->is_automatic(), + 'istrackeduser' => $cmcompletion->is_tracked_user(), + 'overallstatus' => $cmcompletion->get_overall_completion(), + 'uservisible' => $this->cminfo->uservisible, + 'details' => $details, + ]; + } + + /** + * Return the list of additional properties used only for display. + * + * @return array Keys are the property names, and value their definition. + */ + public static function define_other_properties(): array { + return [ + 'state' => [ + 'type' => PARAM_INT, + 'description' => 'overall completion state of this course module.', + ], + 'timecompleted' => [ + 'type' => PARAM_INT, + 'description' => 'course completion timestamp.', + ], + 'overrideby' => [ + 'type' => PARAM_INT, + 'description' => 'user ID that has overridden the completion state of this activity for the user.', + 'null' => NULL_ALLOWED, + ], + 'valueused' => [ + 'type' => PARAM_BOOL, + 'description' => 'True if module is used in a condition, false otherwise.', + ], + 'hascompletion' => [ + 'type' => PARAM_BOOL, + 'description' => 'Whether this activity module has completion enabled.' + ], + 'isautomatic' => [ + 'type' => PARAM_BOOL, + 'description' => 'Whether this activity module instance tracks completion automatically.' + ], + 'istrackeduser' => [ + 'type' => PARAM_BOOL, + 'description' => 'Checks whether completion is being tracked for this user.' + ], + 'uservisible' => [ + 'type' => PARAM_BOOL, + 'description' => 'Whether this activity is visible to user.' + ], + 'details' => [ + 'multiple' => true, + 'description' => 'An array of completion details containing the description and status.', + 'type' => [ + 'rulename' => [ + 'type' => PARAM_TEXT, + ], + 'rulevalue' => [ + 'type' => [ + 'status' => [ + 'type' => PARAM_INT, + ], + 'description' => [ + 'type' => PARAM_TEXT, + ] + ] + ] + ] + ], + ]; + } +} diff --git a/completion/tests/cm_completion_details_test.php b/completion/tests/cm_completion_details_test.php index 1c44c86ce6fbf..fa3f49a98a376 100644 --- a/completion/tests/cm_completion_details_test.php +++ b/completion/tests/cm_completion_details_test.php @@ -54,7 +54,8 @@ class cm_completion_details_test extends advanced_testcase { * @param array $completionoptions Completion options (e.g. completionview, completionusegrade, etc.) * @return cm_completion_details */ - protected function setup_data(?int $completion, array $completionoptions = []): cm_completion_details { + protected function setup_data(?int $completion, array $completionoptions = [], + object $mockcompletiondata = null): cm_completion_details { if (is_null($completion)) { $completion = COMPLETION_TRACKING_AUTOMATIC; } @@ -69,6 +70,12 @@ protected function setup_data(?int $completion, array $completionoptions = []): ->method('is_enabled') ->willReturn($completion); + if (!empty($mockcompletiondata)) { + $this->completioninfo->expects($this->any()) + ->method('get_data') + ->willReturn($mockcompletiondata); + } + // Build a mock cm_info instance. $mockcminfo = $this->getMockBuilder(cm_info::class) ->disableOriginalConstructor() @@ -171,12 +178,8 @@ public function overall_completion_provider(): array { * @param int $state */ public function test_get_overall_completion(int $state) { - $cmcompletion = $this->setup_data(COMPLETION_TRACKING_AUTOMATIC); - - $this->completioninfo->expects($this->once()) - ->method('get_data') - ->willReturn((object)['completionstate' => $state]); - + $completiondata = (object)['completionstate' => $state]; + $cmcompletion = $this->setup_data(COMPLETION_TRACKING_AUTOMATIC, [], $completiondata); $this->assertEquals($state, $cmcompletion->get_overall_completion()); } @@ -274,12 +277,7 @@ public function test_get_details(int $completion, ?int $completionview, ?int $co $options['completionusegrade'] = true; } - $cmcompletion = $this->setup_data($completion, $options); - - $this->completioninfo->expects($this->any()) - ->method('get_data') - ->willReturn($getdatareturn); - + $cmcompletion = $this->setup_data($completion, $options, $getdatareturn); $this->assertEquals($expecteddetails, $cmcompletion->get_details()); } } diff --git a/completion/tests/externallib_test.php b/completion/tests/externallib_test.php index dc0d76e3eecb7..2d5c6e8f7297b 100644 --- a/completion/tests/externallib_test.php +++ b/completion/tests/externallib_test.php @@ -92,7 +92,7 @@ public function test_update_activity_completion_status_manually() { * Test update_activity_completion_status */ public function test_get_activities_completion_status() { - global $DB, $CFG; + global $DB, $CFG, $PAGE; $this->resetAfterTest(true); @@ -105,17 +105,29 @@ public function test_get_activities_completion_status() { 'groupmodeforce' => 1)); availability_completion\condition::wipe_static_cache(); - $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), - array('completion' => 1)); - $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), - array('completion' => 1)); + $data = $this->getDataGenerator()->create_module('data', + ['course' => $course->id], + ['completion' => COMPLETION_TRACKING_MANUAL], + ); + $forum = $this->getDataGenerator()->create_module('forum', + ['course' => $course->id], + ['completion' => COMPLETION_TRACKING_MANUAL], + ); + $forumautocompletion = $this->getDataGenerator()->create_module('forum', + ['course' => $course->id], + ['showdescription' => true, 'completionview' => 1, 'completion' => COMPLETION_TRACKING_AUTOMATIC], + ); $availability = '{"op":"&","c":[{"type":"completion","cm":' . $forum->cmid .',"e":1}],"showc":[true]}'; - $assign = $this->getDataGenerator()->create_module('assign', ['course' => $course->id], ['availability' => $availability]); + $assign = $this->getDataGenerator()->create_module('assign', + ['course' => $course->id], + ['availability' => $availability], + ); $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id), array('completion' => 1, 'visible' => 0)); $cmdata = get_coursemodule_from_id('data', $data->cmid); $cmforum = get_coursemodule_from_id('forum', $forum->cmid); + $cmforumautocompletion = get_coursemodule_from_id('forum', $forumautocompletion->cmid); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); @@ -139,8 +151,13 @@ public function test_get_activities_completion_status() { $result = external_api::clean_returnvalue( core_completion_external::get_activities_completion_status_returns(), $result); - // We added 4 activities, but only 3 with completion enabled and one of those is hidden. - $this->assertCount(2, $result['statuses']); + // We added 5 activities, but only 4 with completion enabled and one of those is hidden. + $numberofactivities = 5; + $numberofhidden = 1; + $numberofcompletions = $numberofactivities - $numberofhidden; + $numberofstatusstudent = 3; + + $this->assertCount($numberofstatusstudent, $result['statuses']); $activitiesfound = 0; foreach ($result['statuses'] as $status) { @@ -149,14 +166,41 @@ public function test_get_activities_completion_status() { $this->assertEquals(COMPLETION_COMPLETE, $status['state']); $this->assertEquals(COMPLETION_TRACKING_MANUAL, $status['tracking']); $this->assertTrue($status['valueused']); + $this->assertTrue($status['hascompletion']); + $this->assertFalse($status['isautomatic']); + $this->assertTrue($status['istrackeduser']); + $this->assertTrue($status['uservisible']); + $details = $status['details']; + $this->assertCount(0, $details); + } else if ($status['cmid'] == $forumautocompletion->cmid) { + $activitiesfound++; + $this->assertEquals(COMPLETION_INCOMPLETE, $status['state']); + $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $status['tracking']); + $this->assertFalse($status['valueused']); + $this->assertTrue($status['hascompletion']); + $this->assertTrue($status['isautomatic']); + $this->assertTrue($status['istrackeduser']); + $this->assertTrue($status['uservisible']); + $details = $status['details']; + $this->assertCount(1, $details); + $this->assertEquals('completionview', $details[0]['rulename']); + $this->assertEquals(0, $details[0]['rulevalue']['status']); + } else if ($status['cmid'] == $data->cmid and $status['modname'] == 'data' and $status['instance'] == $data->id) { $activitiesfound++; $this->assertEquals(COMPLETION_INCOMPLETE, $status['state']); $this->assertEquals(COMPLETION_TRACKING_MANUAL, $status['tracking']); $this->assertFalse($status['valueused']); + $this->assertFalse($status['valueused']); + $this->assertTrue($status['hascompletion']); + $this->assertFalse($status['isautomatic']); + $this->assertTrue($status['istrackeduser']); + $this->assertTrue($status['uservisible']); + $details = $status['details']; + $this->assertCount(0, $details); } } - $this->assertEquals(2, $activitiesfound); + $this->assertEquals(3, $activitiesfound); // Teacher should see students status, they are in different groups but the teacher can access all groups. $this->setUser($teacher); @@ -165,8 +209,7 @@ public function test_get_activities_completion_status() { $result = external_api::clean_returnvalue( core_completion_external::get_activities_completion_status_returns(), $result); - // We added 4 activities, but only 3 with completion enabled and one of those is hidden. - $this->assertCount(3, $result['statuses']); + $this->assertCount($numberofcompletions, $result['statuses']); // Override status by teacher. $completion->update_state($cmforum, COMPLETION_INCOMPLETE, $student->id, true); @@ -197,8 +240,7 @@ public function test_get_activities_completion_status() { $result = external_api::clean_returnvalue( core_completion_external::get_activities_completion_status_returns(), $result); - // We added 4 activities, but only 3 with completion enabled (one of those is hidden but the teacher can see it). - $this->assertCount(3, $result['statuses']); + $this->assertCount($numberofcompletions, $result['statuses']); $activitiesfound = 0; foreach ($result['statuses'] as $status) { @@ -206,13 +248,17 @@ public function test_get_activities_completion_status() { $activitiesfound++; $this->assertEquals(COMPLETION_COMPLETE, $status['state']); $this->assertEquals(COMPLETION_TRACKING_MANUAL, $status['tracking']); + } else if ($status['cmid'] == $forumautocompletion->cmid) { + $activitiesfound++; + $this->assertEquals(COMPLETION_INCOMPLETE, $status['state']); + $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $status['tracking']); } else { $activitiesfound++; $this->assertEquals(COMPLETION_INCOMPLETE, $status['state']); $this->assertEquals(COMPLETION_TRACKING_MANUAL, $status['tracking']); } } - $this->assertEquals(3, $activitiesfound); + $this->assertEquals(4, $activitiesfound); // Change teacher role capabilities (disable access all groups). $context = context_course::instance($course->id); @@ -232,8 +278,7 @@ public function test_get_activities_completion_status() { // We need to execute the return values cleaning process to simulate the web service server. $result = external_api::clean_returnvalue( core_completion_external::get_activities_completion_status_returns(), $result); - // We added 4 activities, but only 3 with completion enabled and one of those is hidden. - $this->assertCount(3, $result['statuses']); + $this->assertCount($numberofcompletions, $result['statuses']); } /** diff --git a/course/classes/external/course_summary_exporter.php b/course/classes/external/course_summary_exporter.php index 3ac98879ac54f..0916abda09450 100644 --- a/course/classes/external/course_summary_exporter.php +++ b/course/classes/external/course_summary_exporter.php @@ -108,7 +108,15 @@ public static function define_properties() { ), 'visible' => array( 'type' => PARAM_BOOL, - ) + ), + 'showactivitydates' => [ + 'type' => PARAM_BOOL, + 'null' => NULL_ALLOWED + ], + 'showcompletionconditions' => [ + 'type' => PARAM_BOOL, + 'null' => NULL_ALLOWED + ], ); } diff --git a/course/externallib.php b/course/externallib.php index 0e61c579ebb37..fbcbcfd750d7d 100644 --- a/course/externallib.php +++ b/course/externallib.php @@ -27,9 +27,11 @@ defined('MOODLE_INTERNAL') || die; use core_course\external\course_summary_exporter; +use core_availability\info; + require_once("$CFG->libdir/externallib.php"); -require_once("lib.php"); +require_once(__DIR__ . "/lib.php"); /** * Course external functions @@ -84,7 +86,7 @@ public static function get_course_contents_parameters() { * @since Moodle 2.2 */ public static function get_course_contents($courseid, $options = array()) { - global $CFG, $DB; + global $CFG, $DB, $USER, $PAGE; require_once($CFG->dirroot . "/course/lib.php"); require_once($CFG->libdir . '/completionlib.php'); @@ -221,6 +223,8 @@ public static function get_course_contents($courseid, $options = array()) { if (empty($filters['excludemodules']) and !empty($modinfosections[$section->section])) { foreach ($modinfosections[$section->section] as $cmid) { $cm = $modinfo->cms[$cmid]; + $cminfo = cm_info::create($cm); + $activitydates = \core\activity_dates::get_dates_for_module($cminfo, $USER->id); // Stop here if the module is not visible to the user on the course main page: // The user can't access the module and the user can't view the module on the course page. @@ -272,17 +276,15 @@ public static function get_course_contents($courseid, $options = array()) { $module['customdata'] = json_encode($cm->customdata); $module['completion'] = $cm->completion; $module['noviewlink'] = plugin_supports('mod', $cm->modname, FEATURE_NO_VIEW_LINK, false); + $module['dates'] = $activitydates; // Check module completion. $completion = $completioninfo->is_enabled($cm); if ($completion != COMPLETION_DISABLED) { - $completiondata = $completioninfo->get_data($cm, true); - $module['completiondata'] = array( - 'state' => $completiondata->completionstate, - 'timecompleted' => $completiondata->timemodified, - 'overrideby' => $completiondata->overrideby, - 'valueused' => core_availability\info::completion_value_used($course, $cm->id) - ); + $exporter = new \core_completion\external\completion_info_exporter($course, $cm, $USER->id); + $renderer = $PAGE->get_renderer('core'); + $modulecompletiondata = (array)$exporter->export($renderer); + $module['completiondata'] = $modulecompletiondata; } if (!empty($cm->showdescription) or $module['noviewlink']) { @@ -430,6 +432,8 @@ public static function get_course_contents($courseid, $options = array()) { * @since Moodle 2.2 */ public static function get_course_contents_returns() { + $completiondefinition = \core_completion\external\completion_info_exporter::get_read_structure(VALUE_DEFAULT, []); + return new external_multiple_structure( new external_single_structure( array( @@ -472,16 +476,16 @@ public static function get_course_contents_returns() { VALUE_OPTIONAL), 'completion' => new external_value(PARAM_INT, 'Type of completion tracking: 0 means none, 1 manual, 2 automatic.', VALUE_OPTIONAL), - 'completiondata' => new external_single_structure( - array( - 'state' => new external_value(PARAM_INT, 'Completion state value: - 0 means incomplete, 1 complete, 2 complete pass, 3 complete fail'), - 'timecompleted' => new external_value(PARAM_INT, 'Timestamp for completion status.'), - 'overrideby' => new external_value(PARAM_INT, 'The user id who has overriden the - status.'), - 'valueused' => new external_value(PARAM_BOOL, 'Whether the completion status affects - the availability of another activity.', VALUE_OPTIONAL), - ), 'Module completion data.', VALUE_OPTIONAL + 'completiondata' => $completiondefinition, + 'dates' => new external_multiple_structure( + new external_single_structure( + array( + 'label' => new external_value(PARAM_TEXT, 'date label'), + 'timestamp' => new external_value(PARAM_INT, 'date timestamp'), + ) + ), + VALUE_DEFAULT, + [] ), 'contents' => new external_multiple_structure( new external_single_structure( @@ -606,6 +610,8 @@ public static function get_courses($options = array()) { $courseinfo['format'] = $course->format; $courseinfo['startdate'] = $course->startdate; $courseinfo['enddate'] = $course->enddate; + $courseinfo['showactivitydates'] = $course->showactivitydates; + $courseinfo['showcompletionconditions'] = $course->showcompletionconditions; if (array_key_exists('numsections', $courseformatoptions)) { // For backward-compartibility $courseinfo['numsections'] = $courseformatoptions['numsections']; @@ -737,6 +743,9 @@ public static function get_courses_returns() { 'value' => new external_value(PARAM_RAW, 'course format option value') )), 'additional options for particular course format', VALUE_OPTIONAL ), + 'showactivitydates' => new external_value(PARAM_BOOL, 'Whether the activity dates are shown or not'), + 'showcompletionconditions' => new external_value(PARAM_BOOL, + 'Whether the activity completion conditions are shown or not'), 'customfields' => new external_multiple_structure( new external_single_structure( ['name' => new external_value(PARAM_RAW, 'The name of the custom field'), @@ -2490,6 +2499,8 @@ protected static function get_course_public_information(core_course_list_element $coursereturns['contacts'] = $coursecontacts; $coursereturns['enrollmentmethods'] = $enroltypes; $coursereturns['sortorder'] = $course->sortorder; + $coursereturns['showactivitydates'] = $course->showactivitydates; + $coursereturns['showcompletionconditions'] = $course->showcompletionconditions; $handler = core_course\customfield\course_handler::create(); if ($customfields = $handler->export_instance_data($course->id)) { @@ -2629,6 +2640,9 @@ protected static function get_course_structure($onlypublicdata = true) { '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'), + 'showactivitydates' => new external_value(PARAM_BOOL, 'Whether the activity dates are shown or not'), + 'showcompletionconditions' => new external_value(PARAM_BOOL, + 'Whether the activity completion conditions are shown or not'), 'contacts' => new external_multiple_structure( new external_single_structure( array( diff --git a/course/lib.php b/course/lib.php index 83c8380c40dd6..755f8f60d821e 100644 --- a/course/lib.php +++ b/course/lib.php @@ -4735,8 +4735,11 @@ function course_get_recent_courses(int $userid = null, int $limit = 0, int $offs $userid = $USER->id; } - $basefields = array('id', 'idnumber', 'summary', 'summaryformat', 'startdate', 'enddate', 'category', - 'shortname', 'fullname', 'timeaccess', 'component', 'visible'); + $basefields = [ + 'id', 'idnumber', 'summary', 'summaryformat', 'startdate', 'enddate', 'category', + 'shortname', 'fullname', 'timeaccess', 'component', 'visible', + 'showactivitydates', 'showcompletionconditions', + ]; $sort = trim($sort); if (empty($sort)) { @@ -4755,7 +4758,7 @@ function course_get_recent_courses(int $userid = null, int $limit = 0, int $offs $ctxfields = context_helper::get_preload_record_columns_sql('ctx'); - $coursefields = 'c.' .join(',', $basefields); + $coursefields = 'c.' . join(',', $basefields); // Ask the favourites service to give us the join SQL for favourited courses, // so we can include favourite information in the query. diff --git a/course/tests/externallib_test.php b/course/tests/externallib_test.php index 5e93c26f472e7..6882d6dd85d15 100644 --- a/course/tests/externallib_test.php +++ b/course/tests/externallib_test.php @@ -1080,9 +1080,14 @@ private function prepare_get_course_contents_test() { $DB->set_field('course_sections', 'visible', 0, array('course' => $course->id, 'section' => 4)); + $forumcompleteauto = $this->getDataGenerator()->create_module('forum', + array('course' => $course->id, 'intro' => 'forum completion tracking auto', 'trackingtype' => 2), + array('showdescription' => true, 'completionview' => 1, 'completion' => COMPLETION_TRACKING_AUTOMATIC)); + $forumcompleteautocm = get_coursemodule_from_id('forum', $forumcompleteauto->cmid); + rebuild_course_cache($course->id, true); - return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm); + return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm); } /** @@ -1145,7 +1150,7 @@ public function test_get_course_contents() { $this->assertEquals(5, $testexecuted); $this->assertEquals(0, $sections[0]['section']); - $this->assertCount(5, $sections[0]['modules']); + $this->assertCount(6, $sections[0]['modules']); $this->assertCount(1, $sections[1]['modules']); $this->assertCount(1, $sections[2]['modules']); $this->assertCount(1, $sections[3]['modules']); // One module for the section with availability restrictions. @@ -1187,7 +1192,7 @@ public function test_get_course_contents_student() { $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); $this->assertCount(4, $sections); // Nothing for the not visible section. - $this->assertCount(5, $sections[0]['modules']); + $this->assertCount(6, $sections[0]['modules']); $this->assertCount(1, $sections[1]['modules']); $this->assertCount(1, $sections[2]['modules']); $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions. @@ -1208,7 +1213,7 @@ public function test_get_course_contents_student() { $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); $this->assertCount(5, $sections); // Include fake section with stealth activities. - $this->assertCount(5, $sections[0]['modules']); + $this->assertCount(6, $sections[0]['modules']); $this->assertCount(1, $sections[1]['modules']); $this->assertCount(1, $sections[2]['modules']); $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions. @@ -1273,7 +1278,7 @@ public function test_get_course_contents_section_number() { $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); $this->assertCount(1, $sections); - $this->assertCount(5, $sections[0]['modules']); + $this->assertCount(6, $sections[0]['modules']); } /** @@ -1333,7 +1338,7 @@ public function test_get_course_contents_modname() { $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); $this->assertCount(4, $sections); - $this->assertCount(1, $sections[0]['modules']); + $this->assertCount(2, $sections[0]['modules']); $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]); } @@ -1361,13 +1366,14 @@ public function test_get_course_contents_modid() { } /** - * Test get course contents completion + * Test get course contents completion manual */ - public function test_get_course_contents_completion() { + public function test_get_course_contents_completion_manual() { global $CFG; $this->resetAfterTest(true); - list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); + list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm) = + $this->prepare_get_course_contents_test(); availability_completion\condition::wipe_static_cache(); // Test activity not completed yet. @@ -1376,13 +1382,18 @@ public function test_get_course_contents_completion() { // We need to execute the return values cleaning process to simulate the web service server. $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result); + $completiondata = $result[0]['modules'][0]["completiondata"]; $this->assertCount(1, $result[0]['modules']); $this->assertEquals("forum", $result[0]['modules'][0]["modname"]); $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]); - $this->assertEquals(0, $result[0]['modules'][0]["completiondata"]['state']); - $this->assertEquals(0, $result[0]['modules'][0]["completiondata"]['timecompleted']); - $this->assertEmpty($result[0]['modules'][0]["completiondata"]['overrideby']); - $this->assertFalse($result[0]['modules'][0]["completiondata"]['valueused']); + $this->assertEquals(0, $completiondata['state']); + $this->assertEquals(0, $completiondata['timecompleted']); + $this->assertEmpty($completiondata['overrideby']); + $this->assertFalse($completiondata['valueused']); + $this->assertTrue($completiondata['hascompletion']); + $this->assertFalse($completiondata['isautomatic']); + $this->assertFalse($completiondata['istrackeduser']); + $this->assertTrue($completiondata['uservisible']); // Set activity completed. core_completion_external::update_activity_completion_status_manually($forumcm->id, true); @@ -1402,13 +1413,18 @@ public function test_get_course_contents_completion() { // We need to execute the return values cleaning process to simulate the web service server. $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result); + $completiondata = $result[0]['modules'][0]["completiondata"]; $this->assertCount(1, $result[0]['modules']); $this->assertEquals("label", $result[0]['modules'][0]["modname"]); $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]); - $this->assertEquals(0, $result[0]['modules'][0]["completiondata"]['state']); - $this->assertEquals(0, $result[0]['modules'][0]["completiondata"]['timecompleted']); - $this->assertEmpty($result[0]['modules'][0]["completiondata"]['overrideby']); - $this->assertTrue($result[0]['modules'][0]["completiondata"]['valueused']); + $this->assertEquals(0, $completiondata['state']); + $this->assertEquals(0, $completiondata['timecompleted']); + $this->assertEmpty($completiondata['overrideby']); + $this->assertTrue($completiondata['valueused']); + $this->assertTrue($completiondata['hascompletion']); + $this->assertFalse($completiondata['isautomatic']); + $this->assertFalse($completiondata['istrackeduser']); + $this->assertTrue($completiondata['uservisible']); // Disable completion. $CFG->enablecompletion = 0; @@ -1420,6 +1436,47 @@ public function test_get_course_contents_completion() { $this->assertArrayNotHasKey('completiondata', $result[0]['modules'][0]); } + /** + * Test get course contents completion auto + */ + public function test_get_course_contents_completion_auto() { + global $CFG; + $this->resetAfterTest(true); + + list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm) = + $this->prepare_get_course_contents_test(); + availability_completion\condition::wipe_static_cache(); + + // Test activity not completed yet. + $result = core_course_external::get_course_contents($course->id, [ + [ + "name" => "modname", + "value" => "forum" + ], + [ + "name" => "modid", + "value" => $forumcompleteautocm->instance + ] + ]); + // We need to execute the return values cleaning process to simulate the web service server. + $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result); + + $forummod = $result[0]['modules'][0]; + $completiondata = $forummod["completiondata"]; + $this->assertCount(1, $result[0]['modules']); + $this->assertEquals("forum", $forummod["modname"]); + $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $forummod["completion"]); + $this->assertEquals(0, $completiondata['state']); + $this->assertEquals(0, $completiondata['timecompleted']); + $this->assertEmpty($completiondata['overrideby']); + $this->assertFalse($completiondata['valueused']); + $this->assertTrue($completiondata['hascompletion']); + $this->assertTrue($completiondata['isautomatic']); + $this->assertFalse($completiondata['istrackeduser']); + $this->assertTrue($completiondata['uservisible']); + $this->assertCount(1, $completiondata['details']); + } + /** * Test mimetype is returned for resources with showtype set. */ @@ -1542,7 +1599,7 @@ public function test_get_course_contents_hiddensections() { $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); $this->assertCount(5, $sections); // All the sections, including the "not visible" one. - $this->assertCount(5, $sections[0]['modules']); + $this->assertCount(6, $sections[0]['modules']); $this->assertCount(1, $sections[1]['modules']); $this->assertCount(1, $sections[2]['modules']); $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions. @@ -1564,7 +1621,7 @@ public function test_get_course_contents_hiddensections() { $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); $this->assertCount(6, $sections); // Include fake section with stealth activities. - $this->assertCount(5, $sections[0]['modules']); + $this->assertCount(6, $sections[0]['modules']); $this->assertCount(1, $sections[1]['modules']); $this->assertCount(1, $sections[2]['modules']); $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions. @@ -2576,16 +2633,16 @@ public function test_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(38, $result['courses'][0]); - $this->assertCount(39, $result['courses'][1]); // One more field because is not the site course. - $this->assertCount(39, $result['courses'][2]); // One more field because is not the site course. + $this->assertCount(40, $result['courses'][0]); + $this->assertCount(41, $result['courses'][1]); // One more field because is not the site course. + $this->assertCount(41, $result['courses'][2]); // One more field because is not the site course. $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(39, $result['courses'][0]); + $this->assertCount(41, $result['courses'][0]); // Check default values for course format topics. $this->assertCount(2, $result['courses'][0]['courseformatoptions']); foreach ($result['courses'][0]['courseformatoptions'] as $option) { @@ -2646,15 +2703,15 @@ public function test_get_courses_by_field() { $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(31, $result['courses'][0]); - $this->assertCount(32, $result['courses'][1]); // One field more (course format options), not present in site course. + $this->assertCount(33, $result['courses'][0]); + $this->assertCount(34, $result['courses'][1]); // One field more (course format options), not present in site course. $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(32, $result['courses'][0]); + $this->assertCount(34, $result['courses'][0]); // Check default filters. $filters = $result['courses'][0]['filters']; @@ -2699,15 +2756,15 @@ public function test_get_courses_by_field() { $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(31, $result['courses'][0]); // Site course. - $this->assertCount(14, $result['courses'][1]); // Only public information, not enrolled. + $this->assertCount(33, $result['courses'][0]); // Site course. + $this->assertCount(16, $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(14, $result['courses'][0]); + $this->assertCount(16, $result['courses'][0]); // Course 2 is not visible. $result = core_course_external::get_courses_by_field('id', $course2->id); diff --git a/enrol/externallib.php b/enrol/externallib.php index 0254bde5a4f39..afe0e83944f30 100644 --- a/enrol/externallib.php +++ b/enrol/externallib.php @@ -433,6 +433,8 @@ function($favourite) { 'isfavourite' => isset($favouritecourseids[$course->id]), 'hidden' => $hidden, 'overviewfiles' => $overviewfiles, + 'showactivitydates' => $course->showactivitydates, + 'showcompletionconditions' => $course->showcompletionconditions, ]; if ($returnusercount) { $courseresult['enrolledusercount'] = $enrolledusercount; @@ -479,6 +481,8 @@ public static function get_users_courses_returns() { 'isfavourite' => new external_value(PARAM_BOOL, 'If the user marked this course a favourite.', VALUE_OPTIONAL), 'hidden' => new external_value(PARAM_BOOL, 'If the user hide the course from the dashboard.', VALUE_OPTIONAL), 'overviewfiles' => new external_files('Overview files attached to this course.', VALUE_OPTIONAL), + 'showactivitydates' => new external_value(PARAM_BOOL, 'Whether the activity dates are shown or not'), + 'showcompletionconditions' => new external_value(PARAM_BOOL, 'Whether the activity completion conditions are shown or not'), ) ) ); diff --git a/lib/classes/external/exporter.php b/lib/classes/external/exporter.php index 6195288734f03..82ff9d9101dbd 100644 --- a/lib/classes/external/exporter.php +++ b/lib/classes/external/exporter.php @@ -489,12 +489,15 @@ final public static function get_create_structure() { /** * Returns the read structure. * + * @param int $required Whether is required. + * @param mixed $default The default value. + * * @return external_single_structure */ - final public static function get_read_structure() { + final public static function get_read_structure($required = VALUE_REQUIRED, $default = null) { $properties = self::read_properties_definition(); - return self::get_read_structure_from_properties($properties); + return self::get_read_structure_from_properties($properties, $required, $default); } /** diff --git a/lib/enrollib.php b/lib/enrollib.php index 286eb63e1699b..060292c0d2261 100644 --- a/lib/enrollib.php +++ b/lib/enrollib.php @@ -575,10 +575,13 @@ function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $coursei return array(); } - $basefields = array('id', 'category', 'sortorder', - 'shortname', 'fullname', 'idnumber', - 'startdate', 'visible', - 'groupmode', 'groupmodeforce', 'cacherev'); + $basefields = [ + 'id', 'category', 'sortorder', + 'shortname', 'fullname', 'idnumber', + 'startdate', 'visible', + 'groupmode', 'groupmodeforce', 'cacherev', + 'showactivitydates', 'showcompletionconditions', + ]; if (empty($fields)) { $fields = $basefields;