Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 311 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Feedback module external functions tests
  19   *
  20   * @package    mod_feedback
  21   * @category   external
  22   * @copyright  2017 Juan Leyva <juan@moodle.com>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   * @since      Moodle 3.3
  25   */
  26  
  27  namespace mod_feedback\external;
  28  
  29  use externallib_advanced_testcase;
  30  use feedback_item_multichoice;
  31  use mod_feedback_external;
  32  use moodle_exception;
  33  
  34  defined('MOODLE_INTERNAL') || die();
  35  
  36  global $CFG;
  37  
  38  require_once($CFG->dirroot . '/webservice/tests/helpers.php');
  39  require_once($CFG->dirroot . '/mod/feedback/lib.php');
  40  
  41  /**
  42   * Feedback module external functions tests
  43   *
  44   * @package    mod_feedback
  45   * @category   external
  46   * @copyright  2017 Juan Leyva <juan@moodle.com>
  47   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  48   * @since      Moodle 3.3
  49   * @covers     \mod_feedback_external
  50   */
  51  class external_test extends externallib_advanced_testcase {
  52  
  53      // TODO These should be removed.
  54      // Testcase classes should not have any properties or store state.
  55      protected $course;
  56      protected $feedback;
  57      protected $context;
  58      protected $cm;
  59      protected $student;
  60      protected $teacher;
  61      protected $studentrole;
  62      protected $teacherrole;
  63  
  64      /**
  65       * Set up for every test
  66       */
  67      public function setUp(): void {
  68          global $DB;
  69          $this->resetAfterTest();
  70          $this->setAdminUser();
  71  
  72          // Setup test data.
  73          $this->course = $this->getDataGenerator()->create_course();
  74          $this->feedback = $this->getDataGenerator()->create_module('feedback',
  75              array('course' => $this->course->id, 'email_notification' => 1));
  76          $this->context = \context_module::instance($this->feedback->cmid);
  77          $this->cm = get_coursemodule_from_instance('feedback', $this->feedback->id);
  78  
  79          // Create users.
  80          $this->student = self::getDataGenerator()->create_user();
  81          $this->teacher = self::getDataGenerator()->create_user();
  82  
  83          // Users enrolments.
  84          $this->studentrole = $DB->get_record('role', array('shortname' => 'student'));
  85          $this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
  86          $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual');
  87          $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual');
  88      }
  89  
  90      /**
  91       * Helper method to add items to an existing feedback.
  92       *
  93       * @param \stdClass $feedback feedback instance
  94       * @param integer $pagescount the number of pages we want in the feedback
  95       * @return array list of items created
  96       */
  97      public function populate_feedback($feedback, $pagescount = 1) {
  98          $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
  99          $itemscreated = [];
 100  
 101          // Create at least one page.
 102          $itemscreated[] = $feedbackgenerator->create_item_label($feedback);
 103          $itemscreated[] = $feedbackgenerator->create_item_info($feedback);
 104          $itemscreated[] = $feedbackgenerator->create_item_numeric($feedback);
 105  
 106          // Check if we want more pages.
 107          for ($i = 1; $i < $pagescount; $i++) {
 108              $itemscreated[] = $feedbackgenerator->create_item_pagebreak($feedback);
 109              $itemscreated[] = $feedbackgenerator->create_item_multichoice($feedback);
 110              $itemscreated[] = $feedbackgenerator->create_item_multichoicerated($feedback);
 111              $itemscreated[] = $feedbackgenerator->create_item_textarea($feedback);
 112              $itemscreated[] = $feedbackgenerator->create_item_textfield($feedback);
 113              $itemscreated[] = $feedbackgenerator->create_item_numeric($feedback);
 114          }
 115          return $itemscreated;
 116      }
 117  
 118  
 119      /**
 120       * Test test_mod_feedback_get_feedbacks_by_courses
 121       */
 122      public function test_mod_feedback_get_feedbacks_by_courses() {
 123  
 124          // Create additional course.
 125          $course2 = self::getDataGenerator()->create_course();
 126  
 127          // Second feedback.
 128          $record = new \stdClass();
 129          $record->course = $course2->id;
 130          $feedback2 = self::getDataGenerator()->create_module('feedback', $record);
 131  
 132          // Execute real Moodle enrolment as we'll call unenrol() method on the instance later.
 133          $enrol = enrol_get_plugin('manual');
 134          $enrolinstances = enrol_get_instances($course2->id, true);
 135          $instance2 = (object) [];
 136          foreach ($enrolinstances as $courseenrolinstance) {
 137              if ($courseenrolinstance->enrol == "manual") {
 138                  $instance2 = $courseenrolinstance;
 139                  break;
 140              }
 141          }
 142  
 143          $enrol->enrol_user($instance2, $this->student->id, $this->studentrole->id);
 144  
 145          self::setUser($this->student);
 146  
 147          $returndescription = mod_feedback_external::get_feedbacks_by_courses_returns();
 148  
 149          // Create what we expect to be returned when querying the two courses.
 150          // First for the student user.
 151          $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'lang', 'anonymous',
 152              'multiple_submit', 'autonumbering', 'page_after_submitformat', 'publish_stats', 'completionsubmit');
 153  
 154          $properties = feedback_summary_exporter::read_properties_definition();
 155  
 156          // Add expected coursemodule and data.
 157          $feedback1 = $this->feedback;
 158          $feedback1->coursemodule = $feedback1->cmid;
 159          $feedback1->introformat = 1;
 160          $feedback1->introfiles = [];
 161          $feedback1->lang = '';
 162  
 163          $feedback2->coursemodule = $feedback2->cmid;
 164          $feedback2->introformat = 1;
 165          $feedback2->introfiles = [];
 166          $feedback2->lang = '';
 167  
 168          foreach ($expectedfields as $field) {
 169              if (!empty($properties[$field]) && $properties[$field]['type'] == PARAM_BOOL) {
 170                  $feedback1->{$field} = (bool) $feedback1->{$field};
 171                  $feedback2->{$field} = (bool) $feedback2->{$field};
 172              }
 173              $expected1[$field] = $feedback1->{$field};
 174              $expected2[$field] = $feedback2->{$field};
 175          }
 176  
 177          $expectedfeedbacks = array($expected2, $expected1);
 178  
 179          // Call the external function passing course ids.
 180          $result = mod_feedback_external::get_feedbacks_by_courses(array($course2->id, $this->course->id));
 181          $result = \external_api::clean_returnvalue($returndescription, $result);
 182  
 183          $this->assertEquals($expectedfeedbacks, $result['feedbacks']);
 184          $this->assertCount(0, $result['warnings']);
 185  
 186          // Call the external function without passing course id.
 187          $result = mod_feedback_external::get_feedbacks_by_courses();
 188          $result = \external_api::clean_returnvalue($returndescription, $result);
 189          $this->assertEquals($expectedfeedbacks, $result['feedbacks']);
 190          $this->assertCount(0, $result['warnings']);
 191  
 192          // Unenrol user from second course and alter expected feedbacks.
 193          $enrol->unenrol_user($instance2, $this->student->id);
 194          array_shift($expectedfeedbacks);
 195  
 196          // Call the external function without passing course id.
 197          $result = mod_feedback_external::get_feedbacks_by_courses();
 198          $result = \external_api::clean_returnvalue($returndescription, $result);
 199          $this->assertEquals($expectedfeedbacks, $result['feedbacks']);
 200  
 201          // Call for the second course we unenrolled the user from, expected warning.
 202          $result = mod_feedback_external::get_feedbacks_by_courses(array($course2->id));
 203          $this->assertCount(1, $result['warnings']);
 204          $this->assertEquals('1', $result['warnings'][0]['warningcode']);
 205          $this->assertEquals($course2->id, $result['warnings'][0]['itemid']);
 206  
 207          // Now, try as a teacher for getting all the additional fields.
 208          self::setUser($this->teacher);
 209  
 210          $additionalfields = array('email_notification', 'site_after_submit', 'page_after_submit', 'timeopen', 'timeclose',
 211              'timemodified', 'pageaftersubmitfiles');
 212  
 213          $feedback1->pageaftersubmitfiles = [];
 214  
 215          foreach ($additionalfields as $field) {
 216              if (!empty($properties[$field]) && $properties[$field]['type'] == PARAM_BOOL) {
 217                  $feedback1->{$field} = (bool) $feedback1->{$field};
 218              }
 219              $expectedfeedbacks[0][$field] = $feedback1->{$field};
 220          }
 221          $expectedfeedbacks[0]['page_after_submitformat'] = 1;
 222  
 223          $result = mod_feedback_external::get_feedbacks_by_courses();
 224          $result = \external_api::clean_returnvalue($returndescription, $result);
 225          $this->assertEquals($expectedfeedbacks, $result['feedbacks']);
 226  
 227          // Admin also should get all the information.
 228          self::setAdminUser();
 229  
 230          $result = mod_feedback_external::get_feedbacks_by_courses(array($this->course->id));
 231          $result = \external_api::clean_returnvalue($returndescription, $result);
 232          $this->assertEquals($expectedfeedbacks, $result['feedbacks']);
 233      }
 234  
 235      /**
 236       * Test get_feedback_access_information function with basic defaults for student.
 237       */
 238      public function test_get_feedback_access_information_student() {
 239  
 240          self::setUser($this->student);
 241          $result = mod_feedback_external::get_feedback_access_information($this->feedback->id);
 242          $result = \external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result);
 243  
 244          $this->assertFalse($result['canviewanalysis']);
 245          $this->assertFalse($result['candeletesubmissions']);
 246          $this->assertFalse($result['canviewreports']);
 247          $this->assertFalse($result['canedititems']);
 248          $this->assertTrue($result['cancomplete']);
 249          $this->assertTrue($result['cansubmit']);
 250          $this->assertTrue($result['isempty']);
 251          $this->assertTrue($result['isopen']);
 252          $this->assertTrue($result['isanonymous']);
 253          $this->assertFalse($result['isalreadysubmitted']);
 254      }
 255  
 256      /**
 257       * Test get_feedback_access_information function with basic defaults for teacher.
 258       */
 259      public function test_get_feedback_access_information_teacher() {
 260  
 261          self::setUser($this->teacher);
 262          $result = mod_feedback_external::get_feedback_access_information($this->feedback->id);
 263          $result = \external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result);
 264  
 265          $this->assertTrue($result['canviewanalysis']);
 266          $this->assertTrue($result['canviewreports']);
 267          $this->assertTrue($result['canedititems']);
 268          $this->assertTrue($result['candeletesubmissions']);
 269          $this->assertFalse($result['cancomplete']);
 270          $this->assertTrue($result['cansubmit']);
 271          $this->assertTrue($result['isempty']);
 272          $this->assertTrue($result['isopen']);
 273          $this->assertTrue($result['isanonymous']);
 274          $this->assertFalse($result['isalreadysubmitted']);
 275  
 276          // Add some items to the feedback and check is not empty any more.
 277          self::populate_feedback($this->feedback);
 278          $result = mod_feedback_external::get_feedback_access_information($this->feedback->id);
 279          $result = \external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result);
 280          $this->assertFalse($result['isempty']);
 281      }
 282  
 283      /**
 284       * Test view_feedback invalid id.
 285       */
 286      public function test_view_feedback_invalid_id() {
 287          // Test invalid instance id.
 288          $this->expectException(moodle_exception::class);
 289          mod_feedback_external::view_feedback(0);
 290      }
 291      /**
 292       * Test view_feedback not enrolled user.
 293       */
 294      public function test_view_feedback_not_enrolled_user() {
 295          $usernotenrolled = self::getDataGenerator()->create_user();
 296          $this->setUser($usernotenrolled);
 297          $this->expectException(moodle_exception::class);
 298          mod_feedback_external::view_feedback(0);
 299      }
 300      /**
 301       * Test view_feedback no capabilities.
 302       */
 303      public function test_view_feedback_no_capabilities() {
 304          // Test user with no capabilities.
 305          // We need a explicit prohibit since this capability is allowed for students by default.
 306          assign_capability('mod/feedback:view', CAP_PROHIBIT, $this->studentrole->id, $this->context->id);
 307          accesslib_clear_all_caches_for_unit_testing();
 308          $this->expectException(moodle_exception::class);
 309          mod_feedback_external::view_feedback(0);
 310      }
 311      /**
 312       * Test view_feedback.
 313       */
 314      public function test_view_feedback() {
 315          // Test user with full capabilities.
 316          $this->setUser($this->student);
 317          // Trigger and capture the event.
 318          $sink = $this->redirectEvents();
 319          $result = mod_feedback_external::view_feedback($this->feedback->id);
 320          $result = \external_api::clean_returnvalue(mod_feedback_external::view_feedback_returns(), $result);
 321          $events = $sink->get_events();
 322          $this->assertCount(1, $events);
 323          $event = array_shift($events);
 324          // Checking that the event contains the expected values.
 325          $this->assertInstanceOf('\mod_feedback\event\course_module_viewed', $event);
 326          $this->assertEquals($this->context, $event->get_context());
 327          $moodledata = new \moodle_url('/mod/feedback/view.php', array('id' => $this->cm->id));
 328          $this->assertEquals($moodledata, $event->get_url());
 329          $this->assertEventContextNotUsed($event);
 330          $this->assertNotEmpty($event->get_name());
 331      }
 332  
 333      /**
 334       * Test get_current_completed_tmp.
 335       */
 336      public function test_get_current_completed_tmp() {
 337          global $DB;
 338  
 339          // Force non anonymous.
 340          $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_NO, array('id' => $this->feedback->id));
 341          // Add a completed_tmp record.
 342          $record = [
 343              'feedback' => $this->feedback->id,
 344              'userid' => $this->student->id,
 345              'guestid' => '',
 346              'timemodified' => time() - DAYSECS,
 347              'random_response' => 0,
 348              'anonymous_response' => FEEDBACK_ANONYMOUS_NO,
 349              'courseid' => $this->course->id,
 350          ];
 351          $record['id'] = $DB->insert_record('feedback_completedtmp', (object) $record);
 352  
 353          // Test user with full capabilities.
 354          $this->setUser($this->student);
 355  
 356          $result = mod_feedback_external::get_current_completed_tmp($this->feedback->id);
 357          $result = \external_api::clean_returnvalue(mod_feedback_external::get_current_completed_tmp_returns(), $result);
 358          $this->assertEquals($record['id'], $result['feedback']['id']);
 359      }
 360  
 361      /**
 362       * Test get_items.
 363       */
 364      public function test_get_items() {
 365          // Test user with full capabilities.
 366          $this->setUser($this->student);
 367  
 368          // Add questions to the feedback, we are adding 2 pages of questions.
 369          $itemscreated = self::populate_feedback($this->feedback, 2);
 370  
 371          $result = mod_feedback_external::get_items($this->feedback->id);
 372          $result = \external_api::clean_returnvalue(mod_feedback_external::get_items_returns(), $result);
 373          $this->assertCount(count($itemscreated), $result['items']);
 374          $index = 1;
 375          foreach ($result['items'] as $key => $item) {
 376              if (is_numeric($itemscreated[$key])) {
 377                  continue; // Page break.
 378              }
 379              // Cannot compare directly the exporter and the db object (exporter have more fields).
 380              $this->assertEquals($itemscreated[$key]->id, $item['id']);
 381              $this->assertEquals($itemscreated[$key]->typ, $item['typ']);
 382              $this->assertEquals($itemscreated[$key]->name, $item['name']);
 383              $this->assertEquals($itemscreated[$key]->label, $item['label']);
 384  
 385              if ($item['hasvalue']) {
 386                  $this->assertEquals($index, $item['itemnumber']);
 387                  $index++;
 388              }
 389          }
 390      }
 391  
 392      /**
 393       * Test get_items, to confirm validation is done too.
 394       *
 395       * @dataProvider items_provider
 396       * @param string $role Whether the current user should be a student or a teacher.
 397       * @param array $info Settings to create the feedback.
 398       * @param string|null $warning The warning message to display or null if warnings result is empty.
 399       */
 400      public function test_get_items_validation(string $role, array $info, ?string $warning): void {
 401          global $DB;
 402  
 403          // Test user with full capabilities.
 404          if ($role === 'teacher') {
 405              $this->setUser($this->teacher);
 406          } else {
 407              $this->setUser($this->student);
 408          }
 409  
 410          // Create the feedback.
 411          $data = ['course' => $this->course->id];
 412          if (array_key_exists('closed', $info) && $info['closed']) {
 413              $data['timeopen'] = time() + DAYSECS;
 414          }
 415          $feedback = $this->getDataGenerator()->create_module('feedback', $data);
 416  
 417          $empty = true;
 418          if (!array_key_exists('empty', $info) || !$info['empty']) {
 419              $empty = false;
 420              /** @var \mod_feedback_generator $feedbackgenerator */
 421              $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
 422              // Add,at least, one item to the feedback.
 423              $feedbackgenerator->create_item_label($feedback);
 424          }
 425  
 426          if (array_key_exists('complete', $info) && !$info['complete']) {
 427              $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 428              $coursecontext = \context_course::instance($this->course->id);
 429              assign_capability('mod/feedback:complete', CAP_PROHIBIT, $studentrole->id, $coursecontext->id);
 430              // Empty all the caches that may be affected by this change.
 431              accesslib_clear_all_caches_for_unit_testing();
 432              \course_modinfo::clear_instance_cache();
 433          }
 434  
 435          $result = mod_feedback_external::get_items($feedback->id);
 436          $result = \external_api::clean_returnvalue(mod_feedback_external::get_items_returns(), $result);
 437          if ($warning) {
 438              $this->assertEmpty($result['items']);
 439              $this->assertCount(1, $result['warnings']);
 440              $resultwarning = reset($result['warnings']);
 441              if ($warning == 'required_capability_exception') {
 442                  $this->assertStringContainsString('error/nopermission', $resultwarning['message']);
 443              } else {
 444                  $this->assertEquals($warning, $resultwarning['message']);
 445              }
 446          } else {
 447              if ($empty) {
 448                  $this->assertEmpty($result['items']);
 449              } else {
 450                  $this->assertCount(1, $result['items']);
 451              }
 452              $this->assertEmpty($result['warnings']);
 453          }
 454      }
 455  
 456      /**
 457       * Data provider for test_get_items_validation() and test_get_page_items_validation().
 458       *
 459       * @return array
 460       */
 461      public function items_provider(): array {
 462          return [
 463              'Valid feedback (as student)' => [
 464                  'role' => 'student',
 465                  'info' => [],
 466                  'warning' => null,
 467              ],
 468              'Closed feedback (as student)' => [
 469                  'role' => 'student',
 470                  'info' => ['closed' => true],
 471                  'warning' => get_string('feedback_is_not_open', 'feedback'),
 472              ],
 473              'Empty feedback (as student)' => [
 474                  'role' => 'student',
 475                  'info' => ['empty' => true],
 476                  'warning' => get_string('no_items_available_yet', 'feedback'),
 477              ],
 478              'Closed feedback (as student)' => [
 479                  'role' => 'student',
 480                  'info' => ['closed' => true],
 481                  'warning' => get_string('feedback_is_not_open', 'feedback'),
 482              ],
 483              'Cannot complete feedback (as student)' => [
 484                  'role' => 'student',
 485                  'info' => ['complete' => false],
 486                  'warning' => 'required_capability_exception',
 487              ],
 488              'Valid feedback (as teacher)' => [
 489                  'role' => 'teacher',
 490                  'info' => [],
 491                  'warning' => null,
 492              ],
 493              'Closed feedback (as teacher)' => [
 494                  'role' => 'teacher',
 495                  'info' => ['closed' => true],
 496                  'warning' => null,
 497              ],
 498              'Empty feedback (as teacher)' => [
 499                  'role' => 'teacher',
 500                  'info' => ['empty' => true],
 501                  'warning' => null,
 502              ],
 503              'Closed feedback (as teacher)' => [
 504                  'role' => 'teacher',
 505                  'info' => ['closed' => true],
 506                  'warning' => null,
 507              ],
 508              'Cannot complete feedback (as teacher)' => [
 509                  'role' => 'teacher',
 510                  'info' => ['complete' => false],
 511                  'warning' => null,
 512              ],
 513          ];
 514      }
 515  
 516      /**
 517       * Test launch_feedback.
 518       */
 519      public function test_launch_feedback() {
 520          global $DB;
 521  
 522          // Test user with full capabilities.
 523          $this->setUser($this->student);
 524  
 525          // Add questions to the feedback, we are adding 2 pages of questions.
 526          $itemscreated = self::populate_feedback($this->feedback, 2);
 527  
 528          // First try a feedback we didn't attempt.
 529          $result = mod_feedback_external::launch_feedback($this->feedback->id);
 530          $result = \external_api::clean_returnvalue(mod_feedback_external::launch_feedback_returns(), $result);
 531          $this->assertEquals(0, $result['gopage']);
 532  
 533          // Now, try a feedback that we attempted.
 534          // Force non anonymous.
 535          $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_NO, array('id' => $this->feedback->id));
 536          // Add a completed_tmp record.
 537          $record = [
 538              'feedback' => $this->feedback->id,
 539              'userid' => $this->student->id,
 540              'guestid' => '',
 541              'timemodified' => time() - DAYSECS,
 542              'random_response' => 0,
 543              'anonymous_response' => FEEDBACK_ANONYMOUS_NO,
 544              'courseid' => $this->course->id,
 545          ];
 546          $record['id'] = $DB->insert_record('feedback_completedtmp', (object) $record);
 547  
 548          // Add a response to the feedback for each question type with possible values.
 549          $response = [
 550              'course_id' => $this->course->id,
 551              'item' => $itemscreated[1]->id, // First item is the info question.
 552              'completed' => $record['id'],
 553              'tmp_completed' => $record['id'],
 554              'value' => 'A',
 555          ];
 556          $DB->insert_record('feedback_valuetmp', (object) $response);
 557          $response = [
 558              'course_id' => $this->course->id,
 559              'item' => $itemscreated[2]->id, // Second item is the numeric question.
 560              'completed' => $record['id'],
 561              'tmp_completed' => $record['id'],
 562              'value' => 5,
 563          ];
 564          $DB->insert_record('feedback_valuetmp', (object) $response);
 565  
 566          $result = mod_feedback_external::launch_feedback($this->feedback->id);
 567          $result = \external_api::clean_returnvalue(mod_feedback_external::launch_feedback_returns(), $result);
 568          $this->assertEquals(1, $result['gopage']);
 569      }
 570  
 571      /**
 572       * Test get_page_items.
 573       */
 574      public function test_get_page_items() {
 575          // Test user with full capabilities.
 576          $this->setUser($this->student);
 577  
 578          // Add questions to the feedback, we are adding 2 pages of questions.
 579          $itemscreated = self::populate_feedback($this->feedback, 2);
 580  
 581          // Retrieve first page.
 582          $result = mod_feedback_external::get_page_items($this->feedback->id, 0);
 583          $result = \external_api::clean_returnvalue(mod_feedback_external::get_page_items_returns(), $result);
 584          $this->assertCount(3, $result['items']);    // The first page has 3 items.
 585          $this->assertTrue($result['hasnextpage']);
 586          $this->assertFalse($result['hasprevpage']);
 587  
 588          // Retrieve second page.
 589          $result = mod_feedback_external::get_page_items($this->feedback->id, 1);
 590          $result = \external_api::clean_returnvalue(mod_feedback_external::get_page_items_returns(), $result);
 591          $this->assertCount(5, $result['items']);    // The second page has 5 items (page break doesn't count).
 592          $this->assertFalse($result['hasnextpage']);
 593          $this->assertTrue($result['hasprevpage']);
 594      }
 595  
 596      /**
 597       * Test get_page_items, to confirm validation is done too.
 598       *
 599       * @dataProvider items_provider
 600       * @param string $role Whether the current user should be a student or a teacher.
 601       * @param array $info Settings to create the feedback.
 602       * @param string|null $warning The warning message to display or null if warnings result is empty.
 603       */
 604      public function test_get_page_items_validation(string $role, array $info, ?string $warning): void {
 605          global $DB;
 606  
 607          // Test user with full capabilities.
 608          if ($role === 'teacher') {
 609              $this->setUser($this->teacher);
 610          } else {
 611              $this->setUser($this->student);
 612          }
 613  
 614          // Create the feedback.
 615          $data = ['course' => $this->course->id];
 616          if (array_key_exists('closed', $info) && $info['closed']) {
 617              $data['timeopen'] = time() + DAYSECS;
 618          }
 619          $feedback = $this->getDataGenerator()->create_module('feedback', $data);
 620  
 621          $empty = true;
 622          if (!array_key_exists('empty', $info) || !$info['empty']) {
 623              $empty = false;
 624              /** @var \mod_feedback_generator $feedbackgenerator */
 625              $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
 626              // Add,at least, one item to the feedback.
 627              $feedbackgenerator->create_item_label($feedback);
 628          }
 629  
 630          if (array_key_exists('complete', $info) && !$info['complete']) {
 631              $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 632              $coursecontext = \context_course::instance($this->course->id);
 633              assign_capability('mod/feedback:complete', CAP_PROHIBIT, $studentrole->id, $coursecontext->id);
 634              // Empty all the caches that may be affected by this change.
 635              accesslib_clear_all_caches_for_unit_testing();
 636              \course_modinfo::clear_instance_cache();
 637          }
 638  
 639          $result = mod_feedback_external::get_page_items($feedback->id, 0);
 640          $result = \external_api::clean_returnvalue(mod_feedback_external::get_items_returns(), $result);
 641          if ($warning) {
 642              $this->assertEmpty($result['items']);
 643              $this->assertCount(1, $result['warnings']);
 644              $resultwarning = reset($result['warnings']);
 645              if ($warning == 'required_capability_exception') {
 646                  $this->assertStringContainsString('error/nopermission', $resultwarning['message']);
 647              } else {
 648                  $this->assertEquals($warning, $resultwarning['message']);
 649              }
 650          } else {
 651              if ($empty) {
 652                  $this->assertEmpty($result['items']);
 653              } else {
 654                  $this->assertCount(1, $result['items']);
 655              }
 656              $this->assertEmpty($result['warnings']);
 657          }
 658      }
 659  
 660      /**
 661       * Test process_page.
 662       */
 663      public function test_process_page() {
 664          global $DB;
 665  
 666          // Test user with full capabilities.
 667          $this->setUser($this->student);
 668          $pagecontents = 'You finished it!';
 669          $DB->set_field('feedback', 'page_after_submit', $pagecontents, array('id' => $this->feedback->id));
 670  
 671          // Add questions to the feedback, we are adding 2 pages of questions.
 672          $itemscreated = self::populate_feedback($this->feedback, 2);
 673  
 674          $data = [];
 675          foreach ($itemscreated as $item) {
 676  
 677              if (empty($item->hasvalue)) {
 678                  continue;
 679              }
 680  
 681              switch ($item->typ) {
 682                  case 'textarea':
 683                  case 'textfield':
 684                      $value = 'Lorem ipsum';
 685                      break;
 686                  case 'numeric':
 687                      $value = 5;
 688                      break;
 689                  case 'multichoice':
 690                      $value = '1';
 691                      break;
 692                  case 'multichoicerated':
 693                      $value = '1';
 694                      break;
 695                  case 'info':
 696                      $value = format_string($this->course->shortname, true, array('context' => $this->context));
 697                      break;
 698                  default:
 699                      $value = '';
 700              }
 701              $data[] = ['name' => $item->typ . '_' . $item->id, 'value' => $value];
 702          }
 703  
 704          // Process first page.
 705          $firstpagedata = [$data[0], $data[1]];
 706          $result = mod_feedback_external::process_page($this->feedback->id, 0, $firstpagedata);
 707          $result = \external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
 708          $this->assertEquals(1, $result['jumpto']);
 709          $this->assertFalse($result['completed']);
 710  
 711          // Now, process the second page. But first we are going back to the first page.
 712          $secondpagedata = [$data[2], $data[3], $data[4], $data[5], $data[6]];
 713          $result = mod_feedback_external::process_page($this->feedback->id, 1, $secondpagedata, true);
 714          $result = \external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
 715          $this->assertFalse($result['completed']);
 716          $this->assertEquals(0, $result['jumpto']);  // We jumped to the first page.
 717          // Check the values were correctly saved.
 718          $tmpitems = $DB->get_records('feedback_valuetmp');
 719          $this->assertCount(7, $tmpitems);   // 2 from the first page + 5 from the second page.
 720  
 721          // Go forward again (sending the same data).
 722          $result = mod_feedback_external::process_page($this->feedback->id, 0, $firstpagedata);
 723          $result = \external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
 724          $this->assertEquals(1, $result['jumpto']);
 725          $this->assertFalse($result['completed']);
 726          $tmpitems = $DB->get_records('feedback_valuetmp');
 727          $this->assertCount(7, $tmpitems);   // 2 from the first page + 5 from the second page.
 728  
 729          // And finally, save everything! We are going to modify one previous recorded value.
 730          $messagessink = $this->redirectMessages();
 731          $data[2]['value'] = 2; // 2 is value of the option 'b'.
 732          $secondpagedata = [$data[2], $data[3], $data[4], $data[5], $data[6]];
 733          $result = mod_feedback_external::process_page($this->feedback->id, 1, $secondpagedata);
 734          $result = \external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
 735          $this->assertTrue($result['completed']);
 736          $this->assertTrue(strpos($result['completionpagecontents'], $pagecontents) !== false);
 737          // Check all the items were saved.
 738          $items = $DB->get_records('feedback_value');
 739          $this->assertCount(7, $items);
 740          // Check if the one we modified was correctly saved.
 741          $itemid = $itemscreated[4]->id;
 742          $itemsaved = $DB->get_field('feedback_value', 'value', array('item' => $itemid));
 743          $mcitem = new feedback_item_multichoice();
 744          $itemval = $mcitem->get_printval($itemscreated[4], (object) ['value' => $itemsaved]);
 745          $this->assertEquals('b', $itemval);
 746  
 747          // Check that the answers are saved for course 0.
 748          foreach ($items as $item) {
 749              $this->assertEquals(0, $item->course_id);
 750          }
 751          $completed = $DB->get_record('feedback_completed', []);
 752          $this->assertEquals(0, $completed->courseid);
 753  
 754          // Test notifications sent.
 755          $messages = $messagessink->get_messages();
 756          $messagessink->close();
 757          // Test customdata.
 758          $customdata = json_decode($messages[0]->customdata);
 759          $this->assertEquals($this->feedback->id, $customdata->instance);
 760          $this->assertEquals($this->feedback->cmid, $customdata->cmid);
 761          $this->assertObjectHasAttribute('notificationiconurl', $customdata);
 762      }
 763  
 764      /**
 765       * Test process_page for a site feedback.
 766       */
 767      public function test_process_page_site_feedback() {
 768          global $DB;
 769          $pagecontents = 'You finished it!';
 770          $this->feedback = $this->getDataGenerator()->create_module('feedback',
 771              array('course' => SITEID, 'page_after_submit' => $pagecontents));
 772  
 773          // Test user with full capabilities.
 774          $this->setUser($this->student);
 775  
 776          // Add questions to the feedback, we are adding 2 pages of questions.
 777          $itemscreated = self::populate_feedback($this->feedback, 2);
 778  
 779          $data = [];
 780          foreach ($itemscreated as $item) {
 781  
 782              if (empty($item->hasvalue)) {
 783                  continue;
 784              }
 785  
 786              switch ($item->typ) {
 787                  case 'textarea':
 788                  case 'textfield':
 789                      $value = 'Lorem ipsum';
 790                      break;
 791                  case 'numeric':
 792                      $value = 5;
 793                      break;
 794                  case 'multichoice':
 795                      $value = '1';
 796                      break;
 797                  case 'multichoicerated':
 798                      $value = '1';
 799                      break;
 800                  case 'info':
 801                      $value = format_string($this->course->shortname, true, array('context' => $this->context));
 802                      break;
 803                  default:
 804                      $value = '';
 805              }
 806              $data[] = ['name' => $item->typ . '_' . $item->id, 'value' => $value];
 807          }
 808  
 809          // Process first page.
 810          $firstpagedata = [$data[0], $data[1]];
 811          $result = mod_feedback_external::process_page($this->feedback->id, 0, $firstpagedata, false, $this->course->id);
 812          $result = \external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
 813          $this->assertEquals(1, $result['jumpto']);
 814          $this->assertFalse($result['completed']);
 815  
 816          // Process second page.
 817          $data[2]['value'] = 2; // 2 is value of the option 'b';
 818          $secondpagedata = [$data[2], $data[3], $data[4], $data[5], $data[6]];
 819          $result = mod_feedback_external::process_page($this->feedback->id, 1, $secondpagedata, false, $this->course->id);
 820          $result = \external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
 821          $this->assertTrue($result['completed']);
 822          $this->assertTrue(strpos($result['completionpagecontents'], $pagecontents) !== false);
 823          // Check all the items were saved.
 824          $items = $DB->get_records('feedback_value');
 825          $this->assertCount(7, $items);
 826          // Check if the one we modified was correctly saved.
 827          $itemid = $itemscreated[4]->id;
 828          $itemsaved = $DB->get_field('feedback_value', 'value', array('item' => $itemid));
 829          $mcitem = new feedback_item_multichoice();
 830          $itemval = $mcitem->get_printval($itemscreated[4], (object) ['value' => $itemsaved]);
 831          $this->assertEquals('b', $itemval);
 832  
 833          // Check that the answers are saved for the correct course.
 834          foreach ($items as $item) {
 835              $this->assertEquals($this->course->id, $item->course_id);
 836          }
 837          $completed = $DB->get_record('feedback_completed', []);
 838          $this->assertEquals($this->course->id, $completed->courseid);
 839      }
 840  
 841      /**
 842       * Test get_analysis.
 843       */
 844      public function test_get_analysis() {
 845          // Test user with full capabilities.
 846          $this->setUser($this->student);
 847  
 848          // Create a very simple feedback.
 849          $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
 850          $numericitem = $feedbackgenerator->create_item_numeric($this->feedback);
 851          $textfielditem = $feedbackgenerator->create_item_textfield($this->feedback);
 852  
 853          $pagedata = [
 854              ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 5],
 855              ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'abc'],
 856          ];
 857          // Process the feedback, there is only one page so the feedback will be completed.
 858          $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata);
 859          $result = \external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
 860          $this->assertTrue($result['completed']);
 861  
 862          // Retrieve analysis.
 863          $this->setUser($this->teacher);
 864          $result = mod_feedback_external::get_analysis($this->feedback->id);
 865          $result = \external_api::clean_returnvalue(mod_feedback_external::get_analysis_returns(), $result);
 866          $this->assertEquals(1, $result['completedcount']);  // 1 feedback completed.
 867          $this->assertEquals(2, $result['itemscount']);  // 2 items in the feedback.
 868          $this->assertCount(2, $result['itemsdata']);
 869          $this->assertCount(1, $result['itemsdata'][0]['data']); // There are 1 response per item.
 870          $this->assertCount(1, $result['itemsdata'][1]['data']);
 871          // Check we receive the info the students filled.
 872          foreach ($result['itemsdata'] as $data) {
 873              if ($data['item']['id'] == $numericitem->id) {
 874                  $this->assertEquals(5, $data['data'][0]);
 875              } else {
 876                  $this->assertEquals('abc', $data['data'][0]);
 877              }
 878          }
 879  
 880          // Create another user / response.
 881          $anotherstudent = self::getDataGenerator()->create_user();
 882          $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual');
 883          $this->setUser($anotherstudent);
 884  
 885          // Process the feedback, there is only one page so the feedback will be completed.
 886          $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata);
 887          $result = \external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
 888          $this->assertTrue($result['completed']);
 889  
 890          // Retrieve analysis.
 891          $this->setUser($this->teacher);
 892          $result = mod_feedback_external::get_analysis($this->feedback->id);
 893          $result = \external_api::clean_returnvalue(mod_feedback_external::get_analysis_returns(), $result);
 894          $this->assertEquals(2, $result['completedcount']);  // 2 feedback completed.
 895          $this->assertEquals(2, $result['itemscount']);
 896          $this->assertCount(2, $result['itemsdata'][0]['data']); // There are 2 responses per item.
 897          $this->assertCount(2, $result['itemsdata'][1]['data']);
 898      }
 899  
 900      /**
 901       * Test get_unfinished_responses.
 902       */
 903      public function test_get_unfinished_responses() {
 904          // Test user with full capabilities.
 905          $this->setUser($this->student);
 906  
 907          // Create a very simple feedback.
 908          $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
 909          $numericitem = $feedbackgenerator->create_item_numeric($this->feedback);
 910          $textfielditem = $feedbackgenerator->create_item_textfield($this->feedback);
 911          $feedbackgenerator->create_item_pagebreak($this->feedback);
 912          $labelitem = $feedbackgenerator->create_item_label($this->feedback);
 913          $numericitem2 = $feedbackgenerator->create_item_numeric($this->feedback);
 914  
 915          $pagedata = [
 916              ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 5],
 917              ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'abc'],
 918          ];
 919          // Process the feedback, there are two pages so the feedback will be unfinished yet.
 920          $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata);
 921          $result = \external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
 922          $this->assertFalse($result['completed']);
 923  
 924          // Retrieve the unfinished responses.
 925          $result = mod_feedback_external::get_unfinished_responses($this->feedback->id);
 926          $result = \external_api::clean_returnvalue(mod_feedback_external::get_unfinished_responses_returns(), $result);
 927          // Check that ids and responses match.
 928          foreach ($result['responses'] as $r) {
 929              if ($r['item'] == $numericitem->id) {
 930                  $this->assertEquals(5, $r['value']);
 931              } else {
 932                  $this->assertEquals($textfielditem->id, $r['item']);
 933                  $this->assertEquals('abc', $r['value']);
 934              }
 935          }
 936      }
 937  
 938      /**
 939       * Test get_finished_responses.
 940       */
 941      public function test_get_finished_responses() {
 942          // Test user with full capabilities.
 943          $this->setUser($this->student);
 944  
 945          // Create a very simple feedback.
 946          $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
 947          $numericitem = $feedbackgenerator->create_item_numeric($this->feedback);
 948          $textfielditem = $feedbackgenerator->create_item_textfield($this->feedback);
 949  
 950          $pagedata = [
 951              ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 5],
 952              ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'abc'],
 953          ];
 954  
 955          // Process the feedback, there is only one page so the feedback will be completed.
 956          $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata);
 957          $result = \external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
 958          $this->assertTrue($result['completed']);
 959  
 960          // Retrieve the responses.
 961          $result = mod_feedback_external::get_finished_responses($this->feedback->id);
 962          $result = \external_api::clean_returnvalue(mod_feedback_external::get_finished_responses_returns(), $result);
 963          // Check that ids and responses match.
 964          foreach ($result['responses'] as $r) {
 965              if ($r['item'] == $numericitem->id) {
 966                  $this->assertEquals(5, $r['value']);
 967              } else {
 968                  $this->assertEquals($textfielditem->id, $r['item']);
 969                  $this->assertEquals('abc', $r['value']);
 970              }
 971          }
 972      }
 973  
 974      /**
 975       * Test get_non_respondents (student trying to get this information).
 976       */
 977      public function test_get_non_respondents_no_permissions() {
 978          $this->setUser($this->student);
 979          $this->expectException(moodle_exception::class);
 980          mod_feedback_external::get_non_respondents($this->feedback->id);
 981      }
 982  
 983      /**
 984       * Test get_non_respondents from an anonymous feedback.
 985       */
 986      public function test_get_non_respondents_from_anonymous_feedback() {
 987          $this->setUser($this->student);
 988          $this->expectException(moodle_exception::class);
 989          $this->expectExceptionMessage(get_string('anonymous', 'feedback'));
 990          mod_feedback_external::get_non_respondents($this->feedback->id);
 991      }
 992  
 993      /**
 994       * Test get_non_respondents.
 995       */
 996      public function test_get_non_respondents() {
 997          global $DB;
 998  
 999          // Force non anonymous.
1000          $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_NO, array('id' => $this->feedback->id));
1001  
1002          // Create another student.
1003          $anotherstudent = self::getDataGenerator()->create_user();
1004          $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual');
1005          $this->setUser($anotherstudent);
1006  
1007          // Test user with full capabilities.
1008          $this->setUser($this->student);
1009  
1010          // Create a very simple feedback.
1011          $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
1012          $numericitem = $feedbackgenerator->create_item_numeric($this->feedback);
1013  
1014          $pagedata = [
1015              ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 5],
1016          ];
1017  
1018          // Process the feedback, there is only one page so the feedback will be completed.
1019          $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata);
1020          $result = \external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
1021          $this->assertTrue($result['completed']);
1022  
1023          // Retrieve the non-respondent users.
1024          $this->setUser($this->teacher);
1025          $result = mod_feedback_external::get_non_respondents($this->feedback->id);
1026          $result = \external_api::clean_returnvalue(mod_feedback_external::get_non_respondents_returns(), $result);
1027          $this->assertCount(0, $result['warnings']);
1028          $this->assertCount(1, $result['users']);
1029          $this->assertEquals($anotherstudent->id, $result['users'][0]['userid']);
1030  
1031          // Create another student.
1032          $anotherstudent2 = self::getDataGenerator()->create_user();
1033          $this->getDataGenerator()->enrol_user($anotherstudent2->id, $this->course->id, $this->studentrole->id, 'manual');
1034          $this->setUser($anotherstudent2);
1035          $this->setUser($this->teacher);
1036          $result = mod_feedback_external::get_non_respondents($this->feedback->id);
1037          $result = \external_api::clean_returnvalue(mod_feedback_external::get_non_respondents_returns(), $result);
1038          $this->assertCount(0, $result['warnings']);
1039          $this->assertCount(2, $result['users']);
1040  
1041          // Test pagination.
1042          $result = mod_feedback_external::get_non_respondents($this->feedback->id, 0, 'lastaccess', 0, 1);
1043          $result = \external_api::clean_returnvalue(mod_feedback_external::get_non_respondents_returns(), $result);
1044          $this->assertCount(0, $result['warnings']);
1045          $this->assertCount(1, $result['users']);
1046      }
1047  
1048      /**
1049       * Helper function that completes the feedback for two students.
1050       */
1051      protected function complete_basic_feedback() {
1052          global $DB;
1053  
1054          $generator = $this->getDataGenerator();
1055          // Create separated groups.
1056          $DB->set_field('course', 'groupmode', SEPARATEGROUPS);
1057          $DB->set_field('course', 'groupmodeforce', 1);
1058          assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $this->teacherrole->id, $this->context);
1059          accesslib_clear_all_caches_for_unit_testing();
1060  
1061          $group1 = $generator->create_group(array('courseid' => $this->course->id));
1062          $group2 = $generator->create_group(array('courseid' => $this->course->id));
1063  
1064          // Create another students.
1065          $anotherstudent1 = self::getDataGenerator()->create_user();
1066          $anotherstudent2 = self::getDataGenerator()->create_user();
1067          $generator->enrol_user($anotherstudent1->id, $this->course->id, $this->studentrole->id, 'manual');
1068          $generator->enrol_user($anotherstudent2->id, $this->course->id, $this->studentrole->id, 'manual');
1069  
1070          $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $this->student->id));
1071          $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $this->teacher->id));
1072          $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $anotherstudent1->id));
1073          $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $anotherstudent2->id));
1074  
1075          // Test user with full capabilities.
1076          $this->setUser($this->student);
1077  
1078          // Create a very simple feedback.
1079          $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
1080          $numericitem = $feedbackgenerator->create_item_numeric($this->feedback);
1081          $textfielditem = $feedbackgenerator->create_item_textfield($this->feedback);
1082  
1083          $pagedata = [
1084              ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 5],
1085              ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'abc'],
1086          ];
1087  
1088          // Process the feedback, there is only one page so the feedback will be completed.
1089          $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata);
1090          $result = \external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
1091          $this->assertTrue($result['completed']);
1092  
1093          $this->setUser($anotherstudent1);
1094  
1095          $pagedata = [
1096              ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 10],
1097              ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'def'],
1098          ];
1099  
1100          $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata);
1101          $result = \external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
1102          $this->assertTrue($result['completed']);
1103  
1104          $this->setUser($anotherstudent2);
1105  
1106          $pagedata = [
1107              ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 10],
1108              ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'def'],
1109          ];
1110  
1111          $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata);
1112          $result = \external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
1113          $this->assertTrue($result['completed']);
1114      }
1115  
1116      /**
1117       * Test get_responses_analysis for anonymous feedback.
1118       */
1119      public function test_get_responses_analysis_anonymous() {
1120          self::complete_basic_feedback();
1121  
1122          // Retrieve the responses analysis.
1123          $this->setUser($this->teacher);
1124          $result = mod_feedback_external::get_responses_analysis($this->feedback->id);
1125          $result = \external_api::clean_returnvalue(mod_feedback_external::get_responses_analysis_returns(), $result);
1126          $this->assertCount(0, $result['warnings']);
1127          $this->assertEquals(0, $result['totalattempts']);
1128          $this->assertEquals(2, $result['totalanonattempts']);   // Only see my groups.
1129  
1130          foreach ($result['attempts'] as $attempt) {
1131              $this->assertEmpty($attempt['userid']); // Is anonymous.
1132          }
1133      }
1134  
1135      /**
1136       * Test get_responses_analysis for non-anonymous feedback.
1137       */
1138      public function test_get_responses_analysis_non_anonymous() {
1139          global $DB;
1140  
1141          // Force non anonymous.
1142          $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_NO, array('id' => $this->feedback->id));
1143  
1144          self::complete_basic_feedback();
1145          // Retrieve the responses analysis.
1146          $this->setUser($this->teacher);
1147          $result = mod_feedback_external::get_responses_analysis($this->feedback->id);
1148          $result = \external_api::clean_returnvalue(mod_feedback_external::get_responses_analysis_returns(), $result);
1149          $this->assertCount(0, $result['warnings']);
1150          $this->assertEquals(2, $result['totalattempts']);
1151          $this->assertEquals(0, $result['totalanonattempts']);   // Only see my groups.
1152  
1153          foreach ($result['attempts'] as $attempt) {
1154              $this->assertNotEmpty($attempt['userid']);  // Is not anonymous.
1155          }
1156      }
1157  
1158      /**
1159       * Test get_last_completed for feedback anonymous not completed.
1160       */
1161      public function test_get_last_completed_anonymous_not_completed() {
1162          global $DB;
1163  
1164          // Force anonymous.
1165          $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_YES, array('id' => $this->feedback->id));
1166  
1167          // Test user with full capabilities that didn't complete the feedback.
1168          $this->setUser($this->student);
1169  
1170          $this->expectExceptionMessage(get_string('anonymous', 'feedback'));
1171          $this->expectException(moodle_exception::class);
1172          mod_feedback_external::get_last_completed($this->feedback->id);
1173      }
1174  
1175      /**
1176       * Test get_last_completed for feedback anonymous and completed.
1177       */
1178      public function test_get_last_completed_anonymous_completed() {
1179          global $DB;
1180  
1181          // Force anonymous.
1182          $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_YES, array('id' => $this->feedback->id));
1183          // Add one completion record..
1184          $record = [
1185              'feedback' => $this->feedback->id,
1186              'userid' => $this->student->id,
1187              'timemodified' => time() - DAYSECS,
1188              'random_response' => 0,
1189              'anonymous_response' => FEEDBACK_ANONYMOUS_YES,
1190              'courseid' => $this->course->id,
1191          ];
1192          $record['id'] = $DB->insert_record('feedback_completed', (object) $record);
1193  
1194          // Test user with full capabilities.
1195          $this->setUser($this->student);
1196  
1197          $this->expectExceptionMessage(get_string('anonymous', 'feedback'));
1198          $this->expectException(moodle_exception::class);
1199          mod_feedback_external::get_last_completed($this->feedback->id);
1200      }
1201  
1202      /**
1203       * Test get_last_completed for feedback not anonymous and completed.
1204       */
1205      public function test_get_last_completed_not_anonymous_completed() {
1206          global $DB;
1207  
1208          // Force non anonymous.
1209          $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_NO, array('id' => $this->feedback->id));
1210          // Add one completion record..
1211          $record = [
1212              'feedback' => $this->feedback->id,
1213              'userid' => $this->student->id,
1214              'timemodified' => time() - DAYSECS,
1215              'random_response' => 0,
1216              'anonymous_response' => FEEDBACK_ANONYMOUS_NO,
1217              'courseid' => $this->course->id,
1218          ];
1219          $record['id'] = $DB->insert_record('feedback_completed', (object) $record);
1220  
1221          // Test user with full capabilities.
1222          $this->setUser($this->student);
1223          $result = mod_feedback_external::get_last_completed($this->feedback->id);
1224          $result = \external_api::clean_returnvalue(mod_feedback_external::get_last_completed_returns(), $result);
1225          $this->assertEquals($record, $result['completed']);
1226      }
1227  
1228      /**
1229       * Test get_last_completed for feedback not anonymous and not completed.
1230       */
1231      public function test_get_last_completed_not_anonymous_not_completed() {
1232          global $DB;
1233  
1234          // Force anonymous.
1235          $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_NO, array('id' => $this->feedback->id));
1236  
1237          // Test user with full capabilities that didn't complete the feedback.
1238          $this->setUser($this->student);
1239  
1240          $this->expectExceptionMessage(get_string('not_completed_yet', 'feedback'));
1241          $this->expectException(moodle_exception::class);
1242          mod_feedback_external::get_last_completed($this->feedback->id);
1243      }
1244  
1245      /**
1246       * Test get_feedback_access_information for site feedback.
1247       */
1248      public function test_get_feedback_access_information_for_site_feedback() {
1249  
1250          $sitefeedback = $this->getDataGenerator()->create_module('feedback', array('course' => SITEID));
1251          $this->setUser($this->student);
1252          // Access the site feedback via the site activity.
1253          $result = mod_feedback_external::get_feedback_access_information($sitefeedback->id);
1254          $result = \external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result);
1255          $this->assertTrue($result['cancomplete']);
1256          $this->assertTrue($result['cansubmit']);
1257  
1258          // Access the site feedback via course where I'm enrolled.
1259          $result = mod_feedback_external::get_feedback_access_information($sitefeedback->id, $this->course->id);
1260          $result = \external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result);
1261          $this->assertTrue($result['cancomplete']);
1262          $this->assertTrue($result['cansubmit']);
1263  
1264          // Access the site feedback via course where I'm not enrolled.
1265          $othercourse = $this->getDataGenerator()->create_course();
1266  
1267          $this->expectException(moodle_exception::class);
1268          mod_feedback_external::get_feedback_access_information($sitefeedback->id, $othercourse->id);
1269      }
1270  
1271      /**
1272       * Test get_feedback_access_information for site feedback mapped.
1273       */
1274      public function test_get_feedback_access_information_for_site_feedback_mapped() {
1275          global $DB;
1276  
1277          $sitefeedback = $this->getDataGenerator()->create_module('feedback', array('course' => SITEID));
1278          $this->setUser($this->student);
1279          $DB->insert_record('feedback_sitecourse_map', array('feedbackid' => $sitefeedback->id, 'courseid' => $this->course->id));
1280  
1281          // Access the site feedback via course where I'm enrolled and mapped.
1282          $result = mod_feedback_external::get_feedback_access_information($sitefeedback->id, $this->course->id);
1283          $result = \external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result);
1284          $this->assertTrue($result['cancomplete']);
1285          $this->assertTrue($result['cansubmit']);
1286  
1287          // Access the site feedback via course where I'm enrolled but not mapped.
1288          $othercourse = $this->getDataGenerator()->create_course();
1289          $this->getDataGenerator()->enrol_user($this->student->id, $othercourse->id, $this->studentrole->id, 'manual');
1290  
1291          $this->expectException(moodle_exception::class);
1292          $this->expectExceptionMessage(get_string('cannotaccess', 'mod_feedback'));
1293          mod_feedback_external::get_feedback_access_information($sitefeedback->id, $othercourse->id);
1294      }
1295  }