Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]

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