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 310 and 401] [Versions 311 and 401] [Versions 39 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   * Unit tests for (some of) mod/assign/locallib.php.
  19   *
  20   * @package    mod_assign
  21   * @category   phpunit
  22   * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  namespace mod_assign;
  26  
  27  use mod_assign_grade_form;
  28  use mod_assign_test_generator;
  29  use mod_assign_testable_assign;
  30  
  31  defined('MOODLE_INTERNAL') || die();
  32  
  33  global $CFG;
  34  require_once($CFG->dirroot . '/mod/assign/locallib.php');
  35  require_once($CFG->dirroot . '/mod/assign/upgradelib.php');
  36  require_once($CFG->dirroot . '/mod/assign/tests/generator.php');
  37  
  38  /**
  39   * Unit tests for (some of) mod/assign/locallib.php.
  40   *
  41   * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
  42   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  43   */
  44  class locallib_test extends \advanced_testcase {
  45  
  46      // Use the generator helper.
  47      use mod_assign_test_generator;
  48  
  49      public function test_return_links() {
  50          global $PAGE;
  51  
  52          $this->resetAfterTest();
  53          $course = $this->getDataGenerator()->create_course();
  54  
  55          $assign = $this->create_instance($course);
  56          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
  57  
  58          $assign->register_return_link('RETURNACTION', ['param' => 1]);
  59          $this->assertEquals('RETURNACTION', $assign->get_return_action());
  60          $this->assertEquals(['param' => 1], $assign->get_return_params());
  61      }
  62  
  63      public function test_get_feedback_plugins() {
  64          $this->resetAfterTest();
  65          $course = $this->getDataGenerator()->create_course();
  66          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
  67  
  68          $this->setUser($teacher);
  69          $assign = $this->create_instance($course);
  70          $installedplugins = array_keys(\core_component::get_plugin_list('assignfeedback'));
  71  
  72          foreach ($assign->get_feedback_plugins() as $plugin) {
  73              $this->assertContains($plugin->get_type(), $installedplugins, 'Feedback plugin not in list of installed plugins');
  74          }
  75      }
  76  
  77      public function test_get_submission_plugins() {
  78          $this->resetAfterTest();
  79          $course = $this->getDataGenerator()->create_course();
  80          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
  81  
  82          $this->setUser($teacher);
  83          $assign = $this->create_instance($course);
  84          $installedplugins = array_keys(\core_component::get_plugin_list('assignsubmission'));
  85  
  86          foreach ($assign->get_submission_plugins() as $plugin) {
  87              $this->assertContains($plugin->get_type(), $installedplugins, 'Submission plugin not in list of installed plugins');
  88          }
  89      }
  90  
  91      public function test_is_blind_marking() {
  92          $this->resetAfterTest();
  93          $course = $this->getDataGenerator()->create_course();
  94          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
  95          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
  96  
  97          $this->setUser($teacher);
  98          $assign = $this->create_instance($course, ['blindmarking' => 1]);
  99          $this->assertEquals(true, $assign->is_blind_marking());
 100  
 101          // Test cannot see student names.
 102          $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
 103          $output = $assign->get_renderer()->render($gradingtable);
 104          $this->assertEquals(true, strpos($output, get_string('hiddenuser', 'assign')));
 105  
 106          // Test students cannot reveal identities.
 107          $nopermission = false;
 108          $student->ignoresesskey = true;
 109          $this->setUser($student);
 110          $this->expectException('required_capability_exception');
 111          $assign->reveal_identities();
 112          $student->ignoresesskey = false;
 113  
 114          // Test teachers cannot reveal identities.
 115          $nopermission = false;
 116          $teacher->ignoresesskey = true;
 117          $this->setUser($teacher);
 118          $this->expectException('required_capability_exception');
 119          $assign->reveal_identities();
 120          $teacher->ignoresesskey = false;
 121  
 122          // Test sesskey is required.
 123          $this->setUser($teacher);
 124          $this->expectException('moodle_exception');
 125          $assign->reveal_identities();
 126  
 127          // Test editingteacher can reveal identities if sesskey is ignored.
 128          $teacher->ignoresesskey = true;
 129          $this->setUser($teacher);
 130          $assign->reveal_identities();
 131          $this->assertEquals(false, $assign->is_blind_marking());
 132          $teacher->ignoresesskey = false;
 133  
 134          // Test student names are visible.
 135          $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
 136          $output = $assign->get_renderer()->render($gradingtable);
 137          $this->assertEquals(false, strpos($output, get_string('hiddenuser', 'assign')));
 138  
 139          // Set this back to default.
 140          $teacher->ignoresesskey = false;
 141      }
 142  
 143      /**
 144       * Data provider for test_get_assign_perpage
 145       *
 146       * @return array Provider data
 147       */
 148      public function get_assign_perpage_provider() {
 149          return array(
 150              array(
 151                  'maxperpage' => -1,
 152                  'userprefs' => array(
 153                      -1 => -1,
 154                      10 => 10,
 155                      20 => 20,
 156                      50 => 50,
 157                  ),
 158              ),
 159              array(
 160                  'maxperpage' => 15,
 161                  'userprefs' => array(
 162                      -1 => 15,
 163                      10 => 10,
 164                      20 => 15,
 165                      50 => 15,
 166                  ),
 167              ),
 168          );
 169      }
 170  
 171      /**
 172       * Test maxperpage
 173       *
 174       * @dataProvider get_assign_perpage_provider
 175       * @param integer $maxperpage site config value
 176       * @param array $userprefs Array of user preferences and expected page sizes
 177       */
 178      public function test_get_assign_perpage($maxperpage, $userprefs) {
 179          $this->resetAfterTest();
 180          $course = $this->getDataGenerator()->create_course();
 181          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 182          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 183  
 184          $this->setUser($teacher);
 185          $assign = $this->create_instance($course);
 186  
 187          set_config('maxperpage', $maxperpage, 'assign');
 188          set_user_preference('assign_perpage', null);
 189          $this->assertEquals(10, $assign->get_assign_perpage());
 190          foreach ($userprefs as $pref => $perpage) {
 191              set_user_preference('assign_perpage', $pref);
 192              $this->assertEquals($perpage, $assign->get_assign_perpage());
 193          }
 194      }
 195  
 196      /**
 197       * Test filter by requires grading.
 198       *
 199       * This is specifically checking an assignment with no grade to make sure we do not
 200       * get an exception thrown when rendering the grading table for this type of assignment.
 201       */
 202      public function test_gradingtable_filter_by_requiresgrading_no_grade() {
 203          global $PAGE;
 204  
 205          $this->resetAfterTest();
 206  
 207          $course = $this->getDataGenerator()->create_course();
 208          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 209          $this->setUser($teacher);
 210          $assign = $this->create_instance($course, [
 211                  'assignsubmission_onlinetext_enabled' => 1,
 212                  'assignfeedback_comments_enabled' => 0,
 213                  'grade' => GRADE_TYPE_NONE
 214              ]);
 215  
 216          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', array(
 217              'id' => $assign->get_course_module()->id,
 218              'action' => 'grading',
 219          )));
 220  
 221          // Render the table with the requires grading filter.
 222          $gradingtable = new \assign_grading_table($assign, 1, ASSIGN_FILTER_REQUIRE_GRADING, 0, true);
 223          $output = $assign->get_renderer()->render($gradingtable);
 224  
 225          // Test that the filter function does not throw errors for assignments with no grade.
 226          $this->assertStringContainsString(get_string('nothingtodisplay'), $output);
 227      }
 228  
 229  
 230      /**
 231       * Test submissions with extension date.
 232       */
 233      public function test_gradingtable_extension_due_date() {
 234          global $PAGE;
 235  
 236          $this->resetAfterTest();
 237          $course = $this->getDataGenerator()->create_course();
 238          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 239          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 240  
 241          // Setup the assignment.
 242          $this->setUser($teacher);
 243          $time = time();
 244          $assign = $this->create_instance($course, [
 245                  'assignsubmission_onlinetext_enabled' => 1,
 246                  'duedate' => time() - (4 * DAYSECS),
 247              ]);
 248          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', array(
 249              'id' => $assign->get_course_module()->id,
 250              'action' => 'grading',
 251          )));
 252  
 253          // Check that the assignment is late.
 254          $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
 255          $output = $assign->get_renderer()->render($gradingtable);
 256          $this->assertStringContainsString(get_string('submissionstatus_', 'assign'), $output);
 257          $this->assertStringContainsString(get_string('overdue', 'assign', format_time((4 * DAYSECS))), $output);
 258  
 259          // Grant an extension.
 260          $extendedtime = $time + (2 * DAYSECS);
 261          $assign->testable_save_user_extension($student->id, $extendedtime);
 262          $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
 263          $output = $assign->get_renderer()->render($gradingtable);
 264          $this->assertStringContainsString(get_string('submissionstatus_', 'assign'), $output);
 265          $this->assertStringContainsString(get_string('userextensiondate', 'assign', userdate($extendedtime)), $output);
 266  
 267          // Simulate a submission.
 268          $this->setUser($student);
 269          $submission = $assign->get_user_submission($student->id, true);
 270          $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
 271          $assign->testable_update_submission($submission, $student->id, true, false);
 272          $data = new \stdClass();
 273          $data->onlinetext_editor = [
 274              'itemid' => file_get_unused_draft_itemid(),
 275              'text' => 'Submission text',
 276              'format' => FORMAT_MOODLE,
 277          ];
 278          $plugin = $assign->get_submission_plugin_by_type('onlinetext');
 279          $plugin->save($submission, $data);
 280  
 281          // Verify output.
 282          $this->setUser($teacher);
 283          $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
 284          $output = $assign->get_renderer()->render($gradingtable);
 285          $this->assertStringContainsString(get_string('submissionstatus_submitted', 'assign'), $output);
 286          $this->assertStringContainsString(get_string('userextensiondate', 'assign', userdate($extendedtime)), $output);
 287      }
 288  
 289      /**
 290       * Test that late submissions with extension date calculate correctly.
 291       */
 292      public function test_gradingtable_extension_date_calculation_for_lateness() {
 293          global $PAGE;
 294  
 295          $this->resetAfterTest();
 296          $course = $this->getDataGenerator()->create_course();
 297          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 298          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 299  
 300          // Setup the assignment.
 301          $this->setUser($teacher);
 302          $time = time();
 303          $assign = $this->create_instance($course, [
 304                  'assignsubmission_onlinetext_enabled' => 1,
 305                  'duedate' => time() - (4 * DAYSECS),
 306              ]);
 307          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', array(
 308              'id' => $assign->get_course_module()->id,
 309              'action' => 'grading',
 310          )));
 311  
 312          // Check that the assignment is late.
 313          $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
 314          $output = $assign->get_renderer()->render($gradingtable);
 315          $this->assertStringContainsString(get_string('submissionstatus_', 'assign'), $output);
 316          $difftime = time() - $time;
 317          $this->assertStringContainsString(get_string('overdue', 'assign', format_time((4 * DAYSECS) + $difftime)), $output);
 318  
 319          // Grant an extension that is in the past.
 320          $assign->testable_save_user_extension($student->id, $time - (2 * DAYSECS));
 321          $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
 322          $output = $assign->get_renderer()->render($gradingtable);
 323          $this->assertStringContainsString(get_string('submissionstatus_', 'assign'), $output);
 324          $this->assertStringContainsString(get_string('userextensiondate', 'assign', userdate($time - (2 * DAYSECS))), $output);
 325          $difftime = time() - $time;
 326          $this->assertStringContainsString(get_string('overdue', 'assign', format_time((2 * DAYSECS) + $difftime)), $output);
 327  
 328          // Simulate a submission.
 329          $this->setUser($student);
 330          $submission = $assign->get_user_submission($student->id, true);
 331          $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
 332          $assign->testable_update_submission($submission, $student->id, true, false);
 333          $data = new \stdClass();
 334          $data->onlinetext_editor = [
 335              'itemid' => file_get_unused_draft_itemid(),
 336              'text' => 'Submission text',
 337              'format' => FORMAT_MOODLE,
 338          ];
 339          $plugin = $assign->get_submission_plugin_by_type('onlinetext');
 340          $plugin->save($submission, $data);
 341          $submittedtime = time();
 342  
 343          // Verify output.
 344          $this->setUser($teacher);
 345          $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
 346          $output = $assign->get_renderer()->render($gradingtable);
 347          $this->assertStringContainsString(get_string('submissionstatus_submitted', 'assign'), $output);
 348          $this->assertStringContainsString(get_string('userextensiondate', 'assign', userdate($time - (2 * DAYSECS))), $output);
 349  
 350          $difftime = $submittedtime - $time;
 351          $this->assertStringContainsString(get_string('submittedlateshort', 'assign', format_time((2 * DAYSECS) + $difftime)),
 352              $output);
 353      }
 354  
 355      public function test_gradingtable_status_rendering() {
 356          global $PAGE;
 357  
 358          $this->resetAfterTest();
 359          $course = $this->getDataGenerator()->create_course();
 360          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 361          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 362  
 363          // Setup the assignment.
 364          $this->setUser($teacher);
 365          $time = time();
 366          $assign = $this->create_instance($course, [
 367              'assignsubmission_onlinetext_enabled' => 1,
 368              'duedate' => $time - (4 * DAYSECS),
 369           ]);
 370          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', array(
 371              'id' => $assign->get_course_module()->id,
 372              'action' => 'grading',
 373          )));
 374  
 375          // Check that the assignment is late.
 376          $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
 377          $output = $assign->get_renderer()->render($gradingtable);
 378          $this->assertStringContainsString(get_string('submissionstatus_', 'assign'), $output);
 379          $difftime = time() - $time;
 380          $this->assertStringContainsString(get_string('overdue', 'assign', format_time((4 * DAYSECS) + $difftime)), $output);
 381  
 382          // Simulate a student viewing the assignment without submitting.
 383          $this->setUser($student);
 384          $submission = $assign->get_user_submission($student->id, true);
 385          $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
 386          $assign->testable_update_submission($submission, $student->id, true, false);
 387          $submittedtime = time();
 388  
 389          // Verify output.
 390          $this->setUser($teacher);
 391          $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
 392          $output = $assign->get_renderer()->render($gradingtable);
 393          $difftime = $submittedtime - $time;
 394          $this->assertStringContainsString(get_string('overdue', 'assign', format_time((4 * DAYSECS) + $difftime)), $output);
 395  
 396          $document = new \DOMDocument();
 397          @$document->loadHTML($output);
 398          $xpath = new \DOMXPath($document);
 399          $this->assertEmpty($xpath->evaluate('string(//td[@id="mod_assign_grading-' . $assign->get_context()->id. '_r0_c8"])'));
 400      }
 401  
 402      /**
 403       * Check that group submission information is rendered correctly in the
 404       * grading table.
 405       */
 406      public function test_gradingtable_group_submissions_rendering() {
 407          global $PAGE;
 408  
 409          $this->resetAfterTest();
 410          $course = $this->getDataGenerator()->create_course();
 411          $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
 412  
 413          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 414          groups_add_member($group, $teacher);
 415  
 416          $students = [];
 417  
 418          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 419          $students[] = $student;
 420          groups_add_member($group, $student);
 421  
 422          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 423          $students[] = $student;
 424          groups_add_member($group, $student);
 425  
 426          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 427          $students[] = $student;
 428          groups_add_member($group, $student);
 429  
 430          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 431          $students[] = $student;
 432          groups_add_member($group, $student);
 433  
 434          // Verify group assignments.
 435          $this->setUser($teacher);
 436          $assign = $this->create_instance($course, [
 437              'teamsubmission' => 1,
 438              'assignsubmission_onlinetext_enabled' => 1,
 439              'submissiondrafts' => 1,
 440              'requireallteammemberssubmit' => 0,
 441          ]);
 442          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', array(
 443              'id' => $assign->get_course_module()->id,
 444              'action' => 'grading',
 445          )));
 446  
 447          // Add a submission.
 448          $this->setUser($student);
 449          $data = new \stdClass();
 450          $data->onlinetext_editor = [
 451              'itemid' => file_get_unused_draft_itemid(),
 452              'text' => 'Submission text',
 453              'format' => FORMAT_MOODLE,
 454          ];
 455          $notices = array();
 456          $assign->save_submission($data, $notices);
 457  
 458          $submission = $assign->get_group_submission($student->id, 0, true);
 459          $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
 460          $assign->testable_update_submission($submission, $student->id, true, true);
 461  
 462          // Check output.
 463          $this->setUser($teacher);
 464          $gradingtable = new \assign_grading_table($assign, 4, '', 0, true);
 465          $output = $assign->get_renderer()->render($gradingtable);
 466          $document = new \DOMDocument();
 467          @$document->loadHTML($output);
 468          $xpath = new \DOMXPath($document);
 469  
 470          // The XPath expression is based on the unique ID of the table.
 471          $xpathuniqueidroot = 'mod_assign_grading-' . $assign->get_context()->id;
 472  
 473          // Check status.
 474          $this->assertSame(get_string('submissionstatus_submitted', 'assign'),
 475              $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c4"]/div[@class="submissionstatussubmitted"])'));
 476          $this->assertSame(get_string('submissionstatus_submitted', 'assign'),
 477              $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c4"]/div[@class="submissionstatussubmitted"])'));
 478  
 479          // Check submission last modified date.
 480          $this->assertGreaterThan(0, strtotime($xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c8"])')));
 481          $this->assertGreaterThan(0, strtotime($xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c8"])')));
 482  
 483          // Check group.
 484          $this->assertSame($group->name, $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c5"])'));
 485          $this->assertSame($group->name, $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c5"])'));
 486  
 487          // Check submission text.
 488          $this->assertSame('Submission text', $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c9"]/div/div)'));
 489          $this->assertSame('Submission text', $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c9"]/div/div)'));
 490  
 491          // Check comments can be made.
 492          $this->assertEquals(1, $xpath->evaluate('count(//td[@id="' . $xpathuniqueidroot . '_r0_c10"]//textarea)'));
 493          $this->assertEquals(1, $xpath->evaluate('count(//td[@id="' . $xpathuniqueidroot . '_r3_c10"]//textarea)'));
 494      }
 495  
 496      public function test_show_intro() {
 497          $this->resetAfterTest();
 498          $course = $this->getDataGenerator()->create_course();
 499          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 500  
 501          // Test whether we are showing the intro at the correct times.
 502          $this->setUser($teacher);
 503          $assign = $this->create_instance($course, ['alwaysshowdescription' => 1]);
 504  
 505          $this->assertEquals(true, $assign->testable_show_intro());
 506  
 507          $tomorrow = time() + DAYSECS;
 508  
 509          $assign = $this->create_instance($course, [
 510                  'alwaysshowdescription' => 0,
 511                  'allowsubmissionsfromdate' => $tomorrow,
 512              ]);
 513          $this->assertEquals(false, $assign->testable_show_intro());
 514          $yesterday = time() - DAYSECS;
 515          $assign = $this->create_instance($course, [
 516                  'alwaysshowdescription' => 0,
 517                  'allowsubmissionsfromdate' => $yesterday,
 518              ]);
 519          $this->assertEquals(true, $assign->testable_show_intro());
 520      }
 521  
 522      public function test_has_submissions_or_grades() {
 523          $this->resetAfterTest();
 524          $course = $this->getDataGenerator()->create_course();
 525          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 526          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 527  
 528          $this->setUser($teacher);
 529          $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]);
 530          $instance = $assign->get_instance();
 531  
 532          // Should start empty.
 533          $this->assertEquals(false, $assign->has_submissions_or_grades());
 534  
 535          // Simulate a submission.
 536          $this->setUser($student);
 537          $submission = $assign->get_user_submission($student->id, true);
 538  
 539          // The submission is still new.
 540          $this->assertEquals(false, $assign->has_submissions_or_grades());
 541  
 542          // Submit the submission.
 543          $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
 544          $assign->testable_update_submission($submission, $student->id, true, false);
 545          $data = new \stdClass();
 546          $data->onlinetext_editor = array(
 547              'itemid' => file_get_unused_draft_itemid(),
 548              'text' => 'Submission text',
 549              'format' => FORMAT_MOODLE);
 550          $plugin = $assign->get_submission_plugin_by_type('onlinetext');
 551          $plugin->save($submission, $data);
 552  
 553          // Now test again.
 554          $this->assertEquals(true, $assign->has_submissions_or_grades());
 555      }
 556  
 557      public function test_delete_grades() {
 558          $this->resetAfterTest();
 559          $course = $this->getDataGenerator()->create_course();
 560          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 561          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 562  
 563          $this->setUser($teacher);
 564          $assign = $this->create_instance($course);
 565  
 566          // Simulate adding a grade.
 567          $this->setUser($teacher);
 568          $data = new \stdClass();
 569          $data->grade = '50.0';
 570          $assign->testable_apply_grade_to_user($data, $student->id, 0);
 571  
 572          // Now see if the data is in the gradebook.
 573          $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id);
 574  
 575          $this->assertNotEquals(0, count($gradinginfo->items));
 576  
 577          $assign->testable_delete_grades();
 578          $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id);
 579  
 580          $this->assertEquals(0, count($gradinginfo->items));
 581      }
 582  
 583      public function test_delete_instance() {
 584          $this->resetAfterTest();
 585          $course = $this->getDataGenerator()->create_course();
 586          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 587          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 588  
 589          $this->setUser($teacher);
 590          $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]);
 591  
 592          // Simulate adding a grade.
 593          $this->setUser($teacher);
 594          $data = new \stdClass();
 595          $data->grade = '50.0';
 596          $assign->testable_apply_grade_to_user($data, $student->id, 0);
 597  
 598          // Simulate a submission.
 599          $this->add_submission($student, $assign);
 600  
 601          // Now try and delete.
 602          $this->setUser($teacher);
 603          $this->assertEquals(true, $assign->delete_instance());
 604      }
 605  
 606      public function test_reset_userdata() {
 607          global $DB;
 608  
 609          $this->resetAfterTest();
 610          $course = $this->getDataGenerator()->create_course();
 611          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 612          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 613  
 614          $now = time();
 615          $this->setUser($teacher);
 616          $assign = $this->create_instance($course, [
 617                  'assignsubmission_onlinetext_enabled' => 1,
 618                  'duedate' => $now,
 619              ]);
 620  
 621          // Simulate adding a grade.
 622          $this->add_submission($student, $assign);
 623          $this->submit_for_grading($student, $assign);
 624          $this->mark_submission($teacher, $assign, $student, 50.0);
 625  
 626          // Simulate a submission.
 627          $this->setUser($student);
 628          $submission = $assign->get_user_submission($student->id, true);
 629          $data = new \stdClass();
 630          $data->onlinetext_editor = array(
 631              'itemid' => file_get_unused_draft_itemid(),
 632              'text' => 'Submission text',
 633              'format' => FORMAT_MOODLE);
 634          $plugin = $assign->get_submission_plugin_by_type('onlinetext');
 635          $plugin->save($submission, $data);
 636  
 637          $this->assertEquals(true, $assign->has_submissions_or_grades());
 638          // Now try and reset.
 639          $data = new \stdClass();
 640          $data->reset_assign_submissions = 1;
 641          $data->reset_gradebook_grades = 1;
 642          $data->reset_assign_user_overrides = 1;
 643          $data->reset_assign_group_overrides = 1;
 644          $data->courseid = $course->id;
 645          $data->timeshift = DAYSECS;
 646          $this->setUser($teacher);
 647          $assign->reset_userdata($data);
 648          $this->assertEquals(false, $assign->has_submissions_or_grades());
 649  
 650          // Reload the instance data.
 651          $instance = $DB->get_record('assign', array('id' => $assign->get_instance()->id));
 652          $this->assertEquals($now + DAYSECS, $instance->duedate);
 653  
 654          // Test reset using assign_reset_userdata().
 655          $assignduedate = $instance->duedate; // Keep old updated value for comparison.
 656          $data->timeshift = (2 * DAYSECS);
 657          assign_reset_userdata($data);
 658          $instance = $DB->get_record('assign', array('id' => $assign->get_instance()->id));
 659          $this->assertEquals($assignduedate + (2 * DAYSECS), $instance->duedate);
 660  
 661          // Create one more assignment and reset, make sure time shifted for previous assignment is not changed.
 662          $assign2 = $this->create_instance($course, [
 663                  'assignsubmission_onlinetext_enabled' => 1,
 664                  'duedate' => $now,
 665              ]);
 666          $assignduedate = $instance->duedate;
 667          $data->timeshift = 3 * DAYSECS;
 668          $assign2->reset_userdata($data);
 669          $instance = $DB->get_record('assign', array('id' => $assign->get_instance()->id));
 670          $this->assertEquals($assignduedate, $instance->duedate);
 671          $instance2 = $DB->get_record('assign', array('id' => $assign2->get_instance()->id));
 672          $this->assertEquals($now + 3 * DAYSECS, $instance2->duedate);
 673  
 674          // Reset both assignments using assign_reset_userdata() and make sure both assignments have same date.
 675          $assignduedate = $instance->duedate;
 676          $assign2duedate = $instance2->duedate;
 677          $data->timeshift = (4 * DAYSECS);
 678          assign_reset_userdata($data);
 679          $instance = $DB->get_record('assign', array('id' => $assign->get_instance()->id));
 680          $this->assertEquals($assignduedate + (4 * DAYSECS), $instance->duedate);
 681          $instance2 = $DB->get_record('assign', array('id' => $assign2->get_instance()->id));
 682          $this->assertEquals($assign2duedate + (4 * DAYSECS), $instance2->duedate);
 683      }
 684  
 685      public function test_plugin_settings() {
 686          global $DB;
 687  
 688          $this->resetAfterTest();
 689  
 690          $course = $this->getDataGenerator()->create_course();
 691          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 692  
 693          $now = time();
 694          $this->setUser($teacher);
 695          $assign = $this->create_instance($course, [
 696                  'assignsubmission_file_enabled' => 1,
 697                  'assignsubmission_file_maxfiles' => 12,
 698                  'assignsubmission_file_maxsizebytes' => 10,
 699              ]);
 700  
 701          $plugin = $assign->get_submission_plugin_by_type('file');
 702          $this->assertEquals('12', $plugin->get_config('maxfilesubmissions'));
 703      }
 704  
 705      public function test_update_calendar() {
 706          global $DB;
 707  
 708          $this->resetAfterTest();
 709  
 710          $course = $this->getDataGenerator()->create_course();
 711          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 712  
 713          $this->setUser($teacher);
 714          $userctx = \context_user::instance($teacher->id)->id;
 715  
 716          // Hack to pretend that there was an editor involved. We need both $_POST and $_REQUEST, and a sesskey.
 717          $draftid = file_get_unused_draft_itemid();
 718          $_REQUEST['introeditor'] = $draftid;
 719          $_POST['introeditor'] = $draftid;
 720          $_POST['sesskey'] = sesskey();
 721  
 722          // Write links to a draft area.
 723          $fakearealink1 = file_rewrite_pluginfile_urls('<a href="@@PLUGINFILE@@/pic.gif">link</a>', 'draftfile.php', $userctx,
 724              'user', 'draft', $draftid);
 725          $fakearealink2 = file_rewrite_pluginfile_urls('<a href="@@PLUGINFILE@@/pic.gif">new</a>', 'draftfile.php', $userctx,
 726              'user', 'draft', $draftid);
 727  
 728          // Create a new \assignment with links to a draft area.
 729          $now = time();
 730          $assign = $this->create_instance($course, [
 731                  'duedate' => $now,
 732                  'intro' => $fakearealink1,
 733                  'introformat' => FORMAT_HTML
 734              ]);
 735  
 736          // See if there is an event in the calendar.
 737          $params = array('modulename' => 'assign', 'instance' => $assign->get_instance()->id);
 738          $event = $DB->get_record('event', $params);
 739          $this->assertNotEmpty($event);
 740          $this->assertSame('link', $event->description);     // The pluginfile links are removed.
 741  
 742          // Make sure the same works when updating the assignment.
 743          $instance = $assign->get_instance();
 744          $instance->instance = $instance->id;
 745          $instance->intro = $fakearealink2;
 746          $instance->introformat = FORMAT_HTML;
 747          $assign->update_instance($instance);
 748          $params = array('modulename' => 'assign', 'instance' => $assign->get_instance()->id);
 749          $event = $DB->get_record('event', $params);
 750          $this->assertNotEmpty($event);
 751          $this->assertSame('new', $event->description);     // The pluginfile links are removed.
 752  
 753          // Create an assignment with a description that should be hidden.
 754          $assign = $this->create_instance($course, [
 755                  'duedate' => $now + 160,
 756                  'alwaysshowdescription' => false,
 757                  'allowsubmissionsfromdate' => $now + 60,
 758                  'intro' => 'Some text',
 759              ]);
 760  
 761          // Get the event from the calendar.
 762          $params = array('modulename' => 'assign', 'instance' => $assign->get_instance()->id);
 763          $event = $DB->get_record('event', [
 764              'modulename' => 'assign',
 765              'instance' => $assign->get_instance()->id,
 766          ]);
 767  
 768          $this->assertEmpty($event->description);
 769  
 770          // Change the allowsubmissionfromdate to the past - do this directly in the DB
 771          // because if we call the assignment update method - it will update the calendar
 772          // and we want to test that this works from cron.
 773          $DB->set_field('assign', 'allowsubmissionsfromdate', $now - 60, array('id' => $assign->get_instance()->id));
 774          // Run cron to update the event in the calendar.
 775          \assign::cron();
 776          $event = $DB->get_record('event', $params);
 777  
 778          $this->assertStringContainsString('Some text', $event->description);
 779  
 780      }
 781  
 782      public function test_update_instance() {
 783          global $DB;
 784  
 785          $this->resetAfterTest();
 786  
 787          $course = $this->getDataGenerator()->create_course();
 788          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 789  
 790          $this->setUser($teacher);
 791          $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]);
 792  
 793          $now = time();
 794          $instance = $assign->get_instance();
 795          $instance->duedate = $now;
 796          $instance->instance = $instance->id;
 797          $instance->assignsubmission_onlinetext_enabled = 1;
 798  
 799          $assign->update_instance($instance);
 800  
 801          $instance = $DB->get_record('assign', ['id' => $assign->get_instance()->id]);
 802          $this->assertEquals($now, $instance->duedate);
 803      }
 804  
 805      public function test_cannot_submit_empty() {
 806          global $PAGE;
 807  
 808          $this->resetAfterTest();
 809  
 810          $course = $this->getDataGenerator()->create_course();
 811          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 812  
 813          $assign = $this->create_instance($course, ['submissiondrafts' => 1]);
 814  
 815          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
 816  
 817          // Test you cannot see the submit button for an offline assignment regardless.
 818          $this->setUser($student);
 819          $output = $assign->view_student_summary($student, true);
 820          $this->assertStringNotContainsString(get_string('submitassignment', 'assign'),
 821              $output, 'Can submit empty offline assignment');
 822      }
 823  
 824      public function test_cannot_submit_empty_no_submission() {
 825          global $PAGE;
 826  
 827          $this->resetAfterTest();
 828  
 829          $course = $this->getDataGenerator()->create_course();
 830          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 831  
 832          $assign = $this->create_instance($course, [
 833              'submissiondrafts' => 1,
 834              'assignsubmission_onlinetext_enabled' => 1,
 835          ]);
 836  
 837          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
 838  
 839          // Test you cannot see the submit button for an online text assignment with no submission.
 840          $this->setUser($student);
 841          $output = $assign->view_student_summary($student, true);
 842          $this->assertStringNotContainsString(get_string('submitassignment', 'assign'),
 843              $output, 'Cannot submit empty onlinetext assignment');
 844      }
 845  
 846      public function test_can_submit_with_submission() {
 847          global $PAGE;
 848  
 849          $this->resetAfterTest();
 850  
 851          $course = $this->getDataGenerator()->create_course();
 852          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 853  
 854          $assign = $this->create_instance($course, [
 855              'submissiondrafts' => 1,
 856              'assignsubmission_onlinetext_enabled' => 1,
 857          ]);
 858  
 859          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
 860  
 861          // Add a draft.
 862          $this->add_submission($student, $assign);
 863  
 864          // Test you can see the submit button for an online text assignment with a submission.
 865          $this->setUser($student);
 866          $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
 867          $this->assertStringContainsString(get_string('submitassignment', 'assign'),
 868              $output, 'Can submit non empty onlinetext assignment');
 869      }
 870  
 871      /**
 872       * Test new_submission_empty
 873       *
 874       * We only test combinations of plugins here. Individual plugins are tested
 875       * in their respective test files.
 876       *
 877       * @dataProvider new_submission_empty_testcases
 878       * @param string $data The file submission data
 879       * @param bool $expected The expected return value
 880       */
 881      public function test_new_submission_empty($data, $expected) {
 882          $this->resetAfterTest();
 883  
 884          $course = $this->getDataGenerator()->create_course();
 885          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 886  
 887          $assign = $this->create_instance($course, [
 888                  'assignsubmission_file_enabled' => 1,
 889                  'assignsubmission_file_maxfiles' => 12,
 890                  'assignsubmission_file_maxsizebytes' => 10,
 891                  'assignsubmission_onlinetext_enabled' => 1,
 892              ]);
 893          $this->setUser($student);
 894          $submission = new \stdClass();
 895  
 896          if ($data['file'] && isset($data['file']['filename'])) {
 897              $itemid = file_get_unused_draft_itemid();
 898              $submission->files_filemanager = $itemid;
 899              $data['file'] += ['contextid' => \context_user::instance($student->id)->id, 'itemid' => $itemid];
 900              $fs = get_file_storage();
 901              $fs->create_file_from_string((object)$data['file'], 'Content of ' . $data['file']['filename']);
 902          }
 903  
 904          if ($data['onlinetext']) {
 905              $submission->onlinetext_editor = ['text' => $data['onlinetext']];
 906          }
 907  
 908          $result = $assign->new_submission_empty($submission);
 909          $this->assertTrue($result === $expected);
 910      }
 911  
 912      /**
 913       * Dataprovider for the test_new_submission_empty testcase
 914       *
 915       * @return array of testcases
 916       */
 917      public function new_submission_empty_testcases() {
 918          return [
 919              'With file and onlinetext' => [
 920                  [
 921                      'file' => [
 922                          'component' => 'user',
 923                          'filearea' => 'draft',
 924                          'filepath' => '/',
 925                          'filename' => 'not_a_virus.exe'
 926                      ],
 927                      'onlinetext' => 'Balin Fundinul Uzbadkhazaddumu'
 928                  ],
 929                  false
 930              ]
 931          ];
 932      }
 933  
 934      public function test_list_participants() {
 935          global $CFG;
 936  
 937          $this->resetAfterTest();
 938  
 939          $course = $this->getDataGenerator()->create_course();
 940          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 941  
 942          // Create 10 students.
 943          for ($i = 0; $i < 10; $i++) {
 944              $this->getDataGenerator()->create_and_enrol($course, 'student');
 945          }
 946  
 947          $this->setUser($teacher);
 948          $assign = $this->create_instance($course, ['grade' => 100]);
 949  
 950          $this->assertCount(10, $assign->list_participants(null, true));
 951      }
 952  
 953      public function test_list_participants_activeenrol() {
 954          global $CFG, $DB;
 955  
 956          $this->resetAfterTest();
 957  
 958          $course = $this->getDataGenerator()->create_course();
 959          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 960  
 961          // Create 10 students.
 962          for ($i = 0; $i < 10; $i++) {
 963              $this->getDataGenerator()->create_and_enrol($course, 'student');
 964          }
 965  
 966          // Create 10 suspended students.
 967          for ($i = 0; $i < 10; $i++) {
 968              $this->getDataGenerator()->create_and_enrol($course, 'student', null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
 969          }
 970  
 971          $this->setUser($teacher);
 972          set_user_preference('grade_report_showonlyactiveenrol', false);
 973          $assign = $this->create_instance($course, ['grade' => 100]);
 974  
 975          $this->assertCount(10, $assign->list_participants(null, true));
 976      }
 977  
 978      public function test_list_participants_with_group_restriction() {
 979          global $CFG;
 980  
 981          $this->resetAfterTest();
 982  
 983          $course = $this->getDataGenerator()->create_course();
 984          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 985          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 986          $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
 987          $unrelatedstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
 988  
 989          // Turn on availability and a group restriction, and check that it doesn't show users who aren't in the group.
 990          $CFG->enableavailability = true;
 991  
 992          $specialgroup = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
 993          $assign = $this->create_instance($course, [
 994              'grade' => 100,
 995              'availability' => json_encode(
 996                  \core_availability\tree::get_root_json([\availability_group\condition::get_json($specialgroup->id)])
 997              ),
 998          ]);
 999  
1000          groups_add_member($specialgroup, $student);
1001          groups_add_member($specialgroup, $otherstudent);
1002          $this->assertEquals(2, count($assign->list_participants(null, true)));
1003      }
1004  
1005      public function test_get_participant_user_not_exist() {
1006          $this->resetAfterTest();
1007          $course = $this->getDataGenerator()->create_course();
1008  
1009          $assign = $this->create_instance($course);
1010          $this->assertNull($assign->get_participant('-1'));
1011      }
1012  
1013      public function test_get_participant_not_enrolled() {
1014          $this->resetAfterTest();
1015          $course = $this->getDataGenerator()->create_course();
1016          $assign = $this->create_instance($course);
1017  
1018          $user = $this->getDataGenerator()->create_user();
1019          $this->assertNull($assign->get_participant($user->id));
1020      }
1021  
1022      public function test_get_participant_no_submission() {
1023          $this->resetAfterTest();
1024          $course = $this->getDataGenerator()->create_course();
1025          $assign = $this->create_instance($course);
1026          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1027  
1028          $participant = $assign->get_participant($student->id);
1029  
1030          $this->assertEquals($student->id, $participant->id);
1031          $this->assertFalse($participant->submitted);
1032          $this->assertFalse($participant->requiregrading);
1033          $this->assertFalse($participant->grantedextension);
1034      }
1035  
1036      public function test_get_participant_granted_extension() {
1037          $this->resetAfterTest();
1038          $course = $this->getDataGenerator()->create_course();
1039          $assign = $this->create_instance($course);
1040          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1041          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1042  
1043          $this->setUser($teacher);
1044          $assign->save_user_extension($student->id, time());
1045          $participant = $assign->get_participant($student->id);
1046  
1047          $this->assertEquals($student->id, $participant->id);
1048          $this->assertFalse($participant->submitted);
1049          $this->assertFalse($participant->requiregrading);
1050          $this->assertTrue($participant->grantedextension);
1051      }
1052  
1053      public function test_get_participant_with_ungraded_submission() {
1054          $this->resetAfterTest();
1055          $course = $this->getDataGenerator()->create_course();
1056          $assign = $this->create_instance($course);
1057          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1058          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1059  
1060          // Simulate a submission.
1061          $this->add_submission($student, $assign);
1062          $this->submit_for_grading($student, $assign);
1063  
1064          $participant = $assign->get_participant($student->id);
1065  
1066          $this->assertEquals($student->id, $participant->id);
1067          $this->assertTrue($participant->submitted);
1068          $this->assertTrue($participant->requiregrading);
1069          $this->assertFalse($participant->grantedextension);
1070      }
1071  
1072      /**
1073       * Tests that if a student with no submission who can no longer submit is not a participant.
1074       */
1075      public function test_get_participant_with_no_submission_no_capability() {
1076          global $DB;
1077          $this->resetAfterTest();
1078          $course = self::getDataGenerator()->create_course();
1079          $coursecontext = \context_course::instance($course->id);
1080          $assign = $this->create_instance($course);
1081          $teacher = self::getDataGenerator()->create_and_enrol($course, 'teacher');
1082          $student = self::getDataGenerator()->create_and_enrol($course, 'student');
1083  
1084          // Remove the students capability to submit.
1085          $role = $DB->get_field('role', 'id', ['shortname' => 'student']);
1086          assign_capability('mod/assign:submit', CAP_PROHIBIT, $role, $coursecontext);
1087  
1088          $participant = $assign->get_participant($student->id);
1089  
1090          self::assertNull($participant);
1091      }
1092  
1093      /**
1094       * Tests that if a student that has submitted but can no longer submit is a participant.
1095       */
1096      public function test_get_participant_with_submission_no_capability() {
1097          global $DB;
1098          $this->resetAfterTest();
1099          $course = self::getDataGenerator()->create_course();
1100          $coursecontext = \context_course::instance($course->id);
1101          $assign = $this->create_instance($course);
1102          $teacher = self::getDataGenerator()->create_and_enrol($course, 'teacher');
1103          $student = self::getDataGenerator()->create_and_enrol($course, 'student');
1104  
1105          // Simulate a submission.
1106          $this->add_submission($student, $assign);
1107          $this->submit_for_grading($student, $assign);
1108  
1109          // Remove the students capability to submit.
1110          $role = $DB->get_field('role', 'id', ['shortname' => 'student']);
1111          assign_capability('mod/assign:submit', CAP_PROHIBIT, $role, $coursecontext);
1112  
1113          $participant = $assign->get_participant($student->id);
1114  
1115          self::assertNotNull($participant);
1116          self::assertEquals($student->id, $participant->id);
1117          self::assertTrue($participant->submitted);
1118          self::assertTrue($participant->requiregrading);
1119          self::assertFalse($participant->grantedextension);
1120      }
1121  
1122      public function test_get_participant_with_graded_submission() {
1123          $this->resetAfterTest();
1124          $course = $this->getDataGenerator()->create_course();
1125          $assign = $this->create_instance($course);
1126          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1127          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1128  
1129          // Simulate a submission.
1130          $this->add_submission($student, $assign);
1131          $this->submit_for_grading($student, $assign);
1132  
1133          $this->mark_submission($teacher, $assign, $student, 50.0);
1134  
1135          $data = new \stdClass();
1136          $data->grade = '50.0';
1137          $assign->testable_apply_grade_to_user($data, $student->id, 0);
1138  
1139          $participant = $assign->get_participant($student->id);
1140  
1141          $this->assertEquals($student->id, $participant->id);
1142          $this->assertTrue($participant->submitted);
1143          $this->assertFalse($participant->requiregrading);
1144          $this->assertFalse($participant->grantedextension);
1145      }
1146  
1147      /**
1148       * No active group and non-group submissions disallowed => 2 groups.
1149       */
1150      public function test_count_teams_no_active_non_group_allowed() {
1151          $this->resetAfterTest();
1152  
1153          $course = $this->getDataGenerator()->create_course();
1154          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1155          $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1156          $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1157  
1158          $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
1159          $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1160          $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1161          groups_add_member($group1, $student1);
1162          groups_add_member($group2, $student2);
1163  
1164          $this->setUser($teacher);
1165          $assign = $this->create_instance($course, ['teamsubmission' => 1]);
1166  
1167          $this->assertEquals(2, $assign->count_teams());
1168      }
1169  
1170      /**
1171       * No active group and non group submissions allowed => 2 groups + the default one.
1172       */
1173      public function test_count_teams_non_group_allowed() {
1174          $this->resetAfterTest();
1175  
1176          $course = $this->getDataGenerator()->create_course();
1177          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1178          $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1179          $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1180          $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1181  
1182          $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
1183          $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1184  
1185          $this->getDataGenerator()->create_grouping_group(array('groupid' => $group1->id, 'groupingid' => $grouping->id));
1186          $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1187          $this->getDataGenerator()->create_grouping_group(array('groupid' => $group2->id, 'groupingid' => $grouping->id));
1188  
1189          groups_add_member($group1, $student1);
1190          groups_add_member($group2, $student2);
1191  
1192          $assign = $this->create_instance($course, [
1193              'teamsubmission' => 1,
1194              'teamsubmissiongroupingid' => $grouping->id,
1195              'preventsubmissionnotingroup' => false,
1196          ]);
1197  
1198          $this->setUser($teacher);
1199          $this->assertEquals(3, $assign->count_teams());
1200  
1201          // Active group only.
1202          $this->assertEquals(1, $assign->count_teams($group1->id));
1203          $this->assertEquals(1, $assign->count_teams($group2->id));
1204      }
1205  
1206      /**
1207       * Active group => just selected one.
1208       */
1209      public function test_count_teams_no_active_group() {
1210          $this->resetAfterTest();
1211  
1212          $course = $this->getDataGenerator()->create_course();
1213          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1214          $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1215          $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1216          $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1217  
1218          $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
1219          $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1220  
1221          $this->getDataGenerator()->create_grouping_group(array('groupid' => $group1->id, 'groupingid' => $grouping->id));
1222          $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1223          $this->getDataGenerator()->create_grouping_group(array('groupid' => $group2->id, 'groupingid' => $grouping->id));
1224  
1225          groups_add_member($group1, $student1);
1226          groups_add_member($group2, $student2);
1227  
1228          $assign = $this->create_instance($course, [
1229              'teamsubmission' => 1,
1230              'preventsubmissionnotingroup' => true,
1231          ]);
1232  
1233          $this->setUser($teacher);
1234          $this->assertEquals(2, $assign->count_teams());
1235  
1236          // Active group only.
1237          $this->assertEquals(1, $assign->count_teams($group1->id));
1238          $this->assertEquals(1, $assign->count_teams($group2->id));
1239      }
1240  
1241      /**
1242       * Active group => just selected one.
1243       */
1244      public function test_count_teams_groups_only() {
1245          $this->resetAfterTest();
1246  
1247          $course = $this->getDataGenerator()->create_course();
1248          $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
1249  
1250          $assign = $this->create_instance($course, [
1251              'teamsubmission' => 1,
1252              'teamsubmissiongroupingid' => $grouping->id,
1253              'preventsubmissionnotingroup' => false,
1254          ]);
1255          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1256  
1257          $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1258          $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1259          groups_add_member($group1, $student1);
1260  
1261          $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1262          $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1263          groups_add_member($group2, $student2);
1264  
1265          $this->getDataGenerator()->create_grouping_group(array('groupid' => $group1->id, 'groupingid' => $grouping->id));
1266          $this->getDataGenerator()->create_grouping_group(array('groupid' => $group2->id, 'groupingid' => $grouping->id));
1267  
1268          $this->setUser($teacher);
1269  
1270          $assign = $this->create_instance($course, [
1271              'teamsubmission' => 1,
1272              'preventsubmissionnotingroup' => true,
1273          ]);
1274          $this->assertEquals(2, $assign->count_teams());
1275      }
1276  
1277      public function test_submit_to_default_group() {
1278          global $DB, $SESSION;
1279  
1280          $this->resetAfterTest();
1281  
1282          $course = $this->getDataGenerator()->create_course();
1283          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1284          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1285  
1286          $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
1287          $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1288  
1289          $assign = $this->create_instance($course, [
1290              'teamsubmission' => 1,
1291              'assignsubmission_onlinetext_enabled' => 1,
1292              'submissiondrafts' => 0,
1293              'groupmode' => VISIBLEGROUPS,
1294          ]);
1295  
1296          $usergroup = $assign->get_submission_group($student->id);
1297          $this->assertFalse($usergroup, 'New student is in default group');
1298  
1299          // Add a submission.
1300          $this->add_submission($student, $assign);
1301          $this->submit_for_grading($student, $assign);
1302  
1303          // Set active groups to all groups.
1304          $this->setUser($teacher);
1305          $SESSION->activegroup[$course->id]['aag'][0] = 0;
1306          $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1307  
1308          // Set an active group.
1309          $SESSION->activegroup[$course->id]['aag'][0] = (int) $group->id;
1310          $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1311      }
1312  
1313      public function test_count_submissions_no_draft() {
1314          $this->resetAfterTest();
1315  
1316          $course = $this->getDataGenerator()->create_course();
1317          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1318          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1319  
1320          $assign = $this->create_instance($course, [
1321              'assignsubmission_onlinetext_enabled' => 1,
1322          ]);
1323  
1324          $assign->get_user_submission($student->id, true);
1325  
1326          // Note: Drafts count as a submission.
1327          $this->assertEquals(0, $assign->count_grades());
1328          $this->assertEquals(0, $assign->count_submissions());
1329          $this->assertEquals(1, $assign->count_submissions(true));
1330          $this->assertEquals(0, $assign->count_submissions_need_grading());
1331          $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_NEW));
1332          $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT));
1333          $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1334          $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_REOPENED));
1335      }
1336  
1337      public function test_count_submissions_draft() {
1338          $this->resetAfterTest();
1339  
1340          $course = $this->getDataGenerator()->create_course();
1341          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1342          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1343  
1344          $assign = $this->create_instance($course, [
1345              'assignsubmission_onlinetext_enabled' => 1,
1346          ]);
1347  
1348          $this->add_submission($student, $assign);
1349  
1350          // Note: Drafts count as a submission.
1351          $this->assertEquals(0, $assign->count_grades());
1352          $this->assertEquals(1, $assign->count_submissions());
1353          $this->assertEquals(1, $assign->count_submissions(true));
1354          $this->assertEquals(0, $assign->count_submissions_need_grading());
1355          $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_NEW));
1356          $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT));
1357          $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1358          $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_REOPENED));
1359      }
1360  
1361      public function test_count_submissions_submitted() {
1362          global $SESSION;
1363  
1364          $this->resetAfterTest();
1365  
1366          $course = $this->getDataGenerator()->create_course();
1367          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1368          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1369  
1370          $assign = $this->create_instance($course, [
1371              'assignsubmission_onlinetext_enabled' => 1,
1372          ]);
1373  
1374          $this->add_submission($student, $assign);
1375          $this->submit_for_grading($student, $assign);
1376  
1377          $this->assertEquals(0, $assign->count_grades());
1378          $this->assertEquals(1, $assign->count_submissions());
1379          $this->assertEquals(1, $assign->count_submissions(true));
1380          $this->assertEquals(1, $assign->count_submissions_need_grading());
1381          $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_NEW));
1382          $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT));
1383          $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1384          $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_REOPENED));
1385      }
1386  
1387      public function test_count_submissions_graded() {
1388          $this->resetAfterTest();
1389  
1390          $course = $this->getDataGenerator()->create_course();
1391          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1392          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1393  
1394          $assign = $this->create_instance($course, [
1395              'assignsubmission_onlinetext_enabled' => 1,
1396          ]);
1397  
1398          $this->add_submission($student, $assign);
1399          $this->submit_for_grading($student, $assign);
1400          $this->mark_submission($teacher, $assign, $student, 50.0);
1401  
1402          // Although it has been graded, it is still marked as submitted.
1403          $this->assertEquals(1, $assign->count_grades());
1404          $this->assertEquals(1, $assign->count_submissions());
1405          $this->assertEquals(1, $assign->count_submissions(true));
1406          $this->assertEquals(0, $assign->count_submissions_need_grading());
1407          $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_NEW));
1408          $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT));
1409          $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1410          $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_REOPENED));
1411      }
1412  
1413      public function test_count_submissions_graded_group() {
1414          global $SESSION;
1415  
1416          $this->resetAfterTest();
1417  
1418          $course = $this->getDataGenerator()->create_course();
1419          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1420          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1421          $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1422          $othergroup = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1423          groups_add_member($group, $student);
1424  
1425          $assign = $this->create_instance($course, [
1426              'assignsubmission_onlinetext_enabled' => 1,
1427              'groupmode' => VISIBLEGROUPS,
1428          ]);
1429  
1430          $this->add_submission($student, $assign);
1431          $this->submit_for_grading($student, $assign);
1432  
1433          // The user should still be listed when fetching all groups.
1434          $this->setUser($teacher);
1435          $SESSION->activegroup[$course->id]['aag'][0] = 0;
1436          $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1437  
1438          // The user should still be listed when fetching just their group.
1439          $SESSION->activegroup[$course->id]['aag'][0] = $group->id;
1440          $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1441  
1442          // The user should still be listed when fetching just their group.
1443          $SESSION->activegroup[$course->id]['aag'][0] = $othergroup->id;
1444          $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1445      }
1446  
1447      // TODO
1448      public function x_test_count_submissions_for_team() {
1449          $this->resetAfterTest();
1450  
1451          $course = $this->getDataGenerator()->create_course();
1452          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1453          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1454          $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1455          $othergroup = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1456          groups_add_member($group, $student);
1457  
1458          $assign = $this->create_instance($course, [
1459              'assignsubmission_onlinetext_enabled' => 1,
1460              'teamsubmission' => 1,
1461          ]);
1462  
1463          // Add a graded submission.
1464          $this->add_submission($student, $assign);
1465  
1466          // Simulate adding a grade.
1467          $this->setUser($teacher);
1468          $data = new \stdClass();
1469          $data->grade = '50.0';
1470          $assign->testable_apply_grade_to_user($data, $this->extrastudents[0]->id, 0);
1471  
1472          // Simulate a submission.
1473          $this->setUser($this->extrastudents[1]);
1474          $submission = $assign->get_group_submission($this->extrastudents[1]->id, $groupid, true);
1475          $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
1476          $assign->testable_update_submission($submission, $this->extrastudents[1]->id, true, false);
1477          $data = new \stdClass();
1478          $data->onlinetext_editor = array(
1479              'itemid' => file_get_unused_draft_itemid(),
1480              'text' => 'Submission text',
1481              'format' => FORMAT_MOODLE);
1482          $plugin = $assign->get_submission_plugin_by_type('onlinetext');
1483          $plugin->save($submission, $data);
1484  
1485          // Simulate a submission.
1486          $this->setUser($this->extrastudents[2]);
1487          $submission = $assign->get_group_submission($this->extrastudents[2]->id, $groupid, true);
1488          $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
1489          $assign->testable_update_submission($submission, $this->extrastudents[2]->id, true, false);
1490          $data = new \stdClass();
1491          $data->onlinetext_editor = array(
1492              'itemid' => file_get_unused_draft_itemid(),
1493              'text' => 'Submission text',
1494              'format' => FORMAT_MOODLE);
1495          $plugin = $assign->get_submission_plugin_by_type('onlinetext');
1496          $plugin->save($submission, $data);
1497  
1498          // Simulate a submission.
1499          $this->setUser($this->extrastudents[3]);
1500          $submission = $assign->get_group_submission($this->extrastudents[3]->id, $groupid, true);
1501          $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
1502          $assign->testable_update_submission($submission, $this->extrastudents[3]->id, true, false);
1503          $data = new \stdClass();
1504          $data->onlinetext_editor = array(
1505              'itemid' => file_get_unused_draft_itemid(),
1506              'text' => 'Submission text',
1507              'format' => FORMAT_MOODLE);
1508          $plugin = $assign->get_submission_plugin_by_type('onlinetext');
1509          $plugin->save($submission, $data);
1510  
1511          // Simulate adding a grade.
1512          $this->setUser($teacher);
1513          $data = new \stdClass();
1514          $data->grade = '50.0';
1515          $assign->testable_apply_grade_to_user($data, $this->extrastudents[3]->id, 0);
1516          $assign->testable_apply_grade_to_user($data, $this->extrasuspendedstudents[0]->id, 0);
1517  
1518          // Create a new submission with status NEW.
1519          $this->setUser($this->extrastudents[4]);
1520          $submission = $assign->get_group_submission($this->extrastudents[4]->id, $groupid, true);
1521  
1522          $this->assertEquals(2, $assign->count_grades());
1523          $this->assertEquals(4, $assign->count_submissions());
1524          $this->assertEquals(5, $assign->count_submissions(true));
1525          $this->assertEquals(3, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1526          $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT));
1527      }
1528  
1529      public function test_get_grading_userid_list_only_active() {
1530          $this->resetAfterTest();
1531  
1532          $course = $this->getDataGenerator()->create_course();
1533          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1534          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1535          $suspendedstudent = $this->getDataGenerator()->create_and_enrol(
1536              $course, 'student', null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
1537  
1538          $this->setUser($teacher);
1539  
1540          $assign = $this->create_instance($course);
1541          $this->assertCount(1, $assign->testable_get_grading_userid_list());
1542      }
1543  
1544      public function test_get_grading_userid_list_all() {
1545          $this->resetAfterTest();
1546  
1547          $course = $this->getDataGenerator()->create_course();
1548          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1549          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1550          $suspendedstudent = $this->getDataGenerator()->create_and_enrol(
1551              $course, 'student', null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
1552  
1553          $this->setUser($teacher);
1554          set_user_preference('grade_report_showonlyactiveenrol', false);
1555  
1556          $assign = $this->create_instance($course);
1557          $this->assertCount(2, $assign->testable_get_grading_userid_list());
1558      }
1559  
1560      public function test_cron() {
1561          global $PAGE;
1562          $this->resetAfterTest();
1563  
1564          // First run cron so there are no messages waiting to be sent (from other tests).
1565          cron_setup_user();
1566          \assign::cron();
1567  
1568          $course = $this->getDataGenerator()->create_course();
1569          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1570          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1571  
1572          // Now create an assignment and add some feedback.
1573          $this->setUser($teacher);
1574          $assign = $this->create_instance($course, [
1575              'sendstudentnotifications' => 1,
1576          ]);
1577  
1578          $this->add_submission($student, $assign);
1579          $this->submit_for_grading($student, $assign);
1580          $this->mark_submission($teacher, $assign, $student, 50.0);
1581  
1582          $this->expectOutputRegex('/Done processing 1 assignment submissions/');
1583          cron_setup_user();
1584          $sink = $this->redirectMessages();
1585          \assign::cron();
1586          $messages = $sink->get_messages();
1587  
1588          $this->assertEquals(1, count($messages));
1589          $this->assertEquals(1, $messages[0]->notification);
1590          $this->assertEquals($assign->get_instance()->name, $messages[0]->contexturlname);
1591          // Test customdata.
1592          $customdata = json_decode($messages[0]->customdata);
1593          $this->assertEquals($assign->get_course_module()->id, $customdata->cmid);
1594          $this->assertEquals($assign->get_instance()->id, $customdata->instance);
1595          $this->assertEquals('feedbackavailable', $customdata->messagetype);
1596          $userpicture = new \user_picture($teacher);
1597          $userpicture->size = 1; // Use f1 size.
1598          $this->assertEquals($userpicture->get_url($PAGE)->out(false), $customdata->notificationiconurl);
1599          $this->assertEquals(0, $customdata->uniqueidforuser);   // Not used in this case.
1600          $this->assertFalse($customdata->blindmarking);
1601      }
1602  
1603      public function test_cron_without_notifications() {
1604          $this->resetAfterTest();
1605  
1606          // First run cron so there are no messages waiting to be sent (from other tests).
1607          cron_setup_user();
1608          \assign::cron();
1609  
1610          $course = $this->getDataGenerator()->create_course();
1611          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1612          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1613  
1614          // Now create an assignment and add some feedback.
1615          $this->setUser($teacher);
1616          $assign = $this->create_instance($course, [
1617              'sendstudentnotifications' => 1,
1618          ]);
1619  
1620          $this->add_submission($student, $assign);
1621          $this->submit_for_grading($student, $assign);
1622          $this->mark_submission($teacher, $assign, $student, 50.0, [
1623              'sendstudentnotifications' => 0,
1624          ]);
1625  
1626          cron_setup_user();
1627          $sink = $this->redirectMessages();
1628          \assign::cron();
1629          $messages = $sink->get_messages();
1630  
1631          $this->assertEquals(0, count($messages));
1632      }
1633  
1634      public function test_cron_regraded() {
1635          $this->resetAfterTest();
1636  
1637          // First run cron so there are no messages waiting to be sent (from other tests).
1638          cron_setup_user();
1639          \assign::cron();
1640  
1641          $course = $this->getDataGenerator()->create_course();
1642          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1643          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1644  
1645          // Now create an assignment and add some feedback.
1646          $this->setUser($teacher);
1647          $assign = $this->create_instance($course, [
1648              'sendstudentnotifications' => 1,
1649          ]);
1650  
1651          $this->add_submission($student, $assign);
1652          $this->submit_for_grading($student, $assign);
1653          $this->mark_submission($teacher, $assign, $student, 50.0);
1654  
1655          $this->expectOutputRegex('/Done processing 1 assignment submissions/');
1656          cron_setup_user();
1657          \assign::cron();
1658  
1659          // Regrade.
1660          $this->mark_submission($teacher, $assign, $student, 50.0);
1661  
1662          $this->expectOutputRegex('/Done processing 1 assignment submissions/');
1663          cron_setup_user();
1664          $sink = $this->redirectMessages();
1665          \assign::cron();
1666          $messages = $sink->get_messages();
1667  
1668          $this->assertEquals(1, count($messages));
1669          $this->assertEquals(1, $messages[0]->notification);
1670          $this->assertEquals($assign->get_instance()->name, $messages[0]->contexturlname);
1671      }
1672  
1673      /**
1674       * Test delivery of grade notifications as controlled by marking workflow.
1675       */
1676      public function test_markingworkflow_cron() {
1677          $this->resetAfterTest();
1678  
1679          // First run cron so there are no messages waiting to be sent (from other tests).
1680          cron_setup_user();
1681          \assign::cron();
1682  
1683          $course = $this->getDataGenerator()->create_course();
1684          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1685          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1686  
1687          // Now create an assignment and add some feedback.
1688          $this->setUser($teacher);
1689          $assign = $this->create_instance($course, [
1690              'sendstudentnotifications' => 1,
1691              'markingworkflow' => 1,
1692          ]);
1693  
1694          // Mark a submission but set the workflowstate to an unreleased state.
1695          // This should not trigger a notification.
1696          $this->add_submission($student, $assign);
1697          $this->submit_for_grading($student, $assign);
1698          $this->mark_submission($teacher, $assign, $student, 50.0, [
1699              'sendstudentnotifications' => 1,
1700              'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE,
1701          ]);
1702  
1703          cron_setup_user();
1704          $sink = $this->redirectMessages();
1705          \assign::cron();
1706          $messages = $sink->get_messages();
1707  
1708          $this->assertEquals(0, count($messages));
1709  
1710          // Transition to the released state.
1711          $this->setUser($teacher);
1712          $submission = $assign->get_user_submission($student->id, true);
1713          $submission->workflowstate = ASSIGN_MARKING_WORKFLOW_STATE_RELEASED;
1714          $assign->testable_apply_grade_to_user($submission, $student->id, 0);
1715  
1716          // Now run cron and see that one message was sent.
1717          cron_setup_user();
1718          $sink = $this->redirectMessages();
1719          $this->expectOutputRegex('/Done processing 1 assignment submissions/');
1720          \assign::cron();
1721          $messages = $sink->get_messages();
1722  
1723          $this->assertEquals(1, count($messages));
1724          $this->assertEquals(1, $messages[0]->notification);
1725          $this->assertEquals($assign->get_instance()->name, $messages[0]->contexturlname);
1726      }
1727  
1728      public function test_cron_message_includes_courseid() {
1729          $this->resetAfterTest();
1730  
1731          // First run cron so there are no messages waiting to be sent (from other tests).
1732          cron_setup_user();
1733          \assign::cron();
1734  
1735          $course = $this->getDataGenerator()->create_course();
1736          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1737          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1738  
1739          // Now create an assignment and add some feedback.
1740          $this->setUser($teacher);
1741          $assign = $this->create_instance($course, [
1742              'sendstudentnotifications' => 1,
1743          ]);
1744  
1745          // Mark a submission but set the workflowstate to an unreleased state.
1746          // This should not trigger a notification.
1747          $this->add_submission($student, $assign);
1748          $this->submit_for_grading($student, $assign);
1749          $this->mark_submission($teacher, $assign, $student);
1750          \phpunit_util::stop_message_redirection();
1751  
1752          // Now run cron and see that one message was sent.
1753          cron_setup_user();
1754          $this->preventResetByRollback();
1755          $sink = $this->redirectEvents();
1756          $this->expectOutputRegex('/Done processing 1 assignment submissions/');
1757          \assign::cron();
1758  
1759          $events = $sink->get_events();
1760          $event = reset($events);
1761          $this->assertInstanceOf('\core\event\notification_sent', $event);
1762          $this->assertEquals($assign->get_course()->id, $event->other['courseid']);
1763          $sink->close();
1764      }
1765  
1766      public function test_is_graded() {
1767          $this->resetAfterTest();
1768  
1769          $course = $this->getDataGenerator()->create_course();
1770          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1771          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1772          $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
1773  
1774          $assign = $this->create_instance($course);
1775  
1776          $this->add_submission($student, $assign);
1777          $this->submit_for_grading($student, $assign);
1778          $this->mark_submission($teacher, $assign, $student, 50.0);
1779  
1780          $this->setUser($teacher);
1781          $this->assertEquals(true, $assign->testable_is_graded($student->id));
1782          $this->assertEquals(false, $assign->testable_is_graded($otherstudent->id));
1783      }
1784  
1785      public function test_can_grade() {
1786          global $DB;
1787  
1788          $this->resetAfterTest();
1789  
1790          $course = $this->getDataGenerator()->create_course();
1791          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1792          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1793  
1794          $assign = $this->create_instance($course);
1795  
1796          $this->setUser($student);
1797          $this->assertEquals(false, $assign->can_grade());
1798  
1799          $this->setUser($teacher);
1800          $this->assertEquals(true, $assign->can_grade());
1801  
1802          // Test the viewgrades capability for other users.
1803          $this->setUser();
1804          $this->assertTrue($assign->can_grade($teacher->id));
1805          $this->assertFalse($assign->can_grade($student->id));
1806  
1807          // Test the viewgrades capability - without mod/assign:grade.
1808          $this->setUser($student);
1809  
1810          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1811          assign_capability('mod/assign:viewgrades', CAP_ALLOW, $studentrole->id, $assign->get_context()->id);
1812          $this->assertEquals(false, $assign->can_grade());
1813      }
1814  
1815      public function test_can_view_submission() {
1816          global $DB;
1817  
1818          $this->resetAfterTest();
1819  
1820          $course = $this->getDataGenerator()->create_course();
1821          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1822          $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1823          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1824          $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
1825          $suspendedstudent = $this->getDataGenerator()->create_and_enrol(
1826              $course, 'student', null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
1827  
1828          $assign = $this->create_instance($course);
1829  
1830          $this->setUser($student);
1831          $this->assertEquals(true, $assign->can_view_submission($student->id));
1832          $this->assertEquals(false, $assign->can_view_submission($otherstudent->id));
1833          $this->assertEquals(false, $assign->can_view_submission($teacher->id));
1834  
1835          $this->setUser($teacher);
1836          $this->assertEquals(true, $assign->can_view_submission($student->id));
1837          $this->assertEquals(true, $assign->can_view_submission($otherstudent->id));
1838          $this->assertEquals(true, $assign->can_view_submission($teacher->id));
1839          $this->assertEquals(false, $assign->can_view_submission($suspendedstudent->id));
1840  
1841          $this->setUser($editingteacher);
1842          $this->assertEquals(true, $assign->can_view_submission($student->id));
1843          $this->assertEquals(true, $assign->can_view_submission($otherstudent->id));
1844          $this->assertEquals(true, $assign->can_view_submission($teacher->id));
1845          $this->assertEquals(true, $assign->can_view_submission($suspendedstudent->id));
1846  
1847          // Test the viewgrades capability - without mod/assign:grade.
1848          $this->setUser($student);
1849          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1850          assign_capability('mod/assign:viewgrades', CAP_ALLOW, $studentrole->id, $assign->get_context()->id);
1851          $this->assertEquals(true, $assign->can_view_submission($student->id));
1852          $this->assertEquals(true, $assign->can_view_submission($otherstudent->id));
1853          $this->assertEquals(true, $assign->can_view_submission($teacher->id));
1854          $this->assertEquals(false, $assign->can_view_submission($suspendedstudent->id));
1855      }
1856  
1857      public function test_update_submission() {
1858          $this->resetAfterTest();
1859  
1860          $course = $this->getDataGenerator()->create_course();
1861          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1862          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1863  
1864          $assign = $this->create_instance($course);
1865  
1866          $this->add_submission($student, $assign);
1867          $submission = $assign->get_user_submission($student->id, 0);
1868          $assign->testable_update_submission($submission, $student->id, true, true);
1869  
1870          $this->setUser($teacher);
1871  
1872          // Verify the gradebook update.
1873          $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $student->id);
1874  
1875          $this->assertTrue(isset($gradinginfo->items[0]->grades[$student->id]));
1876          $this->assertEquals($student->id, $gradinginfo->items[0]->grades[$student->id]->usermodified);
1877      }
1878  
1879      public function test_update_submission_team() {
1880          $this->resetAfterTest();
1881  
1882          $course = $this->getDataGenerator()->create_course();
1883          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1884          $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1885  
1886          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1887          groups_add_member($group, $student);
1888  
1889          $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
1890          groups_add_member($group, $otherstudent);
1891  
1892          $assign = $this->create_instance($course, [
1893              'teamsubmission' => 1,
1894          ]);
1895  
1896          $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $student->id);
1897          $this->assertTrue(isset($gradinginfo->items[0]->grades[$student->id]));
1898          $this->assertNull($gradinginfo->items[0]->grades[$student->id]->usermodified);
1899  
1900          $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $otherstudent->id);
1901          $this->asserttrue(isset($gradinginfo->items[0]->grades[$otherstudent->id]));
1902          $this->assertNull($gradinginfo->items[0]->grades[$otherstudent->id]->usermodified);
1903  
1904          $this->add_submission($student, $assign);
1905          $submission = $assign->get_group_submission($student->id, 0, true);
1906          $assign->testable_update_submission($submission, $student->id, true, true);
1907  
1908          // Verify the gradebook update for the student.
1909          $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $student->id);
1910  
1911          $this->assertTrue(isset($gradinginfo->items[0]->grades[$student->id]));
1912          $this->assertEquals($student->id, $gradinginfo->items[0]->grades[$student->id]->usermodified);
1913  
1914          // Verify the gradebook update for the other student.
1915          $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $otherstudent->id);
1916  
1917          $this->assertTrue(isset($gradinginfo->items[0]->grades[$otherstudent->id]));
1918          $this->assertEquals($otherstudent->id, $gradinginfo->items[0]->grades[$otherstudent->id]->usermodified);
1919      }
1920  
1921      public function test_update_submission_suspended() {
1922          $this->resetAfterTest();
1923  
1924          $course = $this->getDataGenerator()->create_course();
1925          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1926          $student = $this->getDataGenerator()->create_and_enrol($course, 'student', null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
1927  
1928          $assign = $this->create_instance($course);
1929  
1930          $this->add_submission($student, $assign);
1931          $submission = $assign->get_user_submission($student->id, 0);
1932          $assign->testable_update_submission($submission, $student->id, true, false);
1933  
1934          $this->setUser($teacher);
1935  
1936          // Verify the gradebook update.
1937          $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $student->id);
1938  
1939          $this->assertTrue(isset($gradinginfo->items[0]->grades[$student->id]));
1940          $this->assertEquals($student->id, $gradinginfo->items[0]->grades[$student->id]->usermodified);
1941      }
1942  
1943      public function test_update_submission_blind() {
1944          $this->resetAfterTest();
1945  
1946          $course = $this->getDataGenerator()->create_course();
1947          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1948          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1949  
1950          $assign = $this->create_instance($course, [
1951              'blindmarking' => 1,
1952          ]);
1953  
1954          $this->add_submission($student, $assign);
1955          $submission = $assign->get_user_submission($student->id, 0);
1956          $assign->testable_update_submission($submission, $student->id, true, false);
1957  
1958          // Verify the gradebook update.
1959          $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $student->id);
1960  
1961          // The usermodified is not set because this is blind marked.
1962          $this->assertTrue(isset($gradinginfo->items[0]->grades[$student->id]));
1963          $this->assertNull($gradinginfo->items[0]->grades[$student->id]->usermodified);
1964      }
1965  
1966      public function test_group_submissions_submit_for_marking_requireallteammemberssubmit() {
1967          global $PAGE;
1968  
1969          $this->resetAfterTest();
1970  
1971          $course = $this->getDataGenerator()->create_course();
1972          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1973          $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1974  
1975          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1976          groups_add_member($group, $student);
1977  
1978          $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
1979          groups_add_member($group, $otherstudent);
1980  
1981          $assign = $this->create_instance($course, [
1982              'teamsubmission' => 1,
1983              'assignsubmission_onlinetext_enabled' => 1,
1984              'submissiondrafts' => 1,
1985              'requireallteammemberssubmit' => 1,
1986          ]);
1987  
1988          // Now verify group assignments.
1989          $this->setUser($teacher);
1990          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
1991  
1992          // Add a submission.
1993          $this->add_submission($student, $assign);
1994  
1995          // Check we can see the submit button.
1996          $this->setUser($student);
1997          $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
1998          $this->assertStringContainsString(get_string('submitassignment', 'assign'), $output);
1999  
2000          $submission = $assign->get_group_submission($student->id, 0, true);
2001          $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
2002          $assign->testable_update_submission($submission, $student->id, true, true);
2003  
2004          // Check that the student does not see "Submit" button.
2005          $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
2006          $this->assertStringNotContainsString(get_string('submitassignment', 'assign'), $output);
2007  
2008          // Change to another user in the same group.
2009          $this->setUser($otherstudent);
2010          $output = $assign->view_submission_action_bar($assign->get_instance(), $otherstudent);
2011          $this->assertStringContainsString(get_string('submitassignment', 'assign'), $output);
2012  
2013          $submission = $assign->get_group_submission($otherstudent->id, 0, true);
2014          $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
2015          $assign->testable_update_submission($submission, $otherstudent->id, true, true);
2016          $output = $assign->view_submission_action_bar($assign->get_instance(), $otherstudent);
2017          $this->assertStringNotContainsString(get_string('submitassignment', 'assign'), $output);
2018      }
2019  
2020      public function test_group_submissions_submit_for_marking() {
2021          global $PAGE;
2022  
2023          $this->resetAfterTest();
2024  
2025          $course = $this->getDataGenerator()->create_course();
2026          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2027          $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
2028  
2029          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2030          groups_add_member($group, $student);
2031  
2032          $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
2033          groups_add_member($group, $otherstudent);
2034  
2035          // Now verify group assignments.
2036          $this->setUser($teacher);
2037          $time = time();
2038          $assign = $this->create_instance($course, [
2039              'teamsubmission' => 1,
2040              'assignsubmission_onlinetext_enabled' => 1,
2041              'submissiondrafts' => 1,
2042              'requireallteammemberssubmit' => 0,
2043              'duedate' => $time - (2 * DAYSECS),
2044          ]);
2045          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2046  
2047          // Add a submission.
2048          $this->add_submission($student, $assign);
2049  
2050          // Check we can see the submit button.
2051          $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
2052          $this->assertStringContainsString(get_string('submitassignment', 'assign'), $output);
2053          $output = $assign->view_student_summary($student, true);
2054          $this->assertStringContainsString(get_string('timeremaining', 'assign'), $output);
2055          $difftime = time() - $time;
2056          $this->assertStringContainsString(get_string('overdue', 'assign', format_time((2 * DAYSECS) + $difftime)), $output);
2057  
2058          $submission = $assign->get_group_submission($student->id, 0, true);
2059          $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
2060          $assign->testable_update_submission($submission, $student->id, true, true);
2061  
2062          // Check that the student does not see "Submit" button.
2063          $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
2064          $this->assertStringNotContainsString(get_string('submitassignment', 'assign'), $output);
2065  
2066          // Change to another user in the same group.
2067          $this->setUser($otherstudent);
2068          $output = $assign->view_submission_action_bar($assign->get_instance(), $otherstudent);
2069          $this->assertStringNotContainsString(get_string('submitassignment', 'assign'), $output);
2070  
2071          // Check that time remaining is not overdue.
2072          $output = $assign->view_student_summary($otherstudent, true);
2073          $this->assertStringContainsString(get_string('timeremaining', 'assign'), $output);
2074          $difftime = time() - $time;
2075          $this->assertStringContainsString(get_string('submittedlate', 'assign', format_time((2 * DAYSECS) + $difftime)), $output);
2076  
2077          $submission = $assign->get_group_submission($otherstudent->id, 0, true);
2078          $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
2079          $assign->testable_update_submission($submission, $otherstudent->id, true, true);
2080          $output = $assign->view_submission_action_bar($assign->get_instance(), $otherstudent);
2081          $this->assertStringNotContainsString(get_string('submitassignment', 'assign'), $output);
2082      }
2083  
2084      public function test_submissions_open() {
2085          global $DB;
2086  
2087          $this->resetAfterTest();
2088  
2089          $course = $this->getDataGenerator()->create_course();
2090          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2091          $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
2092          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2093          $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
2094          $suspendedstudent = $this->getDataGenerator()->create_and_enrol(
2095              $course, 'student', null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
2096  
2097          $this->setAdminUser();
2098  
2099          $now = time();
2100          $tomorrow = $now + DAYSECS;
2101          $oneweek = $now + WEEKSECS;
2102          $yesterday = $now - DAYSECS;
2103  
2104          $assign = $this->create_instance($course);
2105          $this->assertEquals(true, $assign->testable_submissions_open($student->id));
2106  
2107          $assign = $this->create_instance($course, ['duedate' => $tomorrow]);
2108          $this->assertEquals(true, $assign->testable_submissions_open($student->id));
2109  
2110          $assign = $this->create_instance($course, ['duedate' => $yesterday]);
2111          $this->assertEquals(true, $assign->testable_submissions_open($student->id));
2112  
2113          $assign = $this->create_instance($course, ['duedate' => $yesterday, 'cutoffdate' => $tomorrow]);
2114          $this->assertEquals(true, $assign->testable_submissions_open($student->id));
2115  
2116          $assign = $this->create_instance($course, ['duedate' => $yesterday, 'cutoffdate' => $yesterday]);
2117          $this->assertEquals(false, $assign->testable_submissions_open($student->id));
2118  
2119          $assign->testable_save_user_extension($student->id, $tomorrow);
2120          $this->assertEquals(true, $assign->testable_submissions_open($student->id));
2121  
2122          $assign = $this->create_instance($course, ['submissiondrafts' => 1]);
2123          $this->assertEquals(true, $assign->testable_submissions_open($student->id));
2124  
2125          $this->setUser($student);
2126          $submission = $assign->get_user_submission($student->id, true);
2127          $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
2128          $assign->testable_update_submission($submission, $student->id, true, false);
2129  
2130          $this->setUser($teacher);
2131          $this->assertEquals(false, $assign->testable_submissions_open($student->id));
2132      }
2133  
2134      public function test_get_graders() {
2135          global $DB;
2136  
2137          $this->resetAfterTest();
2138  
2139          $course = $this->getDataGenerator()->create_course();
2140          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2141          $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
2142          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2143  
2144          $this->setAdminUser();
2145  
2146          // Create an assignment with no groups.
2147          $assign = $this->create_instance($course);
2148          $this->assertCount(2, $assign->testable_get_graders($student->id));
2149      }
2150  
2151      public function test_get_graders_separate_groups() {
2152          global $DB;
2153  
2154          $this->resetAfterTest();
2155  
2156          $course = $this->getDataGenerator()->create_course();
2157          $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2158          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2159          $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
2160          $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
2161          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2162          $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
2163  
2164          $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
2165          $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
2166          groups_add_member($group1, $student);
2167  
2168          $this->setAdminUser();
2169  
2170          // Force create an assignment with SEPARATEGROUPS.
2171          $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
2172          $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
2173  
2174          $assign = $this->create_instance($course, [
2175              'groupingid' => $grouping->id,
2176              'groupmode' => SEPARATEGROUPS,
2177          ]);
2178  
2179          $this->assertCount(4, $assign->testable_get_graders($student->id));
2180  
2181          // Note the second student is in a group that is not in the grouping.
2182          // This means that we get all graders that are not in a group in the grouping.
2183          $this->assertCount(4, $assign->testable_get_graders($otherstudent->id));
2184      }
2185  
2186      public function test_get_notified_users() {
2187          global $CFG, $DB;
2188  
2189          $this->resetAfterTest();
2190  
2191          $course = $this->getDataGenerator()->create_course();
2192          $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
2193          $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
2194          $this->getDataGenerator()->create_grouping_group(array('groupid' => $group1->id, 'groupingid' => $grouping->id));
2195  
2196          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2197          groups_add_member($group1, $teacher);
2198  
2199          $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
2200          groups_add_member($group1, $editingteacher);
2201  
2202          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2203          groups_add_member($group1, $student);
2204  
2205          $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
2206          $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2207  
2208          $capability = 'mod/assign:receivegradernotifications';
2209          $coursecontext = \context_course::instance($course->id);
2210          $role = $DB->get_record('role', array('shortname' => 'teacher'));
2211  
2212          $this->setUser($teacher);
2213  
2214          // Create an assignment with no groups.
2215          $assign = $this->create_instance($course);
2216  
2217          $this->assertCount(3, $assign->testable_get_notifiable_users($student->id));
2218  
2219          // Change nonediting teachers role to not receive grader notifications.
2220          assign_capability($capability, CAP_PROHIBIT, $role->id, $coursecontext);
2221  
2222          // Only the editing teachers will be returned.
2223          $this->assertCount(1, $assign->testable_get_notifiable_users($student->id));
2224  
2225          // Note the second student is in a group that is not in the grouping.
2226          // This means that we get all graders that are not in a group in the grouping.
2227          $this->assertCount(1, $assign->testable_get_notifiable_users($otherstudent->id));
2228      }
2229  
2230      public function test_get_notified_users_in_grouping() {
2231          global $CFG, $DB;
2232  
2233          $this->resetAfterTest();
2234  
2235          $course = $this->getDataGenerator()->create_course();
2236          $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
2237          $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
2238          $this->getDataGenerator()->create_grouping_group(array('groupid' => $group1->id, 'groupingid' => $grouping->id));
2239  
2240          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2241          groups_add_member($group1, $teacher);
2242  
2243          $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
2244          groups_add_member($group1, $editingteacher);
2245  
2246          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2247          groups_add_member($group1, $student);
2248  
2249          $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
2250          $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2251  
2252          // Force create an assignment with SEPARATEGROUPS.
2253          $assign = $this->create_instance($course, [
2254              'groupingid' => $grouping->id,
2255              'groupmode' => SEPARATEGROUPS,
2256          ]);
2257  
2258          // Student is in a group - only the tacher and editing teacher in the group shoudl be present.
2259          $this->setUser($student);
2260          $this->assertCount(2, $assign->testable_get_notifiable_users($student->id));
2261  
2262          // Note the second student is in a group that is not in the grouping.
2263          // This means that we get all graders that are not in a group in the grouping.
2264          $this->assertCount(1, $assign->testable_get_notifiable_users($otherstudent->id));
2265  
2266          // Change nonediting teachers role to not receive grader notifications.
2267          $capability = 'mod/assign:receivegradernotifications';
2268          $coursecontext = \context_course::instance($course->id);
2269          $role = $DB->get_record('role', ['shortname' => 'teacher']);
2270          assign_capability($capability, CAP_PROHIBIT, $role->id, $coursecontext);
2271  
2272          // Only the editing teachers will be returned.
2273          $this->assertCount(1, $assign->testable_get_notifiable_users($student->id));
2274  
2275          // Note the second student is in a group that is not in the grouping.
2276          // This means that we get all graders that are not in a group in the grouping.
2277          // Unfortunately there are no editing teachers who are not in a group.
2278          $this->assertCount(0, $assign->testable_get_notifiable_users($otherstudent->id));
2279      }
2280  
2281      public function test_group_members_only() {
2282          global $CFG;
2283  
2284          $this->resetAfterTest();
2285  
2286          $course = $this->getDataGenerator()->create_course();
2287          $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
2288          $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
2289          $this->getDataGenerator()->create_grouping_group([
2290              'groupid' => $group1->id,
2291              'groupingid' => $grouping->id,
2292          ]);
2293  
2294          $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
2295          $this->getDataGenerator()->create_grouping_group([
2296              'groupid' => $group2->id,
2297              'groupingid' => $grouping->id,
2298          ]);
2299  
2300          $group3 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
2301  
2302          // Add users in the following groups
2303          // - Teacher - Group 1.
2304          // - Student - Group 1.
2305          // - Student - Group 2.
2306          // - Student - Unrelated Group
2307          // - Student - No group.
2308          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2309          groups_add_member($group1, $teacher);
2310  
2311          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2312          groups_add_member($group1, $student);
2313  
2314          $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
2315          groups_add_member($group2, $otherstudent);
2316  
2317          $yetotherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
2318          groups_add_member($group2, $otherstudent);
2319  
2320          $this->getDataGenerator()->create_and_enrol($course, 'student');
2321  
2322          $this->setAdminUser();
2323  
2324          $CFG->enableavailability = true;
2325          $assign = $this->create_instance($course, [], [
2326              'availability' => json_encode(
2327                  \core_availability\tree::get_root_json([\availability_grouping\condition::get_json()])
2328              ),
2329              'groupingid' => $grouping->id,
2330          ]);
2331  
2332          // The two students in groups should be returned, but not the teacher in the group, or the student not in the
2333          // group, or the student in an unrelated group.
2334          $this->setUser($teacher);
2335          $participants = $assign->list_participants(0, true);
2336          $this->assertCount(2, $participants);
2337          $this->assertTrue(isset($participants[$student->id]));
2338          $this->assertTrue(isset($participants[$otherstudent->id]));
2339      }
2340  
2341      public function test_get_uniqueid_for_user() {
2342          $this->resetAfterTest();
2343  
2344          $course = $this->getDataGenerator()->create_course();
2345          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2346          $students = [];
2347          for ($i = 0; $i < 10; $i++) {
2348              $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2349              $students[$student->id] = $student;
2350          }
2351  
2352          $this->setUser($teacher);
2353          $assign = $this->create_instance($course);
2354  
2355          foreach ($students as $student) {
2356              $uniqueid = $assign->get_uniqueid_for_user($student->id);
2357              $this->assertEquals($student->id, $assign->get_user_id_for_uniqueid($uniqueid));
2358          }
2359      }
2360  
2361      public function test_show_student_summary() {
2362          global $CFG, $PAGE;
2363  
2364          $this->resetAfterTest();
2365  
2366          $course = $this->getDataGenerator()->create_course();
2367          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2368          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2369          $this->setUser($teacher);
2370          $assign = $this->create_instance($course);
2371          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2372  
2373          // No feedback should be available because this student has not been graded.
2374          $this->setUser($student);
2375          $output = $assign->view_student_summary($student, true);
2376          $this->assertDoesNotMatchRegularExpression('/Feedback/', $output, 'Do not show feedback if there is no grade');
2377  
2378          // Simulate adding a grade.
2379          $this->add_submission($student, $assign);
2380          $this->submit_for_grading($student, $assign);
2381          $this->mark_submission($teacher, $assign, $student);
2382  
2383          // Now we should see the feedback.
2384          $this->setUser($student);
2385          $output = $assign->view_student_summary($student, true);
2386          $this->assertMatchesRegularExpression('/Feedback/', $output, 'Show feedback if there is a grade');
2387  
2388          // Now hide the grade in gradebook.
2389          $this->setUser($teacher);
2390          require_once($CFG->libdir.'/gradelib.php');
2391          $gradeitem = new \grade_item(array(
2392              'itemtype'      => 'mod',
2393              'itemmodule'    => 'assign',
2394              'iteminstance'  => $assign->get_instance()->id,
2395              'courseid'      => $course->id));
2396  
2397          $gradeitem->set_hidden(1, false);
2398  
2399          // No feedback should be available because the grade is hidden.
2400          $this->setUser($student);
2401          $output = $assign->view_student_summary($student, true);
2402          $this->assertDoesNotMatchRegularExpression('/Feedback/', $output,
2403              'Do not show feedback if the grade is hidden in the gradebook');
2404  
2405          // Freeze the context.
2406          $this->setAdminUser();
2407          $context = $assign->get_context();
2408          $CFG->contextlocking = true;
2409          $context->set_locked(true);
2410  
2411          // No feedback should be available because the grade is hidden.
2412          $this->setUser($student);
2413          $output = $assign->view_student_summary($student, true);
2414          $this->assertDoesNotMatchRegularExpression('/Feedback/', $output, 'Do not show feedback if the grade is hidden in the gradebook');
2415  
2416          // Show the feedback again - it should still be visible even in a frozen context.
2417          $this->setUser($teacher);
2418          $gradeitem->set_hidden(0, false);
2419  
2420          $this->setUser($student);
2421          $output = $assign->view_student_summary($student, true);
2422          $this->assertMatchesRegularExpression('/Feedback/', $output, 'Show feedback if there is a grade');
2423      }
2424  
2425      public function test_show_student_summary_with_feedback() {
2426          global $CFG, $PAGE;
2427  
2428          $this->resetAfterTest();
2429  
2430          $course = $this->getDataGenerator()->create_course();
2431          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2432          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2433          $this->setUser($teacher);
2434          $assign = $this->create_instance($course, [
2435              'assignfeedback_comments_enabled' => 1
2436          ]);
2437          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2438  
2439          // No feedback should be available because this student has not been graded.
2440          $this->setUser($student);
2441          $output = $assign->view_student_summary($student, true);
2442          $this->assertDoesNotMatchRegularExpression('/Feedback/', $output);
2443  
2444          // Simulate adding a grade.
2445          $this->add_submission($student, $assign);
2446          $this->submit_for_grading($student, $assign);
2447          $this->mark_submission($teacher, $assign, $student, null, [
2448              'assignfeedbackcomments_editor' => [
2449                  'text' => 'Tomato sauce',
2450                  'format' => FORMAT_MOODLE,
2451              ],
2452          ]);
2453  
2454          // Should have feedback but no grade.
2455          $this->setUser($student);
2456          $output = $assign->view_student_summary($student, true);
2457          $this->assertMatchesRegularExpression('/Feedback/', $output);
2458          $this->assertMatchesRegularExpression('/Tomato sauce/', $output);
2459          $this->assertDoesNotMatchRegularExpression('/Grade/', $output, 'Do not show grade when there is no grade.');
2460          $this->assertDoesNotMatchRegularExpression('/Graded on/', $output, 'Do not show graded date when there is no grade.');
2461  
2462          // Add a grade now.
2463          $this->mark_submission($teacher, $assign, $student, 50.0, [
2464              'assignfeedbackcomments_editor' => [
2465                  'text' => 'Bechamel sauce',
2466                  'format' => FORMAT_MOODLE,
2467              ],
2468          ]);
2469  
2470          // Should have feedback but no grade.
2471          $this->setUser($student);
2472          $output = $assign->view_student_summary($student, true);
2473          $this->assertDoesNotMatchRegularExpression('/Tomato sauce/', $output);
2474          $this->assertMatchesRegularExpression('/Bechamel sauce/', $output);
2475          $this->assertMatchesRegularExpression('/Grade/', $output);
2476          $this->assertMatchesRegularExpression('/Graded on/', $output);
2477  
2478          // Now hide the grade in gradebook.
2479          $this->setUser($teacher);
2480          $gradeitem = new \grade_item(array(
2481              'itemtype'      => 'mod',
2482              'itemmodule'    => 'assign',
2483              'iteminstance'  => $assign->get_instance()->id,
2484              'courseid'      => $course->id));
2485  
2486          $gradeitem->set_hidden(1, false);
2487  
2488          // No feedback should be available because the grade is hidden.
2489          $this->setUser($student);
2490          $output = $assign->view_student_summary($student, true);
2491          $this->assertDoesNotMatchRegularExpression('/Feedback/', $output,
2492              'Do not show feedback if the grade is hidden in the gradebook');
2493      }
2494  
2495      /**
2496       * Test reopen behavior when in "Manual" mode.
2497       */
2498      public function test_attempt_reopen_method_manual() {
2499          global $PAGE;
2500  
2501          $this->resetAfterTest();
2502          $course = $this->getDataGenerator()->create_course();
2503          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2504          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2505  
2506          $assign = $this->create_instance($course, [
2507              'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
2508              'maxattempts' => 3,
2509              'submissiondrafts' => 1,
2510              'assignsubmission_onlinetext_enabled' => 1,
2511          ]);
2512          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2513  
2514          // Student should be able to see an add submission button.
2515          $this->setUser($student);
2516          $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
2517          $this->assertNotEquals(false, strpos($output, get_string('addsubmission', 'assign')));
2518  
2519          // Add a submission.
2520          $this->add_submission($student, $assign);
2521          $this->submit_for_grading($student, $assign);
2522  
2523          // Verify the student cannot make changes to the submission.
2524          $output = $assign->view_student_summary($student, true);
2525          $this->assertEquals(false, strpos($output, get_string('addsubmission', 'assign')));
2526  
2527          // Mark the submission.
2528          $this->mark_submission($teacher, $assign, $student);
2529  
2530          // Check the student can see the grade.
2531          $this->setUser($student);
2532          $output = $assign->view_student_summary($student, true);
2533          $this->assertNotEquals(false, strpos($output, '50.0'));
2534  
2535          // Allow the student another attempt.
2536          $teacher->ignoresesskey = true;
2537          $this->setUser($teacher);
2538          $result = $assign->testable_process_add_attempt($student->id);
2539          $this->assertEquals(true, $result);
2540  
2541          // Check that the previous attempt is now in the submission history table.
2542          $this->setUser($student);
2543          $output = $assign->view_student_summary($student, true);
2544          // Need a better check.
2545          $this->assertNotEquals(false, strpos($output, 'Submission text'), 'Contains: Submission text');
2546  
2547          // Check that the student now has a submission history.
2548          $this->assertNotEquals(false, strpos($output, get_string('attempthistory', 'assign')));
2549  
2550          // Check that the student now does not have a button for Submit.
2551          $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
2552          $this->assertEquals(false, strpos($output, get_string('submitassignment', 'assign')));
2553  
2554          // Check that the student now has a button for Add a new attempt".
2555          $this->assertNotEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
2556  
2557          $this->setUser($teacher);
2558          // Check that the grading table loads correctly and contains this user.
2559          // This is also testing that we do not get duplicate rows in the grading table.
2560          $gradingtable = new \assign_grading_table($assign, 100, '', 0, true);
2561          $output = $assign->get_renderer()->render($gradingtable);
2562          $this->assertEquals(true, strpos($output, $student->lastname));
2563  
2564          // Should be 1 not 2.
2565          $this->assertEquals(1, $assign->count_submissions());
2566          $this->assertEquals(1, $assign->count_submissions_with_status('reopened'));
2567          $this->assertEquals(0, $assign->count_submissions_need_grading());
2568          $this->assertEquals(1, $assign->count_grades());
2569  
2570          // Change max attempts to unlimited.
2571          $formdata = clone($assign->get_instance());
2572          $formdata->maxattempts = ASSIGN_UNLIMITED_ATTEMPTS;
2573          $formdata->instance = $formdata->id;
2574          $assign->update_instance($formdata);
2575  
2576          // Mark the submission again.
2577          $this->mark_submission($teacher, $assign, $student, 60.0, [], 1);
2578  
2579          // Check the grade exists.
2580          $this->setUser($teacher);
2581          $grades = $assign->get_user_grades_for_gradebook($student->id);
2582          $this->assertEquals(60, (int) $grades[$student->id]->rawgrade);
2583  
2584          // Check we can reopen still.
2585          $result = $assign->testable_process_add_attempt($student->id);
2586          $this->assertEquals(true, $result);
2587  
2588          // Should no longer have a grade because there is no grade for the latest attempt.
2589          $grades = $assign->get_user_grades_for_gradebook($student->id);
2590          $this->assertEmpty($grades);
2591      }
2592  
2593      /**
2594       * Test reopen behavior when in "Reopen until pass" mode.
2595       */
2596      public function test_attempt_reopen_method_untilpass() {
2597          global $PAGE;
2598  
2599          $this->resetAfterTest();
2600          $course = $this->getDataGenerator()->create_course();
2601          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2602          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2603  
2604          $assign = $this->create_instance($course, [
2605              'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS,
2606              'maxattempts' => 3,
2607              'submissiondrafts' => 1,
2608              'assignsubmission_onlinetext_enabled' => 1,
2609          ]);
2610          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2611  
2612          // Set grade to pass to 80.
2613          $gradeitem = $assign->get_grade_item();
2614          $gradeitem->gradepass = '80.0';
2615          $gradeitem->update();
2616  
2617          // Student should be able to see an add submission button.
2618          $this->setUser($student);
2619          $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
2620          $this->assertNotEquals(false, strpos($output, get_string('addsubmission', 'assign')));
2621  
2622          // Add a submission.
2623          $this->add_submission($student, $assign);
2624          $this->submit_for_grading($student, $assign);
2625  
2626          // Verify the student cannot make a new attempt.
2627          $output = $assign->view_student_summary($student, true);
2628          $this->assertEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
2629  
2630          // Mark the submission as non-passing.
2631          $this->mark_submission($teacher, $assign, $student, 50.0);
2632  
2633          // Check the student can see the grade.
2634          $this->setUser($student);
2635          $output = $assign->view_student_summary($student, true);
2636          $this->assertNotEquals(false, strpos($output, '50.0'));
2637  
2638          // Check that the student now has a submission history.
2639          $this->assertNotEquals(false, strpos($output, get_string('attempthistory', 'assign')));
2640  
2641          // Check that the student now does not have a button for Submit.
2642          $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
2643          $this->assertEquals(false, strpos($output, get_string('submitassignment', 'assign')));
2644  
2645          // Check that the student now has a button for Add a new attempt.
2646          $this->assertNotEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
2647  
2648          // Add a second submission.
2649          $this->add_submission($student, $assign);
2650          $this->submit_for_grading($student, $assign);
2651  
2652          // Mark the submission as passing.
2653          $this->mark_submission($teacher, $assign, $student, 80.0);
2654  
2655          // Check that the student does not have a button for Add a new attempt.
2656          $this->setUser($student);
2657          $output = $assign->view_student_summary($student, true);
2658          $this->assertEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
2659  
2660          // Re-mark the submission as not passing.
2661          $this->mark_submission($teacher, $assign, $student, 40.0, [], 1);
2662  
2663          // Check that the student now has a button for Add a new attempt.
2664          $this->setUser($student);
2665          $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
2666          $this->assertMatchesRegularExpression('/' . get_string('addnewattempt', 'assign') . '/', $output);
2667          $this->assertNotEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
2668      }
2669  
2670      public function test_attempt_reopen_method_untilpass_passing() {
2671          global $PAGE;
2672  
2673          $this->resetAfterTest();
2674          $course = $this->getDataGenerator()->create_course();
2675          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2676          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2677  
2678          $assign = $this->create_instance($course, [
2679              'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS,
2680              'maxattempts' => 3,
2681              'submissiondrafts' => 1,
2682              'assignsubmission_onlinetext_enabled' => 1,
2683          ]);
2684          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2685  
2686          // Set grade to pass to 80.
2687          $gradeitem = $assign->get_grade_item();
2688          $gradeitem->gradepass = '80.0';
2689          $gradeitem->update();
2690  
2691          // Student should be able to see an add submission button.
2692          $this->setUser($student);
2693          $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
2694          $this->assertNotEquals(false, strpos($output, get_string('addsubmission', 'assign')));
2695  
2696          // Add a submission as a student.
2697          $this->add_submission($student, $assign);
2698          $this->submit_for_grading($student, $assign);
2699  
2700          // Mark the submission as passing.
2701          $this->mark_submission($teacher, $assign, $student, 100.0);
2702  
2703          // Check the student can see the grade.
2704          $this->setUser($student);
2705          $output = $assign->view_student_summary($student, true);
2706          $this->assertNotEquals(false, strpos($output, '100.0'));
2707  
2708          // Check that the student does not have a button for Add a new attempt.
2709          $output = $assign->view_student_summary($student, true);
2710          $this->assertEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
2711      }
2712  
2713      public function test_attempt_reopen_method_untilpass_no_passing_requirement() {
2714          global $PAGE;
2715  
2716          $this->resetAfterTest();
2717          $course = $this->getDataGenerator()->create_course();
2718          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2719          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2720  
2721          $assign = $this->create_instance($course, [
2722              'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS,
2723              'maxattempts' => 3,
2724              'submissiondrafts' => 1,
2725              'assignsubmission_onlinetext_enabled' => 1,
2726          ]);
2727          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2728  
2729          // Set grade to pass to 0, so that no attempts should reopen.
2730          $gradeitem = $assign->get_grade_item();
2731          $gradeitem->gradepass = '0';
2732          $gradeitem->update();
2733  
2734          // Student should be able to see an add submission button.
2735          $this->setUser($student);
2736          $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
2737          $this->assertNotEquals(false, strpos($output, get_string('addsubmission', 'assign')));
2738  
2739          // Add a submission.
2740          $this->add_submission($student, $assign);
2741          $this->submit_for_grading($student, $assign);
2742  
2743          // Mark the submission with any grade.
2744          $this->mark_submission($teacher, $assign, $student, 0.0);
2745  
2746          // Check the student can see the grade.
2747          $this->setUser($student);
2748          $output = $assign->view_student_summary($student, true);
2749          $this->assertNotEquals(false, strpos($output, '0.0'));
2750  
2751          // Check that the student does not have a button for Add a new attempt.
2752          $output = $assign->view_student_summary($student, true);
2753          $this->assertEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
2754      }
2755  
2756      /**
2757       * Test student visibility for each stage of the marking workflow.
2758       */
2759      public function test_markingworkflow() {
2760          global $PAGE;
2761  
2762          $this->resetAfterTest();
2763          $course = $this->getDataGenerator()->create_course();
2764          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2765          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2766  
2767          $assign = $this->create_instance($course, [
2768              'markingworkflow' => 1,
2769          ]);
2770  
2771          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2772  
2773          // Mark the submission and set to notmarked.
2774          $this->mark_submission($teacher, $assign, $student, 50.0,  [
2775              'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED,
2776          ]);
2777  
2778          // Check the student can't see the grade.
2779          $this->setUser($student);
2780          $output = $assign->view_student_summary($student, true);
2781          $this->assertEquals(false, strpos($output, '50.0'));
2782  
2783          // Make sure the grade isn't pushed to the gradebook.
2784          $grades = $assign->get_user_grades_for_gradebook($student->id);
2785          $this->assertEmpty($grades);
2786  
2787          // Mark the submission and set to inmarking.
2788          $this->mark_submission($teacher, $assign, $student, 50.0,  [
2789              'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_INMARKING,
2790          ]);
2791  
2792          // Check the student can't see the grade.
2793          $this->setUser($student);
2794          $output = $assign->view_student_summary($student, true);
2795          $this->assertEquals(false, strpos($output, '50.0'));
2796  
2797          // Make sure the grade isn't pushed to the gradebook.
2798          $grades = $assign->get_user_grades_for_gradebook($student->id);
2799          $this->assertEmpty($grades);
2800  
2801          // Mark the submission and set to readyforreview.
2802          $this->mark_submission($teacher, $assign, $student, 50.0,  [
2803              'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW,
2804          ]);
2805  
2806          // Check the student can't see the grade.
2807          $this->setUser($student);
2808          $output = $assign->view_student_summary($student, true);
2809          $this->assertEquals(false, strpos($output, '50.0'));
2810  
2811          // Make sure the grade isn't pushed to the gradebook.
2812          $grades = $assign->get_user_grades_for_gradebook($student->id);
2813          $this->assertEmpty($grades);
2814  
2815          // Mark the submission and set to inreview.
2816          $this->mark_submission($teacher, $assign, $student, 50.0,  [
2817              'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW,
2818          ]);
2819  
2820          // Check the student can't see the grade.
2821          $this->setUser($student);
2822          $output = $assign->view_student_summary($student, true);
2823          $this->assertEquals(false, strpos($output, '50.0'));
2824  
2825          // Make sure the grade isn't pushed to the gradebook.
2826          $grades = $assign->get_user_grades_for_gradebook($student->id);
2827          $this->assertEmpty($grades);
2828  
2829          // Mark the submission and set to readyforrelease.
2830          $this->mark_submission($teacher, $assign, $student, 50.0,  [
2831              'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE,
2832          ]);
2833  
2834          // Check the student can't see the grade.
2835          $this->setUser($student);
2836          $output = $assign->view_student_summary($student, true);
2837          $this->assertEquals(false, strpos($output, '50.0'));
2838  
2839          // Make sure the grade isn't pushed to the gradebook.
2840          $grades = $assign->get_user_grades_for_gradebook($student->id);
2841          $this->assertEmpty($grades);
2842  
2843          // Mark the submission and set to released.
2844          $this->mark_submission($teacher, $assign, $student, 50.0,  [
2845              'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_RELEASED,
2846          ]);
2847  
2848          // Check the student can see the grade.
2849          $this->setUser($student);
2850          $output = $assign->view_student_summary($student, true);
2851          $this->assertNotEquals(false, strpos($output, '50.0'));
2852  
2853          // Make sure the grade is pushed to the gradebook.
2854          $grades = $assign->get_user_grades_for_gradebook($student->id);
2855          $this->assertEquals(50, (int)$grades[$student->id]->rawgrade);
2856      }
2857  
2858      /**
2859       * Test that a student allocated a specific marker is only shown to that marker.
2860       */
2861      public function test_markerallocation() {
2862          global $PAGE;
2863  
2864          $this->resetAfterTest();
2865          $course = $this->getDataGenerator()->create_course();
2866          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2867          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2868          $otherteacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2869  
2870          $assign = $this->create_instance($course, [
2871              'markingworkflow' => 1,
2872              'markingallocation' => 1
2873          ]);
2874  
2875          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2876  
2877          // Allocate marker to submission.
2878          $this->mark_submission($teacher, $assign, $student, null, [
2879              'allocatedmarker' => $teacher->id,
2880          ]);
2881  
2882          // Check the allocated marker can view the submission.
2883          $this->setUser($teacher);
2884          $users = $assign->list_participants(0, true);
2885          $this->assertEquals(1, count($users));
2886          $this->assertTrue(isset($users[$student->id]));
2887  
2888          $cm = get_coursemodule_from_instance('assign', $assign->get_instance()->id);
2889          $context = \context_module::instance($cm->id);
2890          $assign = new mod_assign_testable_assign($context, $cm, $course);
2891  
2892          // Check that other teachers can't view this submission.
2893          $this->setUser($otherteacher);
2894          $users = $assign->list_participants(0, true);
2895          $this->assertEquals(0, count($users));
2896      }
2897  
2898      /**
2899       * Ensure that a teacher cannot submit for students as standard.
2900       */
2901      public function test_teacher_submit_for_student() {
2902          global $PAGE;
2903  
2904          $this->resetAfterTest();
2905          $course = $this->getDataGenerator()->create_course();
2906          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2907          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
2908  
2909          $assign = $this->create_instance($course, [
2910              'assignsubmission_onlinetext_enabled' => 1,
2911              'submissiondrafts' => 1,
2912          ]);
2913  
2914          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2915  
2916          // Add a submission but do not submit.
2917          $this->add_submission($student, $assign, 'Student submission text');
2918  
2919          $this->setUser($student);
2920          $output = $assign->view_student_summary($student, true);
2921          $this->assertStringContainsString('Student submission text', $output, 'Contains student submission text');
2922  
2923          // Check that a teacher can not edit the submission as they do not have the capability.
2924          $this->setUser($teacher);
2925          $this->expectException('moodle_exception');
2926          $this->expectExceptionMessage('error/nopermission');
2927          $this->add_submission($student, $assign, 'Teacher edited submission text', false);
2928      }
2929  
2930      /**
2931       * Ensure that a teacher with the editothersubmission capability can submit on behalf of a student.
2932       */
2933      public function test_teacher_submit_for_student_with_capability() {
2934          global $PAGE;
2935  
2936          $this->resetAfterTest();
2937          $course = $this->getDataGenerator()->create_course();
2938          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2939          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
2940          $otherteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
2941  
2942          $assign = $this->create_instance($course, [
2943              'assignsubmission_onlinetext_enabled' => 1,
2944              'submissiondrafts' => 1,
2945          ]);
2946  
2947          // Add the required capability.
2948          $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description');
2949          assign_capability('mod/assign:editothersubmission', CAP_ALLOW, $roleid, $assign->get_context()->id);
2950          role_assign($roleid, $teacher->id, $assign->get_context()->id);
2951          accesslib_clear_all_caches_for_unit_testing();
2952  
2953          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2954  
2955          // Add a submission but do not submit.
2956          $this->add_submission($student, $assign, 'Student submission text');
2957  
2958          $this->setUser($student);
2959          $output = $assign->view_student_summary($student, true);
2960          $this->assertStringContainsString('Student submission text', $output, 'Contains student submission text');
2961  
2962          // Check that a teacher can edit the submission.
2963          $this->setUser($teacher);
2964          $this->add_submission($student, $assign, 'Teacher edited submission text', false);
2965  
2966          $this->setUser($student);
2967          $output = $assign->view_student_summary($student, true);
2968          $this->assertStringNotContainsString('Student submission text', $output, 'Contains student submission text');
2969          $this->assertStringContainsString('Teacher edited submission text', $output, 'Contains teacher edited submission text');
2970  
2971          // Check that the teacher can submit the students work.
2972          $this->setUser($teacher);
2973          $this->submit_for_grading($student, $assign, [], false);
2974  
2975          // Revert to draft so the student can edit it.
2976          $assign->revert_to_draft($student->id);
2977  
2978          $this->setUser($student);
2979  
2980          // Check that the submission text was saved.
2981          $output = $assign->view_student_summary($student, true);
2982          $this->assertStringContainsString('Teacher edited submission text', $output, 'Contains student submission text');
2983  
2984          // Check that the student can submit their work.
2985          $this->submit_for_grading($student, $assign, []);
2986  
2987          $output = $assign->view_student_summary($student, true);
2988          $this->assertStringNotContainsString(get_string('addsubmission', 'assign'), $output);
2989  
2990          // An editing teacher without the extra role should still be able to revert to draft.
2991          $this->setUser($otherteacher);
2992  
2993          // Revert to draft so the submission is editable.
2994          $assign->revert_to_draft($student->id);
2995      }
2996  
2997      /**
2998       * Ensure that disabling submit after the cutoff date works as expected.
2999       */
3000      public function test_disable_submit_after_cutoff_date() {
3001          global $PAGE;
3002  
3003          $this->resetAfterTest();
3004          $course = $this->getDataGenerator()->create_course();
3005          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3006  
3007          $now = time();
3008          $tomorrow = $now + DAYSECS;
3009          $lastweek = $now - (7 * DAYSECS);
3010          $yesterday = $now - DAYSECS;
3011  
3012          $this->setAdminUser();
3013          $assign = $this->create_instance($course, [
3014              'duedate' => $yesterday,
3015              'cutoffdate' => $tomorrow,
3016              'assignsubmission_onlinetext_enabled' => 1,
3017          ]);
3018  
3019          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
3020  
3021          // Student should be able to see an add submission button.
3022          $this->setUser($student);
3023          $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
3024          $this->assertNotEquals(false, strpos($output, get_string('addsubmission', 'assign')));
3025  
3026          // Add a submission but don't submit now.
3027          $this->add_submission($student, $assign);
3028  
3029          // Create another instance with cut-off and due-date already passed.
3030          $this->setAdminUser();
3031          $assign = $this->create_instance($course, [
3032              'duedate' => $lastweek,
3033              'cutoffdate' => $yesterday,
3034              'assignsubmission_onlinetext_enabled' => 1,
3035          ]);
3036  
3037          $this->setUser($student);
3038          $output = $assign->view_student_summary($student, true);
3039          $this->assertStringNotContainsString($output, get_string('editsubmission', 'assign'),
3040              'Should not be able to edit after cutoff date.');
3041          $this->assertStringNotContainsString($output, get_string('submitassignment', 'assign'),
3042              'Should not be able to submit after cutoff date.');
3043      }
3044  
3045      /**
3046       * Testing for submission comment plugin settings.
3047       *
3048       * @dataProvider submission_plugin_settings_provider
3049       * @param   bool    $globalenabled
3050       * @param   array   $instanceconfig
3051       * @param   bool    $isenabled
3052       */
3053      public function test_submission_comment_plugin_settings($globalenabled, $instanceconfig, $isenabled) {
3054          global $CFG;
3055  
3056          $this->resetAfterTest();
3057          $course = $this->getDataGenerator()->create_course();
3058  
3059          $CFG->usecomments = $globalenabled;
3060          $assign = $this->create_instance($course, $instanceconfig);
3061          $plugin = $assign->get_submission_plugin_by_type('comments');
3062          $this->assertEquals($isenabled, (bool) $plugin->is_enabled('enabled'));
3063      }
3064  
3065      public function submission_plugin_settings_provider() {
3066          return [
3067              'CFG->usecomments true, empty config => Enabled by default' => [
3068                  true,
3069                  [],
3070                  true,
3071              ],
3072              'CFG->usecomments true, config enabled => Comments enabled' => [
3073                  true,
3074                  [
3075                      'assignsubmission_comments_enabled' => 1,
3076                  ],
3077                  true,
3078              ],
3079              'CFG->usecomments true, config idisabled => Comments enabled' => [
3080                  true,
3081                  [
3082                      'assignsubmission_comments_enabled' => 0,
3083                  ],
3084                  true,
3085              ],
3086              'CFG->usecomments false, empty config => Disabled by default' => [
3087                  false,
3088                  [],
3089                  false,
3090              ],
3091              'CFG->usecomments false, config enabled => Comments disabled' => [
3092                  false,
3093                  [
3094                      'assignsubmission_comments_enabled' => 1,
3095                  ],
3096                  false,
3097              ],
3098              'CFG->usecomments false, config disabled => Comments disabled' => [
3099                  false,
3100                  [
3101                      'assignsubmission_comments_enabled' => 0,
3102                  ],
3103                  false,
3104              ],
3105          ];
3106      }
3107  
3108      /**
3109       * Testing for comment inline settings
3110       */
3111      public function test_feedback_comment_commentinline() {
3112          global $CFG, $USER;
3113  
3114          $this->resetAfterTest();
3115          $course = $this->getDataGenerator()->create_course();
3116          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3117          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3118  
3119          $sourcetext = "Hello!
3120  
3121  I'm writing to you from the Moodle Majlis in Muscat, Oman, where we just had several days of Moodle community goodness.
3122  
3123  URL outside a tag: https://moodle.org/logo/logo-240x60.gif
3124  Plugin url outside a tag: @@PLUGINFILE@@/logo-240x60.gif
3125  
3126  External link 1:<img src='https://moodle.org/logo/logo-240x60.gif' alt='Moodle'/>
3127  External link 2:<img alt=\"Moodle\" src=\"https://moodle.org/logo/logo-240x60.gif\"/>
3128  Internal link 1:<img src='@@PLUGINFILE@@/logo-240x60.gif' alt='Moodle'/>
3129  Internal link 2:<img alt=\"Moodle\" src=\"@@PLUGINFILE@@logo-240x60.gif\"/>
3130  Anchor link 1:<a href=\"@@PLUGINFILE@@logo-240x60.gif\" alt=\"bananas\">Link text</a>
3131  Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
3132  ";
3133  
3134          $this->setUser($teacher);
3135          $assign = $this->create_instance($course, [
3136              'assignsubmission_onlinetext_enabled' => 1,
3137              'assignfeedback_comments_enabled' => 1,
3138              'assignfeedback_comments_commentinline' => 1,
3139          ]);
3140  
3141          $this->setUser($student);
3142  
3143          // Add a submission but don't submit now.
3144          $this->add_submission($student, $assign, $sourcetext);
3145  
3146          $this->setUser($teacher);
3147  
3148          $data = new \stdClass();
3149          require_once($CFG->dirroot . '/mod/assign/gradeform.php');
3150          $pagination = [
3151              'userid' => $student->id,
3152              'rownum' => 0,
3153              'last' => true,
3154              'useridlistid' => $assign->get_useridlist_key_id(),
3155              'attemptnumber' => 0,
3156          ];
3157          $formparams = array($assign, $data, $pagination);
3158          $mform = new mod_assign_grade_form(null, [$assign, $data, $pagination]);
3159  
3160          // We need to get the URL these will be transformed to.
3161          $context = \context_user::instance($USER->id);
3162          $itemid = $data->assignfeedbackcomments_editor['itemid'];
3163          $url = $CFG->wwwroot . '/draftfile.php/' . $context->id . '/user/draft/' . $itemid;
3164  
3165          // Note the internal images have been stripped and the html is purified (quotes fixed in this case).
3166          $filteredtext = "Hello!
3167  
3168  I'm writing to you from the Moodle Majlis in Muscat, Oman, where we just had several days of Moodle community goodness.
3169  
3170  URL outside a tag: https://moodle.org/logo/logo-240x60.gif
3171  Plugin url outside a tag: $url/logo-240x60.gif
3172  
3173  External link 1:<img src=\"https://moodle.org/logo/logo-240x60.gif\" alt=\"Moodle\" />
3174  External link 2:<img alt=\"Moodle\" src=\"https://moodle.org/logo/logo-240x60.gif\" />
3175  Internal link 1:<img src=\"$url/logo-240x60.gif\" alt=\"Moodle\" />
3176  Internal link 2:<img alt=\"Moodle\" src=\"@@PLUGINFILE@@logo-240x60.gif\" />
3177  Anchor link 1:<a href=\"@@PLUGINFILE@@logo-240x60.gif\">Link text</a>
3178  Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
3179  ";
3180  
3181          $this->assertEquals($filteredtext, $data->assignfeedbackcomments_editor['text']);
3182      }
3183  
3184      /**
3185       * Testing for feedback comment plugin settings.
3186       *
3187       * @dataProvider feedback_plugin_settings_provider
3188       * @param   array   $instanceconfig
3189       * @param   bool    $isenabled
3190       */
3191      public function test_feedback_plugin_settings($instanceconfig, $isenabled) {
3192          $this->resetAfterTest();
3193          $course = $this->getDataGenerator()->create_course();
3194  
3195          $assign = $this->create_instance($course, $instanceconfig);
3196          $plugin = $assign->get_feedback_plugin_by_type('comments');
3197          $this->assertEquals($isenabled, (bool) $plugin->is_enabled('enabled'));
3198      }
3199  
3200      public function feedback_plugin_settings_provider() {
3201          return [
3202              'No configuration => disabled' => [
3203                  [],
3204                  false,
3205              ],
3206              'Actively disabled' => [
3207                  [
3208                      'assignfeedback_comments_enabled' => 0,
3209                  ],
3210                  false,
3211              ],
3212              'Actively enabled' => [
3213                  [
3214                      'assignfeedback_comments_enabled' => 1,
3215                  ],
3216                  true,
3217              ],
3218          ];
3219      }
3220  
3221      /**
3222       * Testing if gradebook feedback plugin is enabled.
3223       */
3224      public function test_is_gradebook_feedback_enabled() {
3225          $this->resetAfterTest();
3226          $course = $this->getDataGenerator()->create_course();
3227          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3228          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3229  
3230          $adminconfig = get_config('assign');
3231          $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
3232  
3233          // Create assignment with gradebook feedback enabled and grade = 0.
3234          $assign = $this->create_instance($course, [
3235              "{$gradebookplugin}_enabled" => 1,
3236              'grades' => 0,
3237          ]);
3238  
3239          // Get gradebook feedback plugin.
3240          $gradebookplugintype = str_replace('assignfeedback_', '', $gradebookplugin);
3241          $plugin = $assign->get_feedback_plugin_by_type($gradebookplugintype);
3242          $this->assertEquals(1, $plugin->is_enabled('enabled'));
3243          $this->assertEquals(1, $assign->is_gradebook_feedback_enabled());
3244      }
3245  
3246      /**
3247       * Testing if gradebook feedback plugin is disabled.
3248       */
3249      public function test_is_gradebook_feedback_disabled() {
3250          $this->resetAfterTest();
3251          $course = $this->getDataGenerator()->create_course();
3252          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3253          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3254  
3255          $adminconfig = get_config('assign');
3256          $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
3257  
3258          // Create assignment with gradebook feedback disabled and grade = 0.
3259          $assign = $this->create_instance($course, [
3260              "{$gradebookplugin}_enabled" => 0,
3261              'grades' => 0,
3262          ]);
3263  
3264          $gradebookplugintype = str_replace('assignfeedback_', '', $gradebookplugin);
3265          $plugin = $assign->get_feedback_plugin_by_type($gradebookplugintype);
3266          $this->assertEquals(0, $plugin->is_enabled('enabled'));
3267      }
3268  
3269      /**
3270       * Testing can_edit_submission.
3271       */
3272      public function test_can_edit_submission() {
3273          $this->resetAfterTest();
3274          $course = $this->getDataGenerator()->create_course();
3275          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3276          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3277          $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
3278  
3279          $assign = $this->create_instance($course, [
3280              'assignsubmission_onlinetext_enabled' => 1,
3281              'submissiondrafts' => 1,
3282          ]);
3283  
3284          // Check student can edit their own submission.
3285          $this->assertTrue($assign->can_edit_submission($student->id, $student->id));
3286  
3287          // Check student cannot edit others submission.
3288          $this->assertFalse($assign->can_edit_submission($otherstudent->id, $student->id));
3289  
3290          // Check teacher cannot (by default) edit a students submission.
3291          $this->assertFalse($assign->can_edit_submission($student->id, $teacher->id));
3292      }
3293  
3294      /**
3295       * Testing can_edit_submission with the editothersubmission capability.
3296       */
3297      public function test_can_edit_submission_with_editothersubmission() {
3298          $this->resetAfterTest();
3299          $course = $this->getDataGenerator()->create_course();
3300          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3301          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3302          $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
3303  
3304          $assign = $this->create_instance($course, [
3305              'assignsubmission_onlinetext_enabled' => 1,
3306              'submissiondrafts' => 1,
3307          ]);
3308  
3309          // Add the required capability to edit a student submission.
3310          $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description');
3311          assign_capability('mod/assign:editothersubmission', CAP_ALLOW, $roleid, $assign->get_context()->id);
3312          role_assign($roleid, $teacher->id, $assign->get_context()->id);
3313          accesslib_clear_all_caches_for_unit_testing();
3314  
3315          // Check student can edit their own submission.
3316          $this->assertTrue($assign->can_edit_submission($student->id, $student->id));
3317  
3318          // Check student cannot edit others submission.
3319          $this->assertFalse($assign->can_edit_submission($otherstudent->id, $student->id));
3320  
3321          // Retest - should now have access.
3322          $this->assertTrue($assign->can_edit_submission($student->id, $teacher->id));
3323      }
3324  
3325      /**
3326       * Testing can_edit_submission
3327       */
3328      public function test_can_edit_submission_separategroups() {
3329          $this->resetAfterTest();
3330          $course = $this->getDataGenerator()->create_course();
3331          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3332  
3333          $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3334          $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3335          $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3336          $student4 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3337  
3338          $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
3339          $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3340          groups_assign_grouping($grouping->id, $group1->id);
3341          groups_add_member($group1, $student1);
3342          groups_add_member($group1, $student2);
3343  
3344          $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3345          groups_assign_grouping($grouping->id, $group2->id);
3346          groups_add_member($group2, $student3);
3347          groups_add_member($group2, $student4);
3348  
3349          $assign = $this->create_instance($course, [
3350              'assignsubmission_onlinetext_enabled' => 1,
3351              'submissiondrafts' => 1,
3352              'groupingid' => $grouping->id,
3353              'groupmode' => SEPARATEGROUPS,
3354          ]);
3355  
3356          // Verify a student does not have the ability to edit submissions for other users.
3357          $this->assertTrue($assign->can_edit_submission($student1->id, $student1->id));
3358          $this->assertFalse($assign->can_edit_submission($student2->id, $student1->id));
3359          $this->assertFalse($assign->can_edit_submission($student3->id, $student1->id));
3360          $this->assertFalse($assign->can_edit_submission($student4->id, $student1->id));
3361      }
3362  
3363      /**
3364       * Testing can_edit_submission
3365       */
3366      public function test_can_edit_submission_separategroups_with_editothersubmission() {
3367          $this->resetAfterTest();
3368          $course = $this->getDataGenerator()->create_course();
3369          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3370  
3371          $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3372          $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3373          $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3374          $student4 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3375  
3376          $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
3377          $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3378          groups_assign_grouping($grouping->id, $group1->id);
3379          groups_add_member($group1, $student1);
3380          groups_add_member($group1, $student2);
3381  
3382          $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3383          groups_assign_grouping($grouping->id, $group2->id);
3384          groups_add_member($group2, $student3);
3385          groups_add_member($group2, $student4);
3386  
3387          $assign = $this->create_instance($course, [
3388              'assignsubmission_onlinetext_enabled' => 1,
3389              'submissiondrafts' => 1,
3390              'groupingid' => $grouping->id,
3391              'groupmode' => SEPARATEGROUPS,
3392          ]);
3393  
3394          // Add the capability to the new \assignment for student 1.
3395          $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description');
3396          assign_capability('mod/assign:editothersubmission', CAP_ALLOW, $roleid, $assign->get_context()->id);
3397          role_assign($roleid, $student1->id, $assign->get_context()->id);
3398          accesslib_clear_all_caches_for_unit_testing();
3399  
3400          // Verify student1 has the ability to edit submissions for other users in their group, but not other groups.
3401          $this->assertTrue($assign->can_edit_submission($student1->id, $student1->id));
3402          $this->assertTrue($assign->can_edit_submission($student2->id, $student1->id));
3403          $this->assertFalse($assign->can_edit_submission($student3->id, $student1->id));
3404          $this->assertFalse($assign->can_edit_submission($student4->id, $student1->id));
3405  
3406          // Verify other students do not have the ability to edit submissions for other users.
3407          $this->assertTrue($assign->can_edit_submission($student2->id, $student2->id));
3408          $this->assertFalse($assign->can_edit_submission($student1->id, $student2->id));
3409          $this->assertFalse($assign->can_edit_submission($student3->id, $student2->id));
3410          $this->assertFalse($assign->can_edit_submission($student4->id, $student2->id));
3411      }
3412  
3413      /**
3414       * Test if the view blind details capability works
3415       */
3416      public function test_can_view_blind_details() {
3417          global $DB;
3418  
3419          $this->resetAfterTest();
3420          $course = $this->getDataGenerator()->create_course();
3421          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3422          $manager = $this->getDataGenerator()->create_and_enrol($course, 'manager');
3423          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3424  
3425          $assign = $this->create_instance($course, [
3426              'blindmarking' => 1,
3427          ]);
3428  
3429          $this->assertTrue($assign->is_blind_marking());
3430  
3431          // Test student names are hidden to teacher.
3432          $this->setUser($teacher);
3433          $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
3434          $output = $assign->get_renderer()->render($gradingtable);
3435          $this->assertEquals(true, strpos($output, get_string('hiddenuser', 'assign')));    // "Participant" is somewhere on the page.
3436          $this->assertEquals(false, strpos($output, fullname($student)));    // Students full name doesn't appear.
3437  
3438          // Test student names are visible to manager.
3439          $this->setUser($manager);
3440          $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
3441          $output = $assign->get_renderer()->render($gradingtable);
3442          $this->assertEquals(true, strpos($output, get_string('hiddenuser', 'assign')));
3443          $this->assertEquals(true, strpos($output, fullname($student)));
3444      }
3445  
3446      /**
3447       * Testing get_shared_group_members
3448       */
3449      public function test_get_shared_group_members() {
3450          $this->resetAfterTest();
3451          $course = $this->getDataGenerator()->create_course();
3452          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3453  
3454          $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3455          $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3456          $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3457          $student4 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3458  
3459          $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
3460          $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3461          groups_assign_grouping($grouping->id, $group1->id);
3462          groups_add_member($group1, $student1);
3463          groups_add_member($group1, $student2);
3464  
3465          $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3466          groups_assign_grouping($grouping->id, $group2->id);
3467          groups_add_member($group2, $student3);
3468          groups_add_member($group2, $student4);
3469  
3470          $assign = $this->create_instance($course, [
3471              'groupingid' => $grouping->id,
3472              'groupmode' => SEPARATEGROUPS,
3473          ]);
3474  
3475          $cm = $assign->get_course_module();
3476  
3477          // Get shared group members for students 0 and 1.
3478          $groupmembers = $assign->get_shared_group_members($cm, $student1->id);
3479          $this->assertCount(2, $groupmembers);
3480          $this->assertContainsEquals($student1->id, $groupmembers);
3481          $this->assertContainsEquals($student2->id, $groupmembers);
3482  
3483          $groupmembers = $assign->get_shared_group_members($cm, $student2->id);
3484          $this->assertCount(2, $groupmembers);
3485          $this->assertContainsEquals($student1->id, $groupmembers);
3486          $this->assertContainsEquals($student2->id, $groupmembers);
3487  
3488          $groupmembers = $assign->get_shared_group_members($cm, $student3->id);
3489          $this->assertCount(2, $groupmembers);
3490          $this->assertContainsEquals($student3->id, $groupmembers);
3491          $this->assertContainsEquals($student4->id, $groupmembers);
3492  
3493          $groupmembers = $assign->get_shared_group_members($cm, $student4->id);
3494          $this->assertCount(2, $groupmembers);
3495          $this->assertContainsEquals($student3->id, $groupmembers);
3496          $this->assertContainsEquals($student4->id, $groupmembers);
3497      }
3498  
3499      /**
3500       * Testing get_shared_group_members
3501       */
3502      public function test_get_shared_group_members_override() {
3503          $this->resetAfterTest();
3504          $course = $this->getDataGenerator()->create_course();
3505          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3506  
3507          $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3508          $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3509          $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3510          $student4 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3511  
3512          $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
3513          $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3514          groups_assign_grouping($grouping->id, $group1->id);
3515          groups_add_member($group1, $student1);
3516          groups_add_member($group1, $student2);
3517  
3518          $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3519          groups_assign_grouping($grouping->id, $group2->id);
3520          groups_add_member($group2, $student3);
3521          groups_add_member($group2, $student4);
3522  
3523          $assign = $this->create_instance($course, [
3524              'groupingid' => $grouping->id,
3525              'groupmode' => SEPARATEGROUPS,
3526          ]);
3527  
3528          $cm = $assign->get_course_module();
3529  
3530          // Add the capability to access allgroups for one of the students.
3531          $roleid = create_role('Access all groups role', 'accessallgroupsrole', '');
3532          assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $assign->get_context()->id);
3533          role_assign($roleid, $student1->id, $assign->get_context()->id);
3534          accesslib_clear_all_caches_for_unit_testing();
3535  
3536          // Get shared group members for students 0 and 1.
3537          $groupmembers = $assign->get_shared_group_members($cm, $student1->id);
3538          $this->assertCount(4, $groupmembers);
3539          $this->assertContainsEquals($student1->id, $groupmembers);
3540          $this->assertContainsEquals($student2->id, $groupmembers);
3541          $this->assertContainsEquals($student3->id, $groupmembers);
3542          $this->assertContainsEquals($student4->id, $groupmembers);
3543  
3544          $groupmembers = $assign->get_shared_group_members($cm, $student2->id);
3545          $this->assertCount(2, $groupmembers);
3546          $this->assertContainsEquals($student1->id, $groupmembers);
3547          $this->assertContainsEquals($student2->id, $groupmembers);
3548  
3549          $groupmembers = $assign->get_shared_group_members($cm, $student3->id);
3550          $this->assertCount(2, $groupmembers);
3551          $this->assertContainsEquals($student3->id, $groupmembers);
3552          $this->assertContainsEquals($student4->id, $groupmembers);
3553  
3554          $groupmembers = $assign->get_shared_group_members($cm, $student4->id);
3555          $this->assertCount(2, $groupmembers);
3556          $this->assertContainsEquals($student3->id, $groupmembers);
3557          $this->assertContainsEquals($student4->id, $groupmembers);
3558      }
3559  
3560      /**
3561       * Test get plugins file areas
3562       */
3563      public function test_get_plugins_file_areas() {
3564          global $DB;
3565  
3566          $this->resetAfterTest();
3567          $course = $this->getDataGenerator()->create_course();
3568          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3569  
3570          $assign = $this->create_instance($course);
3571  
3572          // Test that all the submission and feedback plugins are returning the expected file aras.
3573          $usingfilearea = 0;
3574          $coreplugins = \core_plugin_manager::standard_plugins_list('assignsubmission');
3575          foreach ($assign->get_submission_plugins() as $plugin) {
3576              $type = $plugin->get_type();
3577              if (!in_array($type, $coreplugins)) {
3578                  continue;
3579              }
3580              $fileareas = $plugin->get_file_areas();
3581  
3582              if ($type == 'onlinetext') {
3583                  $this->assertEquals(array('submissions_onlinetext' => 'Online text'), $fileareas);
3584                  $usingfilearea++;
3585              } else if ($type == 'file') {
3586                  $this->assertEquals(array('submission_files' => 'File submissions'), $fileareas);
3587                  $usingfilearea++;
3588              } else {
3589                  $this->assertEmpty($fileareas);
3590              }
3591          }
3592          $this->assertEquals(2, $usingfilearea);
3593  
3594          $usingfilearea = 0;
3595          $coreplugins = \core_plugin_manager::standard_plugins_list('assignfeedback');
3596          foreach ($assign->get_feedback_plugins() as $plugin) {
3597              $type = $plugin->get_type();
3598              if (!in_array($type, $coreplugins)) {
3599                  continue;
3600              }
3601              $fileareas = $plugin->get_file_areas();
3602  
3603              if ($type == 'editpdf') {
3604                  $checkareas = [
3605                      'download' => 'Annotate PDF',
3606                      'combined' => 'Annotate PDF',
3607                      'partial' => 'Annotate PDF',
3608                      'importhtml' => 'Annotate PDF',
3609                      'pages' => 'Annotate PDF',
3610                      'readonlypages' => 'Annotate PDF',
3611                      'stamps' => 'Annotate PDF',
3612                      'tmp_jpg_to_pdf' => 'Annotate PDF',
3613                      'tmp_rotated_jpg' => 'Annotate PDF'
3614                  ];
3615                  $this->assertEquals($checkareas, $fileareas);
3616                  $usingfilearea++;
3617              } else if ($type == 'file') {
3618                  $this->assertEquals(array('feedback_files' => 'Feedback files'), $fileareas);
3619                  $usingfilearea++;
3620              } else if ($type == 'comments') {
3621                  $this->assertEquals(array('feedback' => 'Feedback comments'), $fileareas);
3622                  $usingfilearea++;
3623              } else {
3624                  $this->assertEmpty($fileareas);
3625              }
3626          }
3627          $this->assertEquals(3, $usingfilearea);
3628      }
3629  
3630      /**
3631       * Test override exists
3632       *
3633       * This function needs to obey the group override logic as per the assign grading table and
3634       * the overview block.
3635       */
3636      public function test_override_exists() {
3637          global $DB;
3638  
3639          $this->resetAfterTest();
3640          $course = $this->getDataGenerator()->create_course();
3641          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3642  
3643          $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
3644          $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3645          $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3646  
3647          // Data:
3648          // - student1 => group A only
3649          // - student2 => group B only
3650          // - student3 => Group A + Group B (No user override)
3651          // - student4 => Group A + Group B (With user override)
3652          // - student4 => No groups.
3653          $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3654          groups_add_member($group1, $student1);
3655  
3656          $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3657          groups_add_member($group2, $student2);
3658  
3659          $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3660          groups_add_member($group1, $student3);
3661          groups_add_member($group2, $student3);
3662  
3663          $student4 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3664          groups_add_member($group1, $student4);
3665          groups_add_member($group2, $student4);
3666  
3667          $student5 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3668  
3669          $assign = $this->create_instance($course);
3670          $instance = $assign->get_instance();
3671  
3672          // Overrides for each of the groups, and a user override.
3673          $overrides = [
3674              (object) [
3675                  // Override for group 1, highest priority (numerically lowest sortorder).
3676                  'assignid' => $instance->id,
3677                  'groupid' => $group1->id,
3678                  'userid' => null,
3679                  'sortorder' => 1,
3680                  'allowsubmissionsfromdate' => 1,
3681                  'duedate' => 2,
3682                  'cutoffdate' => 3,
3683                  'timelimit' => null
3684              ],
3685              (object) [
3686                  // Override for group 2, lower priority (numerically higher sortorder).
3687                  'assignid' => $instance->id,
3688                  'groupid' => $group2->id,
3689                  'userid' => null,
3690                  'sortorder' => 2,
3691                  'allowsubmissionsfromdate' => 5,
3692                  'duedate' => 6,
3693                  'cutoffdate' => 6,
3694                  'timelimit' => null
3695              ],
3696              (object) [
3697                  // User override.
3698                  'assignid' => $instance->id,
3699                  'groupid' => null,
3700                  'userid' => $student3->id,
3701                  'sortorder' => null,
3702                  'allowsubmissionsfromdate' => 7,
3703                  'duedate' => 8,
3704                  'cutoffdate' => 9,
3705                  'timelimit' => null
3706              ],
3707          ];
3708  
3709          foreach ($overrides as &$override) {
3710              $override->id = $DB->insert_record('assign_overrides', $override);
3711          }
3712  
3713          // User only in group 1 should see the group 1 override.
3714          $this->assertEquals($overrides[0], $assign->override_exists($student1->id));
3715  
3716          // User only in group 2 should see the group 2 override.
3717          $this->assertEquals($overrides[1], $assign->override_exists($student2->id));
3718  
3719          // User only in both groups with an override should see the user override as it has higher priority.
3720          $this->assertEquals($overrides[2], $assign->override_exists($student3->id));
3721  
3722          // User only in both groups with no override should see the group 1 override as it has higher priority.
3723          $this->assertEquals($overrides[0], $assign->override_exists($student4->id));
3724  
3725          // User with no overrides shoudl get nothing.
3726          $override = $assign->override_exists($student5->id);
3727          $this->assertNull($override->duedate);
3728          $this->assertNull($override->cutoffdate);
3729          $this->assertNull($override->allowsubmissionsfromdate);
3730      }
3731  
3732      /**
3733       * Test the quicksave grades processor
3734       */
3735      public function test_process_save_quick_grades() {
3736          $this->resetAfterTest();
3737          $course = $this->getDataGenerator()->create_course();
3738          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3739          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3740  
3741          $teacher->ignoresesskey = true;
3742          $this->setUser($teacher);
3743          $assign = $this->create_instance($course, [
3744                  'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
3745              ]);
3746  
3747          // Initially grade the user.
3748          $grade = (object) [
3749              'attemptnumber' => '',
3750              'timemodified' => '',
3751          ];
3752          $data = [
3753              "grademodified_{$student->id}" => $grade->timemodified,
3754              "gradeattempt_{$student->id}" => $grade->attemptnumber,
3755              "quickgrade_{$student->id}" => '60.0',
3756          ];
3757  
3758          $result = $assign->testable_process_save_quick_grades($data);
3759          $this->assertStringContainsString(get_string('quickgradingchangessaved', 'assign'), $result);
3760          $grade = $assign->get_user_grade($student->id, false);
3761          $this->assertEquals(60.0, $grade->grade);
3762  
3763          // Attempt to grade with a past attempts grade info.
3764          $assign->testable_process_add_attempt($student->id);
3765          $data = array(
3766              'grademodified_' . $student->id => $grade->timemodified,
3767              'gradeattempt_' . $student->id => $grade->attemptnumber,
3768              'quickgrade_' . $student->id => '50.0'
3769          );
3770          $result = $assign->testable_process_save_quick_grades($data);
3771          $this->assertStringContainsString(get_string('errorrecordmodified', 'assign'), $result);
3772          $grade = $assign->get_user_grade($student->id, false);
3773          $this->assertFalse($grade);
3774  
3775          // Attempt to grade a the attempt.
3776          $submission = $assign->get_user_submission($student->id, false);
3777          $data = array(
3778              'grademodified_' . $student->id => '',
3779              'gradeattempt_' . $student->id => $submission->attemptnumber,
3780              'quickgrade_' . $student->id => '40.0'
3781          );
3782          $result = $assign->testable_process_save_quick_grades($data);
3783          $this->assertStringContainsString(get_string('quickgradingchangessaved', 'assign'), $result);
3784          $grade = $assign->get_user_grade($student->id, false);
3785          $this->assertEquals(40.0, $grade->grade);
3786  
3787          // Catch grade update conflicts.
3788          // Save old data for later.
3789          $pastdata = $data;
3790          // Update the grade the 'good' way.
3791          $data = array(
3792              'grademodified_' . $student->id => $grade->timemodified,
3793              'gradeattempt_' . $student->id => $grade->attemptnumber,
3794              'quickgrade_' . $student->id => '30.0'
3795          );
3796          $result = $assign->testable_process_save_quick_grades($data);
3797          $this->assertStringContainsString(get_string('quickgradingchangessaved', 'assign'), $result);
3798          $grade = $assign->get_user_grade($student->id, false);
3799          $this->assertEquals(30.0, $grade->grade);
3800  
3801          // Now update using 'old' data. Should fail.
3802          $result = $assign->testable_process_save_quick_grades($pastdata);
3803          $this->assertStringContainsString(get_string('errorrecordmodified', 'assign'), $result);
3804          $grade = $assign->get_user_grade($student->id, false);
3805          $this->assertEquals(30.0, $grade->grade);
3806      }
3807  
3808      /**
3809       * Test updating activity completion when submitting an assessment.
3810       */
3811      public function test_update_activity_completion_records_solitary_submission() {
3812          $this->resetAfterTest();
3813  
3814          $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
3815          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3816          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3817  
3818          $this->setUser($teacher);
3819          $assign = $this->create_instance($course, [
3820              'grade' => 100,
3821              'completion' => COMPLETION_TRACKING_AUTOMATIC,
3822              'requireallteammemberssubmit' => 0,
3823          ]);
3824          $cm = $assign->get_course_module();
3825  
3826          // Submit the assignment as the student.
3827          $this->add_submission($student, $assign);
3828  
3829          // Check that completion is not met yet.
3830          $completion = new \completion_info($course);
3831          $completiondata = $completion->get_data($cm, false, $student->id);
3832          $this->assertEquals(0, $completiondata->completionstate);
3833  
3834          // Update to mark as complete.
3835          $submission = $assign->get_user_submission($student->id, true);
3836          $assign->testable_update_activity_completion_records(0, 0, $submission,
3837              $student->id, COMPLETION_COMPLETE, $completion);
3838  
3839          // Completion should now be met.
3840          $completiondata = $completion->get_data($cm, false, $student->id);
3841          $this->assertEquals(1, $completiondata->completionstate);
3842      }
3843  
3844      /**
3845       * Test updating activity completion when submitting an assessment.
3846       */
3847      public function test_update_activity_completion_records_team_submission() {
3848          $this->resetAfterTest();
3849  
3850          $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
3851          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3852          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3853          $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
3854  
3855          $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
3856          $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3857  
3858          groups_add_member($group1, $student);
3859          groups_add_member($group1, $otherstudent);
3860  
3861          $assign = $this->create_instance($course, [
3862              'grade' => 100,
3863              'completion' => COMPLETION_TRACKING_AUTOMATIC,
3864              'teamsubmission' => 1,
3865          ]);
3866  
3867          $cm = $assign->get_course_module();
3868  
3869          $this->add_submission($student, $assign);
3870          $this->submit_for_grading($student, $assign, ['groupid' => $group1->id]);
3871  
3872          $completion = new \completion_info($course);
3873  
3874          // Check that completion is not met yet.
3875          $completiondata = $completion->get_data($cm, false, $student->id);
3876          $this->assertEquals(0, $completiondata->completionstate);
3877  
3878          $completiondata = $completion->get_data($cm, false, $otherstudent->id);
3879          $this->assertEquals(0, $completiondata->completionstate);
3880  
3881          $submission = $assign->get_user_submission($student->id, true);
3882          $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
3883          $submission->groupid = $group1->id;
3884  
3885          $assign->testable_update_activity_completion_records(1, 0, $submission, $student->id, COMPLETION_COMPLETE, $completion);
3886  
3887          // Completion should now be met.
3888          $completiondata = $completion->get_data($cm, false, $student->id);
3889          $this->assertEquals(1, $completiondata->completionstate);
3890  
3891          $completiondata = $completion->get_data($cm, false, $otherstudent->id);
3892          $this->assertEquals(1, $completiondata->completionstate);
3893      }
3894  
3895      /**
3896       * Test updating activity completion when submitting an assessment for MDL-67126.
3897       */
3898      public function test_update_activity_completion_records_team_submission_new() {
3899          $this->resetAfterTest();
3900  
3901          $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
3902          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3903          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3904          $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
3905  
3906          $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
3907          $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3908  
3909          groups_add_member($group1, $student);
3910          groups_add_member($group1, $otherstudent);
3911  
3912          $assign = $this->create_instance($course, [
3913              'submissiondrafts' => 0,
3914              'completion' => COMPLETION_TRACKING_AUTOMATIC,
3915              'completionsubmit' => 1,
3916              'teamsubmission' => 1,
3917              'assignsubmission_onlinetext_enabled' => 1
3918          ]);
3919  
3920          $cm = $assign->get_course_module();
3921  
3922          $this->add_submission($student, $assign);
3923  
3924          $completion = new \completion_info($course);
3925  
3926          // Completion should now be met.
3927          $completiondata = $completion->get_data($cm, false, $student->id);
3928          $this->assertEquals(1, $completiondata->completionstate);
3929  
3930          $completiondata = $completion->get_data($cm, false, $otherstudent->id);
3931          $this->assertEquals(1, $completiondata->completionstate);
3932      }
3933  
3934      /**
3935       * Data provider for test_fix_null_grades
3936       * @return array[] Test data for test_fix_null_grades. Each element should contain grade, expectedcount and gradebookvalue
3937       */
3938      public function fix_null_grades_provider() {
3939          return [
3940              'Negative less than one is errant' => [
3941                  'grade' => -0.64,
3942                  'gradebookvalue' => null,
3943              ],
3944              'Negative more than one is errant' => [
3945                  'grade' => -30.18,
3946                  'gradebookvalue' => null,
3947              ],
3948              'Negative one exactly is not errant, but shouldn\'t be pushed to gradebook' => [
3949                  'grade' => ASSIGN_GRADE_NOT_SET,
3950                  'gradebookvalue' => null,
3951              ],
3952              'Positive grade is not errant' => [
3953                  'grade' => 1,
3954                  'gradebookvalue' => 1,
3955              ],
3956              'Large grade is not errant' => [
3957                  'grade' => 100,
3958                  'gradebookvalue' => 100,
3959              ],
3960              'Zero grade is not errant' => [
3961                  'grade' => 0,
3962                  'gradebookvalue' => 0,
3963              ],
3964          ];
3965      }
3966  
3967      /**
3968       * Test fix_null_grades
3969       * @param number $grade The grade we should set in the assign grading table.
3970       * @param number $expectedcount The finalgrade we expect in the gradebook after fixing the grades.
3971       * @dataProvider fix_null_grades_provider
3972       */
3973      public function test_fix_null_grades($grade, $gradebookvalue) {
3974          global $DB;
3975  
3976          $this->resetAfterTest();
3977  
3978          $course = $this->getDataGenerator()->create_course();
3979          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3980          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3981  
3982          $this->setUser($teacher);
3983          $assign = $this->create_instance($course);
3984  
3985          // Try getting a student's grade. This will give a grade of -1.
3986          // Then we can override it with a bad negative grade.
3987          $assign->get_user_grade($student->id, true);
3988  
3989          // Set the grade to something errant.
3990          // We don't set the grader here, so we expect it to be -1 as a result.
3991          $DB->set_field(
3992              'assign_grades',
3993              'grade',
3994              $grade,
3995              [
3996                  'userid' => $student->id,
3997                  'assignment' => $assign->get_instance()->id,
3998              ]
3999          );
4000          $assign->grade = $grade;
4001          $assigntemp = clone $assign->get_instance();
4002          $assigntemp->cmidnumber = $assign->get_course_module()->idnumber;
4003          assign_update_grades($assigntemp);
4004  
4005          // Check that the gradebook was updated with the assign grade. So we can guarentee test results later on.
4006          $expectedgrade = $grade == -1 ? null : $grade; // Assign sends null to the gradebook for -1 grades.
4007          $gradegrade = \grade_grade::fetch(array('userid' => $student->id, 'itemid' => $assign->get_grade_item()->id));
4008          $this->assertEquals(-1, $gradegrade->usermodified);
4009          $this->assertEquals($expectedgrade, $gradegrade->rawgrade);
4010  
4011          // Call fix_null_grades().
4012          $method = new \ReflectionMethod(\assign::class, 'fix_null_grades');
4013          $method->setAccessible(true);
4014          $result = $method->invoke($assign);
4015  
4016          $this->assertSame(true, $result);
4017  
4018          $gradegrade = \grade_grade::fetch(array('userid' => $student->id, 'itemid' => $assign->get_grade_item()->id));
4019  
4020          $this->assertEquals(-1, $gradegrade->usermodified);
4021          $this->assertEquals($gradebookvalue, $gradegrade->finalgrade);
4022  
4023          // Check that the grade was updated in the gradebook by fix_null_grades.
4024          $this->assertEquals($gradebookvalue, $gradegrade->finalgrade);
4025      }
4026  
4027      /**
4028       * Test grade override displays 'Graded' for students
4029       */
4030      public function test_grade_submission_override() {
4031          global $DB, $PAGE, $OUTPUT;
4032  
4033          $this->resetAfterTest();
4034  
4035          $course = $this->getDataGenerator()->create_course();
4036          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
4037          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
4038  
4039          $this->setUser($teacher);
4040          $assign = $this->create_instance($course, [
4041              'assignsubmission_onlinetext_enabled' => 1,
4042          ]);
4043  
4044          // Simulate adding a grade.
4045          $this->setUser($teacher);
4046          $data = new \stdClass();
4047          $data->grade = '50.0';
4048          $assign->testable_apply_grade_to_user($data, $student->id, 0);
4049  
4050          // Set grade override.
4051          $gradegrade = \grade_grade::fetch([
4052              'userid' => $student->id,
4053              'itemid' => $assign->get_grade_item()->id,
4054          ]);
4055  
4056          // Check that grade submission is not overridden yet.
4057          $this->assertEquals(false, $gradegrade->is_overridden());
4058  
4059          // Simulate a submission.
4060          $this->setUser($student);
4061          $submission = $assign->get_user_submission($student->id, true);
4062  
4063          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
4064  
4065          // Set override grade grade, and check that grade submission has been overridden.
4066          $gradegrade->set_overridden(true);
4067          $this->assertEquals(true, $gradegrade->is_overridden());
4068  
4069          // Check that submissionslocked message 'This assignment is not accepting submissions' does not appear for student.
4070          $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
4071          $output = $assign->get_renderer()->render($gradingtable);
4072          $this->assertStringContainsString(get_string('submissionstatus_', 'assign'), $output);
4073  
4074          $assignsubmissionstatus = $assign->get_assign_submission_status_renderable($student, true);
4075          $output2 = $assign->get_renderer()->render($assignsubmissionstatus);
4076  
4077          // Check that submissionslocked 'This assignment is not accepting submissions' message does not appear for student.
4078          $this->assertStringNotContainsString(get_string('submissionslocked', 'assign'), $output2);
4079          // Check that submissionstatus_marked 'Graded' message does appear for student.
4080          $this->assertStringContainsString(get_string('submissionstatus_marked', 'assign'), $output2);
4081      }
4082  
4083      /**
4084       * Test the result of get_filters is consistent.
4085       */
4086      public function test_get_filters() {
4087          $this->resetAfterTest();
4088  
4089          $course = $this->getDataGenerator()->create_course();
4090          $assign = $this->create_instance($course);
4091          $valid = $assign->get_filters();
4092  
4093          $this->assertEquals(count($valid), 6);
4094      }
4095  
4096      /**
4097       * Test assign->get_instance() for a number of cases, as defined in the data provider.
4098       *
4099       * @dataProvider assign_get_instance_provider
4100       * @param array $courseconfig the config to use when creating the course.
4101       * @param array $assignconfig the config to use when creating the assignment.
4102       * @param array $enrolconfig the config to use when enrolling the user (this will be the active user).
4103       * @param array $expectedproperties an map containing the expected names and values for the assign instance data.
4104       */
4105      public function test_assign_get_instance(array $courseconfig, array $assignconfig, array $enrolconfig,
4106              array $expectedproperties) {
4107          $this->resetAfterTest();
4108  
4109          set_config('enablecourserelativedates', true); // Enable relative dates at site level.
4110  
4111          $course = $this->getDataGenerator()->create_course($courseconfig);
4112          $assign = $this->create_instance($course, $assignconfig);
4113          $user = $this->getDataGenerator()->create_and_enrol($course, ...array_values($enrolconfig));
4114  
4115          $instance = $assign->get_instance($user->id);
4116  
4117          foreach ($expectedproperties as $propertyname => $propertyval) {
4118              $this->assertEquals($propertyval, $instance->$propertyname);
4119          }
4120      }
4121  
4122      /**
4123       * The test_assign_get_instance data provider.
4124       */
4125      public function assign_get_instance_provider() {
4126          $timenow = time();
4127  
4128          // The get_default_instance() method shouldn't calculate any properties per-user. It should just return the record data.
4129          // We'll confirm this works for a few different user types anyway, just like we do for get_instance().
4130          return [
4131              'Teacher whose enrolment starts after the course start date, relative dates mode enabled' => [
4132                  'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
4133                  'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4134                  'enrolconfig' => ['shortname' => 'teacher', 'userparams' => null, 'method' => 'manual',
4135                      'startdate' => $timenow - 8 * DAYSECS],
4136                  'expectedproperties' => ['duedate' => $timenow + 6 * DAYSECS]
4137              ],
4138              'Teacher whose enrolment starts before the course start date, relative dates mode enabled' => [
4139                  'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
4140                  'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4141                  'enrolconfig' => ['shortname' => 'teacher', 'userparams' => null, 'method' => 'manual',
4142                      'startdate' => $timenow - 12 * DAYSECS],
4143                  'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS]
4144              ],
4145              'Teacher whose enrolment starts after the course start date, relative dates mode disabled' => [
4146                  'courseconfig' => ['relativedatesmode' => false, 'startdate' => $timenow - 10 * DAYSECS],
4147                  'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4148                  'enrolconfig' => ['shortname' => 'teacher', 'userparams' => null, 'method' => 'manual',
4149                      'startdate' => $timenow - 8 * DAYSECS],
4150                  'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS]
4151              ],
4152              'Student whose enrolment starts after the course start date, relative dates mode enabled' => [
4153                  'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
4154                  'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4155                  'enrolconfig' => ['shortname' => 'student', 'userparams' => null, 'method' => 'manual',
4156                      'startdate' => $timenow - 8 * DAYSECS],
4157                  'expectedproperties' => ['duedate' => $timenow + 6 * DAYSECS]
4158              ],
4159              'Student whose enrolment starts before the course start date, relative dates mode enabled' => [
4160                  'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
4161                  'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4162                  'enrolconfig' => ['shortname' => 'student', 'userparams' => null, 'method' => 'manual',
4163                      'startdate' => $timenow - 12 * DAYSECS],
4164                  'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS]
4165              ],
4166              'Student whose enrolment starts after the course start date, relative dates mode disabled' => [
4167                  'courseconfig' => ['relativedatesmode' => false, 'startdate' => $timenow - 10 * DAYSECS],
4168                  'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4169                  'enrolconfig' => ['shortname' => 'student', 'userparams' => null, 'method' => 'manual',
4170                      'startdate' => $timenow - 8 * DAYSECS],
4171                  'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS]
4172              ],
4173          ];
4174      }
4175  
4176      /**
4177       * Test assign->get_default_instance() for a number of cases, as defined in the date provider.
4178       *
4179       * @dataProvider assign_get_default_instance_provider
4180       * @param array $courseconfig the config to use when creating the course.
4181       * @param array $assignconfig the config to use when creating the assignment.
4182       * @param array $enrolconfig the config to use when enrolling the user (this will be the active user).
4183       * @param array $expectedproperties an map containing the expected names and values for the assign instance data.
4184       */
4185      public function test_assign_get_default_instance(array $courseconfig, array $assignconfig, array $enrolconfig,
4186              array $expectedproperties) {
4187          $this->resetAfterTest();
4188  
4189          set_config('enablecourserelativedates', true); // Enable relative dates at site level.
4190  
4191          $course = $this->getDataGenerator()->create_course($courseconfig);
4192          $assign = $this->create_instance($course, $assignconfig);
4193          $user = $this->getDataGenerator()->create_and_enrol($course, ...array_values($enrolconfig));
4194  
4195          $this->setUser($user);
4196          $defaultinstance = $assign->get_default_instance();
4197  
4198          foreach ($expectedproperties as $propertyname => $propertyval) {
4199              $this->assertEquals($propertyval, $defaultinstance->$propertyname);
4200          }
4201      }
4202  
4203      /**
4204       * The test_assign_get_default_instance data provider.
4205       */
4206      public function assign_get_default_instance_provider() {
4207          $timenow = time();
4208  
4209          // The get_default_instance() method shouldn't calculate any properties per-user. It should just return the record data.
4210          // We'll confirm this works for a few different user types anyway, just like we do for get_instance().
4211          return [
4212              'Teacher whose enrolment starts after the course start date, relative dates mode enabled' => [
4213                  'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
4214                  'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4215                  'enrolconfig' => ['shortname' => 'teacher', 'userparams' => null, 'method' => 'manual',
4216                      'startdate' => $timenow - 8 * DAYSECS],
4217                  'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS]
4218              ],
4219              'Teacher whose enrolment starts before the course start date, relative dates mode enabled' => [
4220                  'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
4221                  'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4222                  'enrolconfig' => ['shortname' => 'teacher', 'userparams' => null, 'method' => 'manual',
4223                      'startdate' => $timenow - 12 * DAYSECS],
4224                  'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS]
4225              ],
4226              'Teacher whose enrolment starts after the course start date, relative dates mode disabled' => [
4227                  'courseconfig' => ['relativedatesmode' => false, 'startdate' => $timenow - 10 * DAYSECS],
4228                  'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4229                  'enrolconfig' => ['shortname' => 'teacher', 'userparams' => null, 'method' => 'manual',
4230                      'startdate' => $timenow - 8 * DAYSECS],
4231                  'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS]
4232              ],
4233              'Student whose enrolment starts after the course start date, relative dates mode enabled' => [
4234                  'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
4235                  'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4236                  'enrolconfig' => ['shortname' => 'student', 'userparams' => null, 'method' => 'manual',
4237                      'startdate' => $timenow - 8 * DAYSECS],
4238                  'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS]
4239              ],
4240          ];
4241      }
4242  
4243      /**
4244       * Test that cron task uses task API to get its last run time.
4245       */
4246      public function test_cron_use_task_api_to_get_lastruntime() {
4247          global $DB;
4248          $this->resetAfterTest();
4249          $course = $this->getDataGenerator()->create_course();
4250  
4251          // Create an assignment which allows submissions from 3 days ago.
4252          $assign1 = $this->create_instance($course, [
4253              'duedate' => time() + DAYSECS,
4254              'alwaysshowdescription' => 0,
4255              'allowsubmissionsfromdate' => time() - 3 * DAYSECS,
4256              'intro' => 'This one should not be re-created',
4257          ]);
4258  
4259          // Create an assignment which allows submissions from 1 day ago.
4260          $assign2 = $this->create_instance($course, [
4261              'duedate' => time() + DAYSECS,
4262              'alwaysshowdescription' => 0,
4263              'allowsubmissionsfromdate' => time() - DAYSECS,
4264              'intro' => 'This one should be re-created',
4265          ]);
4266  
4267          // Set last run time 2 days ago.
4268          $DB->set_field('task_scheduled', 'lastruntime', time() - 2 * DAYSECS, ['classname' => '\mod_assign\task\cron_task']);
4269  
4270          // Remove events to make sure cron will update calendar and re-create one of them.
4271          $params = array('modulename' => 'assign', 'instance' => $assign1->get_instance()->id);
4272          $DB->delete_records('event', $params);
4273          $params = array('modulename' => 'assign', 'instance' => $assign2->get_instance()->id);
4274          $DB->delete_records('event', $params);
4275  
4276          // Run cron.
4277          \assign::cron();
4278  
4279          // Assert that calendar hasn't been updated for the first assignment as it's supposed to be
4280          // updated as part of previous cron runs (allowsubmissionsfromdate is less than lastruntime).
4281          $params = array('modulename' => 'assign', 'instance' => $assign1->get_instance()->id);
4282          $event1 = $DB->get_record('event', $params);
4283          $this->assertEmpty($event1);
4284  
4285          // Assert that calendar has been updated for the second assignment
4286          // because its allowsubmissionsfromdate is greater than lastruntime.
4287          $params = array('modulename' => 'assign', 'instance' => $assign2->get_instance()->id);
4288          $event2 = $DB->get_record('event', $params);
4289          $this->assertNotEmpty($event2);
4290          $this->assertSame('This one should be re-created', $event2->description);
4291      }
4292  
4293      /**
4294       * Test submissions that need grading output after one ungraded submission
4295       */
4296      public function test_submissions_need_grading() {
4297          global $PAGE;
4298  
4299          $this->resetAfterTest();
4300          $course = $this->getDataGenerator()->create_course();
4301          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
4302          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
4303  
4304          // Setup the assignment.
4305          $this->setUser($teacher);
4306          $time = time();
4307          $assign = $this->create_instance($course, [
4308                  'assignsubmission_onlinetext_enabled' => 1,
4309              ]);
4310          $PAGE->set_url(new \moodle_url('/mod/assign/view.php', [
4311              'id' => $assign->get_course_module()->id,
4312              'action' => 'grading',
4313          ]));
4314  
4315          // Check for 0 submissions.
4316          $summary = $assign->view('viewcourseindex');
4317  
4318          $this->assertStringContainsString('/mod/assign/view.php?id=' .
4319              $assign->get_course_module()->id . '&amp;action=grading">' .
4320              get_string('numberofsubmissionsneedgradinglabel', 'assign', 0) . '</a>', $summary);
4321  
4322          // Simulate an assignment submission.
4323          $this->setUser($student);
4324          $submission = $assign->get_user_submission($student->id, true);
4325          $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
4326          $assign->testable_update_submission($submission, $student->id, true, false);
4327          $data = new \stdClass();
4328          $data->onlinetext_editor = [
4329              'itemid' => file_get_unused_draft_itemid(),
4330              'text' => 'Submission text',
4331              'format' => FORMAT_MOODLE,
4332          ];
4333          $plugin = $assign->get_submission_plugin_by_type('onlinetext');
4334          $plugin->save($submission, $data);
4335  
4336          // Check for 1 ungraded submission.
4337          $this->setUser($teacher);
4338  
4339          $summary = $assign->view('viewcourseindex');
4340  
4341          $this->assertStringContainsString('/mod/assign/view.php?id=' .
4342              $assign->get_course_module()->id .  '&amp;action=grading">' .
4343              get_string('numberofsubmissionsneedgradinglabel', 'assign', 1) . '</a>', $summary);
4344      }
4345  
4346      /**
4347       * Test that attachments should not be provided if \assign->show_intro returns false.
4348       *
4349       * @covers \assign::should_provide_intro_attachments
4350       */
4351      public function test_should_provide_intro_attachments_with_show_intro_disabled() {
4352          $this->resetAfterTest();
4353          $futuredate = time() + 300;
4354          list($assign, $instance, $student) = $this->create_submission([
4355              'alwaysshowdescription' => '0',
4356              'allowsubmissionsfromdate' => $futuredate,
4357          ]);
4358          $this->assertFalse($assign->should_provide_intro_attachments($student->id));
4359      }
4360  
4361      /**
4362       * Test that attachments should be provided if user has capability to manage activity.
4363       *
4364       * @covers \assign::should_provide_intro_attachments
4365       */
4366      public function test_should_provide_intro_attachments_with_bypass_capability() {
4367          $this->resetAfterTest();
4368          list($assign, $instance, $student) = $this->create_submission([
4369              'submissionattachments' => 1,
4370          ]);
4371          // Provide teaching role to student1 so they are able to bypass time limit restrictions on viewing attachments.
4372          $this->getDataGenerator()->enrol_user($student->id, $instance->course, 'editingteacher');
4373          $this->assertTrue($assign->should_provide_intro_attachments($student->id));
4374      }
4375  
4376      /**
4377       * Test that attachments should be provided if submissionattachments is disabled.
4378       *
4379       * @covers \assign::should_provide_intro_attachments
4380       */
4381      public function test_should_provide_intro_attachments_with_submissionattachments_disabled() {
4382          $this->resetAfterTest();
4383          list($assign, $instance, $student) = $this->create_submission();
4384          $this->assertTrue($assign->should_provide_intro_attachments($student->id));
4385      }
4386  
4387      /**
4388       * Test that attachments should not be provided if submissionattachments is enabled with no open submission.
4389       *
4390       * @covers \assign::should_provide_intro_attachments
4391       */
4392      public function test_should_provide_intro_attachments_with_submissionattachments_enabled_and_submissions_closed() {
4393          $this->resetAfterTest();
4394          // Set cut-off date to the past.
4395          list($assign, $instance, $student) = $this->create_submission([
4396              'timelimit' => '300',
4397              'submissionattachments' => 1,
4398              'cutoffdate' => time() - 300,
4399          ]);
4400          $this->assertFalse($assign->should_provide_intro_attachments($student->id));
4401      }
4402  
4403      /**
4404       * Test that attachments should be provided if submissionattachments is enabled with an open submission.
4405       *
4406       * @covers \assign::should_provide_intro_attachments
4407       */
4408      public function test_should_provide_intro_attachments_submissionattachments_enabled_and_an_open_submission() {
4409          $this->resetAfterTest();
4410          set_config('enabletimelimit', '1', 'assign');
4411          list($assign, $instance, $student) = $this->create_submission([
4412              'timelimit' => '300',
4413              'submissionattachments' => 1,
4414          ]);
4415  
4416          // Open a submission.
4417          $assign->get_user_submission($student->id, true);
4418  
4419          $this->assertTrue($assign->should_provide_intro_attachments($student->id));
4420      }
4421  
4422      /**
4423       * Test that a submission using a time limit is currently open.
4424       *
4425       * @covers \assign::is_attempt_in_progress
4426       */
4427      public function test_is_attempt_in_progress_with_open_submission() {
4428          global $DB;
4429          $this->resetAfterTest();
4430          set_config('enabletimelimit', '1', 'assign');
4431          list($assign, $instance, $student) = $this->create_submission([
4432              'timelimit' => '300',
4433          ]);
4434          $submission = $assign->get_user_submission($student->id, true);
4435          // Set a timestarted.
4436          $submission->timestarted = time() - 300;
4437          $DB->update_record('assign_submission', $submission);
4438          $this->assertTrue($assign->is_attempt_in_progress());
4439      }
4440  
4441      /**
4442       * Test that a submission using a time limit is started without a start time.
4443       *
4444       * @covers \assign::is_attempt_in_progress
4445       */
4446      public function test_is_attempt_in_progress_with_open_submission_and_no_timestarted() {
4447          $this->resetAfterTest();
4448          set_config('enabletimelimit', '1', 'assign');
4449          list($assign, $instance, $student) = $this->create_submission([
4450              'timelimit' => '300',
4451          ]);
4452          $assign->get_user_submission($student->id, true);
4453          $this->assertFalse($assign->is_attempt_in_progress());
4454      }
4455  
4456      /**
4457       * Test that a submission using a time limit is currently not open.
4458       *
4459       * @covers \assign::is_attempt_in_progress
4460       */
4461      public function test_is_attempt_in_progress_with_no_open_submission() {
4462          global $DB;
4463          $this->resetAfterTest();
4464          set_config('enabletimelimit', '1', 'assign');
4465          list($assign, $instance, $student) = $this->create_submission([
4466              'timelimit' => '300',
4467          ]);
4468          // Clear all current submissions.
4469          $DB->delete_records('assign_submission', ['assignment' => $instance->id]);
4470          $this->assertFalse($assign->is_attempt_in_progress());
4471      }
4472  
4473      /**
4474       * Create a submission for testing.
4475       * @param  array $params Optional params to use for creating assignment instance.
4476       * @return array an array containing all the required data for testing
4477       */
4478      protected function create_submission(array $params = []) {
4479          global $DB;
4480  
4481          // Create a course and assignment and users.
4482          $course = self::getDataGenerator()->create_course(array('groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1));
4483  
4484          $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
4485          $params = array_merge(array(
4486              'course' => $course->id,
4487              'assignsubmission_file_maxfiles' => 1,
4488              'assignsubmission_file_maxsizebytes' => 1024 * 1024,
4489              'assignsubmission_onlinetext_enabled' => 1,
4490              'assignsubmission_file_enabled' => 1,
4491              'submissiondrafts' => 1,
4492              'assignfeedback_file_enabled' => 1,
4493              'assignfeedback_comments_enabled' => 1,
4494              'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
4495              'sendnotifications' => 0
4496          ), $params);
4497  
4498          set_config('submissionreceipts', 0, 'assign');
4499  
4500          $instance = $generator->create_instance($params);
4501          $cm = get_coursemodule_from_instance('assign', $instance->id);
4502          $context = \context_module::instance($cm->id);
4503  
4504          $assign = new \mod_assign_testable_assign($context, $cm, $course);
4505  
4506          $student = self::getDataGenerator()->create_user();
4507          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
4508          $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
4509  
4510          $this->setUser($student);
4511  
4512          // Create a student1 with an online text submission.
4513          // Simulate a submission.
4514          $submission = $assign->get_user_submission($student->id, true);
4515  
4516          $data = new \stdClass();
4517          $data->onlinetext_editor = array(
4518              'itemid' => file_get_unused_draft_itemid(),
4519              'text' => 'Submission text with a <a href="@@PLUGINFILE@@/intro.txt">link</a>',
4520              'format' => FORMAT_MOODLE);
4521  
4522          $draftidfile = file_get_unused_draft_itemid();
4523          $usercontext = \context_user::instance($student->id);
4524          $filerecord = array(
4525              'contextid' => $usercontext->id,
4526              'component' => 'user',
4527              'filearea'  => 'draft',
4528              'itemid'    => $draftidfile,
4529              'filepath'  => '/',
4530              'filename'  => 't.txt',
4531          );
4532          $fs = get_file_storage();
4533          $fs->create_file_from_string($filerecord, 'text contents');
4534  
4535          $data->files_filemanager = $draftidfile;
4536  
4537          $notices = array();
4538          $assign->save_submission($data, $notices);
4539  
4540          return array($assign, $instance, $student);
4541      }
4542  
4543      /**
4544       * Test user filtering by First name, Last name and Submission status.
4545       *
4546       * @covers \assign::is_userid_filtered
4547       */
4548      public function test_is_userid_filtered() {
4549          $this->resetAfterTest();
4550  
4551          // Generate data and simulate student submissions.
4552          $course = $this->getDataGenerator()->create_course();
4553          $params1 = ['firstname' => 'Valentin', 'lastname' => 'Ivanov'];
4554          $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student', $params1);
4555          $params2 = ['firstname' => 'Nikolay', 'lastname' => 'Petrov'];
4556          $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student', $params2);
4557          $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]);
4558          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
4559          $this->setUser($student1);
4560          $submission = $assign->get_user_submission($student1->id, true);
4561          $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
4562          $assign->testable_update_submission($submission, $student1->id, true, false);
4563          $this->setUser($student2);
4564          $submission = $assign->get_user_submission($student2->id, true);
4565          $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
4566          $assign->testable_update_submission($submission, $student2->id, true, false);
4567          $this->setUser($teacher);
4568  
4569          // By default, both users should match filters.
4570          $this->AssertTrue($assign->is_userid_filtered($student1->id));
4571          $this->AssertTrue($assign->is_userid_filtered($student2->id));
4572  
4573          // Filter by First name starting with V.
4574          $_GET['tifirst'] = 'V';
4575          $this->AssertTrue($assign->is_userid_filtered($student1->id));
4576          $this->AssertFalse($assign->is_userid_filtered($student2->id));
4577  
4578          // Add Last name to filter out both users.
4579          $_GET['tilast'] = 'G';
4580          $this->AssertFalse($assign->is_userid_filtered($student1->id));
4581          $this->AssertFalse($assign->is_userid_filtered($student2->id));
4582  
4583          // Unsetting variables doesn't change behaviour because filters are stored in user preferences.
4584          unset($_GET['tifirst']);
4585          unset($_GET['tilast']);
4586          $this->AssertFalse($assign->is_userid_filtered($student1->id));
4587          $this->AssertFalse($assign->is_userid_filtered($student2->id));
4588  
4589          // Reset table preferences.
4590          $_GET['treset'] = '1';
4591          $this->AssertTrue($assign->is_userid_filtered($student1->id));
4592          $this->AssertTrue($assign->is_userid_filtered($student2->id));
4593  
4594          // Display users with submitted submissions only.
4595          set_user_preference('assign_filter', ASSIGN_SUBMISSION_STATUS_SUBMITTED);
4596          $this->AssertTrue($assign->is_userid_filtered($student1->id));
4597          $this->AssertFalse($assign->is_userid_filtered($student2->id));
4598  
4599          // Display users with drafts.
4600          set_user_preference('assign_filter', ASSIGN_SUBMISSION_STATUS_DRAFT);
4601          $this->AssertFalse($assign->is_userid_filtered($student1->id));
4602          $this->AssertTrue($assign->is_userid_filtered($student2->id));
4603  
4604          // Reset the filter.
4605          set_user_preference('assign_filter', '');
4606          $this->AssertTrue($assign->is_userid_filtered($student1->id));
4607          $this->AssertTrue($assign->is_userid_filtered($student2->id));
4608      }
4609  }