From 3df726539bdb33b95dfc52bed29476e70da18f4d Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 30 Jan 2025 15:41:12 +0100 Subject: [PATCH] MDL-78449 choice: Support groups in get_choice_results WS --- .upgradenotes/MDL-78449-2025021002533600.yml | 10 ++ .upgradenotes/MDL-78449-2025030507180697.yml | 9 + mod/choice/classes/external.php | 35 +++- mod/choice/lib.php | 15 +- mod/choice/tests/externallib_test.php | 171 +++++++++++++++++++ 5 files changed, 229 insertions(+), 11 deletions(-) create mode 100644 .upgradenotes/MDL-78449-2025021002533600.yml create mode 100644 .upgradenotes/MDL-78449-2025030507180697.yml diff --git a/.upgradenotes/MDL-78449-2025021002533600.yml b/.upgradenotes/MDL-78449-2025021002533600.yml new file mode 100644 index 00000000000..f3d86ee3c8c --- /dev/null +++ b/.upgradenotes/MDL-78449-2025021002533600.yml @@ -0,0 +1,10 @@ +issueNumber: MDL-78449 +notes: + mod_choice: + - message: > + The WebService `mod_choice_get_choice_results` has a new parameter + `groupid` that allows specifying the group to get the results for. The + default behaviour hasn't changed: if a choice has groups and the + parameter isn't specified the WebService will return the results for the + active group. + type: changed diff --git a/.upgradenotes/MDL-78449-2025030507180697.yml b/.upgradenotes/MDL-78449-2025030507180697.yml new file mode 100644 index 00000000000..128d7632b10 --- /dev/null +++ b/.upgradenotes/MDL-78449-2025030507180697.yml @@ -0,0 +1,9 @@ +issueNumber: MDL-78449 +notes: + mod_choice: + - message: > + The function `choice_get_response_data` has a new parameter that allows + specifying the group to get the results for. The default behaviour + hasn't changed: if a choice has groups and the parameter isn't used, the + function will return the results for the active group. + type: changed diff --git a/mod/choice/classes/external.php b/mod/choice/classes/external.php index f451d793fad..c7cb5c65bb8 100644 --- a/mod/choice/classes/external.php +++ b/mod/choice/classes/external.php @@ -54,20 +54,32 @@ class mod_choice_external extends external_api { * @since Moodle 3.0 */ public static function get_choice_results_parameters() { - return new external_function_parameters (array('choiceid' => new external_value(PARAM_INT, 'choice instance id'))); + return new external_function_parameters ([ + 'choiceid' => new external_value(PARAM_INT, 'choice instance id'), + 'groupid' => new external_value( + PARAM_INT, + 'Group ID. 0 for all participants, empty for active group.', + VALUE_DEFAULT, + null, + ), + ]); } /** * Returns user's results for a specific choice * and a list of those users that did not answered yet. * * @param int $choiceid the choice instance id + * @param int $groupid The group to use, 0 for all participants, null to calculate active group. * @return array of responses details * @since Moodle 3.0 */ - public static function get_choice_results($choiceid) { - global $USER, $PAGE; + public static function get_choice_results($choiceid, ?int $groupid = null) { + global $PAGE; - $params = self::validate_parameters(self::get_choice_results_parameters(), array('choiceid' => $choiceid)); + $params = self::validate_parameters(self::get_choice_results_parameters(), [ + 'choiceid' => $choiceid, + 'groupid' => $groupid, + ]); if (!$choice = choice_get_choice($params['choiceid'])) { throw new moodle_exception("invalidcoursemodule", "error"); @@ -77,10 +89,21 @@ class mod_choice_external extends external_api { $context = context_module::instance($cm->id); self::validate_context($context); - $groupmode = groups_get_activity_groupmode($cm); + if ($groupmode = groups_get_activity_groupmode($cm)) { + if (is_null($groupid)) { + $groupid = groups_get_activity_group($cm); + } + + if (!groups_group_visible($groupid, $course, $cm)) { + throw new moodle_exception('notingroup'); + } + } else { + $groupid = 0; + } + // Check if we have to include responses from inactive users. $onlyactive = $choice->includeinactive ? false : true; - $users = choice_get_response_data($choice, $cm, $groupmode, $onlyactive); + $users = choice_get_response_data($choice, $cm, $groupmode, $onlyactive, $groupid); // Show those who haven't answered the question. if (!empty($choice->showunanswered)) { $choice->option[0] = get_string('notanswered', 'choice'); diff --git a/mod/choice/lib.php b/mod/choice/lib.php index cbea4f270fe..25b05b8c927 100644 --- a/mod/choice/lib.php +++ b/mod/choice/lib.php @@ -782,6 +782,8 @@ function choice_reset_userdata($data) { } /** + * Get response data for a choice and group. + * * @global object * @global object * @global object @@ -790,18 +792,21 @@ function choice_reset_userdata($data) { * @param object $cm * @param int $groupmode * @param bool $onlyactive Whether to get response data for active users only. + * @param int $groupid Group id, null for current group if choice has groups. * @return array */ -function choice_get_response_data($choice, $cm, $groupmode, $onlyactive) { +function choice_get_response_data($choice, $cm, $groupmode, $onlyactive, ?int $groupid = null) { global $CFG, $USER, $DB; $context = context_module::instance($cm->id); -/// Get the current group if ($groupmode > 0) { - $currentgroup = groups_get_activity_group($cm); + if (is_null($groupid)) { + // Get the current group. + $groupid = groups_get_activity_group($cm); + } } else { - $currentgroup = 0; + $groupid = 0; } /// Initialise the returned array, which is a matrix: $allresponses[responseid][userid] = responseobject @@ -812,7 +817,7 @@ function choice_get_response_data($choice, $cm, $groupmode, $onlyactive) { // TODO Does not support custom user profile fields (MDL-70456). $userfieldsapi = \core_user\fields::for_identity($context, false)->with_userpic(); $userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; - $allresponses[0] = get_enrolled_users($context, 'mod/choice:choose', $currentgroup, + $allresponses[0] = get_enrolled_users($context, 'mod/choice:choose', $groupid, $userfields, null, 0, 0, $onlyactive); /// Get all the recorded responses for this choice diff --git a/mod/choice/tests/externallib_test.php b/mod/choice/tests/externallib_test.php index 08de9a5863b..9b48ed38f04 100644 --- a/mod/choice/tests/externallib_test.php +++ b/mod/choice/tests/externallib_test.php @@ -34,6 +34,7 @@ require_once($CFG->dirroot . '/mod/choice/lib.php'); * @category external * @copyright 2015 Costantino Cito * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \mod_choice_external */ final class externallib_test extends externallib_advanced_testcase { @@ -144,6 +145,176 @@ final class externallib_test extends externallib_advanced_testcase { $this->assertEquals(1, $resultsarr[$myanswer]['numberofuser']); } + /** + * Test get_choice_results using groups. + */ + public function test_get_choice_results_with_groups(): void { + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]); + $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]); + $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice'); + + // Create 3 choices: one with separate groups, one with visible groups and one with no groups. + $commonparams = [ + 'course' => $course->id, + 'option' => ['fried rice', 'spring rolls', 'sweet and sour pork', 'satay beef', 'gyouza'], + 'name' => 'Separate groups choice', + 'showresults' => CHOICE_SHOWRESULTS_ALWAYS, + 'publish' => 1, + ]; + + $nogroupschoice = $choicegenerator->create_instance(array_merge($commonparams, ['groupmode' => NOGROUPS])); + $separatechoice = $choicegenerator->create_instance(array_merge($commonparams, ['groupmode' => SEPARATEGROUPS])); + $visiblechoice = $choicegenerator->create_instance(array_merge($commonparams, ['groupmode' => VISIBLEGROUPS])); + $nogroupsoptions = array_keys(choice_get_choice($nogroupschoice->id)->option); + $separateoptions = array_keys(choice_get_choice($separatechoice->id)->option); + $visibleoptions = array_keys(choice_get_choice($visiblechoice->id)->option); + $nogroupscm = get_coursemodule_from_id('choice', $nogroupschoice->cmid); + $separatecm = get_coursemodule_from_id('choice', $separatechoice->cmid); + $visiblecm = get_coursemodule_from_id('choice', $visiblechoice->cmid); + + // Enrol 3 students in the course. One student will have no group, the others will belong one to each group. + $student1 = $this->getDataGenerator()->create_user(); + $student2 = $this->getDataGenerator()->create_user(); + $studentnogroup = $this->getDataGenerator()->create_user(); + + self::getDataGenerator()->enrol_user($student1->id, $course->id, 'student'); + self::getDataGenerator()->enrol_user($student2->id, $course->id, 'student'); + self::getDataGenerator()->enrol_user($studentnogroup->id, $course->id, 'student'); + + groups_add_member($group1, $student1); + groups_add_member($group2, $student2); + + // Add answers for both users in all choices. + $this->setUser($student1); + choice_user_submit_response($nogroupsoptions[0], $nogroupschoice, $student1->id, $course, $nogroupscm); + choice_user_submit_response($separateoptions[0], $separatechoice, $student1->id, $course, $separatecm); + choice_user_submit_response($visibleoptions[0], $visiblechoice, $student1->id, $course, $visiblecm); + + $this->setUser($student2); + choice_user_submit_response($nogroupsoptions[1], $nogroupschoice, $student2->id, $course, $nogroupscm); + choice_user_submit_response($separateoptions[1], $separatechoice, $student2->id, $course, $separatecm); + choice_user_submit_response($visibleoptions[1], $visiblechoice, $student2->id, $course, $visiblecm); + + // No groups: check that the groupid parameter is ignored. + $results = mod_choice_external::get_choice_results($nogroupschoice->id, $group1->id); + $results = external_api::clean_returnvalue(mod_choice_external::get_choice_results_returns(), $results); + + $resultsarr = []; + foreach ($results['options'] as $option) { + $resultsarr[$option['id']] = $option['userresponses']; + } + $this->assertEquals($resultsarr[$nogroupsoptions[0]][0]['userid'], $student1->id); + $this->assertEquals($resultsarr[$nogroupsoptions[1]][0]['userid'], $student2->id); + + $results = mod_choice_external::get_choice_results($nogroupschoice->id, $group2->id); + $results = external_api::clean_returnvalue(mod_choice_external::get_choice_results_returns(), $results); + + $resultsarr = []; + foreach ($results['options'] as $option) { + $resultsarr[$option['id']] = $option['userresponses']; + } + $this->assertEquals($resultsarr[$nogroupsoptions[0]][0]['userid'], $student1->id); + $this->assertEquals($resultsarr[$nogroupsoptions[1]][0]['userid'], $student2->id); + + // Separate groups: check that students can only see results of the group they belong. + $this->setUser($student1); + $results = mod_choice_external::get_choice_results($separatechoice->id, $group1->id); + $results = external_api::clean_returnvalue(mod_choice_external::get_choice_results_returns(), $results); + + $resultsarr = []; + foreach ($results['options'] as $option) { + $resultsarr[$option['id']] = $option['userresponses']; + } + $this->assertEquals($resultsarr[$separateoptions[0]][0]['userid'], $student1->id); + $this->assertCount(0, $resultsarr[$separateoptions[1]]); // The answer for the group 2 is not returned. + + try { + mod_choice_external::get_choice_results($separatechoice->id, $group2->id); + $this->fail('Exception expected due to not visible group.'); + } catch (\moodle_exception $e) { + $this->assertEquals('notingroup', $e->errorcode); + } + + try { + mod_choice_external::get_choice_results($separatechoice->id, 0); // All participants also throws error. + $this->fail('Exception expected due to not visible group.'); + } catch (\moodle_exception $e) { + $this->assertEquals('notingroup', $e->errorcode); + } + + $this->setUser($student2); + $results = mod_choice_external::get_choice_results($separatechoice->id, $group2->id); + $results = external_api::clean_returnvalue(mod_choice_external::get_choice_results_returns(), $results); + + $resultsarr = []; + foreach ($results['options'] as $option) { + $resultsarr[$option['id']] = $option['userresponses']; + } + $this->assertEquals($resultsarr[$separateoptions[1]][0]['userid'], $student2->id); + $this->assertCount(0, $resultsarr[$separateoptions[0]]); // The answer for the group 1 is not returned. + + try { + mod_choice_external::get_choice_results($separatechoice->id, $group1->id); + $this->fail('Exception expected due to not visible group.'); + } catch (\moodle_exception $e) { + $this->assertEquals('notingroup', $e->errorcode); + } + + // Visible groups: check that students can see results of all groups, including 'All participants'. + $this->setUser($student1); + $results = mod_choice_external::get_choice_results($visiblechoice->id, $group1->id); + $results = external_api::clean_returnvalue(mod_choice_external::get_choice_results_returns(), $results); + + $resultsarr = []; + foreach ($results['options'] as $option) { + $resultsarr[$option['id']] = $option['userresponses']; + } + $this->assertEquals($resultsarr[$visibleoptions[0]][0]['userid'], $student1->id); + $this->assertCount(0, $resultsarr[$visibleoptions[1]]); // The answer for the other group is not returned. + + $results = mod_choice_external::get_choice_results($visiblechoice->id, $group2->id); + $results = external_api::clean_returnvalue(mod_choice_external::get_choice_results_returns(), $results); + + $resultsarr = []; + foreach ($results['options'] as $option) { + $resultsarr[$option['id']] = $option['userresponses']; + } + $this->assertEquals($resultsarr[$visibleoptions[1]][0]['userid'], $student2->id); + $this->assertCount(0, $resultsarr[$visibleoptions[0]]); // The answer for the other group is not returned. + + $results = mod_choice_external::get_choice_results($visiblechoice->id, 0); + $results = external_api::clean_returnvalue(mod_choice_external::get_choice_results_returns(), $results); + + $resultsarr = []; + foreach ($results['options'] as $option) { + $resultsarr[$option['id']] = $option['userresponses']; + } + $this->assertEquals($resultsarr[$visibleoptions[0]][0]['userid'], $student1->id); + $this->assertEquals($resultsarr[$visibleoptions[1]][0]['userid'], $student2->id); + + // User with no groups can view results for the visible groups choice, but not for separate groups. + $this->setUser($studentnogroup); + $results = mod_choice_external::get_choice_results($visiblechoice->id, 0); + $results = external_api::clean_returnvalue(mod_choice_external::get_choice_results_returns(), $results); + + $resultsarr = []; + foreach ($results['options'] as $option) { + $resultsarr[$option['id']] = $option['userresponses']; + } + $this->assertEquals($resultsarr[$visibleoptions[0]][0]['userid'], $student1->id); + $this->assertEquals($resultsarr[$visibleoptions[1]][0]['userid'], $student2->id); + + try { + mod_choice_external::get_choice_results($separatechoice->id, 0); + $this->fail('Exception expected due to not visible group.'); + } catch (\moodle_exception $e) { + $this->assertEquals('notingroup', $e->errorcode); + } + } + /** * Test get_choice_options */